mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-01-10 06:00:45 +00:00
Merge branch 'staging' into insta-import-optimizeMedia
This commit is contained in:
commit
632f590c3c
850 changed files with 91083 additions and 31210 deletions
|
@ -7,7 +7,7 @@ jobs:
|
|||
build:
|
||||
docker:
|
||||
# Specify the version you desire here
|
||||
- image: cimg/php:8.2.5
|
||||
- image: cimg/php:8.3.8
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
@ -21,7 +21,12 @@ jobs:
|
|||
steps:
|
||||
- checkout
|
||||
|
||||
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
|
||||
- run:
|
||||
name: "Create Environment file and generate app key"
|
||||
command: |
|
||||
mv .env.testing .env
|
||||
|
||||
- run: sudo apt install zlib1g-dev libsqlite3-dev
|
||||
|
||||
# Download and cache dependencies
|
||||
|
||||
|
@ -36,18 +41,17 @@ jobs:
|
|||
- run: composer install -n --prefer-dist
|
||||
|
||||
- save_cache:
|
||||
key: composer-v2-{{ checksum "composer.lock" }}
|
||||
key: v2-dependencies-{{ checksum "composer.json" }}
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
- run: cp .env.testing .env
|
||||
- run: php artisan config:cache
|
||||
- run: php artisan route:clear
|
||||
- run: php artisan storage:link
|
||||
- run: php artisan key:generate
|
||||
|
||||
# run tests with phpunit or codecept
|
||||
- run: ./vendor/bin/phpunit
|
||||
- run: php artisan test
|
||||
- store_test_results:
|
||||
path: tests/_output
|
||||
- store_artifacts:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,9 +1,27 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{sh,envsh,env,env*}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# ShellCheck config
|
||||
shell_variant = bash # like -ln=bash
|
||||
binary_next_line = true # like -bn
|
||||
switch_case_indent = true # like -ci
|
||||
space_redirects = false # like -sr
|
||||
keep_padding = false # like -kp
|
||||
function_next_line = true # like -fn
|
||||
never_split = true # like -ns
|
||||
simplify = true
|
||||
|
|
1399
.env.docker
1399
.env.docker
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,7 @@ OPEN_REGISTRATION="false"
|
|||
ENFORCE_EMAIL_VERIFICATION="false"
|
||||
PF_MAX_USERS="1000"
|
||||
OAUTH_ENABLED="true"
|
||||
ENABLE_CONFIG_CACHE=true
|
||||
|
||||
# Media Configuration
|
||||
PF_OPTIMIZE_IMAGES="true"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
APP_NAME="Pixelfed Test"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
|
||||
|
|
7
.gitattributes
vendored
7
.gitattributes
vendored
|
@ -3,3 +3,10 @@
|
|||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
|
||||
# Collapse diffs for generated files:
|
||||
public/**/*.js text -diff
|
||||
public/**/*.json text -diff
|
||||
public/**/*.css text -diff
|
||||
public/img/* binary -diff
|
||||
public/fonts/* binary -diff
|
||||
|
|
125
.github/workflows/build-docker.yml
vendored
125
.github/workflows/build-docker.yml
vendored
|
@ -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
|
230
.github/workflows/docker.yml
vendored
Normal file
230
.github/workflows/docker.yml
vendored
Normal file
|
@ -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 }}
|
43
.gitignore
vendored
43
.gitignore
vendored
|
@ -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
|
||||
|
|
6
.hadolint.yaml
Normal file
6
.hadolint.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
ignored:
|
||||
- DL3002 # warning: Last USER should not be root
|
||||
- DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
|
||||
- DL3029 # warning: Do not use --platform flag with FROM
|
||||
- SC2046 # warning: Quote this to prevent word splitting.
|
||||
- SC2086 # info: Double quote to prevent globbing and word splitting.
|
4
.markdownlint.json
Normal file
4
.markdownlint.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"MD013": false,
|
||||
"MD014": false
|
||||
}
|
12
.shellcheckrc
Normal file
12
.shellcheckrc
Normal file
|
@ -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
|
14
.vscode/extensions.json
vendored
Normal file
14
.vscode/extensions.json
vendored
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
21
.vscode/settings.json
vendored
Normal file
21
.vscode/settings.json
vendored
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
305
CHANGELOG.md
305
CHANGELOG.md
|
@ -1,17 +1,261 @@
|
|||
# Release Notes
|
||||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.9...dev)
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
|
||||
|
||||
### OAuth
|
||||
- Fix oauth oob (urn:ietf:wg:oauth:2.0:oob) support. ([8afbdb03](https://github.com/pixelfed/pixelfed/commit/8afbdb03))
|
||||
|
||||
### Updates
|
||||
- Update AP helpers, reject statuses with invalid dates ([960f3849](https://github.com/pixelfed/pixelfed/commit/960f3849))
|
||||
- Update DirectMessage API, fix broken threading ([044d410c](https://github.com/pixelfed/pixelfed/commit/044d410c))
|
||||
- Update Status caption render logic ([fb8dbb95](https://github.com/pixelfed/pixelfed/commit/fb8dbb95))
|
||||
- Update ApiV1Controller, fix bookmark bug. Closes #5216 ([9f7cc52c](https://github.com/pixelfed/pixelfed/commit/9f7cc52c))
|
||||
- Update Status caption logic, stop storing duplicate html caption in db and defer to cached StatusService rendering ([9eeb7b67](https://github.com/pixelfed/pixelfed/commit/9eeb7b67))
|
||||
- Update AutolinkService, optimize lookups ([eac2c196](https://github.com/pixelfed/pixelfed/commit/eac2c196))
|
||||
- Update DirectMessageController, remove 72h limit for admins ([639df410](https://github.com/pixelfed/pixelfed/commit/639df410))
|
||||
- Update StatusService, fix newlines ([56c07b7a](https://github.com/pixelfed/pixelfed/commit/56c07b7a))
|
||||
- Update confirm email template, add plaintext link. Fixes #5375 ([45986707](https://github.com/pixelfed/pixelfed/commit/45986707))
|
||||
- Update UserVerifyEmail command ([77da9ad8](https://github.com/pixelfed/pixelfed/commit/77da9ad8))
|
||||
- Update StatusStatelessTransformer, refactor the caption field to be compliant with the MastoAPI. Fixes #5364 ([79039ba5](https://github.com/pixelfed/pixelfed/commit/79039ba5))
|
||||
- Update mailgun config, add endpoint and scheme ([271d5114](https://github.com/pixelfed/pixelfed/commit/271d5114))
|
||||
- Update search and status logic to fix postgres bugs ([8c39ef4](https://github.com/pixelfed/pixelfed/commit/8c39ef4))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev)
|
||||
|
||||
### Added
|
||||
- Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021))
|
||||
- Authorize Interaction support (for handling remote interactions) ([4ca7c6c3](https://github.com/pixelfed/pixelfed/commit/4ca7c6c3))
|
||||
- Contact Form Admin Responses ([52cc6090](https://github.com/pixelfed/pixelfed/commit/52cc6090))
|
||||
- Profile Carousels ([8af77a3f](https://github.com/pixelfed/pixelfed/commit/8af77a3f))
|
||||
- Moderated Profiles ([39f16321](https://github.com/pixelfed/pixelfed/commit/39f16321))
|
||||
|
||||
### Federation
|
||||
- Add ActiveSharedInboxService, for efficient sharedInbox caching ([1a6a3397](https://github.com/pixelfed/pixelfed/commit/1a6a3397))
|
||||
- Add MovePipeline queue jobs ([9904d05f](https://github.com/pixelfed/pixelfed/commit/9904d05f))
|
||||
- Add ActivityPub Move validator ([909a6c72](https://github.com/pixelfed/pixelfed/commit/909a6c72))
|
||||
- Add delay to move handler to allow for remote cache invalidation ([8a362c12](https://github.com/pixelfed/pixelfed/commit/8a362c12))
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Controller, add support for notification filter types ([f61159a1](https://github.com/pixelfed/pixelfed/commit/f61159a1))
|
||||
- Update ApiV1Dot1Controller, fix mutual api ([a8bb97b2](https://github.com/pixelfed/pixelfed/commit/a8bb97b2))
|
||||
- Update ApiV1Controller, fix /api/v1/favourits pagination ([72f68160](https://github.com/pixelfed/pixelfed/commit/72f68160))
|
||||
- Update RegisterController, update username constraints, require atleast one alpha char ([dd6e3cc2](https://github.com/pixelfed/pixelfed/commit/dd6e3cc2))
|
||||
- Update AdminUser, fix entity casting ([cb5620d4](https://github.com/pixelfed/pixelfed/commit/cb5620d4))
|
||||
- Update instance config, update network cache feed max_hours_old falloff to 90 days instead of 6 hours to allow for less active instances to have more results ([c042d135](https://github.com/pixelfed/pixelfed/commit/c042d135))
|
||||
- Update ApiV1Dot1Controller, add new single media status create endpoint ([b03f5cec](https://github.com/pixelfed/pixelfed/commit/b03f5cec))
|
||||
- Update AdminSettings component, add link to Custom CSS settings ([958daac4](https://github.com/pixelfed/pixelfed/commit/958daac4))
|
||||
- Update ApiV1Controller, fix v1/instance stats, force cast to int ([dcd95d68](https://github.com/pixelfed/pixelfed/commit/dcd95d68))
|
||||
- Update BeagleService, disable discovery if AP is disabled ([6cd1cbb4](https://github.com/pixelfed/pixelfed/commit/6cd1cbb4))
|
||||
- Update NodeinfoService, fix typo ([edad436d](https://github.com/pixelfed/pixelfed/commit/edad436d))
|
||||
- Update ActivityPubFetchService, reduce cache ttl from 1 hour to 7.5 mins and add uncached fetchRequest method ([21da2b64](https://github.com/pixelfed/pixelfed/commit/21da2b64))
|
||||
- Update UserAccountDelete command, increase sharedInbox ttl from 12h to 14d ([be02f48a](https://github.com/pixelfed/pixelfed/commit/be02f48a))
|
||||
- Update HttpSignature, add signRaw method and improve error checking ([d4cf9181](https://github.com/pixelfed/pixelfed/commit/d4cf9181))
|
||||
- Update AP helpers, add forceBanCheck param to validateUrl method ([42424028](https://github.com/pixelfed/pixelfed/commit/42424028))
|
||||
- Update layout, add og:logo ([4cc576e1](https://github.com/pixelfed/pixelfed/commit/4cc576e1))
|
||||
- Update ReblogService, fix cache sync issues ([3de8ceca](https://github.com/pixelfed/pixelfed/commit/3de8ceca))
|
||||
- Update config, allow Beagle discover service to be disabled ([de4ce3c8](https://github.com/pixelfed/pixelfed/commit/de4ce3c8))
|
||||
- Update ApiV1Dot1Controller, allow upto 5 similar push tokens ([7820b506](https://github.com/pixelfed/pixelfed/commit/7820b506))
|
||||
- Update AdminReports, add missing click handler. Fixes #5332 ([fe48b8ad](https://github.com/pixelfed/pixelfed/commit/fe48b8ad))
|
||||
- Improve media filtering by using OffscreenCanvas, if supported ([aea5392](https://github.com/pixelfed/pixelfed/commit/aea5392))
|
||||
|
||||
## [v0.12.3 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.2...v0.12.3)
|
||||
|
||||
### Updates
|
||||
- Fix migrations bug ([4d1180b1](https://github.com/pixelfed/pixelfed/commit/4d1180b1))
|
||||
|
||||
## [v0.12.2 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.1...v0.12.2)
|
||||
|
||||
### Framework
|
||||
- Updated to Laravel 11 (requires php 8.2+)
|
||||
|
||||
### Added
|
||||
- New api/v1/instance/peers API endpoint, disabled by default ([4aad1c22](https://github.com/pixelfed/pixelfed/commit/4aad1c22))
|
||||
- Added disable_embeds setting, and fix cache invalidation in other settings ([c5e7e917](https://github.com/pixelfed/pixelfed/commit/c5e7e917))
|
||||
|
||||
### Updates
|
||||
- Update DirectMessageController, add 72 hour delay for new accounts before they can send a DM ([61d105fd](https://github.com/pixelfed/pixelfed/commit/61d105fd))
|
||||
- Update AdminCuratedRegisterController, increase message length from 1000 to 3000 ([9a5e3471](https://github.com/pixelfed/pixelfed/commit/9a5e3471))
|
||||
- Update ApiV1Controller, add pe (pixelfed entity) support to /api/v1/statuses/{id}/context endpoint ([d645d6ca](https://github.com/pixelfed/pixelfed/commit/d645d6ca))
|
||||
- Update Admin Curated Onboarding, add select-all/mass action operations ([b22cac94](https://github.com/pixelfed/pixelfed/commit/b22cac94))
|
||||
- Update AdminCuratedRegisterController, fix existing account approval ([cbb96cfd](https://github.com/pixelfed/pixelfed/commit/cbb96cfd))
|
||||
- Update ActivityPubFetchService, fix Friendica bug ([e4edc6f1](https://github.com/pixelfed/pixelfed/commit/e4edc6f1))
|
||||
- Update ProfileController, fix atom feed cache ttl. Fixes #5093 ([921e2965](https://github.com/pixelfed/pixelfed/commit/921e2965))
|
||||
- Update CollectionsController, add new self route ([bc2495c6](https://github.com/pixelfed/pixelfed/commit/bc2495c6))
|
||||
- Update FederationController, add webfinger support for actor uri. Fixes #5068 ([24194f7d](https://github.com/pixelfed/pixelfed/commit/24194f7d))
|
||||
- Update FetchNodeinfoPipeline, set last_fetched_at timestamp ([a7fce91e](https://github.com/pixelfed/pixelfed/commit/a7fce91e))
|
||||
- Update task scheduler, add weekly instance scan to check nodeinfo for known instances ([dc6b9f46](https://github.com/pixelfed/pixelfed/commit/dc6b9f46))
|
||||
- Update AP fetch service and domain service ([42915ff9](https://github.com/pixelfed/pixelfed/commit/42915ff9))
|
||||
- Update ApiV1Controller, add settings to verify_credentials endpoint ([3f4e0b94](https://github.com/pixelfed/pixelfed/commit/3f4e0b94))
|
||||
- Update ApiV1Controller, fix update_credentials boolean handling ([19c62aaa](https://github.com/pixelfed/pixelfed/commit/19c62aaa))
|
||||
- Update ApiV1Controller, fix cache invalidation bug in update_credentials ([d56a4108](https://github.com/pixelfed/pixelfed/commit/d56a4108))
|
||||
- Update ApiV1Controller, fix self relationship response ([28bc7aa4](https://github.com/pixelfed/pixelfed/commit/28bc7aa4))
|
||||
- Update ApiController, add pe support to like/unlike endpoints ([679ef677](https://github.com/pixelfed/pixelfed/commit/679ef677))
|
||||
- Update ApiV1Dot1Controller, fix username to id endpoint ([4d6cea9a](https://github.com/pixelfed/pixelfed/commit/4d6cea9a))
|
||||
- Update StatusController, cache AP object ([a75b89b2](https://github.com/pixelfed/pixelfed/commit/a75b89b2))
|
||||
- Update status embed, add support for album carousels ([f4898db9](https://github.com/pixelfed/pixelfed/commit/f4898db9))
|
||||
- Update profile embeds, add support for albums ([4fd156c4](https://github.com/pixelfed/pixelfed/commit/4fd156c4))
|
||||
- Update DirectMessageController, add timestamps to threads ([b24d2554](https://github.com/pixelfed/pixelfed/commit/b24d2554))
|
||||
- Update DirectMessageController, add carousel entity to threads ([96f24f33](https://github.com/pixelfed/pixelfed/commit/96f24f33))
|
||||
- Update and refactor total local post count logic, cache value and schedule updates twice daily to eliminate the perf issue on larger instances ([4f2b8ed2](https://github.com/pixelfed/pixelfed/commit/4f2b8ed2))
|
||||
- Update Media model, fix broken thumbnail/gray thumbnail bug ([e33643c2](https://github.com/pixelfed/pixelfed/commit/e33643c2))
|
||||
- Update StatusController, fix unlisted post guest/ap access bug ([83098428](https://github.com/pixelfed/pixelfed/commit/83098428))
|
||||
- Update discover, add network trending using Beagle API ([2cae8b48](https://github.com/pixelfed/pixelfed/commit/2cae8b48))
|
||||
|
||||
## [v0.12.1 (2024-05-07)](https://github.com/pixelfed/pixelfed/compare/v0.12.0...v0.12.1)
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Dot1Controller, fix in app registration bug that prevents proper auth flow due to missing oauth scopes ([cbf996c9](https://github.com/pixelfed/pixelfed/commit/cbf996c9))
|
||||
- Update ConfigCacheService, fix database race condition and fallback to file config and enable by default ([60a62b59](https://github.com/pixelfed/pixelfed/commit/60a62b59))
|
||||
|
||||
## [v0.12.0 (2024-04-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.13...v0.12.0)
|
||||
|
||||
### Updates
|
||||
|
||||
- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6))
|
||||
- Update Post.vue, fix cache bug ([3a27e637](https://github.com/pixelfed/pixelfed/commit/3a27e637))
|
||||
- Update StatusHashtagService, use more efficient cached count ([592c8412](https://github.com/pixelfed/pixelfed/commit/592c8412))
|
||||
- Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a))
|
||||
- Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c))
|
||||
- Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8))
|
||||
- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7))
|
||||
- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06))
|
||||
- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9))
|
||||
- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4))
|
||||
- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595))
|
||||
- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f))
|
||||
- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843))
|
||||
- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6))
|
||||
- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f))
|
||||
- Update web-api popular accounts route to its own method to remove the breaking oauth scope bug ([a4bc5ce3](https://github.com/pixelfed/pixelfed/commit/a4bc5ce3))
|
||||
- Update config cache ([5e4d4eff](https://github.com/pixelfed/pixelfed/commit/5e4d4eff))
|
||||
- Update Config, use config_cache ([7785a2da](https://github.com/pixelfed/pixelfed/commit/7785a2da))
|
||||
- Update ApiV1Dot1Controller, use config_cache for in-app registration ([b0cb4456](https://github.com/pixelfed/pixelfed/commit/b0cb4456))
|
||||
- Update captcha, use config_cache helper ([8a89e3c9](https://github.com/pixelfed/pixelfed/commit/8a89e3c9))
|
||||
- Update custom emoji, add config_cache support ([481314cd](https://github.com/pixelfed/pixelfed/commit/481314cd))
|
||||
- Update ProfileController, fix permalink redirect bug ([75081e60](https://github.com/pixelfed/pixelfed/commit/75081e60))
|
||||
- Update admin css, use font-display:swap for nucleo icons ([8a0c456e](https://github.com/pixelfed/pixelfed/commit/8a0c456e))
|
||||
- Update PixelfedDirectoryController, fix boolean cast bug ([f08aab22](https://github.com/pixelfed/pixelfed/commit/f08aab22))
|
||||
- Update PixelfedDirectoryController, use cached stats ([f2f2a809](https://github.com/pixelfed/pixelfed/commit/f2f2a809))
|
||||
- Update AdminDirectoryController, fix type casting ([ad506e90](https://github.com/pixelfed/pixelfed/commit/ad506e90))
|
||||
- Update image pipeline, use config_cache ([a72188a7](https://github.com/pixelfed/pixelfed/commit/a72188a7))
|
||||
- Update cloud storage, use config_cache ([665581d8](https://github.com/pixelfed/pixelfed/commit/665581d8))
|
||||
- Update pixelfed.max_album_length, use config_cache ([fecbe189](https://github.com/pixelfed/pixelfed/commit/fecbe189))
|
||||
- Update media_types, use config_cache ([d670de17](https://github.com/pixelfed/pixelfed/commit/d670de17))
|
||||
- Update landing settings, use config_cache ([40478f25](https://github.com/pixelfed/pixelfed/commit/40478f25))
|
||||
- Update activitypub setting, use config_cache ([5071aaf4](https://github.com/pixelfed/pixelfed/commit/5071aaf4))
|
||||
- Update oauth setting, use config_cache ([ce228f7f](https://github.com/pixelfed/pixelfed/commit/ce228f7f))
|
||||
- Update stories config, use config_cache ([d1adb109](https://github.com/pixelfed/pixelfed/commit/d1adb109))
|
||||
- Update ig import, use config_cache ([da0e0ffa](https://github.com/pixelfed/pixelfed/commit/da0e0ffa))
|
||||
- Update autospam config, use config_cache ([a76cb5f4](https://github.com/pixelfed/pixelfed/commit/a76cb5f4))
|
||||
- Update app.name config, use config_cache ([911446c0](https://github.com/pixelfed/pixelfed/commit/911446c0))
|
||||
- Update UserObserver, fix type casting ([949e9979](https://github.com/pixelfed/pixelfed/commit/949e9979))
|
||||
- Update user_filters, use config_cache ([6ce513f8](https://github.com/pixelfed/pixelfed/commit/6ce513f8))
|
||||
- Update filesystems config, add to config_cache ([087b2791](https://github.com/pixelfed/pixelfed/commit/087b2791))
|
||||
- Update web-admin routes, add setting api routes ([828a456f](https://github.com/pixelfed/pixelfed/commit/828a456f))
|
||||
- Update hashtag component ([cee979ed](https://github.com/pixelfed/pixelfed/commit/cee979ed))
|
||||
- Update AdminReadMore component, add .prevent to click action ([704e7b12](https://github.com/pixelfed/pixelfed/commit/704e7b12))
|
||||
- Update admin dashboard, add admin settings partials ([eb487123](https://github.com/pixelfed/pixelfed/commit/eb487123))
|
||||
- Update admin settings, refactor to vue component ([674e560f](https://github.com/pixelfed/pixelfed/commit/674e560f))
|
||||
- Update ConfigCacheService, encrypt keys at rest ([3628b462](https://github.com/pixelfed/pixelfed/commit/3628b462))
|
||||
- Update RemoteFollowImportRecent, use MediaPathService ([5162c070](https://github.com/pixelfed/pixelfed/commit/5162c070))
|
||||
- Update AdminSettingsController, add user filter max limit settings ([ac1f0748](https://github.com/pixelfed/pixelfed/commit/ac1f0748))
|
||||
- Update AdminSettingsController, add AdminSettingsService ([dcc5f416](https://github.com/pixelfed/pixelfed/commit/dcc5f416))
|
||||
- Update AdminSettings component, fix user settings ([aba1e13d](https://github.com/pixelfed/pixelfed/commit/aba1e13d))
|
||||
- Update AdminInstances component ([ec2fdd61](https://github.com/pixelfed/pixelfed/commit/ec2fdd61))
|
||||
- Update AdminSettings, add max_account_size support ([2dcbc1d5](https://github.com/pixelfed/pixelfed/commit/2dcbc1d5))
|
||||
- Update AdminSettings, use better validation for user integer settings ([d946afcc](https://github.com/pixelfed/pixelfed/commit/d946afcc))
|
||||
- Update spa sass, fix timestamp dark mode bug ([4147f7c5](https://github.com/pixelfed/pixelfed/commit/4147f7c5))
|
||||
- Update relationships view, fix unfollow hashtag bug. Fixes #5008 ([8c693640](https://github.com/pixelfed/pixelfed/commit/8c693640))
|
||||
- Update PrivacySettings controller, refresh RelationshipService when unmute/unblocking ([b7322b68](https://github.com/pixelfed/pixelfed/commit/b7322b68))
|
||||
- Update ApiV1Controller, improve refresh relations logic when (un)muting or (un)blocking ([b8e96a5f](https://github.com/pixelfed/pixelfed/commit/b8e96a5f))
|
||||
- Update context menu, add mute/block/unfollow actions and update relationship store accordingly ([81d1e0fd](https://github.com/pixelfed/pixelfed/commit/81d1e0fd))
|
||||
- Update docker env, fix config_cache. Fixes #5033 ([858fcbf6](https://github.com/pixelfed/pixelfed/commit/858fcbf6))
|
||||
- Update UnfollowPipeline, fix follower count cache bug ([6bdf73de](https://github.com/pixelfed/pixelfed/commit/6bdf73de))
|
||||
- Update VideoPresenter component, add webkit-playsinline attribute to video element to prevent the full screen video player ([ad032916](https://github.com/pixelfed/pixelfed/commit/ad032916))
|
||||
- Update VideoPlayer component, add playsinline attribute to video element ([8af23607](https://github.com/pixelfed/pixelfed/commit/8af23607))
|
||||
- Update StatusController, refactor status embeds ([9a7acc12](https://github.com/pixelfed/pixelfed/commit/9a7acc12))
|
||||
- Update ProfileController, refactor profile embeds ([8b8b1ffc](https://github.com/pixelfed/pixelfed/commit/8b8b1ffc))
|
||||
- Update profile embed view, fix height bug ([65166570](https://github.com/pixelfed/pixelfed/commit/65166570))
|
||||
- Update CustomEmojiService, only return local emoji ([7f8bba44](https://github.com/pixelfed/pixelfed/commit/7f8bba44))
|
||||
- Update Like model, increase max likes per day from 500 to 1500 ([4223119f](https://github.com/pixelfed/pixelfed/commit/4223119f))
|
||||
|
||||
## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)
|
||||
|
||||
### Features
|
||||
|
||||
- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212))
|
||||
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
|
||||
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
|
||||
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
|
||||
- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
|
||||
- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
|
||||
- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
|
||||
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
|
||||
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
|
||||
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
|
||||
- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641))
|
||||
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
|
||||
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
|
||||
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
|
||||
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
|
||||
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
|
||||
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
|
||||
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
|
||||
- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607))
|
||||
- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106))
|
||||
- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997))
|
||||
- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b))
|
||||
- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655))
|
||||
- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58))
|
||||
- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3))
|
||||
- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7))
|
||||
- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599))
|
||||
- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a))
|
||||
|
||||
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
|
||||
|
||||
### Features
|
||||
- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
|
||||
- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
|
||||
- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))
|
||||
- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3))
|
||||
- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8))
|
||||
- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194))
|
||||
|
||||
## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11)
|
||||
|
||||
### Fixes
|
||||
- Fix api endpoints ([fd7f5dbb](https://github.com/pixelfed/pixelfed/commit/fd7f5dbb))
|
||||
|
||||
## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10)
|
||||
|
||||
### Added
|
||||
- Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6))
|
||||
- Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12))
|
||||
- Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7))
|
||||
- Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde))
|
||||
- Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610))
|
||||
- Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb))
|
||||
- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6))
|
||||
- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7))
|
||||
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
|
||||
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
|
||||
- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
|
||||
- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1))
|
||||
- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9))
|
||||
|
||||
### Federation
|
||||
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
|
||||
- Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
### Updates
|
||||
- Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59))
|
||||
|
@ -40,7 +284,62 @@
|
|||
- Update ApiV1Dot1Controller, allow iar rate limits to be configurable ([28a80803](https://github.com/pixelfed/pixelfed/commit/28a80803))
|
||||
- Update ApiV1Dot1Controller, add domain to iar redirect ([1f82d47c](https://github.com/pixelfed/pixelfed/commit/1f82d47c))
|
||||
- Update ApiV1Dot1Controller, add configurable app confirm rate limit ttl ([4c6a0719](https://github.com/pixelfed/pixelfed/commit/4c6a0719))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
- Update LikePipeline, dispatch to feed queue. Fixes ([#4723](https://github.com/pixelfed/pixelfed/issues/4723)) ([da510089](https://github.com/pixelfed/pixelfed/commit/da510089))
|
||||
- Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e))
|
||||
- Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a))
|
||||
- Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714))
|
||||
- Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79))
|
||||
- Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c))
|
||||
- Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271))
|
||||
- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80))
|
||||
- Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23))
|
||||
- Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885))
|
||||
- Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae))
|
||||
- Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008))
|
||||
- Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85))
|
||||
- Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8))
|
||||
- Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d))
|
||||
- Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b))
|
||||
- Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b))
|
||||
- Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a))
|
||||
- Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c))
|
||||
- Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
|
||||
- Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
|
||||
- Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce))
|
||||
- Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox ([d848792a](https://github.com/pixelfed/pixelfed/commit/d848792a))
|
||||
- Update DirectMessageController, dispatch deliver and delete actions to the job queue ([7f462a80](https://github.com/pixelfed/pixelfed/commit/7f462a80))
|
||||
- Update Inbox, improve story attribute collection ([06bee36c](https://github.com/pixelfed/pixelfed/commit/06bee36c))
|
||||
- Update DirectMessageController, dispatch local deletes to pipeline ([98186564](https://github.com/pixelfed/pixelfed/commit/98186564))
|
||||
- Update StatusPipeline, fix Direct and Story notification deletion ([4c95306f](https://github.com/pixelfed/pixelfed/commit/4c95306f))
|
||||
- Update Notifications.vue, fix deprecated DM action links for story activities ([4c3823b0](https://github.com/pixelfed/pixelfed/commit/4c3823b0))
|
||||
- Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119))
|
||||
- Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888))
|
||||
- Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2))
|
||||
- Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135))
|
||||
- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393))
|
||||
- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7))
|
||||
- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545))
|
||||
- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a))
|
||||
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
|
||||
- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1))
|
||||
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
|
||||
- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168))
|
||||
- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171))
|
||||
- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a))
|
||||
- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b))
|
||||
- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04))
|
||||
- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381))
|
||||
- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577))
|
||||
- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c))
|
||||
- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389))
|
||||
- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3))
|
||||
- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe))
|
||||
- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47))
|
||||
- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a))
|
||||
- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade))
|
||||
- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
|
||||
- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
|
||||
- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1))
|
||||
|
||||
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
|
||||
|
||||
|
|
18
CODEOWNERS
Normal file
18
CODEOWNERS
Normal file
|
@ -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
|
364
Dockerfile
Normal file
364
Dockerfile
Normal file
|
@ -0,0 +1,364 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# See https://hub.docker.com/r/docker/dockerfile
|
||||
|
||||
#######################################################
|
||||
# Configuration
|
||||
#######################################################
|
||||
|
||||
# See: https://github.com/mlocati/docker-php-extension-installer
|
||||
ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80"
|
||||
|
||||
# See: https://github.com/composer/composer
|
||||
ARG COMPOSER_VERSION="2.6"
|
||||
|
||||
# See: https://nginx.org/
|
||||
ARG NGINX_VERSION="1.25.3"
|
||||
|
||||
# See: https://github.com/ddollar/forego
|
||||
ARG FOREGO_VERSION="0.17.2"
|
||||
|
||||
# See: https://github.com/hairyhenderson/gomplate
|
||||
ARG GOMPLATE_VERSION="v3.11.6"
|
||||
|
||||
# See: https://github.com/jippi/dottie
|
||||
ARG DOTTIE_VERSION="v0.9.5"
|
||||
|
||||
###
|
||||
# PHP base configuration
|
||||
###
|
||||
|
||||
# See: https://hub.docker.com/_/php/tags
|
||||
ARG PHP_VERSION="8.1"
|
||||
|
||||
# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants
|
||||
ARG PHP_BASE_TYPE="apache"
|
||||
ARG PHP_DEBIAN_RELEASE="bullseye"
|
||||
|
||||
ARG RUNTIME_UID=33 # often called 'www-data'
|
||||
ARG RUNTIME_GID=33 # often called 'www-data'
|
||||
|
||||
# APT extra packages
|
||||
ARG APT_PACKAGES_EXTRA=
|
||||
|
||||
# Extensions installed via [pecl install]
|
||||
# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't
|
||||
# ! been released yet (after +10 months)!
|
||||
# ! See: https://github.com/Imagick/imagick/pull/641
|
||||
ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58"
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA=
|
||||
|
||||
# Extensions installed via [docker-php-ext-install]
|
||||
ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd"
|
||||
ARG PHP_EXTENSIONS_EXTRA=""
|
||||
ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite"
|
||||
|
||||
# GPG key for nginx apt repository
|
||||
ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62"
|
||||
|
||||
# GPP key path for nginx apt repository
|
||||
ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg"
|
||||
|
||||
#######################################################
|
||||
# Docker "copy from" images
|
||||
#######################################################
|
||||
|
||||
# Composer docker image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM composer:${COMPOSER_VERSION} AS composer-image
|
||||
|
||||
# php-extension-installer image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer
|
||||
|
||||
# nginx webserver from Docker Hub.
|
||||
# Used to copy some docker-entrypoint files for [nginx-runtime]
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM nginx:${NGINX_VERSION} AS nginx-image
|
||||
|
||||
# Forego is a Procfile "runner" that makes it trival to run multiple
|
||||
# processes under a simple init / PID 1 process.
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/nginx-proxy/forego
|
||||
FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image
|
||||
|
||||
# Dottie makes working with .env files easier and safer
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/jippi/dottie
|
||||
FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image
|
||||
|
||||
# gomplate-image grabs the gomplate binary from GitHub releases
|
||||
#
|
||||
# It's in its own layer so it can be fetched in parallel with other build steps
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
ARG GOMPLATE_VERSION
|
||||
|
||||
RUN set -ex \
|
||||
&& curl \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
--output /usr/local/bin/gomplate \
|
||||
https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \
|
||||
&& chmod +x /usr/local/bin/gomplate \
|
||||
&& /usr/local/bin/gomplate --version
|
||||
|
||||
#######################################################
|
||||
# Base image
|
||||
#######################################################
|
||||
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base
|
||||
|
||||
ARG BUILDKIT_SBOM_SCAN_STAGE="true"
|
||||
|
||||
ARG APT_PACKAGES_EXTRA
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_VERSION
|
||||
ARG RUNTIME_GID
|
||||
ARG RUNTIME_UID
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
|
||||
# Ensure we run all scripts through 'bash' rather than 'sh'
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Set www-data to be RUNTIME_UID/RUNTIME_GID
|
||||
RUN groupmod --gid ${RUNTIME_GID} www-data \
|
||||
&& usermod --uid ${RUNTIME_UID} --gid ${RUNTIME_GID} www-data
|
||||
|
||||
RUN set -ex \
|
||||
&& mkdir -pv /var/www/ \
|
||||
&& chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www
|
||||
|
||||
WORKDIR /var/www/
|
||||
|
||||
ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA}
|
||||
|
||||
# Install and configure base layer
|
||||
COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
/docker/install/base.sh
|
||||
|
||||
#######################################################
|
||||
# PHP: extensions
|
||||
#######################################################
|
||||
|
||||
FROM base AS php-extensions
|
||||
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_EXTENSIONS
|
||||
ARG PHP_EXTENSIONS_DATABASE
|
||||
ARG PHP_EXTENSIONS_EXTRA
|
||||
ARG PHP_PECL_EXTENSIONS
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||
|
||||
COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \
|
||||
--mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
PHP_EXTENSIONS=${PHP_EXTENSIONS} \
|
||||
PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \
|
||||
PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \
|
||||
PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \
|
||||
PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \
|
||||
/docker/install/php-extensions.sh
|
||||
|
||||
#######################################################
|
||||
# Node: Build frontend
|
||||
#######################################################
|
||||
|
||||
# NOTE: Since the nodejs build is CPU architecture agnostic,
|
||||
# we only want to build once and cache it for other architectures.
|
||||
# We force the (CPU) [--platform] here to be architecture
|
||||
# of the "builder"/"server" and not the *target* CPU architecture
|
||||
# (e.g.) building the ARM version of Pixelfed on AMD64.
|
||||
FROM --platform=${BUILDARCH} node:lts AS frontend-build
|
||||
|
||||
ARG BUILDARCH
|
||||
ARG BUILD_FRONTEND=0
|
||||
ARG RUNTIME_UID
|
||||
ARG RUNTIME_GID
|
||||
|
||||
ARG NODE_ENV=production
|
||||
ENV NODE_ENV=$NODE_ENV
|
||||
|
||||
WORKDIR /var/www/
|
||||
|
||||
SHELL [ "/usr/bin/bash", "-c" ]
|
||||
|
||||
# Install NPM dependencies
|
||||
RUN --mount=type=cache,id=pixelfed-node-${BUILDARCH},sharing=locked,target=/tmp/cache \
|
||||
--mount=type=bind,source=package.json,target=/var/www/package.json \
|
||||
--mount=type=bind,source=package-lock.json,target=/var/www/package-lock.json \
|
||||
<<EOF
|
||||
if [[ $BUILD_FRONTEND -eq 1 ]];
|
||||
then
|
||||
npm install --cache /tmp/cache --no-save --dev
|
||||
else
|
||||
echo "Skipping [npm install] as --build-arg [BUILD_FRONTEND] is not set to '1'"
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Copy the frontend source into the image before building
|
||||
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www
|
||||
|
||||
# Build the frontend with "mix" (See package.json)
|
||||
RUN \
|
||||
<<EOF
|
||||
if [[ $BUILD_FRONTEND -eq 1 ]];
|
||||
then
|
||||
npm run production
|
||||
else
|
||||
echo "Skipping [npm run production] as --build-arg [BUILD_FRONTEND] is not set to '1'"
|
||||
fi
|
||||
EOF
|
||||
|
||||
#######################################################
|
||||
# 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,uid=${RUNTIME_UID},gid=${RUNTIME_GID},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 --no-scripts
|
||||
|
||||
# Copy all other files over
|
||||
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/
|
||||
|
||||
# Generate optimized autoloader now that we have all files around
|
||||
RUN set -ex \
|
||||
&& ENABLE_CONFIG_CACHE=false composer dump-autoload --optimize
|
||||
|
||||
# Now we can run the post-install scripts
|
||||
RUN set -ex \
|
||||
&& composer run-script post-update-cmd
|
||||
|
||||
#######################################################
|
||||
# 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
|
||||
COPY --link --from=frontend-build --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www/public /var/www/public
|
||||
|
||||
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"]
|
|
@ -43,3 +43,10 @@ We would like to extend our thanks to the following sponsors for funding Pixelfe
|
|||
- [NLnet Foundation](https://nlnet.nl) and [NGI0
|
||||
Discovery](https://nlnet.nl/discovery/), part of the [Next Generation
|
||||
Internet](https://ngi.eu) initiative.
|
||||
|
||||
<p>This project is supported by:</p>
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=pixelfed">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
|
||||
</a>
|
||||
</p>
|
||||
|
|
|
@ -19,7 +19,6 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke
|
|||
{
|
||||
return [
|
||||
'created_at' => time(),
|
||||
'scope' => 'read write follow push'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
57
app/Console/Commands/AccountPostCountStatUpdate.php
Normal file
57
app/Console/Commands/AccountPostCountStatUpdate.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\Account\AccountStatService;
|
||||
use App\Status;
|
||||
use App\Profile;
|
||||
|
||||
class AccountPostCountStatUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:account-post-count-stat-update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update post counts from recent activities';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$ids = AccountStatService::getAllPostCountIncr();
|
||||
if(!$ids || !count($ids)) {
|
||||
return;
|
||||
}
|
||||
foreach($ids as $id) {
|
||||
$acct = AccountService::get($id, true);
|
||||
if(!$acct) {
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
continue;
|
||||
}
|
||||
$statusCount = Status::whereProfileId($id)->count();
|
||||
if($statusCount != $acct['statuses_count']) {
|
||||
$profile = Profile::find($id);
|
||||
if(!$profile) {
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
continue;
|
||||
}
|
||||
$profile->status_count = $statusCount;
|
||||
$profile->save();
|
||||
AccountService::del($id);
|
||||
}
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
106
app/Console/Commands/AddUserDomainBlock.php
Normal file
106
app/Console/Commands/AddUserDomainBlock.php
Normal file
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class AddUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:add-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Apply a domain block to all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to block');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processBlocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to block ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processBlocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::updateOrCreate([
|
||||
'domain' => $domain
|
||||
]);
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: User::lazyById(500),
|
||||
callback: fn ($user) => $this->performTask($user, $domain),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($user, $domain)
|
||||
{
|
||||
if(!$user->profile_id || $user->delete_after) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($user->status != null && $user->status != 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'domain' => $domain
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -82,7 +82,7 @@ class AvatarStorage extends Command
|
|||
|
||||
$this->line(' ');
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage')) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$this->info('✅ - Cloud storage configured');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ class AvatarStorage extends Command
|
|||
$this->line(' ');
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
|
||||
$disk = Storage::disk(config_cache('filesystems.cloud'));
|
||||
$exists = $disk->exists('cache/avatars/default.jpg');
|
||||
$state = $exists ? '✅' : '❌';
|
||||
|
@ -100,7 +100,7 @@ class AvatarStorage extends Command
|
|||
$this->info($msg);
|
||||
}
|
||||
|
||||
$options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
|
||||
$options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
|
||||
[
|
||||
'Cancel',
|
||||
'Upload default avatar to cloud',
|
||||
|
@ -164,7 +164,7 @@ class AvatarStorage extends Command
|
|||
|
||||
protected function uploadAvatarsToCloud()
|
||||
{
|
||||
if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
|
||||
if(!(bool) config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
|
||||
$this->error('Enable cloud storage and avatar cloud storage to perform this action');
|
||||
return;
|
||||
}
|
||||
|
@ -213,7 +213,7 @@ class AvatarStorage extends Command
|
|||
return;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
|
||||
$this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ class AvatarStorageDeepClean extends Command
|
|||
$this->line(' ');
|
||||
|
||||
$storage = [
|
||||
'cloud' => boolval(config_cache('pixelfed.cloud_storage')),
|
||||
'cloud' => (bool) config_cache('pixelfed.cloud_storage'),
|
||||
'local' => boolval(config_cache('federation.avatars.store_local'))
|
||||
];
|
||||
|
||||
|
|
52
app/Console/Commands/CaptchaToggleCommand.php
Normal file
52
app/Console/Commands/CaptchaToggleCommand.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use function Laravel\Prompts\info;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use App\Services\ConfigCacheService;
|
||||
|
||||
class CaptchaToggleCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:captcha-toggle-command';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$captchaEnabled = (bool) config_cache('captcha.enabled');
|
||||
|
||||
info($captchaEnabled ? 'Captcha is enabled' : 'Captcha is not enabled');
|
||||
|
||||
if(!$captchaEnabled) {
|
||||
info('Enable the Captcha from the admin settings dashboard.');
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm(
|
||||
label: 'Do you want to disable the captcha?',
|
||||
default: false,
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
hint: 'Select an option to proceed.'
|
||||
);
|
||||
|
||||
if($confirmed) {
|
||||
ConfigCacheService::put('captcha.enabled', false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,12 +35,16 @@ class CloudMediaMigrate extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = config('pixelfed.cloud_storage');
|
||||
$enabled = (bool) config_cache('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->error('Cloud storage not enabled. Exiting...');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$this->confirm('Are you sure you want to proceed?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
$hugeMode = $this->option('huge');
|
||||
|
||||
|
|
51
app/Console/Commands/DeleteRemoteProfile.php
Normal file
51
app/Console/Commands/DeleteRemoteProfile.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use App\Profile;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\search;
|
||||
|
||||
class DeleteRemoteProfile extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:delete-remote-profile';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete remote profile';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = search(
|
||||
'Search for the account',
|
||||
fn (string $value) => strlen($value) > 2
|
||||
? Profile::whereNotNull('domain')->where('username', 'like', $value.'%')->pluck('username', 'id')->all()
|
||||
: []
|
||||
);
|
||||
$profile = Profile::whereNotNull('domain')->find($id);
|
||||
|
||||
if (! $profile) {
|
||||
$this->error('Could not find profile.');
|
||||
exit;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to delete '.$profile->username.'\'s account? This action cannot be reversed.');
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('adelete');
|
||||
$this->info('Dispatched delete job, it may take a few minutes...');
|
||||
exit;
|
||||
}
|
||||
}
|
96
app/Console/Commands/DeleteUserDomainBlock.php
Normal file
96
app/Console/Commands/DeleteUserDomainBlock.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class DeleteUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:delete-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Remove a domain block for all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to unblock');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processUnblocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unblock ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processUnblocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::whereDomain($domain)->delete();
|
||||
if(!UserDomainBlock::whereDomain($domain)->count()) {
|
||||
$this->info('No results found!');
|
||||
return;
|
||||
}
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: UserDomainBlock::whereDomain($domain)->lazyById(500),
|
||||
callback: fn ($domainBlock) => $this->performTask($domainBlock),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($domainBlock)
|
||||
{
|
||||
$domainBlock->deleteQuietly();
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\StatusService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class FetchMissingMediaMimeType extends Command
|
||||
{
|
||||
|
@ -36,7 +36,7 @@ class FetchMissingMediaMimeType extends Command
|
|||
continue;
|
||||
}
|
||||
|
||||
if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) {
|
||||
if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ class FixMediaDriver extends Command
|
|||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == false) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage') == false) {
|
||||
$this->error('Cloud storage not enabled, exiting...');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
|
57
app/Console/Commands/HashtagCachedCountUpdate.php
Normal file
57
app/Console/Commands/HashtagCachedCountUpdate.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use DB;
|
||||
|
||||
class HashtagCachedCountUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:hashtag-cached-count-update {--limit=100}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update cached counter of hashtags';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = $this->option('limit');
|
||||
$tags = Hashtag::whereNull('cached_count')->limit($limit)->get();
|
||||
$count = count($tags);
|
||||
if(!$count) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$count = DB::table('status_hashtags')->whereHashtagId($tag->id)->count();
|
||||
if(!$count) {
|
||||
$tag->cached_count = 0;
|
||||
$tag->saveQuietly();
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
$tag->cached_count = $count;
|
||||
$tag->saveQuietly();
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
return;
|
||||
}
|
||||
}
|
94
app/Console/Commands/HashtagRelatedGenerate.php
Normal file
94
app/Console/Commands/HashtagRelatedGenerate.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use App\Models\HashtagRelated;
|
||||
use App\Services\HashtagRelatedService;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use function Laravel\Prompts\multiselect;
|
||||
use function Laravel\Prompts\confirm;
|
||||
|
||||
class HashtagRelatedGenerate extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:hashtag-related-generate {tag}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'tag' => 'Which hashtag should we generate related tags for?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$tag = $this->argument('tag');
|
||||
$hashtag = Hashtag::whereName($tag)->orWhere('slug', $tag)->first();
|
||||
if(!$hashtag) {
|
||||
$this->error('Hashtag not found, aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$exists = HashtagRelated::whereHashtagId($hashtag->id)->exists();
|
||||
|
||||
if($exists) {
|
||||
$confirmed = confirm('Found existing related tags, do you want to regenerate them?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Looking up #' . $tag . '...');
|
||||
|
||||
$tags = StatusHashtag::whereHashtagId($hashtag->id)->count();
|
||||
if(!$tags || $tags < 100) {
|
||||
$this->error('Not enough posts found to generate related hashtags!');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->info('Found ' . $tags . ' posts that use that hashtag');
|
||||
$related = collect(HashtagRelatedService::fetchRelatedTags($tag));
|
||||
|
||||
$selected = multiselect(
|
||||
label: 'Which tags do you want to generate?',
|
||||
options: $related->pluck('name'),
|
||||
required: true,
|
||||
);
|
||||
|
||||
$filtered = $related->filter(fn($i) => in_array($i['name'], $selected))->all();
|
||||
$agg_score = $related->filter(fn($i) => in_array($i['name'], $selected))->sum('related_count');
|
||||
|
||||
HashtagRelated::updateOrCreate([
|
||||
'hashtag_id' => $hashtag->id,
|
||||
], [
|
||||
'related_tags' => array_values($filtered),
|
||||
'agg_score' => $agg_score,
|
||||
'last_calculated_at' => now()
|
||||
]);
|
||||
|
||||
$this->info('Finished!');
|
||||
}
|
||||
}
|
118
app/Console/Commands/ImportEmojis.php
Normal file
118
app/Console/Commands/ImportEmojis.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\CustomEmoji;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImportEmojis extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'import:emojis
|
||||
{path : Path to a tar.gz archive with the emojis}
|
||||
{--prefix : Define a prefix for the emjoi shortcode}
|
||||
{--suffix : Define a suffix for the emjoi shortcode}
|
||||
{--overwrite : Overwrite existing emojis}
|
||||
{--disabled : Import all emojis as disabled}';
|
||||
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import emojis to the database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$path = $this->argument('path');
|
||||
|
||||
if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') {
|
||||
$this->error('Path does not exist or is not a tarfile');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
$tar = new \PharData($path);
|
||||
$tar->decompress();
|
||||
|
||||
foreach (new \RecursiveIteratorIterator($tar) as $entry) {
|
||||
$this->line("Processing {$entry->getFilename()}");
|
||||
if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) {
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME);
|
||||
$extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION);
|
||||
|
||||
// Skip macOS shadow files
|
||||
if (str_starts_with($filename, '._')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$shortcode = implode('', [
|
||||
$this->option('prefix'),
|
||||
$filename,
|
||||
$this->option('suffix'),
|
||||
]);
|
||||
|
||||
$customEmoji = CustomEmoji::whereShortcode($shortcode)->first();
|
||||
|
||||
if ($customEmoji && !$this->option('overwrite')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$emoji = $customEmoji ?? new CustomEmoji();
|
||||
$emoji->shortcode = $shortcode;
|
||||
$emoji->domain = config('pixelfed.domain.app');
|
||||
$emoji->disabled = $this->option('disabled');
|
||||
$emoji->save();
|
||||
|
||||
$fileName = $emoji->id . '.' . $extension;
|
||||
Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName);
|
||||
$emoji->media_path = 'emoji/' . $fileName;
|
||||
$emoji->save();
|
||||
$imported++;
|
||||
Cache::forget('pf:custom_emoji');
|
||||
}
|
||||
|
||||
$this->line("Imported: {$imported}");
|
||||
$this->line("Skipped: {$skipped}");
|
||||
$this->line("Failed: {$failed}");
|
||||
|
||||
//delete file
|
||||
unlink(str_replace('.tar.gz', '.tar', $path));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function isImage($file)
|
||||
{
|
||||
$image = getimagesize($file->getPathname());
|
||||
return $image !== false;
|
||||
}
|
||||
|
||||
private function isEmoji($filename)
|
||||
{
|
||||
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
|
||||
$mimeType = mime_content_type($filename);
|
||||
|
||||
return in_array($mimeType, $allowedMimeTypes);
|
||||
}
|
||||
}
|
54
app/Console/Commands/ImportUploadMediaToCloudStorage.php
Normal file
54
app/Console/Commands/ImportUploadMediaToCloudStorage.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use App\Jobs\ImportPipeline\ImportMediaToCloudPipeline;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class ImportUploadMediaToCloudStorage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-upload-media-to-cloud-storage {--limit=500}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Migrate media imported from Instagram to S3 cloud storage.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(
|
||||
(bool) config('import.instagram.storage.cloud.enabled') === false ||
|
||||
(bool) config_cache('pixelfed.cloud_storage') === false
|
||||
) {
|
||||
$this->error('Aborted. Cloud storage is not enabled for IG imports.');
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
|
||||
$progress = progress(label: 'Migrating import media', steps: $limit);
|
||||
|
||||
$progress->start();
|
||||
|
||||
$posts = ImportPost::whereUploadedToS3(false)->take($limit)->get();
|
||||
|
||||
foreach($posts as $post) {
|
||||
ImportMediaToCloudPipeline::dispatch($post)->onQueue('low');
|
||||
$progress->advance();
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
}
|
||||
}
|
298
app/Console/Commands/InstanceManager.php
Normal file
298
app/Console/Commands/InstanceManager.php
Normal file
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Services\InstanceService;
|
||||
use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
|
||||
use function Laravel\Prompts\select;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
use function Laravel\Prompts\search;
|
||||
use function Laravel\Prompts\table;
|
||||
|
||||
class InstanceManager extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:instance-manager';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Manage Instances';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$action = select(
|
||||
'What action do you want to perform?',
|
||||
[
|
||||
'Recalculate Stats',
|
||||
'Ban Instance',
|
||||
'Unlist Instance',
|
||||
'Unlisted Instances',
|
||||
'Banned Instances',
|
||||
'Unban Instance',
|
||||
'Relist Instance',
|
||||
],
|
||||
);
|
||||
|
||||
switch($action) {
|
||||
case 'Recalculate Stats':
|
||||
return $this->recalculateStats();
|
||||
break;
|
||||
|
||||
case 'Unlisted Instances':
|
||||
return $this->viewUnlistedInstances();
|
||||
break;
|
||||
|
||||
case 'Banned Instances':
|
||||
return $this->viewBannedInstances();
|
||||
break;
|
||||
|
||||
case 'Unlist Instance':
|
||||
return $this->unlistInstance();
|
||||
break;
|
||||
|
||||
case 'Ban Instance':
|
||||
return $this->banInstance();
|
||||
break;
|
||||
|
||||
case 'Unban Instance':
|
||||
return $this->unbanInstance();
|
||||
break;
|
||||
|
||||
case 'Relist Instance':
|
||||
return $this->relistInstance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function recalculateStats()
|
||||
{
|
||||
$instanceCount = Instance::count();
|
||||
$confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$users = progress(
|
||||
label: 'Updating instance stats...',
|
||||
steps: Instance::all(),
|
||||
callback: fn ($instance) => $this->updateInstanceStats($instance),
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateInstanceStats($instance)
|
||||
{
|
||||
FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
|
||||
}
|
||||
|
||||
protected function unlistInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unlist this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance unlisting');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->unlisted = true;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully unlisted ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function relistInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to re-list this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance re-listing');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->unlisted = false;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully re-listed ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function banInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to ban this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance ban');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->banned = true;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully banned ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function unbanInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unban this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance unban');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->banned = false;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully un-banned ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function viewBannedInstances()
|
||||
{
|
||||
$data = Instance::whereBanned(true)
|
||||
->get(['domain', 'user_count', 'status_count'])
|
||||
->map(function($d) {
|
||||
return [
|
||||
'domain' => $d->domain,
|
||||
'user_count' => number_format($d->user_count),
|
||||
'status_count' => number_format($d->status_count),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
table(
|
||||
['Domain', 'User Count', 'Status Count'],
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
protected function viewUnlistedInstances()
|
||||
{
|
||||
$data = Instance::whereUnlisted(true)
|
||||
->get(['domain', 'user_count', 'status_count', 'banned'])
|
||||
->map(function($d) {
|
||||
return [
|
||||
'domain' => $d->domain,
|
||||
'user_count' => number_format($d->user_count),
|
||||
'status_count' => number_format($d->status_count),
|
||||
'banned' => $d->banned ? '✅' : null
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
table(
|
||||
['Domain', 'User Count', 'Status Count', 'Banned'],
|
||||
$data
|
||||
);
|
||||
}
|
||||
}
|
79
app/Console/Commands/InstanceUpdateTotalLocalPosts.php
Normal file
79
app/Console/Commands/InstanceUpdateTotalLocalPosts.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ConfigCacheService;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Console\Command;
|
||||
use Storage;
|
||||
|
||||
class InstanceUpdateTotalLocalPosts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:instance-update-total-local-posts';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update the total number of local statuses/post count';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$cached = $this->checkForCache();
|
||||
if (! $cached) {
|
||||
$this->initCache();
|
||||
|
||||
return;
|
||||
}
|
||||
$cache = $this->getCached();
|
||||
if (! $cache || ! isset($cache['count'])) {
|
||||
$this->error('Problem fetching cache');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->updateAndCache();
|
||||
Cache::forget('api:nodeinfo');
|
||||
|
||||
}
|
||||
|
||||
protected function checkForCache()
|
||||
{
|
||||
return Storage::exists('total_local_posts.json');
|
||||
}
|
||||
|
||||
protected function initCache()
|
||||
{
|
||||
$count = DB::table('statuses')->whereNull(['url', 'deleted_at'])->count();
|
||||
$res = [
|
||||
'count' => $count,
|
||||
];
|
||||
Storage::put('total_local_posts.json', json_encode($res, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||
ConfigCacheService::put('instance.stats.total_local_posts', $res['count']);
|
||||
}
|
||||
|
||||
protected function getCached()
|
||||
{
|
||||
return Storage::json('total_local_posts.json');
|
||||
}
|
||||
|
||||
protected function updateAndCache()
|
||||
{
|
||||
$count = DB::table('statuses')->whereNull(['url', 'deleted_at'])->count();
|
||||
$res = [
|
||||
'count' => $count,
|
||||
];
|
||||
Storage::put('total_local_posts.json', json_encode($res, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||
ConfigCacheService::put('instance.stats.total_local_posts', $res['count']);
|
||||
|
||||
}
|
||||
}
|
140
app/Console/Commands/MediaCloudUrlRewrite.php
Normal file
140
app/Console/Commands/MediaCloudUrlRewrite.php
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use Cache, Storage;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
|
||||
class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:cloud-url-rewrite {oldDomain} {newDomain}';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'oldDomain' => 'The old S3 domain',
|
||||
'newDomain' => 'The new S3 domain'
|
||||
];
|
||||
}
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Rewrite S3 media urls from local users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->preflightCheck();
|
||||
$this->bootMessage();
|
||||
$this->confirmCloudUrl();
|
||||
}
|
||||
|
||||
protected function preflightCheck()
|
||||
{
|
||||
if(!(bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$this->info('Error: Cloud storage is not enabled!');
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
protected function bootMessage()
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Media Cloud Url Rewrite Tool');
|
||||
$this->info(' ===');
|
||||
$this->info(' Old S3: ' . trim($this->argument('oldDomain')));
|
||||
$this->info(' New S3: ' . trim($this->argument('newDomain')));
|
||||
$this->info(' ');
|
||||
}
|
||||
|
||||
protected function confirmCloudUrl()
|
||||
{
|
||||
$disk = Storage::disk(config('filesystems.cloud'))->url('test');
|
||||
$domain = parse_url($disk, PHP_URL_HOST);
|
||||
if(trim($this->argument('newDomain')) !== $domain) {
|
||||
$this->error('Error: The new S3 domain you entered is not currently configured');
|
||||
exit;
|
||||
}
|
||||
|
||||
if(!$this->confirm('Confirm this is correct')) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->updateUrls();
|
||||
}
|
||||
|
||||
protected function updateUrls()
|
||||
{
|
||||
$this->info('Updating urls...');
|
||||
$oldDomain = trim($this->argument('oldDomain'));
|
||||
$newDomain = trim($this->argument('newDomain'));
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$count = Media::whereNotNull('cdn_url')->count();
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$counter = 0;
|
||||
$bar->start();
|
||||
foreach(Media::whereNotNull('cdn_url')->lazyById(1000, 'id') as $media) {
|
||||
if(strncmp($media->media_path, 'http', 4) === 0) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
$cdnHost = parse_url($media->cdn_url, PHP_URL_HOST);
|
||||
if($oldDomain != $cdnHost || $newDomain == $cdnHost) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$media->cdn_url = str_replace($oldDomain, $newDomain, $media->cdn_url);
|
||||
|
||||
if($media->thumbnail_url != null) {
|
||||
$thumbHost = parse_url($media->thumbnail_url, PHP_URL_HOST);
|
||||
if($thumbHost == $oldDomain) {
|
||||
$thumbUrl = $disk->url($media->thumbnail_path);
|
||||
$media->thumbnail_url = $thumbUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if($media->optimized_url != null) {
|
||||
$optiHost = parse_url($media->optimized_url, PHP_URL_HOST);
|
||||
if($optiHost == $oldDomain) {
|
||||
$optiUrl = str_replace($oldDomain, $newDomain, $media->optimized_url);
|
||||
$media->optimized_url = $optiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
$media->save();
|
||||
$counter++;
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
|
||||
$this->line(' ');
|
||||
$this->info('Finished! Updated ' . $counter . ' total records!');
|
||||
$this->line(' ');
|
||||
$this->info('Tip: Run `php artisan cache:clear` to purge cached urls');
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']);
|
||||
$enabled = (bool) config_cache('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->error('Cloud storage not enabled. Exiting...');
|
||||
return;
|
||||
|
|
31
app/Console/Commands/NotificationEpochUpdate.php
Normal file
31
app/Console/Commands/NotificationEpochUpdate.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
|
||||
|
||||
class NotificationEpochUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:notification-epoch-update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update notification epoch';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
NotificationEpochUpdatePipeline::dispatch();
|
||||
}
|
||||
}
|
74
app/Console/Commands/PushGatewayRefresh.php
Normal file
74
app/Console/Commands/PushGatewayRefresh.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\NotificationAppGatewayService;
|
||||
use App\Services\PushNotificationService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use function Laravel\Prompts\select;
|
||||
|
||||
class PushGatewayRefresh extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:push-gateway-refresh';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Refresh push notification gateway support';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Checking Push Notification support...');
|
||||
$this->line(' ');
|
||||
|
||||
$currentState = NotificationAppGatewayService::enabled();
|
||||
|
||||
if ($currentState) {
|
||||
$this->info('Push Notification support is active!');
|
||||
|
||||
return;
|
||||
} else {
|
||||
$this->error('Push notification support is NOT active');
|
||||
|
||||
$action = select(
|
||||
label: 'Do you want to force re-check?',
|
||||
options: ['Yes', 'No'],
|
||||
required: true
|
||||
);
|
||||
|
||||
if ($action === 'Yes') {
|
||||
$recheck = NotificationAppGatewayService::forceSupportRecheck();
|
||||
if ($recheck) {
|
||||
$this->info('Success! Push Notifications are now active!');
|
||||
PushNotificationService::warmList('like');
|
||||
|
||||
return;
|
||||
} else {
|
||||
$this->error('Error, please ensure you have a valid API key.');
|
||||
$this->line(' ');
|
||||
$this->line('For more info, visit https://docs.pixelfed.org/running-pixelfed/push-notifications.html');
|
||||
$this->line(' ');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
exit;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
37
app/Console/Commands/SoftwareUpdateRefresh.php
Normal file
37
app/Console/Commands/SoftwareUpdateRefresh.php
Normal file
|
@ -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!');
|
||||
}
|
||||
}
|
|
@ -2,17 +2,16 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use App\Services\ImportService;
|
||||
use App\Media;
|
||||
use App\Models\ImportPost;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use Storage;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\ImportService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Status;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use Storage;
|
||||
|
||||
class TransformImports extends Command
|
||||
{
|
||||
|
@ -52,6 +51,7 @@ class TransformImports extends Command
|
|||
if (! $profile) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -66,16 +66,24 @@ class TransformImports extends Command
|
|||
if ($exists == true) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day);
|
||||
if (! $idk) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Storage::exists('imports/'.$id.'/'.$ip->filename) === false) {
|
||||
ImportService::clearAttempts($profile->id);
|
||||
ImportService::getPostCount($profile->id, true);
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -91,6 +99,7 @@ class TransformImports extends Command
|
|||
if ($missingMedia === true) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -98,7 +107,6 @@ class TransformImports extends Command
|
|||
$status = new Status;
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = $caption;
|
||||
$status->rendered = strlen(trim($caption)) ? Autolink::create()->autolink($ip->caption) : null;
|
||||
$status->type = $ip->post_type;
|
||||
|
||||
$status->scope = 'unlisted';
|
||||
|
@ -115,6 +123,7 @@ class TransformImports extends Command
|
|||
if (! Storage::exists($og)) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
$size = Storage::size($og);
|
||||
|
|
123
app/Console/Commands/UserAccountDelete.php
Normal file
123
app/Console/Commands/UserAccountDelete.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteActor;
|
||||
use App\User;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Pool;
|
||||
use Illuminate\Console\Command;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\search;
|
||||
use function Laravel\Prompts\table;
|
||||
|
||||
class UserAccountDelete extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:user-account-delete';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Federate Account Deletion';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = search(
|
||||
label: 'Search for the account to delete by username',
|
||||
placeholder: 'john.appleseed',
|
||||
options: fn (string $value) => strlen($value) > 0
|
||||
? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all()
|
||||
: [],
|
||||
);
|
||||
|
||||
$user = User::withTrashed()->find($id);
|
||||
|
||||
table(
|
||||
['Username', 'Name', 'Email', 'Created'],
|
||||
[[$user->username, $user->name, $user->email, $user->created_at]]
|
||||
);
|
||||
|
||||
$confirmed = confirm(
|
||||
label: 'Do you want to federate this account deletion?',
|
||||
default: false,
|
||||
yes: 'Proceed',
|
||||
no: 'Cancel',
|
||||
hint: 'This action is irreversible'
|
||||
);
|
||||
|
||||
if (! $confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$profile = Profile::withTrashed()->find($user->profile_id);
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($profile, new DeleteActor());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched'])
|
||||
->where('nodeinfo_last_fetched', '>', now()->subDays(14))
|
||||
->distinct()
|
||||
->pluck('shared_inbox');
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => 5,
|
||||
]);
|
||||
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
|
||||
|
||||
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
|
||||
foreach ($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity, [
|
||||
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => $userAgent,
|
||||
]);
|
||||
yield function () use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
],
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => 50,
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
},
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
}
|
|
@ -5,8 +5,9 @@ namespace App\Console\Commands;
|
|||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use App\User;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
|
||||
class UserVerifyEmail extends Command
|
||||
class UserVerifyEmail extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
|
@ -39,13 +40,19 @@ class UserVerifyEmail extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$user = User::whereUsername($this->argument('username'))->first();
|
||||
$username = $this->argument('username');
|
||||
$user = User::whereUsername($username)->first();
|
||||
|
||||
if(!$user) {
|
||||
$this->error('Username not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if($user->email_verified_at) {
|
||||
$this->error('Email already verified ' . $user->email_verified_at->diffForHumans());
|
||||
return;
|
||||
}
|
||||
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
$this->info('Successfully verified email address for ' . $user->username);
|
||||
|
|
47
app/Console/Commands/WeeklyInstanceScan.php
Normal file
47
app/Console/Commands/WeeklyInstanceScan.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Instance;
|
||||
use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class WeeklyInstanceScan extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:weekly-instance-scan';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Scan instance nodeinfo';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if ((bool) config_cache('federation.activitypub.enabled') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$users = progress(
|
||||
label: 'Updating instance stats...',
|
||||
steps: Instance::all(),
|
||||
callback: fn ($instance) => $this->updateInstanceStats($instance),
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateInstanceStats($instance)
|
||||
{
|
||||
FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
|
||||
}
|
||||
}
|
|
@ -19,32 +19,41 @@ class Kernel extends ConsoleKernel
|
|||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('media:optimize')->hourlyAt(40);
|
||||
$schedule->command('media:gc')->hourlyAt(5);
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('story:gc')->everyFiveMinutes();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3);
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
||||
$schedule->command('media:optimize')->hourlyAt(40)->onOneServer();
|
||||
$schedule->command('media:gc')->hourlyAt(5)->onOneServer();
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('story:gc')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer();
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
|
||||
$schedule->command('app:weekly-instance-scan')->weeklyOn(2, '4:20')->onOneServer();
|
||||
|
||||
if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
|
||||
if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) {
|
||||
$schedule->command('media:s3gc')->hourlyAt(15);
|
||||
}
|
||||
|
||||
if (config('import.instagram.enabled')) {
|
||||
$schedule->command('app:transform-imports')->everyFourMinutes();
|
||||
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51);
|
||||
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37);
|
||||
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32);
|
||||
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
|
||||
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
|
||||
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
|
||||
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
|
||||
|
||||
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
|
||||
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
|
||||
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
|
||||
$schedule->command('app:instance-update-total-local-posts')->twiceDailyAt(1, 13, 45)->onOneServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
|
|
|
@ -3,9 +3,14 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Contact extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
'responded_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
@ -15,4 +20,14 @@ class Contact extends Model
|
|||
{
|
||||
return url('/i/admin/messages/show/'.$this->id);
|
||||
}
|
||||
|
||||
public function userResponseUrl()
|
||||
{
|
||||
return url('/i/contact-admin-response/'.$this->id);
|
||||
}
|
||||
|
||||
public function getMessageId()
|
||||
{
|
||||
return $this->id.'-'.(string) Str::uuid().'@'.strtolower(config('pixelfed.domain.app', 'example.org'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ class AccountController extends Controller
|
|||
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::muteCount($pid);
|
||||
$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($pid)->count();
|
||||
|
@ -260,7 +260,7 @@ class AccountController extends Controller
|
|||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::blockCount($pid);
|
||||
$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();
|
||||
|
|
|
@ -2,30 +2,20 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use DB, Cache;
|
||||
use App\{
|
||||
DiscoverCategory,
|
||||
DiscoverCategoryHashtag,
|
||||
Hashtag,
|
||||
Media,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
User
|
||||
};
|
||||
use App\Http\Controllers\PixelfedDirectoryController;
|
||||
use App\Models\ConfigCache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\StatusService;
|
||||
use Carbon\Carbon;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use League\ISO3166\ISO3166;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Http\Controllers\PixelfedDirectoryController;
|
||||
use Illuminate\Support\Str;
|
||||
use League\ISO3166\ISO3166;
|
||||
|
||||
trait AdminDirectoryController
|
||||
{
|
||||
|
@ -46,7 +36,7 @@ trait AdminDirectoryController
|
|||
'uid' => (string) $user->id,
|
||||
'pid' => (string) $user->profile_id,
|
||||
'username' => $user->username,
|
||||
'created_at' => $user->created_at
|
||||
'created_at' => $user->created_at,
|
||||
];
|
||||
});
|
||||
$config = ConfigCache::whereK('pixelfed.directory')->first();
|
||||
|
@ -75,22 +65,25 @@ 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'));
|
||||
$res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') &&
|
||||
(file_exists(storage_path('oauth-public.key')) || config_cache('passport.public_key')) &&
|
||||
(file_exists(storage_path('oauth-private.key')) || config_cache('passport.private_key'));
|
||||
|
||||
$res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled');
|
||||
|
||||
$res['feature_config'] = [
|
||||
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
|
||||
'image_quality' => config_cache('pixelfed.image_quality'),
|
||||
'optimize_image' => config_cache('pixelfed.optimize_image'),
|
||||
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
|
||||
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
|
||||
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
|
||||
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
|
||||
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
|
||||
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
|
||||
'max_account_size' => config_cache('pixelfed.max_account_size'),
|
||||
'max_album_length' => config_cache('pixelfed.max_album_length'),
|
||||
'account_deletion' => config_cache('pixelfed.account_deletion'),
|
||||
'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
|
||||
];
|
||||
|
||||
if (config_cache('pixelfed.directory.testimonials')) {
|
||||
|
@ -98,7 +91,7 @@ trait AdminDirectoryController
|
|||
->map(function ($t) {
|
||||
return [
|
||||
'profile' => AccountService::get($t['profile_id']),
|
||||
'body' => $t['body']
|
||||
'body' => $t['body'],
|
||||
];
|
||||
});
|
||||
$res['testimonials'] = $testimonials;
|
||||
|
@ -119,12 +112,12 @@ trait AdminDirectoryController
|
|||
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
|
||||
'max_album_length' => 'required|integer|min:4|max:20',
|
||||
'account_deletion' => 'required|accepted',
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000'
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000',
|
||||
]);
|
||||
|
||||
$res['requirements_validator'] = $validator->errors();
|
||||
|
||||
$res['is_eligible'] = $res['open_registration'] &&
|
||||
$res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) &&
|
||||
$res['oauth_enabled'] &&
|
||||
$res['activitypub_enabled'] &&
|
||||
count($res['requirements_validator']) === 0 &&
|
||||
|
@ -159,10 +152,11 @@ trait AdminDirectoryController
|
|||
|
||||
$submissionState = Http::withoutVerifying()
|
||||
->post('https://pixelfed.org/api/v1/directory/check-submission', [
|
||||
'domain' => config('pixelfed.domain.app')
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
]);
|
||||
|
||||
$res['submission_state'] = $submissionState->json();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -193,11 +187,11 @@ trait AdminDirectoryController
|
|||
'favourite_posts' => 'array|max:12',
|
||||
'favourite_posts.*' => 'distinct',
|
||||
'privacy_pledge' => 'sometimes',
|
||||
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000'
|
||||
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000',
|
||||
]);
|
||||
|
||||
$config = ConfigCache::firstOrNew([
|
||||
'k' => 'pixelfed.directory'
|
||||
'k' => 'pixelfed.directory',
|
||||
]);
|
||||
|
||||
$res = $config->v ? json_decode($config->v, true) : [];
|
||||
|
@ -220,14 +214,15 @@ trait AdminDirectoryController
|
|||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png'
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
|
||||
return ! in_array($name, $protected);
|
||||
})
|
||||
->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)));
|
||||
|
||||
|
@ -242,6 +237,7 @@ trait AdminDirectoryController
|
|||
if (isset($updated['banner_image'])) {
|
||||
$updated['banner_image'] = url(Storage::url($updated['banner_image']));
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
|
@ -249,9 +245,10 @@ trait AdminDirectoryController
|
|||
{
|
||||
$reqs = [];
|
||||
$reqs['feature_config'] = [
|
||||
'open_registration' => config_cache('pixelfed.open_registration'),
|
||||
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
|
||||
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
|
||||
'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
|
||||
'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
|
||||
'oauth_enabled' => (bool) config_cache('pixelfed.oauth_enabled'),
|
||||
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
|
||||
'image_quality' => config_cache('pixelfed.image_quality'),
|
||||
'optimize_image' => config_cache('pixelfed.optimize_image'),
|
||||
|
@ -265,7 +262,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' => [
|
||||
|
@ -282,7 +280,7 @@ trait AdminDirectoryController
|
|||
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
|
||||
'max_album_length' => 'required|integer|min:4|max:20',
|
||||
'account_deletion' => 'required|accepted',
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000'
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000',
|
||||
]);
|
||||
|
||||
if (! $validator->validate()) {
|
||||
|
@ -294,6 +292,7 @@ trait AdminDirectoryController
|
|||
|
||||
$data = (new PixelfedDirectoryController())->buildListing();
|
||||
$res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
@ -309,7 +308,7 @@ trait AdminDirectoryController
|
|||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png'
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
if (! $path || in_array($path, $protected)) {
|
||||
return;
|
||||
|
@ -325,6 +324,7 @@ trait AdminDirectoryController
|
|||
$bannerImage->save();
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
ConfigCacheService::put('pixelfed.directory', $directory);
|
||||
|
||||
return $bannerImage->v;
|
||||
}
|
||||
|
||||
|
@ -354,7 +354,7 @@ trait AdminDirectoryController
|
|||
public function directoryGetAddPostByIdSearch(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|integer'
|
||||
'q' => 'required|integer',
|
||||
]);
|
||||
|
||||
$id = $request->input('q');
|
||||
|
@ -382,6 +382,7 @@ trait AdminDirectoryController
|
|||
})
|
||||
->values();
|
||||
ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
|
@ -389,13 +390,13 @@ trait AdminDirectoryController
|
|||
{
|
||||
$this->validate($request, [
|
||||
'username' => 'required',
|
||||
'body' => 'required|string|min:5|max:500'
|
||||
'body' => 'required|string|min:5|max:500',
|
||||
]);
|
||||
|
||||
$user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
|
||||
|
||||
$configCache = ConfigCache::firstOrCreate([
|
||||
'k' => 'pixelfed.directory.testimonials'
|
||||
'k' => 'pixelfed.directory.testimonials',
|
||||
]);
|
||||
|
||||
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
|
||||
|
@ -406,7 +407,7 @@ trait AdminDirectoryController
|
|||
$testimonials->push([
|
||||
'profile_id' => (string) $user->profile_id,
|
||||
'username' => $request->input('username'),
|
||||
'body' => $request->input('body')
|
||||
'body' => $request->input('body'),
|
||||
]);
|
||||
|
||||
$configCache->v = json_encode($testimonials->toArray());
|
||||
|
@ -414,8 +415,9 @@ trait AdminDirectoryController
|
|||
ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
|
||||
$res = [
|
||||
'profile' => AccountService::get($user->profile_id),
|
||||
'body' => $request->input('body')
|
||||
'body' => $request->input('body'),
|
||||
];
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -423,7 +425,7 @@ trait AdminDirectoryController
|
|||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required',
|
||||
'body' => 'required|string|min:5|max:500'
|
||||
'body' => 'required|string|min:5|max:500',
|
||||
]);
|
||||
|
||||
$profile_id = $request->input('profile_id');
|
||||
|
@ -431,7 +433,7 @@ trait AdminDirectoryController
|
|||
$user = User::whereProfileId($profile_id)->firstOrFail();
|
||||
|
||||
$configCache = ConfigCache::firstOrCreate([
|
||||
'k' => 'pixelfed.directory.testimonials'
|
||||
'k' => 'pixelfed.directory.testimonials',
|
||||
]);
|
||||
|
||||
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
|
||||
|
@ -440,6 +442,7 @@ trait AdminDirectoryController
|
|||
if ($t['profile_id'] == $profile_id) {
|
||||
$t['body'] = $body;
|
||||
}
|
||||
|
||||
return $t;
|
||||
})
|
||||
->values();
|
||||
|
|
49
app/Http/Controllers/Admin/AdminGroupsController.php
Normal file
49
app/Http/Controllers/Admin/AdminGroupsController.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupReport;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait AdminGroupsController
|
||||
{
|
||||
public function groupsHome(Request $request)
|
||||
{
|
||||
$stats = $this->groupAdminStats();
|
||||
|
||||
return view('admin.groups.home', compact('stats'));
|
||||
}
|
||||
|
||||
protected function groupAdminStats()
|
||||
{
|
||||
return Cache::remember('admin:groups:stats', 3, function () {
|
||||
$res = [
|
||||
'total' => Group::count(),
|
||||
'local' => Group::whereLocal(true)->count(),
|
||||
];
|
||||
|
||||
$res['remote'] = $res['total'] - $res['local'];
|
||||
$res['categories'] = GroupCategory::count();
|
||||
$res['posts'] = GroupPost::count();
|
||||
$res['members'] = GroupMember::count();
|
||||
$res['interactions'] = GroupInteraction::count();
|
||||
$res['reports'] = GroupReport::count();
|
||||
|
||||
$res['local_30d'] = Cache::remember('admin:groups:stats:local_30d', 43200, function () {
|
||||
return Group::whereLocal(true)->where('created_at', '>', now()->subMonth())->count();
|
||||
});
|
||||
|
||||
$res['remote_30d'] = Cache::remember('admin:groups:stats:remote_30d', 43200, function () {
|
||||
return Group::whereLocal(false)->where('created_at', '>', now()->subMonth())->count();
|
||||
});
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -2,18 +2,20 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Artisan, Cache, DB;
|
||||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
|
||||
use App\Models\InstanceActor;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use App\Models\ConfigCache;
|
||||
use App\Models\InstanceActor;
|
||||
use App\Page;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminSettingsService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\FilesystemService;
|
||||
use App\User;
|
||||
use App\Util\Site\Config;
|
||||
use Illuminate\Support\Str;
|
||||
use Artisan;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait AdminSettingsController
|
||||
{
|
||||
|
@ -32,13 +34,10 @@ trait AdminSettingsController
|
|||
|
||||
$availableAdmins = User::whereIsAdmin(true)->get();
|
||||
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
|
||||
|
||||
// $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'))
|
||||
// ];
|
||||
$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');
|
||||
|
||||
return view('admin.settings.home', compact(
|
||||
'jpeg',
|
||||
|
@ -51,8 +50,9 @@ trait AdminSettingsController
|
|||
'cloud_disk',
|
||||
'cloud_ready',
|
||||
'availableAdmins',
|
||||
'currentAdmin'
|
||||
// 'system'
|
||||
'currentAdmin',
|
||||
'regState',
|
||||
'accountMigration'
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -71,8 +71,33 @@ trait AdminSettingsController
|
|||
'type_mp4' => 'nullable',
|
||||
'type_webp' => 'nullable',
|
||||
'admin_account_id' => 'nullable',
|
||||
'regs' => 'required|in:open,filtered,closed',
|
||||
'account_migration' => 'nullable',
|
||||
'rule_delete' => 'sometimes',
|
||||
]);
|
||||
|
||||
$orb = false;
|
||||
$cob = false;
|
||||
switch ($request->input('regs')) {
|
||||
case 'open':
|
||||
$orb = true;
|
||||
$cob = false;
|
||||
break;
|
||||
|
||||
case 'filtered':
|
||||
$orb = false;
|
||||
$cob = true;
|
||||
break;
|
||||
|
||||
case 'closed':
|
||||
$orb = false;
|
||||
$cob = false;
|
||||
break;
|
||||
}
|
||||
|
||||
ConfigCacheService::put('pixelfed.open_registration', (bool) $orb);
|
||||
ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob);
|
||||
|
||||
if ($request->filled('admin_account_id')) {
|
||||
ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
|
||||
Cache::forget('api:v1:instance-data:contact');
|
||||
|
@ -90,6 +115,7 @@ trait AdminSettingsController
|
|||
ConfigCacheService::put('app.rules', $json);
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
@ -128,7 +154,7 @@ trait AdminSettingsController
|
|||
'account_limit' => 'pixelfed.max_account_size',
|
||||
'custom_css' => 'uikit.custom.css',
|
||||
'custom_js' => 'uikit.custom.js',
|
||||
'about_title' => 'about.title'
|
||||
'about_title' => 'about.title',
|
||||
];
|
||||
|
||||
foreach ($keys as $key => $value) {
|
||||
|
@ -143,7 +169,7 @@ trait AdminSettingsController
|
|||
|
||||
$bools = [
|
||||
'activitypub' => 'federation.activitypub.enabled',
|
||||
'open_registration' => 'pixelfed.open_registration',
|
||||
// 'open_registration' => 'pixelfed.open_registration',
|
||||
'mobile_apis' => 'pixelfed.oauth_enabled',
|
||||
'stories' => 'instance.stories.enabled',
|
||||
'ig_import' => 'pixelfed.import.instagram.enabled',
|
||||
|
@ -156,6 +182,7 @@ trait AdminSettingsController
|
|||
'account_autofollow' => 'account.autofollow',
|
||||
'show_directory' => 'instance.landing.show_directory',
|
||||
'show_explore_feed' => 'instance.landing.show_explore',
|
||||
'account_migration' => 'federation.migration',
|
||||
];
|
||||
|
||||
foreach ($bools as $key => $value) {
|
||||
|
@ -168,7 +195,9 @@ trait AdminSettingsController
|
|||
if ($key == 'mobile_apis' &&
|
||||
$active &&
|
||||
! file_exists(storage_path('oauth-public.key')) &&
|
||||
!file_exists(storage_path('oauth-private.key'))
|
||||
! config_cache('passport.public_key') &&
|
||||
! file_exists(storage_path('oauth-private.key')) &&
|
||||
! config_cache('passport.private_key')
|
||||
) {
|
||||
Artisan::call('passport:keys');
|
||||
Artisan::call('route:cache');
|
||||
|
@ -217,6 +246,7 @@ trait AdminSettingsController
|
|||
{
|
||||
$path = storage_path('app/'.config('app.name'));
|
||||
$files = is_dir($path) ? new \DirectoryIterator($path) : [];
|
||||
|
||||
return view('admin.settings.backups', compact('files'));
|
||||
}
|
||||
|
||||
|
@ -228,6 +258,7 @@ trait AdminSettingsController
|
|||
public function settingsStorage(Request $request)
|
||||
{
|
||||
$storage = [];
|
||||
|
||||
return view('admin.settings.storage', compact('storage'));
|
||||
}
|
||||
|
||||
|
@ -239,6 +270,7 @@ trait AdminSettingsController
|
|||
public function settingsPages(Request $request)
|
||||
{
|
||||
$pages = Page::orderByDesc('updated_at')->paginate(10);
|
||||
|
||||
return view('admin.pages.home', compact('pages'));
|
||||
}
|
||||
|
||||
|
@ -260,7 +292,7 @@ trait AdminSettingsController
|
|||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'Postgres',
|
||||
'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
|
||||
'version' => explode(' ', DB::select($expQuery)[0]->version)[1],
|
||||
];
|
||||
break;
|
||||
|
||||
|
@ -269,17 +301,589 @@ trait AdminSettingsController
|
|||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'MySQL',
|
||||
'version' => DB::select($expQuery)[0]->{'version()'}
|
||||
'version' => DB::select($expQuery)[0]->{'version()'},
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
$sys['database'] = [
|
||||
'name' => 'Unknown',
|
||||
'version' => '?'
|
||||
'version' => '?',
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return view('admin.settings.system', compact('sys'));
|
||||
}
|
||||
|
||||
public function settingsApiFetch(Request $request)
|
||||
{
|
||||
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
|
||||
$cloud_disk = config('filesystems.cloud');
|
||||
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
|
||||
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
|
||||
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : [];
|
||||
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
|
||||
$png = in_array('image/png', $types);
|
||||
$gif = in_array('image/gif', $types);
|
||||
$mp4 = in_array('video/mp4', $types);
|
||||
$webp = in_array('image/webp', $types);
|
||||
|
||||
$availableAdmins = User::whereIsAdmin(true)->get();
|
||||
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
|
||||
$openReg = (bool) config_cache('pixelfed.open_registration');
|
||||
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
|
||||
$accountMigration = (bool) config_cache('federation.migration');
|
||||
$autoFollow = config_cache('account.autofollow_usernames');
|
||||
if (strlen($autoFollow) > 3) {
|
||||
$autoFollow = explode(',', $autoFollow);
|
||||
}
|
||||
|
||||
$res = AdminSettingsService::getAll();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function settingsApiRulesAdd(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'rule' => 'required|string|min:5|max:1000',
|
||||
]);
|
||||
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$val = $request->input('rule');
|
||||
if (! $rules) {
|
||||
ConfigCacheService::put('app.rules', json_encode([$val]));
|
||||
} else {
|
||||
$json = json_decode($rules, true);
|
||||
$count = count($json);
|
||||
if ($count >= 30) {
|
||||
return response()->json(['message' => 'Max rules limit reached, you can set up to 30 rules at a time.'], 400);
|
||||
}
|
||||
$json[] = $val;
|
||||
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
|
||||
}
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Config::refresh();
|
||||
|
||||
return [$val];
|
||||
}
|
||||
|
||||
public function settingsApiRulesDelete(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'rule' => 'required|string',
|
||||
]);
|
||||
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$val = $request->input('rule');
|
||||
|
||||
if (! $rules) {
|
||||
return [];
|
||||
} else {
|
||||
$json = json_decode($rules, true);
|
||||
$idx = array_search($val, $json);
|
||||
if ($idx !== false) {
|
||||
unset($json[$idx]);
|
||||
$json = array_values($json);
|
||||
}
|
||||
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
|
||||
}
|
||||
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Config::refresh();
|
||||
|
||||
return response()->json($json);
|
||||
}
|
||||
|
||||
public function settingsApiRulesDeleteAll(Request $request)
|
||||
{
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
|
||||
if (! $rules) {
|
||||
return [];
|
||||
} else {
|
||||
ConfigCacheService::put('app.rules', json_encode([]));
|
||||
}
|
||||
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Config::refresh();
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function settingsApiAutofollowDelete(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'username' => 'required|string',
|
||||
]);
|
||||
|
||||
$username = $request->input('username');
|
||||
$names = [];
|
||||
$existing = config_cache('account.autofollow_usernames');
|
||||
if ($existing) {
|
||||
$names = explode(',', $existing);
|
||||
}
|
||||
|
||||
if (in_array($username, $names)) {
|
||||
$key = array_search($username, $names);
|
||||
if ($key !== false) {
|
||||
unset($names[$key]);
|
||||
}
|
||||
}
|
||||
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
|
||||
|
||||
return response()->json(['accounts' => array_values($names)]);
|
||||
}
|
||||
|
||||
public function settingsApiAutofollowAdd(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'username' => 'required|string',
|
||||
]);
|
||||
|
||||
$username = $request->input('username');
|
||||
$names = [];
|
||||
$existing = config_cache('account.autofollow_usernames');
|
||||
if ($existing) {
|
||||
$names = explode(',', $existing);
|
||||
}
|
||||
|
||||
if ($existing && count($names)) {
|
||||
if (count($names) >= 5) {
|
||||
return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'], 400);
|
||||
}
|
||||
if (in_array(strtolower($username), array_map('strtolower', $names))) {
|
||||
return response()->json(['message' => 'User already exists, please try again.'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
$p = User::whereUsername($username)->whereNull('status')->first();
|
||||
if (! $p || in_array($p->username, $names)) {
|
||||
abort(404);
|
||||
}
|
||||
array_push($names, $p->username);
|
||||
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
|
||||
|
||||
return response()->json(['accounts' => array_values($names)]);
|
||||
}
|
||||
|
||||
public function settingsApiUpdateType(Request $request, $type)
|
||||
{
|
||||
abort_unless(in_array($type, [
|
||||
'posts',
|
||||
'platform',
|
||||
'home',
|
||||
'landing',
|
||||
'branding',
|
||||
'media',
|
||||
'users',
|
||||
'storage',
|
||||
]), 400);
|
||||
|
||||
switch ($type) {
|
||||
case 'home':
|
||||
return $this->settingsApiUpdateHomeType($request);
|
||||
break;
|
||||
|
||||
case 'landing':
|
||||
return $this->settingsApiUpdateLandingType($request);
|
||||
break;
|
||||
|
||||
case 'posts':
|
||||
return $this->settingsApiUpdatePostsType($request);
|
||||
break;
|
||||
|
||||
case 'platform':
|
||||
return $this->settingsApiUpdatePlatformType($request);
|
||||
break;
|
||||
|
||||
case 'branding':
|
||||
return $this->settingsApiUpdateBrandingType($request);
|
||||
break;
|
||||
|
||||
case 'media':
|
||||
return $this->settingsApiUpdateMediaType($request);
|
||||
break;
|
||||
|
||||
case 'users':
|
||||
return $this->settingsApiUpdateUsersType($request);
|
||||
break;
|
||||
|
||||
case 'storage':
|
||||
return $this->settingsApiUpdateStorageType($request);
|
||||
break;
|
||||
|
||||
default:
|
||||
abort(404);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function settingsApiUpdateHomeType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'registration_status' => 'required|in:open,filtered,closed',
|
||||
'cloud_storage' => 'required',
|
||||
'activitypub_enabled' => 'required',
|
||||
'authorized_fetch' => 'required',
|
||||
'account_migration' => 'required',
|
||||
'mobile_apis' => 'required',
|
||||
'stories' => 'required',
|
||||
'instagram_import' => 'required',
|
||||
'autospam_enabled' => 'required',
|
||||
]);
|
||||
|
||||
$regStatus = $request->input('registration_status');
|
||||
ConfigCacheService::put('pixelfed.open_registration', $regStatus === 'open');
|
||||
ConfigCacheService::put('instance.curated_registration.enabled', $regStatus === 'filtered');
|
||||
$cloudStorage = $request->boolean('cloud_storage');
|
||||
if ($cloudStorage !== (bool) config_cache('pixelfed.cloud_storage')) {
|
||||
if (! $cloudStorage) {
|
||||
ConfigCacheService::put('pixelfed.cloud_storage', false);
|
||||
} else {
|
||||
$cloud_disk = config('filesystems.cloud');
|
||||
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
|
||||
if (! $cloud_ready) {
|
||||
return redirect()->back()->withErrors(['cloud_storage' => 'Must configure cloud storage before enabling!']);
|
||||
} else {
|
||||
ConfigCacheService::put('pixelfed.cloud_storage', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCacheService::put('federation.activitypub.authorized_fetch', $request->boolean('authorized_fetch'));
|
||||
ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled'));
|
||||
ConfigCacheService::put('federation.migration', $request->boolean('account_migration'));
|
||||
ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis'));
|
||||
ConfigCacheService::put('instance.stories.enabled', $request->boolean('stories'));
|
||||
ConfigCacheService::put('pixelfed.import.instagram.enabled', $request->boolean('instagram_import'));
|
||||
ConfigCacheService::put('pixelfed.bouncer.enabled', $request->boolean('autospam_enabled'));
|
||||
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Cache::forget('api:v1:instance-data:contact');
|
||||
Config::refresh();
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function settingsApiUpdateLandingType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'current_admin' => 'required',
|
||||
'show_directory' => 'required',
|
||||
'show_explore' => 'required',
|
||||
]);
|
||||
|
||||
ConfigCacheService::put('instance.admin.pid', $request->input('current_admin'));
|
||||
ConfigCacheService::put('instance.landing.show_directory', $request->boolean('show_directory'));
|
||||
ConfigCacheService::put('instance.landing.show_explore', $request->boolean('show_explore'));
|
||||
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Cache::forget('api:v1:instance-data:contact');
|
||||
Config::refresh();
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function settingsApiUpdateMediaType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'image_quality' => 'required|integer|min:1|max:100',
|
||||
'max_album_length' => 'required|integer|min:1|max:20',
|
||||
'max_photo_size' => 'required|integer|min:100|max:50000',
|
||||
'media_types' => 'required',
|
||||
'optimize_image' => 'required',
|
||||
'optimize_video' => 'required',
|
||||
]);
|
||||
|
||||
$mediaTypes = $request->input('media_types');
|
||||
$mediaArray = explode(',', $mediaTypes);
|
||||
foreach ($mediaArray as $mediaType) {
|
||||
if (! in_array($mediaType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'])) {
|
||||
return redirect()->back()->withErrors(['media_types' => 'Invalid media type']);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigCacheService::put('pixelfed.media_types', $request->input('media_types'));
|
||||
ConfigCacheService::put('pixelfed.image_quality', $request->input('image_quality'));
|
||||
ConfigCacheService::put('pixelfed.max_album_length', $request->input('max_album_length'));
|
||||
ConfigCacheService::put('pixelfed.max_photo_size', $request->input('max_photo_size'));
|
||||
ConfigCacheService::put('pixelfed.optimize_image', $request->boolean('optimize_image'));
|
||||
ConfigCacheService::put('pixelfed.optimize_video', $request->boolean('optimize_video'));
|
||||
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Cache::forget('api:v1:instance-data:contact');
|
||||
Config::refresh();
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function settingsApiUpdateBrandingType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'short_description' => 'required',
|
||||
'long_description' => 'required',
|
||||
]);
|
||||
|
||||
ConfigCacheService::put('app.name', $request->input('name'));
|
||||
ConfigCacheService::put('app.short_description', $request->input('short_description'));
|
||||
ConfigCacheService::put('app.description', $request->input('long_description'));
|
||||
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Cache::forget('api:v1:instance-data:contact');
|
||||
Config::refresh();
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function settingsApiUpdatePostsType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'max_caption_length' => 'required|integer|min:5|max:10000',
|
||||
'max_altext_length' => 'required|integer|min:5|max:40000',
|
||||
]);
|
||||
|
||||
ConfigCacheService::put('pixelfed.max_caption_length', $request->input('max_caption_length'));
|
||||
ConfigCacheService::put('pixelfed.max_altext_length', $request->input('max_altext_length'));
|
||||
$res = [
|
||||
'max_caption_length' => $request->input('max_caption_length'),
|
||||
'max_altext_length' => $request->input('max_altext_length'),
|
||||
];
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Config::refresh();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function settingsApiUpdatePlatformType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'allow_app_registration' => 'required',
|
||||
'app_registration_rate_limit_attempts' => 'required|integer|min:1',
|
||||
'app_registration_rate_limit_decay' => 'required|integer|min:1',
|
||||
'app_registration_confirm_rate_limit_attempts' => 'required|integer|min:1',
|
||||
'app_registration_confirm_rate_limit_decay' => 'required|integer|min:1',
|
||||
'allow_post_embeds' => 'required',
|
||||
'allow_profile_embeds' => 'required',
|
||||
'captcha_enabled' => 'required',
|
||||
'captcha_on_login' => 'required_if_accepted:captcha_enabled',
|
||||
'captcha_on_register' => 'required_if_accepted:captcha_enabled',
|
||||
'captcha_secret' => 'required_if_accepted:captcha_enabled',
|
||||
'captcha_sitekey' => 'required_if_accepted:captcha_enabled',
|
||||
'custom_emoji_enabled' => 'required',
|
||||
]);
|
||||
|
||||
ConfigCacheService::put('pixelfed.allow_app_registration', $request->boolean('allow_app_registration'));
|
||||
ConfigCacheService::put('pixelfed.app_registration_rate_limit_attempts', $request->input('app_registration_rate_limit_attempts'));
|
||||
ConfigCacheService::put('pixelfed.app_registration_rate_limit_decay', $request->input('app_registration_rate_limit_decay'));
|
||||
ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_attempts', $request->input('app_registration_confirm_rate_limit_attempts'));
|
||||
ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_decay', $request->input('app_registration_confirm_rate_limit_decay'));
|
||||
ConfigCacheService::put('instance.embed.post', $request->boolean('allow_post_embeds'));
|
||||
ConfigCacheService::put('instance.embed.profile', $request->boolean('allow_profile_embeds'));
|
||||
ConfigCacheService::put('federation.custom_emoji.enabled', $request->boolean('custom_emoji_enabled'));
|
||||
$captcha = $request->boolean('captcha_enabled');
|
||||
if ($captcha) {
|
||||
$secret = $request->input('captcha_secret');
|
||||
$sitekey = $request->input('captcha_sitekey');
|
||||
if (config_cache('captcha.secret') != $secret && strpos($secret, '*') === false) {
|
||||
ConfigCacheService::put('captcha.secret', $secret);
|
||||
}
|
||||
if (config_cache('captcha.sitekey') != $sitekey && strpos($sitekey, '*') === false) {
|
||||
ConfigCacheService::put('captcha.sitekey', $sitekey);
|
||||
}
|
||||
ConfigCacheService::put('captcha.active.login', $request->boolean('captcha_on_login'));
|
||||
ConfigCacheService::put('captcha.active.register', $request->boolean('captcha_on_register'));
|
||||
ConfigCacheService::put('captcha.triggers.login.enabled', $request->boolean('captcha_on_login'));
|
||||
ConfigCacheService::put('captcha.enabled', true);
|
||||
} else {
|
||||
ConfigCacheService::put('captcha.enabled', false);
|
||||
}
|
||||
$res = [
|
||||
'allow_app_registration' => $request->boolean('allow_app_registration'),
|
||||
'app_registration_rate_limit_attempts' => $request->input('app_registration_rate_limit_attempts'),
|
||||
'app_registration_rate_limit_decay' => $request->input('app_registration_rate_limit_decay'),
|
||||
'app_registration_confirm_rate_limit_attempts' => $request->input('app_registration_confirm_rate_limit_attempts'),
|
||||
'app_registration_confirm_rate_limit_decay' => $request->input('app_registration_confirm_rate_limit_decay'),
|
||||
'allow_post_embeds' => $request->boolean('allow_post_embeds'),
|
||||
'allow_profile_embeds' => $request->boolean('allow_profile_embeds'),
|
||||
'captcha_enabled' => $request->boolean('captcha_enabled'),
|
||||
'captcha_on_login' => $request->boolean('captcha_on_login'),
|
||||
'captcha_on_register' => $request->boolean('captcha_on_register'),
|
||||
'captcha_secret' => $request->input('captcha_secret'),
|
||||
'captcha_sitekey' => $request->input('captcha_sitekey'),
|
||||
'custom_emoji_enabled' => $request->boolean('custom_emoji_enabled'),
|
||||
];
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Config::refresh();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function settingsApiUpdateUsersType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'require_email_verification' => 'required',
|
||||
'enforce_account_limit' => 'required',
|
||||
'max_account_size' => 'required|integer|min:50000',
|
||||
'admin_autofollow' => 'required',
|
||||
'admin_autofollow_accounts' => 'sometimes',
|
||||
'max_user_blocks' => 'required|integer|min:0|max:5000',
|
||||
'max_user_mutes' => 'required|integer|min:0|max:5000',
|
||||
'max_domain_blocks' => 'required|integer|min:0|max:5000',
|
||||
]);
|
||||
|
||||
$adminAutofollow = $request->boolean('admin_autofollow');
|
||||
$adminAutofollowAccounts = $request->input('admin_autofollow_accounts');
|
||||
if ($adminAutofollow) {
|
||||
if ($request->filled('admin_autofollow_accounts')) {
|
||||
$names = [];
|
||||
$existing = config_cache('account.autofollow_usernames');
|
||||
if ($existing) {
|
||||
$names = explode(',', $existing);
|
||||
foreach (array_map('strtolower', $adminAutofollowAccounts) as $afc) {
|
||||
if (in_array(strtolower($afc), array_map('strtolower', $names))) {
|
||||
continue;
|
||||
}
|
||||
$names[] = $afc;
|
||||
}
|
||||
} else {
|
||||
$names = $adminAutofollowAccounts;
|
||||
}
|
||||
if (! $names || count($names) == 0) {
|
||||
return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
|
||||
}
|
||||
if (count($names) > 5) {
|
||||
return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'.json_encode($names)], 400);
|
||||
}
|
||||
$autofollows = User::whereIn('username', $names)->whereNull('status')->pluck('username');
|
||||
$adminAutofollowAccounts = $autofollows->implode(',');
|
||||
ConfigCacheService::put('account.autofollow_usernames', $adminAutofollowAccounts);
|
||||
} else {
|
||||
return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigCacheService::put('pixelfed.enforce_email_verification', $request->boolean('require_email_verification'));
|
||||
ConfigCacheService::put('pixelfed.enforce_account_limit', $request->boolean('enforce_account_limit'));
|
||||
ConfigCacheService::put('pixelfed.max_account_size', $request->input('max_account_size'));
|
||||
ConfigCacheService::put('account.autofollow', $request->boolean('admin_autofollow'));
|
||||
ConfigCacheService::put('instance.user_filters.max_user_blocks', (int) $request->input('max_user_blocks'));
|
||||
ConfigCacheService::put('instance.user_filters.max_user_mutes', (int) $request->input('max_user_mutes'));
|
||||
ConfigCacheService::put('instance.user_filters.max_domain_blocks', (int) $request->input('max_domain_blocks'));
|
||||
$res = [
|
||||
'require_email_verification' => $request->boolean('require_email_verification'),
|
||||
'enforce_account_limit' => $request->boolean('enforce_account_limit'),
|
||||
'admin_autofollow' => $request->boolean('admin_autofollow'),
|
||||
'admin_autofollow_accounts' => $adminAutofollowAccounts,
|
||||
'max_user_blocks' => $request->input('max_user_blocks'),
|
||||
'max_user_mutes' => $request->input('max_user_mutes'),
|
||||
'max_domain_blocks' => $request->input('max_domain_blocks'),
|
||||
];
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Config::refresh();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function settingsApiUpdateStorageType($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'primary_disk' => 'required|in:local,cloud',
|
||||
'update_disk' => 'sometimes',
|
||||
'disk_config' => 'required_if_accepted:update_disk',
|
||||
'disk_config.driver' => 'required|in:s3,spaces',
|
||||
'disk_config.key' => 'required',
|
||||
'disk_config.secret' => 'required',
|
||||
'disk_config.region' => 'required',
|
||||
'disk_config.bucket' => 'required',
|
||||
'disk_config.visibility' => 'required',
|
||||
'disk_config.endpoint' => 'required',
|
||||
'disk_config.url' => 'nullable',
|
||||
]);
|
||||
|
||||
ConfigCacheService::put('pixelfed.cloud_storage', $request->input('primary_disk') === 'cloud');
|
||||
$res = [
|
||||
'primary_disk' => $request->input('primary_disk'),
|
||||
];
|
||||
if ($request->has('update_disk')) {
|
||||
$res['disk_config'] = $request->input('disk_config');
|
||||
$changes = [];
|
||||
$dkey = $request->input('disk_config.driver') === 's3' ? 'filesystems.disks.s3.' : 'filesystems.disks.spaces.';
|
||||
$key = $request->input('disk_config.key');
|
||||
$ckey = null;
|
||||
$secret = $request->input('disk_config.secret');
|
||||
$csecret = null;
|
||||
$region = $request->input('disk_config.region');
|
||||
$bucket = $request->input('disk_config.bucket');
|
||||
$visibility = $request->input('disk_config.visibility');
|
||||
$url = $request->input('disk_config.url');
|
||||
$endpoint = $request->input('disk_config.endpoint');
|
||||
if (strpos($key, '*') === false && $key != config_cache($dkey.'key')) {
|
||||
array_push($changes, 'key');
|
||||
} else {
|
||||
$ckey = config_cache($dkey.'key');
|
||||
}
|
||||
if (strpos($secret, '*') === false && $secret != config_cache($dkey.'secret')) {
|
||||
array_push($changes, 'secret');
|
||||
} else {
|
||||
$csecret = config_cache($dkey.'secret');
|
||||
}
|
||||
if ($region != config_cache($dkey.'region')) {
|
||||
array_push($changes, 'region');
|
||||
}
|
||||
if ($bucket != config_cache($dkey.'bucket')) {
|
||||
array_push($changes, 'bucket');
|
||||
}
|
||||
if ($visibility != config_cache($dkey.'visibility')) {
|
||||
array_push($changes, 'visibility');
|
||||
}
|
||||
if ($url != config_cache($dkey.'url')) {
|
||||
array_push($changes, 'url');
|
||||
}
|
||||
if ($endpoint != config_cache($dkey.'endpoint')) {
|
||||
array_push($changes, 'endpoint');
|
||||
}
|
||||
|
||||
if ($changes && count($changes)) {
|
||||
$isValid = FilesystemService::getVerifyCredentials(
|
||||
$ckey ?? $key,
|
||||
$csecret ?? $secret,
|
||||
$region,
|
||||
$bucket,
|
||||
$endpoint,
|
||||
);
|
||||
if (! $isValid) {
|
||||
return response()->json(['error' => true, 's3_vce' => true, 'message' => "<div class='border border-danger text-danger p-3 font-weight-bold rounded-lg'>The S3/Spaces credentials you provided are invalid, or the bucket does not have the proper permissions.</div><br/>Please check all fields and try again.<br/><br/><strong>Any cloud storage configuration changes you made have NOT been saved due to invalid credentials.</strong>"], 400);
|
||||
}
|
||||
}
|
||||
$res['changes'] = json_encode($changes);
|
||||
}
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
Cache::forget('api:v2:instance-data-response-v2');
|
||||
Config::refresh();
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,57 +2,47 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Contact,
|
||||
Hashtag,
|
||||
Instance,
|
||||
Newsroom,
|
||||
OauthClient,
|
||||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
Story,
|
||||
User
|
||||
};
|
||||
use DB, Cache, Storage;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Http\Controllers\Admin\{
|
||||
AdminAutospamController,
|
||||
AdminDirectoryController,
|
||||
AdminDiscoverController,
|
||||
AdminHashtagsController,
|
||||
AdminInstanceController,
|
||||
AdminReportController,
|
||||
// AdminGroupsController,
|
||||
AdminMediaController,
|
||||
AdminSettingsController,
|
||||
// AdminStorageController,
|
||||
AdminSupportController,
|
||||
AdminUserController
|
||||
};
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Services\AdminStatsService;
|
||||
use App\Contact;
|
||||
use App\Http\Controllers\Admin\AdminAutospamController;
|
||||
use App\Http\Controllers\Admin\AdminDirectoryController;
|
||||
use App\Http\Controllers\Admin\AdminDiscoverController;
|
||||
use App\Http\Controllers\Admin\AdminHashtagsController;
|
||||
use App\Http\Controllers\Admin\AdminInstanceController;
|
||||
use App\Http\Controllers\Admin\AdminMediaController;
|
||||
use App\Http\Controllers\Admin\AdminReportController;
|
||||
use App\Http\Controllers\Admin\AdminSettingsController;
|
||||
use App\Http\Controllers\Admin\AdminUserController;
|
||||
use App\Instance;
|
||||
use App\Mail\AdminMessageResponse;
|
||||
use App\Models\CustomEmoji;
|
||||
use App\Newsroom;
|
||||
use App\OauthClient;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminStatsService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\StoryService;
|
||||
use App\Models\CustomEmoji;
|
||||
use App\Status;
|
||||
use App\Story;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Mail;
|
||||
use Storage;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
use AdminReportController,
|
||||
AdminAutospamController,
|
||||
use AdminAutospamController,
|
||||
AdminDirectoryController,
|
||||
AdminDiscoverController,
|
||||
AdminHashtagsController,
|
||||
// AdminGroupsController,
|
||||
AdminMediaController,
|
||||
AdminSettingsController,
|
||||
AdminInstanceController,
|
||||
// AdminStorageController,
|
||||
AdminMediaController,
|
||||
AdminReportController,
|
||||
AdminSettingsController,
|
||||
AdminUserController;
|
||||
|
||||
public function __construct()
|
||||
|
@ -67,9 +57,27 @@ class AdminController extends Controller
|
|||
return view('admin.home');
|
||||
}
|
||||
|
||||
public function customCss()
|
||||
{
|
||||
return view('admin.settings.customcss');
|
||||
}
|
||||
|
||||
public function saveCustomCss(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'css' => 'sometimes|max:5000',
|
||||
'show' => 'sometimes',
|
||||
]);
|
||||
ConfigCacheService::put('uikit.custom.css', $request->input('css'));
|
||||
ConfigCacheService::put('uikit.show_custom.css', $request->boolean('show'));
|
||||
|
||||
return view('admin.settings.customcss');
|
||||
}
|
||||
|
||||
public function stats()
|
||||
{
|
||||
$data = AdminStatsService::get();
|
||||
|
||||
return view('admin.stats', compact('data'));
|
||||
}
|
||||
|
||||
|
@ -83,25 +91,27 @@ class AdminController extends Controller
|
|||
$users = User::orderByDesc('id')->cursorPaginate(10);
|
||||
|
||||
$res = [
|
||||
"next_page_url" => $users->nextPageUrl(),
|
||||
"data" => $users->map(function($user) {
|
||||
'next_page_url' => $users->nextPageUrl(),
|
||||
'data' => $users->map(function ($user) {
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
if (! $account) {
|
||||
return [
|
||||
"id" => $user->profile_id,
|
||||
"username" => $user->username,
|
||||
"status" => "deleted",
|
||||
"avatar" => "/storage/avatars/default.jpg",
|
||||
"created_at" => $user->created_at
|
||||
'id' => $user->profile_id,
|
||||
'username' => $user->username,
|
||||
'status' => 'deleted',
|
||||
'avatar' => '/storage/avatars/default.jpg',
|
||||
'created_at' => $user->created_at,
|
||||
];
|
||||
}
|
||||
$account['user_id'] = $user->id;
|
||||
|
||||
return $account;
|
||||
})
|
||||
->filter(function ($user) {
|
||||
return $user;
|
||||
})
|
||||
}),
|
||||
];
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -112,14 +122,15 @@ class AdminController extends Controller
|
|||
->cursorPaginate(10);
|
||||
|
||||
$res = [
|
||||
"next_page_url" => $posts->nextPageUrl(),
|
||||
"data" => $posts->map(function($post) {
|
||||
'next_page_url' => $posts->nextPageUrl(),
|
||||
'data' => $posts->map(function ($post) {
|
||||
$status = StatusService::get($post->id, false);
|
||||
if (! $status) {
|
||||
return ["id" => $post->id, "created_at" => $post->created_at];
|
||||
return ['id' => $post->id, 'created_at' => $post->created_at];
|
||||
}
|
||||
|
||||
return $status;
|
||||
})
|
||||
}),
|
||||
];
|
||||
|
||||
return $res;
|
||||
|
@ -140,6 +151,7 @@ class AdminController extends Controller
|
|||
return $s;
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return view('admin.statuses.home', compact('statuses', 'data'));
|
||||
}
|
||||
|
||||
|
@ -157,8 +169,8 @@ class AdminController extends Controller
|
|||
'filter' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::in(['all', 'local', 'remote'])
|
||||
]
|
||||
Rule::in(['all', 'local', 'remote']),
|
||||
],
|
||||
]);
|
||||
$search = $request->input('search');
|
||||
$filter = $request->input('filter');
|
||||
|
@ -174,6 +186,7 @@ class AdminController extends Controller
|
|||
if ($filter == 'remote') {
|
||||
return $q->whereNotNull('domain');
|
||||
}
|
||||
|
||||
return $q;
|
||||
})->orderByDesc('id')
|
||||
->simplePaginate($limit);
|
||||
|
@ -185,6 +198,7 @@ class AdminController extends Controller
|
|||
{
|
||||
$profile = Profile::findOrFail($id);
|
||||
$user = $profile->user;
|
||||
|
||||
return view('admin.profiles.edit', compact('profile', 'user'));
|
||||
}
|
||||
|
||||
|
@ -203,39 +217,120 @@ class AdminController extends Controller
|
|||
->orderByDesc('id')
|
||||
->paginate(10);
|
||||
}
|
||||
|
||||
return view('admin.apps.home', compact('apps'));
|
||||
}
|
||||
|
||||
public function messagesHome(Request $request)
|
||||
{
|
||||
$messages = Contact::orderByDesc('id')->paginate(10);
|
||||
return view('admin.messages.home', compact('messages'));
|
||||
$this->validate($request, [
|
||||
'sort' => 'sometimes|string|in:all,open,closed',
|
||||
]);
|
||||
$sort = $request->input('sort', 'open');
|
||||
|
||||
$messages = Contact::when($sort, function ($query, $sort) {
|
||||
if ($sort === 'open') {
|
||||
$query->whereNull('read_at');
|
||||
}
|
||||
if ($sort === 'closed') {
|
||||
$query->whereNotNull('read_at');
|
||||
}
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.messages.home', compact('messages', 'sort'));
|
||||
}
|
||||
|
||||
public function messagesShow(Request $request, $id)
|
||||
{
|
||||
$message = Contact::findOrFail($id);
|
||||
$user = User::whereNull('status')->find($message->user_id);
|
||||
if(!$user) {
|
||||
$message->read_at = now();
|
||||
$message->save();
|
||||
return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account');
|
||||
}
|
||||
|
||||
return view('admin.messages.show', compact('message'));
|
||||
}
|
||||
|
||||
public function messagesReply(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'message' => 'required|string|min:1|max:500',
|
||||
]);
|
||||
|
||||
if(config('mail.default') === 'log') {
|
||||
return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.');
|
||||
}
|
||||
|
||||
$message = Contact::whereNull('responded_at')->findOrFail($id);
|
||||
$user = User::whereNull('status')->find($message->user_id);
|
||||
if(!$user) {
|
||||
$message->read_at = now();
|
||||
$message->save();
|
||||
return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account');
|
||||
}
|
||||
$message->response = $request->input('message');
|
||||
$message->read_at = now();
|
||||
$message->responded_at = now();
|
||||
$message->save();
|
||||
|
||||
Mail::to($message->user->email)->send(new AdminMessageResponse($message));
|
||||
|
||||
return redirect('/i/admin/messages/home')->with('status', 'Sent response to '.$message->user->username);
|
||||
}
|
||||
|
||||
public function messagesReplyPreview(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'message' => 'required|string|min:1|max:500',
|
||||
]);
|
||||
|
||||
if(config('mail.default') === 'log') {
|
||||
return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.');
|
||||
}
|
||||
|
||||
$message = Contact::whereNull('read_at')->findOrFail($id);
|
||||
$user = User::whereNull('status')->find($message->user_id);
|
||||
if(!$user) {
|
||||
$message->read_at = now();
|
||||
$message->save();
|
||||
return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account');
|
||||
}
|
||||
return new AdminMessageResponse($message);
|
||||
}
|
||||
|
||||
public function messagesMarkRead(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1'
|
||||
'id' => 'required|integer|min:1',
|
||||
]);
|
||||
$id = $request->input('id');
|
||||
$message = Contact::findOrFail($id);
|
||||
|
||||
$user = User::whereNull('status')->find($message->user_id);
|
||||
if(!$user) {
|
||||
$message->read_at = now();
|
||||
$message->save();
|
||||
return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account');
|
||||
}
|
||||
if ($message->read_at) {
|
||||
return;
|
||||
}
|
||||
$message->read_at = now();
|
||||
$message->save();
|
||||
return;
|
||||
$request->session()->flash('status', 'Marked response from '.$message->user->username.' as read!');
|
||||
|
||||
return ['status' => 200];
|
||||
}
|
||||
|
||||
public function newsroomHome(Request $request)
|
||||
{
|
||||
$newsroom = Newsroom::latest()->paginate(10);
|
||||
|
||||
return view('admin.newsroom.home', compact('newsroom'));
|
||||
}
|
||||
|
||||
|
@ -247,6 +342,7 @@ class AdminController extends Controller
|
|||
public function newsroomEdit(Request $request, $id)
|
||||
{
|
||||
$news = Newsroom::findOrFail($id);
|
||||
|
||||
return view('admin.newsroom.edit', compact('news'));
|
||||
}
|
||||
|
||||
|
@ -254,6 +350,7 @@ class AdminController extends Controller
|
|||
{
|
||||
$news = Newsroom::findOrFail($id);
|
||||
$news->delete();
|
||||
|
||||
return redirect('/i/admin/newsroom');
|
||||
}
|
||||
|
||||
|
@ -262,7 +359,7 @@ class AdminController extends Controller
|
|||
$this->validate($request, [
|
||||
'title' => 'required|string|min:1|max:100',
|
||||
'summary' => 'nullable|string|max:200',
|
||||
'body' => 'nullable|string'
|
||||
'body' => 'nullable|string',
|
||||
]);
|
||||
$changed = false;
|
||||
$changedFields = [];
|
||||
|
@ -280,7 +377,7 @@ class AdminController extends Controller
|
|||
'auth_only' => 'boolean',
|
||||
'show_link' => 'boolean',
|
||||
'force_modal' => 'boolean',
|
||||
'published' => 'published'
|
||||
'published' => 'published',
|
||||
];
|
||||
foreach ($fields as $field => $type) {
|
||||
switch ($type) {
|
||||
|
@ -320,16 +417,16 @@ class AdminController extends Controller
|
|||
$news->save();
|
||||
}
|
||||
$redirect = $news->published_at ? $news->permalink() : $news->editUrl();
|
||||
|
||||
return redirect($redirect);
|
||||
}
|
||||
|
||||
|
||||
public function newsroomStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'title' => 'required|string|min:1|max:100',
|
||||
'summary' => 'nullable|string|max:200',
|
||||
'body' => 'nullable|string'
|
||||
'body' => 'nullable|string',
|
||||
]);
|
||||
$changed = false;
|
||||
$changedFields = [];
|
||||
|
@ -337,7 +434,7 @@ class AdminController extends Controller
|
|||
if (Newsroom::whereSlug($slug)->exists()) {
|
||||
$slug = $slug.'-'.str_random(4);
|
||||
}
|
||||
$news = new Newsroom();
|
||||
$news = new Newsroom;
|
||||
$fields = [
|
||||
'title' => 'string',
|
||||
'summary' => 'string',
|
||||
|
@ -347,7 +444,7 @@ class AdminController extends Controller
|
|||
'auth_only' => 'boolean',
|
||||
'show_link' => 'boolean',
|
||||
'force_modal' => 'boolean',
|
||||
'published' => 'published'
|
||||
'published' => 'published',
|
||||
];
|
||||
foreach ($fields as $field => $type) {
|
||||
switch ($type) {
|
||||
|
@ -387,6 +484,7 @@ class AdminController extends Controller
|
|||
$news->save();
|
||||
}
|
||||
$redirect = $news->published_at ? $news->permalink() : $news->editUrl();
|
||||
|
||||
return redirect($redirect);
|
||||
}
|
||||
|
||||
|
@ -398,7 +496,7 @@ class AdminController extends Controller
|
|||
public function diagnosticsDecrypt(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'payload' => 'required'
|
||||
'payload' => 'required',
|
||||
]);
|
||||
|
||||
$key = 'exception_report:';
|
||||
|
@ -409,7 +507,7 @@ class AdminController extends Controller
|
|||
}
|
||||
|
||||
$res = [
|
||||
'decrypted' => substr($decrypted, strlen($key))
|
||||
'decrypted' => substr($decrypted, strlen($key)),
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
|
@ -419,21 +517,23 @@ class AdminController extends Controller
|
|||
{
|
||||
$stories = Story::with('profile')->latest()->paginate(10);
|
||||
$stats = StoryService::adminStats();
|
||||
|
||||
return view('admin.stories.home', compact('stories', 'stats'));
|
||||
}
|
||||
|
||||
public function customEmojiHome(Request $request)
|
||||
{
|
||||
if(!config('federation.custom_emoji.enabled')) {
|
||||
if (! (bool) config_cache('federation.custom_emoji.enabled')) {
|
||||
return view('admin.custom-emoji.not-enabled');
|
||||
}
|
||||
$this->validate($request, [
|
||||
'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search'
|
||||
'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search',
|
||||
]);
|
||||
|
||||
if ($request->has('cc')) {
|
||||
Cache::forget('pf:admin:custom_emoji:stats');
|
||||
Cache::forget('pf:custom_emoji');
|
||||
|
||||
return redirect(route('admin.custom-emoji'));
|
||||
}
|
||||
|
||||
|
@ -470,6 +570,7 @@ class AdminController extends Controller
|
|||
$q = $q->groupBy('shortcode');
|
||||
}
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
})
|
||||
|
@ -497,24 +598,26 @@ class AdminController extends Controller
|
|||
|
||||
public function customEmojiToggleActive(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::findOrFail($id);
|
||||
$emoji->disabled = ! $emoji->disabled;
|
||||
$emoji->save();
|
||||
$key = CustomEmoji::CACHE_KEY.str_replace(':', '', $emoji->shortcode);
|
||||
Cache::forget($key);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function customEmojiAdd(Request $request)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
|
||||
return view('admin.custom-emoji.add');
|
||||
}
|
||||
|
||||
public function customEmojiStore(Request $request)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$this->validate($request, [
|
||||
'shortcode' => [
|
||||
'required',
|
||||
|
@ -525,9 +628,9 @@ class AdminController extends Controller
|
|||
Rule::unique('custom_emoji')->where(function ($query) use ($request) {
|
||||
return $query->whereDomain(config('pixelfed.domain.app'))
|
||||
->whereShortcode($request->input('shortcode'));
|
||||
})
|
||||
}),
|
||||
],
|
||||
'emoji' => 'required|file|mimes:jpg,png|max:' . (config('federation.custom_emoji.max_size') / 1000)
|
||||
'emoji' => 'required|file|mimes:jpg,png|max:'.(config('federation.custom_emoji.max_size') / 1000),
|
||||
]);
|
||||
|
||||
$emoji = new CustomEmoji;
|
||||
|
@ -540,24 +643,27 @@ class AdminController extends Controller
|
|||
$emoji->media_path = 'emoji/'.$fileName;
|
||||
$emoji->save();
|
||||
Cache::forget('pf:custom_emoji');
|
||||
|
||||
return redirect(route('admin.custom-emoji'));
|
||||
}
|
||||
|
||||
public function customEmojiDelete(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::findOrFail($id);
|
||||
Storage::delete("public/{$emoji->media_path}");
|
||||
Cache::forget('pf:custom_emoji');
|
||||
$emoji->delete();
|
||||
|
||||
return redirect(route('admin.custom-emoji'));
|
||||
}
|
||||
|
||||
public function customEmojiShowDuplicates(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
|
||||
$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
|
||||
|
||||
return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));
|
||||
}
|
||||
}
|
||||
|
|
340
app/Http/Controllers/AdminCuratedRegisterController.php
Normal file
340
app/Http/Controllers/AdminCuratedRegisterController.php
Normal file
|
@ -0,0 +1,340 @@
|
|||
<?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:3000',
|
||||
]);
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->register_id = $record->id;
|
||||
$activity->admin_id = $request->user()->id;
|
||||
$activity->secret_code = Str::random(32);
|
||||
$activity->type = 'request_details';
|
||||
$activity->from_admin = true;
|
||||
$activity->message = $request->input('message');
|
||||
$activity->save();
|
||||
$record->is_awaiting_more_info = true;
|
||||
$record->user_has_responded = false;
|
||||
$record->save();
|
||||
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function previewDetailsMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
|
||||
}
|
||||
|
||||
public function previewMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$record->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterSendMessage($record);
|
||||
}
|
||||
|
||||
public function apiHandleReject(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:reject-email,reject-silent',
|
||||
]);
|
||||
$action = $request->input('action');
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_rejected = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
if ($action === 'reject-email') {
|
||||
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
|
||||
}
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function apiHandleApprove(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_approved = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
|
||||
if (User::withTrashed()->whereEmail($record->email)->exists()) {
|
||||
return [200];
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'name' => $record->username,
|
||||
'username' => $record->username,
|
||||
'email' => $record->email,
|
||||
'password' => $record->password,
|
||||
'app_register_ip' => $record->ip_address,
|
||||
'email_verified_at' => now(),
|
||||
'register_source' => 'cur_onboarding',
|
||||
]);
|
||||
|
||||
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function templates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::paginate(10);
|
||||
|
||||
return view('admin.curated-register.templates', compact('templates'));
|
||||
}
|
||||
|
||||
public function templateCreate(Request $request)
|
||||
{
|
||||
return view('admin.curated-register.template-create');
|
||||
}
|
||||
|
||||
public function templateEdit(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
|
||||
return view('admin.curated-register.template-edit', compact('template'));
|
||||
}
|
||||
|
||||
public function templateEditStore(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->name = $request->input('name');
|
||||
$template->content = $request->input('content');
|
||||
$template->description = $request->input('description');
|
||||
$template->is_active = $request->boolean('active');
|
||||
$template->save();
|
||||
|
||||
return redirect()->back()->with('status', 'Successfully updated template!');
|
||||
}
|
||||
|
||||
public function templateDelete(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->delete();
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!');
|
||||
}
|
||||
|
||||
public function templateStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
CuratedRegisterTemplate::create([
|
||||
'name' => $request->input('name'),
|
||||
'content' => $request->input('content'),
|
||||
'description' => $request->input('description'),
|
||||
'is_active' => $request->boolean('active'),
|
||||
]);
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!');
|
||||
}
|
||||
|
||||
public function getActiveTemplates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::whereIsActive(true)
|
||||
->orderBy('order')
|
||||
->get()
|
||||
->map(function ($tmp) {
|
||||
return [
|
||||
'name' => $tmp->name,
|
||||
'content' => $tmp->content,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($templates);
|
||||
}
|
||||
}
|
|
@ -19,7 +19,8 @@ class AdminShadowFilterController extends Controller
|
|||
{
|
||||
$filter = $request->input('filter');
|
||||
$searchQuery = $request->input('q');
|
||||
$filters = AdminShadowFilter::when($filter, function($q, $filter) {
|
||||
$filters = AdminShadowFilter::whereHas('profile')
|
||||
->when($filter, function($q, $filter) {
|
||||
if($filter == 'all') {
|
||||
return $q;
|
||||
} else if($filter == 'inactive') {
|
||||
|
|
|
@ -2,66 +2,68 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\AccountInterstitial;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\AdminInstance;
|
||||
use App\Http\Resources\AdminUser;
|
||||
use App\Instance;
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use Auth, Cache, DB;
|
||||
use Carbon\Carbon;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Instance,
|
||||
Like,
|
||||
Notification,
|
||||
Media,
|
||||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
User
|
||||
};
|
||||
use App\Models\Conversation;
|
||||
use App\Models\RemoteReport;
|
||||
use App\Notification;
|
||||
use App\Profile;
|
||||
use App\Report;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminStatsService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Services\ModLogService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Http\Resources\AdminInstance;
|
||||
use App\Http\Resources\AdminUser;
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AdminApiController extends Controller
|
||||
{
|
||||
public function supported(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
return response()->json(['supported' => true]);
|
||||
}
|
||||
|
||||
public function getStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$res = AdminStatsService::summary();
|
||||
$res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->count();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function autospam(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$appeals = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
|
@ -73,7 +75,7 @@ class AdminApiController extends Controller
|
|||
'type' => $report->type,
|
||||
'item_id' => $report->item_id,
|
||||
'item_type' => $report->item_type,
|
||||
'created_at' => $report->created_at
|
||||
'created_at' => $report->created_at,
|
||||
];
|
||||
if ($report->item_type === 'App\\Status') {
|
||||
$status = StatusService::get($report->item_id, false);
|
||||
|
@ -87,6 +89,7 @@ class AdminApiController extends Controller
|
|||
$r['parent'] = StatusService::get($status['in_reply_to_id'], false);
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
});
|
||||
|
||||
|
@ -95,12 +98,14 @@ class AdminApiController extends Controller
|
|||
|
||||
public function autospamHandle(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account',
|
||||
'id' => 'required'
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
@ -122,6 +127,7 @@ class AdminApiController extends Controller
|
|||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$profile->id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$profile->id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -140,6 +146,7 @@ class AdminApiController extends Controller
|
|||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
StatusDelete::dispatch($appeal->status)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -159,6 +166,7 @@ class AdminApiController extends Controller
|
|||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
DeleteAccountPipeline::dispatch($appeal->user)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -171,6 +179,7 @@ class AdminApiController extends Controller
|
|||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -198,6 +207,7 @@ class AdminApiController extends Controller
|
|||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -231,6 +241,7 @@ class AdminApiController extends Controller
|
|||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -239,8 +250,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function modReports(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$reports = Report::whereNull('admin_seen')
|
||||
->orderBy('created_at', 'desc')
|
||||
|
@ -252,7 +265,7 @@ class AdminApiController extends Controller
|
|||
'message' => $report->message,
|
||||
'object_id' => $report->object_id,
|
||||
'object_type' => $report->object_type,
|
||||
'created_at' => $report->created_at
|
||||
'created_at' => $report->created_at,
|
||||
];
|
||||
|
||||
if ($report->profile_id) {
|
||||
|
@ -267,14 +280,18 @@ class AdminApiController extends Controller
|
|||
|
||||
$r['status'] = $status;
|
||||
|
||||
if($status['in_reply_to_id']) {
|
||||
if (isset($status['in_reply_to_id'])) {
|
||||
$r['parent'] = StatusService::get($status['in_reply_to_id'], false);
|
||||
}
|
||||
}
|
||||
|
||||
if ($report->object_type === 'App\\Profile') {
|
||||
$r['account'] = AccountService::get($report->object_id, false);
|
||||
$acct = AccountService::get($report->object_id, true);
|
||||
if ($acct) {
|
||||
$r['account'] = $acct;
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
})
|
||||
->filter()
|
||||
|
@ -285,12 +302,14 @@ class AdminApiController extends Controller
|
|||
|
||||
public function modReportHandle(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|string',
|
||||
'id' => 'required'
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
@ -299,7 +318,7 @@ class AdminApiController extends Controller
|
|||
$actions = [
|
||||
'ignore',
|
||||
'cw',
|
||||
'unlist'
|
||||
'unlist',
|
||||
];
|
||||
|
||||
if (! in_array($action, $actions)) {
|
||||
|
@ -343,56 +362,63 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getConfiguration(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
return collect([
|
||||
[
|
||||
'name' => 'ActivityPub Federation',
|
||||
'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
|
||||
'key' => 'federation.activitypub.enabled'
|
||||
'key' => 'federation.activitypub.enabled',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Open Registration',
|
||||
'description' => 'Allow new account registrations.',
|
||||
'key' => 'pixelfed.open_registration'
|
||||
'key' => 'pixelfed.open_registration',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Stories',
|
||||
'description' => 'Enable the ephemeral Stories feature.',
|
||||
'key' => 'instance.stories.enabled'
|
||||
'key' => 'instance.stories.enabled',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Require Email Verification',
|
||||
'description' => 'Require new accounts to verify their email address.',
|
||||
'key' => 'pixelfed.enforce_email_verification'
|
||||
'key' => 'pixelfed.enforce_email_verification',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'AutoSpam Detection',
|
||||
'description' => 'Detect and remove spam from public timelines.',
|
||||
'key' => 'pixelfed.bouncer.enabled'
|
||||
'key' => 'pixelfed.bouncer.enabled',
|
||||
],
|
||||
])
|
||||
->map(function ($s) {
|
||||
$s['state'] = (bool) config_cache($s['key']);
|
||||
|
||||
return $s;
|
||||
});
|
||||
}
|
||||
|
||||
public function updateConfiguration(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
$this->validate($request, [
|
||||
'key' => 'required',
|
||||
'value' => 'required'
|
||||
'value' => 'required',
|
||||
]);
|
||||
|
||||
$allowedKeys = [
|
||||
|
@ -413,43 +439,47 @@ class AdminApiController extends Controller
|
|||
[
|
||||
'name' => 'ActivityPub Federation',
|
||||
'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
|
||||
'key' => 'federation.activitypub.enabled'
|
||||
'key' => 'federation.activitypub.enabled',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Open Registration',
|
||||
'description' => 'Allow new account registrations.',
|
||||
'key' => 'pixelfed.open_registration'
|
||||
'key' => 'pixelfed.open_registration',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Stories',
|
||||
'description' => 'Enable the ephemeral Stories feature.',
|
||||
'key' => 'instance.stories.enabled'
|
||||
'key' => 'instance.stories.enabled',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Require Email Verification',
|
||||
'description' => 'Require new accounts to verify their email address.',
|
||||
'key' => 'pixelfed.enforce_email_verification'
|
||||
'key' => 'pixelfed.enforce_email_verification',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'AutoSpam Detection',
|
||||
'description' => 'Detect and remove spam from public timelines.',
|
||||
'key' => 'pixelfed.bouncer.enabled'
|
||||
'key' => 'pixelfed.bouncer.enabled',
|
||||
],
|
||||
])
|
||||
->map(function ($s) {
|
||||
$s['state'] = (bool) config_cache($s['key']);
|
||||
|
||||
return $s;
|
||||
});
|
||||
}
|
||||
|
||||
public function getUsers(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
]);
|
||||
|
@ -461,19 +491,23 @@ class AdminApiController extends Controller
|
|||
})
|
||||
->orderBy('id', $sort)
|
||||
->cursorPaginate(10);
|
||||
|
||||
return AdminUser::collection($res);
|
||||
}
|
||||
|
||||
public function getUser(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$id = $request->input('user_id');
|
||||
$key = 'pf-admin-api:getUser:byId:'.$id;
|
||||
if ($request->has('refresh')) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
|
||||
return Cache::remember($key, 86400, function () use ($id) {
|
||||
$user = User::findOrFail($id);
|
||||
$profile = $user->profile;
|
||||
|
@ -487,8 +521,8 @@ class AdminApiController extends Controller
|
|||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
'no_autolink' => (bool) $profile->no_autolink,
|
||||
],
|
||||
]]);
|
||||
|
||||
return $res;
|
||||
|
@ -497,13 +531,15 @@ class AdminApiController extends Controller
|
|||
|
||||
public function userAdminAction(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email,delete',
|
||||
'value' => 'sometimes'
|
||||
'value' => 'sometimes',
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
|
@ -561,6 +597,7 @@ class AdminApiController extends Controller
|
|||
AccountService::del($profile->id);
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 200,
|
||||
'msg' => 'deleted',
|
||||
|
@ -587,7 +624,7 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => 'Manually verified email address',
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -600,7 +637,7 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -615,7 +652,7 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -630,7 +667,7 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -648,7 +685,7 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -662,15 +699,17 @@ class AdminApiController extends Controller
|
|||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
'no_autolink' => (bool) $profile->no_autolink,
|
||||
],
|
||||
]]);
|
||||
}
|
||||
|
||||
public function instances(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'sometimes',
|
||||
|
@ -707,8 +746,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$id = $request->input('id');
|
||||
$res = Instance::findOrFail($id);
|
||||
|
@ -718,13 +759,15 @@ class AdminApiController extends Controller
|
|||
|
||||
public function moderateInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'key' => 'required|in:unlisted,auto_cw,banned',
|
||||
'value' => 'required'
|
||||
'value' => 'required',
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
|
@ -742,8 +785,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function refreshInstanceStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
|
@ -760,8 +805,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getAllStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin === 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
if ($request->has('refresh')) {
|
||||
Cache::forget('admin-api:instance-all-stats-v1');
|
||||
|
@ -781,28 +828,28 @@ class AdminApiController extends Controller
|
|||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => User::whereDate('created_at', now()->subDays($day))->count()
|
||||
'count' => User::whereDate('created_at', now()->subDays($day))->count(),
|
||||
];
|
||||
|
||||
$res['posts']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count()
|
||||
'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count(),
|
||||
];
|
||||
|
||||
$res['instances']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => Instance::whereDate('created_at', now()->subDays($day))->count()
|
||||
'count' => Instance::whereDate('created_at', now()->subDays($day))->count(),
|
||||
];
|
||||
}
|
||||
|
||||
$res['users']['total'] = DB::table('users')->count();
|
||||
$res['users']['min'] = collect($res['users']['days'])->min('count');
|
||||
$res['users']['max'] = collect($res['users']['days'])->max('count');
|
||||
$res['users']['change'] = collect($res['users']['days'])->sum('count');;
|
||||
$res['users']['change'] = collect($res['users']['days'])->sum('count');
|
||||
$res['posts']['total'] = DB::table('statuses')->whereNull('uri')->count();
|
||||
$res['posts']['min'] = collect($res['posts']['days'])->min('count');
|
||||
$res['posts']['max'] = collect($res['posts']['days'])->max('count');
|
||||
|
|
38
app/Http/Controllers/Api/ApiController.php
Normal file
38
app/Http/Controllers/Api/ApiController.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class ApiController extends Controller {
|
||||
public function json($res, $headers = [], $code = 200) {
|
||||
return response()->json($res, $code, $this->filterHeaders($headers), JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function linksForCollection($paginator) {
|
||||
$link = null;
|
||||
|
||||
if ($paginator->onFirstPage()) {
|
||||
if ($paginator->hasMorePages()) {
|
||||
$link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
} else {
|
||||
if ($paginator->previousPageUrl()) {
|
||||
$link = '<'.$paginator->previousPageUrl().'>; rel="next"';
|
||||
}
|
||||
|
||||
if ($paginator->hasMorePages()) {
|
||||
$link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
private function filterHeaders($headers) {
|
||||
return array_filter($headers, function($v, $k) {
|
||||
return $v != null;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -2,44 +2,53 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Cache;
|
||||
use DB;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\AccountLog;
|
||||
use App\EmailVerification;
|
||||
use App\Follower;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\StatusController;
|
||||
use App\Http\Resources\StatusStateless;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Jobs\StatusPipeline\RemoteStatusDelete;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\Jobs\VideoPipeline\VideoThumbnail;
|
||||
use App\Mail\ConfirmAppEmail;
|
||||
use App\Mail\PasswordChange;
|
||||
use App\Media;
|
||||
use App\Place;
|
||||
use App\Status;
|
||||
use App\Report;
|
||||
use App\Profile;
|
||||
use App\Report;
|
||||
use App\Rules\ExpoPushTokenRule;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Services\NotificationAppGatewayService;
|
||||
use App\Services\ProfileStatusService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\PushNotificationService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Status;
|
||||
use App\StatusArchived;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\ProfileStatusService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\ReblogService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\EmailService;
|
||||
use Illuminate\Support\Str;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Jenssegers\Agent\Agent;
|
||||
use Mail;
|
||||
use App\Mail\PasswordChange;
|
||||
use App\Mail\ConfirmAppEmail;
|
||||
use App\Http\Resources\StatusStateless;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\Jobs\StatusPipeline\RemoteStatusDelete;
|
||||
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Jenssegers\Agent\Agent;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use Mail;
|
||||
use Purify;
|
||||
|
||||
class ApiV1Dot1Controller extends Controller
|
||||
{
|
||||
|
@ -47,8 +56,8 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
public function __construct()
|
||||
{
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
$this->fractal = new Fractal\Manager;
|
||||
$this->fractal->setSerializer(new ArraySerializer);
|
||||
}
|
||||
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
|
@ -59,17 +68,19 @@ class ApiV1Dot1Controller extends Controller
|
|||
public function error($msg, $code = 400, $extra = [], $headers = [])
|
||||
{
|
||||
$res = [
|
||||
"msg" => $msg,
|
||||
"code" => $code
|
||||
'msg' => $msg,
|
||||
'code' => $code,
|
||||
];
|
||||
|
||||
return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function report(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
abort_if(!$user, 403);
|
||||
$user = $request->user();
|
||||
abort_if($user->status != null, 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
|
@ -89,19 +100,19 @@ class ApiV1Dot1Controller extends Controller
|
|||
'copyright',
|
||||
'impersonation',
|
||||
'scam',
|
||||
'terrorism'
|
||||
'terrorism',
|
||||
];
|
||||
|
||||
if (! $report_type || ! $object_id || ! $object_type) {
|
||||
return $this->error("Invalid or missing parameters", 400, ["error_code" => "ERROR_INVALID_PARAMS"]);
|
||||
return $this->error('Invalid or missing parameters', 400, ['error_code' => 'ERROR_INVALID_PARAMS']);
|
||||
}
|
||||
|
||||
if (! in_array($report_type, $types)) {
|
||||
return $this->error("Invalid report type", 400, ["error_code" => "ERROR_TYPE_INVALID"]);
|
||||
return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_TYPE_INVALID']);
|
||||
}
|
||||
|
||||
if ($object_type === "user" && $object_id == $user->profile_id) {
|
||||
return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
|
||||
if ($object_type === 'user' && $object_id == $user->profile_id) {
|
||||
return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']);
|
||||
}
|
||||
|
||||
$rpid = null;
|
||||
|
@ -110,7 +121,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
case 'post':
|
||||
$object = Status::find($object_id);
|
||||
if (! $object) {
|
||||
return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
|
||||
return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']);
|
||||
}
|
||||
$object_type = 'App\Status';
|
||||
$exists = Report::whereUserId($user->id)
|
||||
|
@ -124,7 +135,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
case 'user':
|
||||
$object = Profile::find($object_id);
|
||||
if (! $object) {
|
||||
return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
|
||||
return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']);
|
||||
}
|
||||
$object_type = 'App\Profile';
|
||||
$exists = Report::whereUserId($user->id)
|
||||
|
@ -135,16 +146,16 @@ class ApiV1Dot1Controller extends Controller
|
|||
break;
|
||||
|
||||
default:
|
||||
return $this->error("Invalid report type", 400, ["error_code" => "ERROR_REPORT_OBJECT_TYPE_INVALID"]);
|
||||
return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_REPORT_OBJECT_TYPE_INVALID']);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($exists !== 0) {
|
||||
return $this->error("Duplicate report", 400, ["error_code" => "ERROR_REPORT_DUPLICATE"]);
|
||||
return $this->error('Duplicate report', 400, ['error_code' => 'ERROR_REPORT_DUPLICATE']);
|
||||
}
|
||||
|
||||
if ($object->profile_id == $user->profile_id) {
|
||||
return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
|
||||
return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']);
|
||||
}
|
||||
|
||||
$report = new Report;
|
||||
|
@ -161,9 +172,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
}
|
||||
|
||||
$res = [
|
||||
"msg" => "Successfully sent report",
|
||||
"code" => 200
|
||||
'msg' => 'Successfully sent report',
|
||||
'code' => 200,
|
||||
];
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
|
@ -174,9 +186,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
*/
|
||||
public function deleteAvatar(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
abort_if(!$user, 403);
|
||||
$user = $request->user();
|
||||
abort_if($user->status != null, 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
|
@ -214,9 +227,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
*/
|
||||
public function accountPosts(Request $request, $id)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
abort_if(!$user, 403);
|
||||
$user = $request->user();
|
||||
abort_if($user->status != null, 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
|
@ -254,8 +268,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
*/
|
||||
public function accountChangePassword(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 403);
|
||||
abort_if($user->status != null, 403);
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -264,9 +280,9 @@ class ApiV1Dot1Controller extends Controller
|
|||
$this->validate($request, [
|
||||
'current_password' => 'bail|required|current_password',
|
||||
'new_password' => 'required|min:'.config('pixelfed.min_password_length', 8),
|
||||
'confirm_password' => 'required|same:new_password'
|
||||
'confirm_password' => 'required|same:new_password',
|
||||
], [
|
||||
'current_password' => 'The password you entered is incorrect'
|
||||
'current_password' => 'The password you entered is incorrect',
|
||||
]);
|
||||
|
||||
$user->password = bcrypt($request->input('new_password'));
|
||||
|
@ -295,13 +311,15 @@ class ApiV1Dot1Controller extends Controller
|
|||
*/
|
||||
public function accountLoginActivity(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 403);
|
||||
abort_if($user->status != null, 403);
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
$agent = new Agent();
|
||||
$agent = new Agent;
|
||||
$currentIp = $request->ip();
|
||||
|
||||
$activity = AccountLog::whereUserId($user->id)
|
||||
|
@ -312,6 +330,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
->get()
|
||||
->map(function ($item) use ($agent, $currentIp) {
|
||||
$agent->setUserAgent($item->user_agent);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'action' => $item->action,
|
||||
|
@ -321,7 +340,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
'device' => $agent->device(),
|
||||
'browser' => $agent->browser(),
|
||||
'platform' => $agent->platform(),
|
||||
'created_at' => $item->created_at->format('c')
|
||||
'created_at' => $item->created_at->format('c'),
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -335,8 +354,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
*/
|
||||
public function accountTwoFactor(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 403);
|
||||
abort_if($user->status != null, 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
|
@ -345,8 +366,9 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
$res = [
|
||||
'active' => (bool) $user->{'2fa_enabled'},
|
||||
'setup_at' => $user->{'2fa_setup_at'}
|
||||
'setup_at' => $user->{'2fa_setup_at'},
|
||||
];
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
|
@ -357,8 +379,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
*/
|
||||
public function accountEmailsFromPixelfed(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 403);
|
||||
abort_if($user->status != null, 403);
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -376,7 +400,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
'subject' => 'Confirm Email',
|
||||
'to_address' => $user->email,
|
||||
'from_address' => $from,
|
||||
'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A'))
|
||||
'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A')),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
@ -393,7 +417,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
'subject' => 'Reset Password Notification',
|
||||
'to_address' => $user->email,
|
||||
'from_address' => $from,
|
||||
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
|
||||
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
@ -410,7 +434,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
'subject' => 'Password Change',
|
||||
'to_address' => $user->email,
|
||||
'from_address' => $from,
|
||||
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
|
||||
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
@ -432,8 +456,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
*/
|
||||
public function accountApps(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 403);
|
||||
abort_if($user->status != null, 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
|
@ -448,7 +474,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
'scopes' => $token->scopes,
|
||||
'revoked' => $token->revoked,
|
||||
'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')),
|
||||
'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A'))
|
||||
'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A')),
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -459,15 +485,15 @@ class ApiV1Dot1Controller extends Controller
|
|||
{
|
||||
return [
|
||||
'open' => (bool) config_cache('pixelfed.open_registration'),
|
||||
'iara' => config('pixelfed.allow_app_registration')
|
||||
'iara' => (bool) config_cache('pixelfed.allow_app_registration'),
|
||||
];
|
||||
}
|
||||
|
||||
public function inAppRegistration(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
abort_unless(config_cache('pixelfed.open_registration'), 404);
|
||||
abort_unless(config('pixelfed.allow_app_registration'), 404);
|
||||
abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
|
||||
abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
|
||||
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -551,7 +577,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
$rtoken = Str::random(64);
|
||||
|
||||
$verify = new EmailVerification();
|
||||
$verify = new EmailVerification;
|
||||
$verify->user_id = $user->id;
|
||||
$verify->email = $user->email;
|
||||
$verify->user_token = $user->app_register_token;
|
||||
|
@ -561,7 +587,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
$params = http_build_query([
|
||||
'ut' => $user->app_register_token,
|
||||
'rt' => $rtoken,
|
||||
'ea' => base64_encode($user->email)
|
||||
'ea' => base64_encode($user->email),
|
||||
]);
|
||||
$appUrl = url('/api/v1.1/auth/iarer?'.$params);
|
||||
|
||||
|
@ -577,7 +603,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
$this->validate($request, [
|
||||
'ut' => 'required',
|
||||
'rt' => 'required',
|
||||
'ea' => 'required'
|
||||
'ea' => 'required',
|
||||
]);
|
||||
$ut = $request->input('ut');
|
||||
$rt = $request->input('rt');
|
||||
|
@ -586,17 +612,18 @@ class ApiV1Dot1Controller extends Controller
|
|||
'ut' => $ut,
|
||||
'rt' => $rt,
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'ea' => $ea
|
||||
'ea' => $ea,
|
||||
]);
|
||||
$url = 'pixelfed://confirm-account/'.$ut.'?'.$params;
|
||||
|
||||
return redirect()->away($url);
|
||||
}
|
||||
|
||||
public function inAppRegistrationConfirm(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
abort_unless(config_cache('pixelfed.open_registration'), 404);
|
||||
abort_unless(config('pixelfed.allow_app_registration'), 404);
|
||||
abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
|
||||
abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
|
||||
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -605,10 +632,10 @@ class ApiV1Dot1Controller extends Controller
|
|||
$rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function () {}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800));
|
||||
abort_if(! $rl, 429, 'Too many requests');
|
||||
|
||||
$this->validate($request, [
|
||||
$request->validate([
|
||||
'user_token' => 'required',
|
||||
'random_token' => 'required',
|
||||
'email' => 'required'
|
||||
'email' => 'required',
|
||||
]);
|
||||
|
||||
$verify = EmailVerification::whereEmail($request->input('email'))
|
||||
|
@ -622,6 +649,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
if ($verify->created_at->lt(now()->subHours(24))) {
|
||||
$verify->delete();
|
||||
|
||||
return response()->json(['error' => 'Invalid tokens'], 403);
|
||||
}
|
||||
|
||||
|
@ -630,16 +658,17 @@ class ApiV1Dot1Controller extends Controller
|
|||
$user->last_active_at = now();
|
||||
$user->save();
|
||||
|
||||
$token = $user->createToken('Pixelfed');
|
||||
$token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'admin:read', 'admin:write', 'push']);
|
||||
|
||||
return response()->json([
|
||||
'access_token' => $token->accessToken
|
||||
'access_token' => $token->accessToken,
|
||||
]);
|
||||
}
|
||||
|
||||
public function archive(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -671,7 +700,8 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
public function unarchive(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -702,7 +732,8 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
public function archivedPosts(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -718,7 +749,8 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
public function placesById(Request $request, $id, $slug)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
|
@ -742,29 +774,29 @@ class ApiV1Dot1Controller extends Controller
|
|||
->values();
|
||||
|
||||
return [
|
||||
'place' =>
|
||||
[
|
||||
'place' => [
|
||||
'id' => $place->id,
|
||||
'name' => $place->name,
|
||||
'slug' => $place->slug,
|
||||
'country' => $place->country,
|
||||
'lat' => $place->lat,
|
||||
'long' => $place->long
|
||||
'long' => $place->long,
|
||||
],
|
||||
'posts' => $posts];
|
||||
}
|
||||
|
||||
public function moderatePost(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_if($request->user()->is_admin != true, 403);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 403);
|
||||
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete'
|
||||
'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
@ -784,7 +816,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
$status->profile->update([
|
||||
'unlisted' => true,
|
||||
'cw' => true,
|
||||
'no_autolink' => true
|
||||
'no_autolink' => true,
|
||||
]);
|
||||
|
||||
Status::whereProfileId($status->profile_id)
|
||||
|
@ -853,6 +885,7 @@ class ApiV1Dot1Controller extends Controller
|
|||
StatusService::del($status->id, true);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -863,28 +896,33 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
public function getWebSettings(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$uid = $request->user()->id;
|
||||
$settings = UserSetting::firstOrCreate([
|
||||
'user_id' => $uid
|
||||
'user_id' => $uid,
|
||||
]);
|
||||
if (! $settings->other) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $settings->other;
|
||||
}
|
||||
|
||||
public function setWebSettings(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'field' => 'required|in:enable_reblogs,hide_reblog_banner',
|
||||
'value' => 'required'
|
||||
'value' => 'required',
|
||||
]);
|
||||
$field = $request->input('field');
|
||||
$value = $request->input('value');
|
||||
$settings = UserSetting::firstOrCreate([
|
||||
'user_id' => $request->user()->id
|
||||
'user_id' => $request->user()->id,
|
||||
]);
|
||||
if (! $settings->other) {
|
||||
$other = [];
|
||||
|
@ -897,4 +935,439 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function getMutualAccounts(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('follow'), 403);
|
||||
|
||||
$account = AccountService::get($id, true);
|
||||
if (! $account || ! isset($account['id'])) {
|
||||
return [];
|
||||
}
|
||||
$res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id))
|
||||
->map(function ($accountId) {
|
||||
return AccountService::get($accountId, true);
|
||||
})
|
||||
->filter()
|
||||
->take(24)
|
||||
->values();
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
public function accountUsernameToId(Request $request, $username)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token() || ! $username, 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
$username = trim($username);
|
||||
$rateLimiting = (bool) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.enabled');
|
||||
$ipRateLimiting = (bool) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_enabled');
|
||||
if ($ipRateLimiting) {
|
||||
$userLimit = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_limit');
|
||||
$userDecay = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.ip_decay');
|
||||
$userKey = 'pf:apiv1.1:acctU2ID:byIp:'.$request->ip();
|
||||
|
||||
if (RateLimiter::tooManyAttempts($userKey, $userLimit)) {
|
||||
$limits = [
|
||||
'X-Rate-Limit-Limit' => $userLimit,
|
||||
'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
|
||||
'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
|
||||
];
|
||||
|
||||
return $this->json(['error' => 'Too many attempts!'], 429, $limits);
|
||||
}
|
||||
|
||||
RateLimiter::increment($userKey, $userDecay);
|
||||
$limits = [
|
||||
'X-Rate-Limit-Limit' => $userLimit,
|
||||
'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
|
||||
'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
|
||||
];
|
||||
}
|
||||
if ($rateLimiting) {
|
||||
$userLimit = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.limit');
|
||||
$userDecay = (int) config_cache('api.rate-limits.v1Dot1.accounts.usernameToId.decay');
|
||||
$userKey = 'pf:apiv1.1:acctU2ID:byUid:'.$request->user()->id;
|
||||
|
||||
if (RateLimiter::tooManyAttempts($userKey, $userLimit)) {
|
||||
$limits = [
|
||||
'X-Rate-Limit-Limit' => $userLimit,
|
||||
'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
|
||||
'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
|
||||
];
|
||||
|
||||
return $this->json(['error' => 'Too many attempts!'], 429, $limits);
|
||||
}
|
||||
|
||||
RateLimiter::increment($userKey, $userDecay);
|
||||
$limits = [
|
||||
'X-Rate-Limit-Limit' => $userLimit,
|
||||
'X-Rate-Limit-Remaining' => RateLimiter::remaining($userKey, $userLimit),
|
||||
'X-Rate-Limit-Reset' => RateLimiter::availableIn($userKey),
|
||||
];
|
||||
}
|
||||
if (str_ends_with($username, config_cache('pixelfed.domain.app'))) {
|
||||
$pre = str_starts_with($username, '@') ? substr($username, 1) : $username;
|
||||
$parts = explode('@', $pre);
|
||||
$username = $parts[0];
|
||||
}
|
||||
$accountId = AccountService::usernameToId($username, true);
|
||||
if (! $accountId) {
|
||||
return [];
|
||||
}
|
||||
$account = AccountService::get($accountId);
|
||||
|
||||
return $this->json($account, 200, $rateLimiting ? $limits : []);
|
||||
}
|
||||
|
||||
public function getPushState(Request $request)
|
||||
{
|
||||
abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found');
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('push'), 403);
|
||||
abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.');
|
||||
$user = $request->user();
|
||||
abort_if($user->status, 422, 'Cannot access this resource at this time');
|
||||
$res = [
|
||||
'version' => PushNotificationService::PUSH_GATEWAY_VERSION,
|
||||
'username' => (string) $user->username,
|
||||
'profile_id' => (string) $user->profile_id,
|
||||
'notify_enabled' => (bool) $user->notify_enabled,
|
||||
'has_token' => (bool) $user->expo_token,
|
||||
'notify_like' => (bool) $user->notify_like,
|
||||
'notify_follow' => (bool) $user->notify_follow,
|
||||
'notify_mention' => (bool) $user->notify_mention,
|
||||
'notify_comment' => (bool) $user->notify_comment,
|
||||
];
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
public function disablePush(Request $request)
|
||||
{
|
||||
abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found');
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('push'), 403);
|
||||
abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.');
|
||||
abort_if($request->user()->status, 422, 'Cannot access this resource at this time');
|
||||
|
||||
$request->user()->update([
|
||||
'notify_enabled' => false,
|
||||
'expo_token' => null,
|
||||
'notify_like' => false,
|
||||
'notify_follow' => false,
|
||||
'notify_mention' => false,
|
||||
'notify_comment' => false,
|
||||
]);
|
||||
|
||||
PushNotificationService::removeMemberFromAll($request->user()->profile_id);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
return $this->json([
|
||||
'version' => PushNotificationService::PUSH_GATEWAY_VERSION,
|
||||
'username' => (string) $user->username,
|
||||
'profile_id' => (string) $user->profile_id,
|
||||
'notify_enabled' => (bool) $user->notify_enabled,
|
||||
'has_token' => (bool) $user->expo_token,
|
||||
'notify_like' => (bool) $user->notify_like,
|
||||
'notify_follow' => (bool) $user->notify_follow,
|
||||
'notify_mention' => (bool) $user->notify_mention,
|
||||
'notify_comment' => (bool) $user->notify_comment,
|
||||
]);
|
||||
}
|
||||
|
||||
public function comparePush(Request $request)
|
||||
{
|
||||
abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found');
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('push'), 403);
|
||||
abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.');
|
||||
abort_if($request->user()->status, 422, 'Cannot access this resource at this time');
|
||||
|
||||
$this->validate($request, [
|
||||
'expo_token' => ['required', 'string', new ExpoPushTokenRule],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if (empty($user->expo_token)) {
|
||||
return $this->json([
|
||||
'version' => PushNotificationService::PUSH_GATEWAY_VERSION,
|
||||
'username' => (string) $user->username,
|
||||
'profile_id' => (string) $user->profile_id,
|
||||
'notify_enabled' => (bool) $user->notify_enabled,
|
||||
'match' => false,
|
||||
'has_existing' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $request->input('expo_token');
|
||||
$knownToken = $user->expo_token;
|
||||
$match = hash_equals($knownToken, $token);
|
||||
|
||||
return $this->json([
|
||||
'version' => PushNotificationService::PUSH_GATEWAY_VERSION,
|
||||
'username' => (string) $user->username,
|
||||
'profile_id' => (string) $user->profile_id,
|
||||
'notify_enabled' => (bool) $user->notify_enabled,
|
||||
'match' => $match,
|
||||
'has_existing' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatePush(Request $request)
|
||||
{
|
||||
abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found');
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('push'), 403);
|
||||
abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.');
|
||||
abort_if($request->user()->status, 422, 'Cannot access this resource at this time');
|
||||
|
||||
$this->validate($request, [
|
||||
'notify_enabled' => 'required',
|
||||
'token' => ['required', 'string', new ExpoPushTokenRule],
|
||||
'notify_like' => 'sometimes',
|
||||
'notify_follow' => 'sometimes',
|
||||
'notify_mention' => 'sometimes',
|
||||
'notify_comment' => 'sometimes',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $pid, 422, 'An error occured');
|
||||
$expoToken = $request->input('token');
|
||||
|
||||
$existing = User::where('profile_id', '!=', $pid)->whereExpoToken($expoToken)->count();
|
||||
abort_if($existing && $existing > 5, 400, 'Push token is already used by another account');
|
||||
|
||||
$request->user()->update([
|
||||
'notify_enabled' => $request->boolean('notify_enabled'),
|
||||
'expo_token' => $expoToken,
|
||||
]);
|
||||
|
||||
if ($request->filled('notify_like')) {
|
||||
$request->user()->update(['notify_like' => (bool) $request->boolean('notify_like')]);
|
||||
$request->boolean('notify_like') == true ?
|
||||
PushNotificationService::set('like', $pid) :
|
||||
PushNotificationService::removeMember('like', $pid);
|
||||
}
|
||||
if ($request->filled('notify_follow')) {
|
||||
$request->user()->update(['notify_follow' => (bool) $request->boolean('notify_follow')]);
|
||||
$request->boolean('notify_follow') == true ?
|
||||
PushNotificationService::set('follow', $pid) :
|
||||
PushNotificationService::removeMember('follow', $pid);
|
||||
}
|
||||
if ($request->filled('notify_mention')) {
|
||||
$request->user()->update(['notify_mention' => (bool) $request->boolean('notify_mention')]);
|
||||
$request->boolean('notify_mention') == true ?
|
||||
PushNotificationService::set('mention', $pid) :
|
||||
PushNotificationService::removeMember('mention', $pid);
|
||||
}
|
||||
if ($request->filled('notify_comment')) {
|
||||
$request->user()->update(['notify_comment' => (bool) $request->boolean('notify_comment')]);
|
||||
$request->boolean('notify_comment') == true ?
|
||||
PushNotificationService::set('comment', $pid) :
|
||||
PushNotificationService::removeMember('comment', $pid);
|
||||
}
|
||||
|
||||
if ($request->boolean('notify_enabled') == false) {
|
||||
PushNotificationService::removeMemberFromAll($request->user()->profile_id);
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$res = [
|
||||
'version' => PushNotificationService::PUSH_GATEWAY_VERSION,
|
||||
'notify_enabled' => (bool) $user->notify_enabled,
|
||||
'has_token' => (bool) $user->expo_token,
|
||||
'notify_like' => (bool) $user->notify_like,
|
||||
'notify_follow' => (bool) $user->notify_follow,
|
||||
'notify_mention' => (bool) $user->notify_mention,
|
||||
'notify_comment' => (bool) $user->notify_comment,
|
||||
];
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1.1/status/create
|
||||
*
|
||||
*
|
||||
* @return StatusTransformer
|
||||
*/
|
||||
public function statusCreate(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'),
|
||||
'file' => [
|
||||
'required',
|
||||
'file',
|
||||
'mimetypes:'.config_cache('pixelfed.media_types'),
|
||||
'max:'.config_cache('pixelfed.max_photo_size'),
|
||||
function ($attribute, $value, $fail) {
|
||||
if (is_array($value) && count($value) > 1) {
|
||||
$fail('Only one file can be uploaded at a time.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'sensitive' => 'nullable',
|
||||
'visibility' => 'string|in:private,unlisted,public',
|
||||
'spoiler_text' => 'sometimes|max:140',
|
||||
]);
|
||||
|
||||
if ($request->hasHeader('idempotency-key')) {
|
||||
$key = 'pf:api:v1:status:idempotency-key:'.$request->user()->id.':'.hash('sha1', $request->header('idempotency-key'));
|
||||
$exists = Cache::has($key);
|
||||
abort_if($exists, 400, 'Duplicate idempotency key.');
|
||||
Cache::put($key, 1, 3600);
|
||||
}
|
||||
|
||||
if (config('costar.enabled') == true) {
|
||||
$blockedKeywords = config('costar.keyword.block');
|
||||
if ($blockedKeywords !== null && $request->status) {
|
||||
$keywords = config('costar.keyword.block');
|
||||
foreach ($keywords as $kw) {
|
||||
if (Str::contains($request->status, $kw) == true) {
|
||||
abort(400, 'Invalid object. Contains banned keyword.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$user = $request->user();
|
||||
|
||||
if ($user->has_roles) {
|
||||
abort_if(! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
|
||||
}
|
||||
|
||||
$profile = $user->profile;
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-upload:'.$user->id;
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$license = null;
|
||||
$mime = $photo->getMimeType();
|
||||
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
|
||||
if ($settings && ! empty($settings->compose_settings)) {
|
||||
$compose = $settings->compose_settings;
|
||||
|
||||
if (isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
$visibility = $profile->is_private ? 'private' : (
|
||||
$profile->unlisted == true &&
|
||||
$request->input('visibility', 'public') == 'public' ?
|
||||
'unlisted' :
|
||||
$request->input('visibility', 'public'));
|
||||
|
||||
if ($user->last_active_at == null) {
|
||||
return [];
|
||||
}
|
||||
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
|
||||
$content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : $defaultCaption;
|
||||
$cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
|
||||
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
|
||||
|
||||
$status = new Status;
|
||||
$status->caption = $content;
|
||||
$status->rendered = $defaultCaption;
|
||||
$status->profile_id = $user->profile_id;
|
||||
$status->is_nsfw = $cw;
|
||||
$status->cw_summary = $spoilerText;
|
||||
$status->scope = $visibility;
|
||||
$status->visibility = $visibility;
|
||||
$status->type = StatusController::mimeTypeCheck([$mime]);
|
||||
$status->save();
|
||||
|
||||
if (! $status) {
|
||||
abort(500, 'An error occured.');
|
||||
}
|
||||
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $mime;
|
||||
$media->order = 1;
|
||||
$media->caption = $request->input('description');
|
||||
if ($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
break;
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media)->onQueue('mmo');
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
Cache::forget('_api:statuses:recent_9:'.$user->profile_id);
|
||||
Cache::forget('profile:status_count:'.$user->profile_id);
|
||||
Cache::forget($user->storageUsedKey());
|
||||
Cache::forget('profile:embed:'.$status->profile_id);
|
||||
Cache::forget($limitKey);
|
||||
|
||||
$res = StatusService::getMastodon($status->id, false);
|
||||
$res['favourited'] = false;
|
||||
$res['language'] = 'en';
|
||||
$res['bookmarked'] = false;
|
||||
$res['card'] = null;
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
public function nagState(Request $request)
|
||||
{
|
||||
abort_unless((bool) config_cache('pixelfed.oauth_enabled'), 404);
|
||||
|
||||
return [
|
||||
'active' => NotificationAppGatewayService::enabled(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,43 +2,32 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
use App\Jobs\VideoPipeline\VideoThumbnail;
|
||||
use App\Media;
|
||||
use App\UserSetting;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\SearchApiV2Service;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Transformer\Api\Mastodon\v1\MediaTransformer;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use App\Util\Media\Filter;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
use App\Jobs\VideoPipeline\{
|
||||
VideoOptimize,
|
||||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\Mastodon\v1\{
|
||||
AccountTransformer,
|
||||
MediaTransformer,
|
||||
NotificationTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use App\Transformer\Api\{
|
||||
RelationshipTransformer,
|
||||
};
|
||||
use App\Util\Site\Nodeinfo;
|
||||
|
||||
class ApiV2Controller extends Controller
|
||||
{
|
||||
const PF_API_ENTITY_KEY = "_pe";
|
||||
const PF_API_ENTITY_KEY = '_pe';
|
||||
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
|
@ -52,6 +41,7 @@ class ApiV2Controller extends Controller
|
|||
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
|
||||
}
|
||||
$admin = User::whereIsAdmin(true)->first();
|
||||
|
||||
return $admin && isset($admin->profile_id) ?
|
||||
AccountService::getMastodon($admin->profile_id, true) :
|
||||
null;
|
||||
|
@ -62,46 +52,51 @@ class ApiV2Controller extends Controller
|
|||
collect(json_decode(config_cache('app.rules'), true))
|
||||
->map(function ($rule, $key) {
|
||||
$id = $key + 1;
|
||||
|
||||
return [
|
||||
'id' => "{$id}",
|
||||
'text' => $rule
|
||||
'text' => $rule,
|
||||
];
|
||||
})
|
||||
->toArray() : [];
|
||||
});
|
||||
|
||||
$res = [
|
||||
$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' => config('pixelfed.version'),
|
||||
'version' => '3.5.3 (compatible; Pixelfed '.config('pixelfed.version').')',
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly()
|
||||
]
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly(),
|
||||
],
|
||||
],
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
|
||||
]
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
],
|
||||
],
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => 'wss://' . config('pixelfed.domain.app'),
|
||||
'status' => null
|
||||
'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_characters' => (int) config_cache('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23
|
||||
'characters_reserved_per_url' => 23,
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
|
@ -109,29 +104,34 @@ class ApiV2Controller extends Controller
|
|||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400
|
||||
'video_matrix_limit' => 3686400,
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 4,
|
||||
'max_characters_per_option' => 50,
|
||||
'min_expiration' => 300,
|
||||
'max_expiration' => 2629746,
|
||||
'max_options' => 0,
|
||||
'max_characters_per_option' => 0,
|
||||
'min_expiration' => 0,
|
||||
'max_expiration' => 0,
|
||||
],
|
||||
'translation' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
'registrations' => [
|
||||
'enabled' => (bool) config_cache('pixelfed.open_registration'),
|
||||
'enabled' => null,
|
||||
'approval_required' => false,
|
||||
'message' => null
|
||||
'message' => null,
|
||||
'url' => null,
|
||||
],
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact
|
||||
'account' => $contact,
|
||||
],
|
||||
'rules' => $rules
|
||||
'rules' => $rules,
|
||||
];
|
||||
});
|
||||
|
||||
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
@ -144,7 +144,8 @@ class ApiV2Controller extends Controller
|
|||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:100',
|
||||
|
@ -156,10 +157,19 @@ class ApiV2Controller extends Controller
|
|||
'resolve' => 'nullable',
|
||||
'limit' => 'nullable|integer|max:40',
|
||||
'offset' => 'nullable|integer',
|
||||
'following' => 'nullable'
|
||||
'following' => 'nullable',
|
||||
]);
|
||||
|
||||
if ($request->user()->has_roles && ! UserRoleService::can('can-view-discover', $request->user()->id)) {
|
||||
return [
|
||||
'accounts' => [],
|
||||
'hashtags' => [],
|
||||
'statuses' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$mastodonMode = ! $request->has('_pe');
|
||||
|
||||
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
|
||||
}
|
||||
|
||||
|
@ -175,7 +185,7 @@ class ApiV2Controller extends Controller
|
|||
'host' => config('broadcasting.connections.pusher.options.host'),
|
||||
'port' => config('broadcasting.connections.pusher.options.port'),
|
||||
'key' => config('broadcasting.connections.pusher.key'),
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster')
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster'),
|
||||
] : [];
|
||||
}
|
||||
|
||||
|
@ -187,7 +197,8 @@ class ApiV2Controller extends Controller
|
|||
*/
|
||||
public function mediaUploadV2(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'file.*' => [
|
||||
|
@ -203,7 +214,7 @@ class ApiV2Controller extends Controller
|
|||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
'description' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
|
||||
'replace_id' => 'sometimes'
|
||||
'replace_id' => 'sometimes',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
@ -227,12 +238,16 @@ class ApiV2Controller extends Controller
|
|||
|
||||
$profile = $user->profile;
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
@ -240,8 +255,6 @@ class ApiV2Controller extends Controller
|
|||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
|
@ -308,6 +321,10 @@ class ApiV2Controller extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
|
@ -315,6 +332,7 @@ class ApiV2Controller extends Controller
|
|||
$res = $fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $media->url().'?v='.time();
|
||||
$res['url'] = null;
|
||||
|
||||
return $this->json($res, 202);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,7 @@ class BaseApiController extends Controller
|
|||
public function avatarUpdate(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
|
||||
]);
|
||||
|
@ -134,8 +135,9 @@ class BaseApiController extends Controller
|
|||
|
||||
public function verifyCredentials(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 403);
|
||||
if ($user->status != null) {
|
||||
Auth::logout();
|
||||
abort(403);
|
||||
|
@ -147,6 +149,7 @@ class BaseApiController extends Controller
|
|||
public function accountLikes(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'page' => 'sometimes|int|min:1|max:20',
|
||||
'limit' => 'sometimes|int|min:1|max:10'
|
||||
|
|
|
@ -4,8 +4,9 @@ namespace App\Http\Controllers\Api;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\{Profile, Status, User};
|
||||
use App\{Profile, Instance, Status, User};
|
||||
use Cache;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class InstanceApiController extends Controller {
|
||||
|
||||
|
@ -40,11 +41,8 @@ class InstanceApiController extends Controller {
|
|||
'urls' => [],
|
||||
'stats' => [
|
||||
'user_count' => User::count(),
|
||||
'status_count' => Status::whereNull('uri')->count(),
|
||||
'domain_count' => Profile::whereNotNull('domain')
|
||||
->groupBy('domain')
|
||||
->pluck('domain')
|
||||
->count()
|
||||
'status_count' => StatusService::totalLocalStatuses(),
|
||||
'domain_count' => Instance::count()
|
||||
],
|
||||
'thumbnail' => '',
|
||||
'languages' => [],
|
||||
|
|
147
app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php
Normal file
147
app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php
Normal file
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Http\Controllers\Api\ApiController;
|
||||
use App\Instance;
|
||||
use App\Services\InstanceService;
|
||||
use App\Http\Resources\MastoApi\Admin\DomainBlockResource;
|
||||
|
||||
class DomainBlocksController extends ApiController {
|
||||
|
||||
public function __construct() {
|
||||
$this->middleware(['auth:api', 'api.admin', 'scope:admin:read,admin:read:domain_blocks'])->only(['index', 'show']);
|
||||
$this->middleware(['auth:api', 'api.admin', 'scope:admin:write,admin:write:domain_blocks'])->only(['create', 'update', 'delete']);
|
||||
}
|
||||
|
||||
public function index(Request $request) {
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|max:100|min:1',
|
||||
]);
|
||||
|
||||
$limit = $request->input('limit', 100);
|
||||
|
||||
$res = Instance::moderated()
|
||||
->orderBy('id')
|
||||
->cursorPaginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
return $this->json(DomainBlockResource::collection($res), [
|
||||
'Link' => $this->linksForCollection($res)
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request, $id) {
|
||||
$domain_block = Instance::moderated()->find($id);
|
||||
|
||||
if (!$domain_block) {
|
||||
return $this->json([ 'error' => 'Record not found'], [], 404);
|
||||
}
|
||||
|
||||
return $this->json(new DomainBlockResource($domain_block));
|
||||
}
|
||||
|
||||
public function create(Request $request) {
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|string|min:1|max:120',
|
||||
'severity' => [
|
||||
'sometimes',
|
||||
Rule::in(['noop', 'silence', 'suspend'])
|
||||
],
|
||||
'reject_media' => 'sometimes|required|boolean',
|
||||
'reject_reports' => 'sometimes|required|boolean',
|
||||
'private_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'public_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'obfuscate' => 'sometimes|required|boolean'
|
||||
]);
|
||||
|
||||
$domain = $request->input('domain');
|
||||
$severity = $request->input('severity', 'silence');
|
||||
$private_comment = $request->input('private_comment');
|
||||
|
||||
abort_if(!strpos($domain, '.'), 400, 'Invalid domain');
|
||||
abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain');
|
||||
|
||||
// This is because Pixelfed can't currently support wildcard domain blocks
|
||||
// We have to find something that could plausibly be an instance
|
||||
$parts = explode('.', $domain);
|
||||
if ($parts[0] == '*') {
|
||||
// If we only have two parts, e.g., "*", "example", then we want to fail:
|
||||
abort_if(count($parts) <= 2, 400, 'Invalid domain: This API does not support wildcard domain blocks yet');
|
||||
|
||||
// Otherwise we convert the *.foo.example to foo.example
|
||||
$domain = implode('.', array_slice($parts, 1));
|
||||
}
|
||||
|
||||
// Double check we definitely haven't let anything through:
|
||||
abort_if(str_contains($domain, '*'), 400, 'Invalid domain');
|
||||
|
||||
$existing_domain_block = Instance::moderated()->whereDomain($domain)->first();
|
||||
|
||||
if ($existing_domain_block) {
|
||||
return $this->json([
|
||||
'error' => 'A domain block already exists for this domain',
|
||||
'existing_domain_block' => new DomainBlockResource($existing_domain_block)
|
||||
], [], 422);
|
||||
}
|
||||
|
||||
$domain_block = Instance::updateOrCreate(
|
||||
[ 'domain' => $domain ],
|
||||
[ 'banned' => $severity === 'suspend', 'unlisted' => $severity === 'silence', 'notes' => [$private_comment]]
|
||||
);
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return $this->json(new DomainBlockResource($domain_block));
|
||||
}
|
||||
|
||||
public function update(Request $request, $id) {
|
||||
$this->validate($request, [
|
||||
'severity' => [
|
||||
'sometimes',
|
||||
Rule::in(['noop', 'silence', 'suspend'])
|
||||
],
|
||||
'reject_media' => 'sometimes|required|boolean',
|
||||
'reject_reports' => 'sometimes|required|boolean',
|
||||
'private_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'public_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'obfuscate' => 'sometimes|required|boolean'
|
||||
]);
|
||||
|
||||
$severity = $request->input('severity', 'silence');
|
||||
$private_comment = $request->input('private_comment');
|
||||
|
||||
$domain_block = Instance::moderated()->find($id);
|
||||
|
||||
if (!$domain_block) {
|
||||
return $this->json([ 'error' => 'Record not found'], [], 404);
|
||||
}
|
||||
|
||||
$domain_block->banned = $severity === 'suspend';
|
||||
$domain_block->unlisted = $severity === 'silence';
|
||||
$domain_block->notes = [$private_comment];
|
||||
$domain_block->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return $this->json(new DomainBlockResource($domain_block));
|
||||
}
|
||||
|
||||
public function delete(Request $request, $id) {
|
||||
$domain_block = Instance::moderated()->find($id);
|
||||
|
||||
if (!$domain_block) {
|
||||
return $this->json([ 'error' => 'Record not found'], [], 404);
|
||||
}
|
||||
|
||||
$domain_block->banned = false;
|
||||
$domain_block->unlisted = false;
|
||||
$domain_block->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return $this->json(null, [], 200);
|
||||
}
|
||||
}
|
119
app/Http/Controllers/Api/V1/DomainBlockController.php
Normal file
119
app/Http/Controllers/Api/V1/DomainBlockController.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\UserFilterService;
|
||||
use Illuminate\Bus\Batch;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Jobs\HomeFeedPipeline\FeedRemoveDomainPipeline;
|
||||
use App\Jobs\ProfilePipeline\ProfilePurgeNotificationsByDomain;
|
||||
use App\Jobs\ProfilePipeline\ProfilePurgeFollowersByDomain;
|
||||
|
||||
class DomainBlockController extends Controller
|
||||
{
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|min:1|max:200'
|
||||
]);
|
||||
$limit = $request->input('limit', 100);
|
||||
$id = $request->user()->profile_id;
|
||||
$filters = UserDomainBlock::whereProfileId($id)->orderByDesc('id')->cursorPaginate($limit);
|
||||
$links = null;
|
||||
$headers = [];
|
||||
|
||||
if($filters->nextCursor()) {
|
||||
$links .= '<'.$filters->nextPageUrl().'&limit='.$limit.'>; rel="next"';
|
||||
}
|
||||
|
||||
if($filters->previousCursor()) {
|
||||
if($links != null) {
|
||||
$links .= ', ';
|
||||
}
|
||||
$links .= '<'.$filters->previousPageUrl().'&limit='.$limit.'>; rel="prev"';
|
||||
}
|
||||
|
||||
if($links) {
|
||||
$headers = ['Link' => $links];
|
||||
}
|
||||
return $this->json($filters->pluck('domain'), 200, $headers);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|active_url|min:1|max:120'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$domain = trim($request->input('domain'));
|
||||
|
||||
if(Helpers::validateUrl($domain) == false) {
|
||||
return abort(500, 'Invalid domain or already blocked by server admins');
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url($domain, PHP_URL_HOST));
|
||||
|
||||
abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server');
|
||||
|
||||
$existingCount = UserDomainBlock::whereProfileId($pid)->count();
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks');
|
||||
$errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]);
|
||||
|
||||
abort_if($existingCount >= $maxLimit, 400, $errorMsg);
|
||||
|
||||
$block = UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $pid,
|
||||
'domain' => $domain
|
||||
]);
|
||||
|
||||
if($block->wasRecentlyCreated) {
|
||||
Bus::batch([
|
||||
[
|
||||
new FeedRemoveDomainPipeline($pid, $domain),
|
||||
new ProfilePurgeNotificationsByDomain($pid, $domain),
|
||||
new ProfilePurgeFollowersByDomain($pid, $domain)
|
||||
]
|
||||
])->allowFailures()->onQueue('feed')->dispatch();
|
||||
|
||||
Cache::forget('profile:following:' . $pid);
|
||||
UserFilterService::domainBlocks($pid, true);
|
||||
}
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|min:1|max:120'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$domain = strtolower(trim($request->input('domain')));
|
||||
|
||||
$filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete();
|
||||
|
||||
UserFilterService::domainBlocks($pid, true);
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
}
|
209
app/Http/Controllers/Api/V1/TagsController.php
Normal file
209
app/Http/Controllers/Api/V1/TagsController.php
Normal file
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Hashtag;
|
||||
use App\HashtagFollow;
|
||||
use App\StatusHashtag;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\HashtagService;
|
||||
use App\Services\HashtagFollowService;
|
||||
use App\Services\HashtagRelatedService;
|
||||
use App\Http\Resources\MastoApi\FollowedTagResource;
|
||||
use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline;
|
||||
use App\Jobs\HomeFeedPipeline\HashtagUnfollowPipeline;
|
||||
|
||||
class TagsController extends Controller
|
||||
{
|
||||
const PF_API_ENTITY_KEY = "_pe";
|
||||
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/tags/:id/related
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function relatedTags(Request $request, $tag)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$tag = Hashtag::whereSlug($tag)->firstOrFail();
|
||||
return HashtagRelatedService::get($tag->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/tags/:id/follow
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function followHashtag(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$account = AccountService::get($pid);
|
||||
|
||||
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
|
||||
$tag = Hashtag::where('name', $operator, $id)
|
||||
->orWhere('slug', $operator, $id)
|
||||
->first();
|
||||
|
||||
abort_if(!$tag, 422, 'Unknown hashtag');
|
||||
|
||||
abort_if(
|
||||
HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT,
|
||||
422,
|
||||
'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.'
|
||||
);
|
||||
|
||||
$follows = HashtagFollow::updateOrCreate(
|
||||
[
|
||||
'profile_id' => $account['id'],
|
||||
'hashtag_id' => $tag->id
|
||||
],
|
||||
[
|
||||
'user_id' => $request->user()->id
|
||||
]
|
||||
);
|
||||
|
||||
HashtagService::follow($pid, $tag->id);
|
||||
HashtagFollowService::add($tag->id, $pid);
|
||||
|
||||
return response()->json(FollowedTagResource::make($follows)->toArray($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/tags/:id/unfollow
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function unfollowHashtag(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$account = AccountService::get($pid);
|
||||
|
||||
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
|
||||
$tag = Hashtag::where('name', $operator, $id)
|
||||
->orWhere('slug', $operator, $id)
|
||||
->first();
|
||||
|
||||
abort_if(!$tag, 422, 'Unknown hashtag');
|
||||
|
||||
$follows = HashtagFollow::whereProfileId($pid)
|
||||
->whereHashtagId($tag->id)
|
||||
->first();
|
||||
|
||||
if(!$follows) {
|
||||
return [
|
||||
'name' => $tag->name,
|
||||
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
|
||||
'history' => [],
|
||||
'following' => false
|
||||
];
|
||||
}
|
||||
|
||||
if($follows) {
|
||||
HashtagService::unfollow($pid, $tag->id);
|
||||
HashtagFollowService::unfollow($tag->id, $pid);
|
||||
HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed');
|
||||
$follows->delete();
|
||||
}
|
||||
|
||||
$res = FollowedTagResource::make($follows)->toArray($request);
|
||||
$res['following'] = false;
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/tags/:id
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getHashtag(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$account = AccountService::get($pid);
|
||||
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
|
||||
$tag = Hashtag::where('name', $operator, $id)
|
||||
->orWhere('slug', $operator, $id)
|
||||
->first();
|
||||
|
||||
if(!$tag) {
|
||||
return [
|
||||
'name' => $id,
|
||||
'url' => config('app.url') . '/i/web/hashtag/' . $id,
|
||||
'history' => [],
|
||||
'following' => false
|
||||
];
|
||||
}
|
||||
|
||||
$res = [
|
||||
'name' => $tag->name,
|
||||
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
|
||||
'history' => [],
|
||||
'following' => HashtagService::isFollowing($pid, $tag->id)
|
||||
];
|
||||
|
||||
if($request->has(self::PF_API_ENTITY_KEY)) {
|
||||
$res['count'] = HashtagService::count($tag->id);
|
||||
}
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/followed_tags
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFollowedTags(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$account = AccountService::get($request->user()->profile_id);
|
||||
|
||||
$this->validate($request, [
|
||||
'cursor' => 'sometimes',
|
||||
'limit' => 'sometimes|integer|min:1|max:200'
|
||||
]);
|
||||
$limit = $request->input('limit', 100);
|
||||
|
||||
$res = HashtagFollow::whereProfileId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->cursorPaginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
$pagination = false;
|
||||
$prevPage = $res->nextPageUrl();
|
||||
$nextPage = $res->previousPageUrl();
|
||||
if($nextPage && $prevPage) {
|
||||
$pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"';
|
||||
} else if($nextPage && !$prevPage) {
|
||||
$pagination = '<' . $nextPage . '>; rel="next"';
|
||||
} else if(!$nextPage && $prevPage) {
|
||||
$pagination = '<' . $prevPage . '>; rel="prev"';
|
||||
}
|
||||
|
||||
if($pagination) {
|
||||
return response()->json(FollowedTagResource::collection($res)->collection)
|
||||
->header('Link', $pagination);
|
||||
}
|
||||
return response()->json(FollowedTagResource::collection($res)->collection);
|
||||
}
|
||||
}
|
|
@ -62,7 +62,7 @@ class ForgotPasswordController extends Controller
|
|||
|
||||
usleep(random_int(100000, 3000000));
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
if((bool) config_cache('captcha.enabled')) {
|
||||
$rules = [
|
||||
'email' => 'required|email',
|
||||
'h-captcha-response' => 'required|captcha'
|
||||
|
|
|
@ -71,20 +71,21 @@ class LoginController extends Controller
|
|||
$this->username() => 'required|email',
|
||||
'password' => 'required|string|min:6',
|
||||
];
|
||||
$messages = [];
|
||||
|
||||
if(
|
||||
config('captcha.enabled') ||
|
||||
config('captcha.active.login') ||
|
||||
(bool) config_cache('captcha.enabled') &&
|
||||
(bool) config_cache('captcha.active.login') ||
|
||||
(
|
||||
config('captcha.triggers.login.enabled') &&
|
||||
(bool) config_cache('captcha.triggers.login.enabled') &&
|
||||
request()->session()->has('login_attempts') &&
|
||||
request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
|
||||
)
|
||||
) {
|
||||
$rules['h-captcha-response'] = 'required|filled|captcha|min:5';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
|
||||
$this->validate($request, $rules);
|
||||
$request->validate($rules, $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\EmailService;
|
||||
use App\User;
|
||||
use Purify;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\BouncerService;
|
||||
use Purify;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
|
@ -56,11 +56,10 @@ class RegisterController extends Controller
|
|||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
public function validator(array $data)
|
||||
{
|
||||
if (config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
|
@ -98,6 +97,10 @@ class RegisterController extends Controller
|
|||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
if (! preg_match('/[a-zA-Z]/', $value)) {
|
||||
return $fail('Username is invalid. Must contain at least one alphabetical character.');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
|
@ -125,7 +128,7 @@ class RegisterController extends Controller
|
|||
if ($value !== $this->getRegisterToken()) {
|
||||
return $fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
$rules = [
|
||||
|
@ -137,7 +140,7 @@ class RegisterController extends Controller
|
|||
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
|
||||
];
|
||||
|
||||
if(config('captcha.enabled') || config('captcha.active.register')) {
|
||||
if ((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
}
|
||||
|
||||
|
@ -147,11 +150,10 @@ class RegisterController extends Controller
|
|||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return \App\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
public function create(array $data)
|
||||
{
|
||||
if (config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
|
@ -163,7 +165,7 @@ class RegisterController extends Controller
|
|||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'app_register_ip' => request()->ip()
|
||||
'app_register_ip' => request()->ip(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -174,31 +176,37 @@ 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);
|
||||
}
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if ($hasLimit) {
|
||||
$limit = config('pixelfed.max_users');
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
$count = User::where(function ($q) {
|
||||
return $q->whereNull('status')->orWhereNotIn('status', ['deleted', 'delete']);
|
||||
})->count();
|
||||
if ($limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
}
|
||||
abort_if($limit <= $count, 404);
|
||||
|
||||
return view('auth.register');
|
||||
} else {
|
||||
return view('auth.register');
|
||||
}
|
||||
} else {
|
||||
if ((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
|
||||
return redirect('/auth/sign_up');
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a registration request for the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function register(Request $request)
|
||||
|
@ -211,7 +219,9 @@ class RegisterController extends Controller
|
|||
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if ($hasLimit) {
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
$count = User::where(function ($q) {
|
||||
return $q->whereNull('status')->orWhereNotIn('status', ['deleted', 'delete']);
|
||||
})->count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
|
||||
if ($limit && $limit <= $count) {
|
||||
|
@ -219,7 +229,6 @@ class RegisterController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
$this->validator($request->all())->validate();
|
||||
|
||||
event(new Registered($user = $this->create($request->all())));
|
||||
|
|
|
@ -50,7 +50,7 @@ class ResetPasswordController extends Controller
|
|||
{
|
||||
usleep(random_int(100000, 3000000));
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
if((bool) config_cache('captcha.enabled')) {
|
||||
return [
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
|
|
37
app/Http/Controllers/AuthorizeInteractionController.php
Normal file
37
app/Http/Controllers/AuthorizeInteractionController.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuthorizeInteractionController extends Controller
|
||||
{
|
||||
public function get(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'uri' => 'required|url',
|
||||
]);
|
||||
|
||||
abort_unless((bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
$uri = Helpers::validateUrl($request->input('uri'), true);
|
||||
abort_unless($uri, 404);
|
||||
|
||||
if (! $request->user()) {
|
||||
return redirect('/login?next='.urlencode($uri));
|
||||
}
|
||||
|
||||
$status = Helpers::statusFetch($uri);
|
||||
if ($status && isset($status['id'])) {
|
||||
return redirect('/i/web/post/'.$status['id']);
|
||||
}
|
||||
|
||||
$profile = Helpers::profileFetch($uri);
|
||||
if ($profile && isset($profile['id'])) {
|
||||
return redirect('/i/web/profile/'.$profile['id']);
|
||||
}
|
||||
|
||||
return redirect('/i/web');
|
||||
}
|
||||
}
|
|
@ -3,11 +3,12 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Bookmark;
|
||||
use App\Status;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Status;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BookmarkController extends Controller
|
||||
{
|
||||
|
@ -22,17 +23,19 @@ class BookmarkController extends Controller
|
|||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$user = $request->user();
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
$account = AccountService::get($status->profile_id);
|
||||
abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot bookmark or unbookmark a post from an account that has migrated');
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
|
||||
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
|
||||
abort_if(! in_array($status->scope, ['public', 'unlisted', 'private']), 404);
|
||||
abort_if(! in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']), 404);
|
||||
|
||||
if ($status->scope == 'private') {
|
||||
if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) {
|
||||
if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) {
|
||||
BookmarkService::del($profile->id, $status->id);
|
||||
if ($user->profile_id !== $status->profile_id && ! FollowerService::follows($user->profile_id, $status->profile_id)) {
|
||||
if ($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) {
|
||||
BookmarkService::del($user->profile_id, $status->id);
|
||||
$exists->delete();
|
||||
|
||||
if ($request->ajax()) {
|
||||
|
@ -46,22 +49,16 @@ class BookmarkController extends Controller
|
|||
}
|
||||
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
['status_id' => $status->id], ['profile_id' => $user->profile_id]
|
||||
);
|
||||
|
||||
if (! $bookmark->wasRecentlyCreated) {
|
||||
BookmarkService::del($profile->id, $status->id);
|
||||
BookmarkService::del($user->profile_id, $status->id);
|
||||
$bookmark->delete();
|
||||
} else {
|
||||
BookmarkService::add($profile->id, $status->id);
|
||||
BookmarkService::add($user->profile_id, $status->id);
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
||||
} else {
|
||||
$response = redirect()->back();
|
||||
}
|
||||
|
||||
return $response;
|
||||
return $request->expectsJson() ? ['code' => 200, 'msg' => 'Bookmark saved!'] : redirect()->back();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,25 +2,15 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use App\{
|
||||
Collection,
|
||||
CollectionItem,
|
||||
Profile,
|
||||
Status
|
||||
};
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Collection;
|
||||
use App\CollectionItem;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CollectionController extends Controller
|
||||
{
|
||||
|
@ -31,10 +21,11 @@ class CollectionController extends Controller
|
|||
|
||||
$collection = Collection::firstOrCreate([
|
||||
'profile_id' => $profile->id,
|
||||
'published_at' => null
|
||||
'published_at' => null,
|
||||
]);
|
||||
$collection->visibility = 'draft';
|
||||
$collection->save();
|
||||
|
||||
return view('collection.create', compact('collection'));
|
||||
}
|
||||
|
||||
|
@ -52,12 +43,14 @@ class CollectionController extends Controller
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('collection.show', compact('collection'));
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(! Auth::check(), 403);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
|
@ -67,7 +60,7 @@ class CollectionController extends Controller
|
|||
$this->validate($request, [
|
||||
'title' => 'nullable|max:50',
|
||||
'description' => 'nullable|max:500',
|
||||
'visibility' => 'nullable|string|in:public,private,draft'
|
||||
'visibility' => 'nullable|string|in:public,private,draft',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
@ -78,6 +71,7 @@ class CollectionController extends Controller
|
|||
$collection->save();
|
||||
|
||||
CollectionService::deleteCollection($id);
|
||||
|
||||
return CollectionService::setCollection($collection->id, $collection);
|
||||
}
|
||||
|
||||
|
@ -87,7 +81,7 @@ class CollectionController extends Controller
|
|||
$this->validate($request, [
|
||||
'title' => 'nullable|max:50',
|
||||
'description' => 'nullable|max:500',
|
||||
'visibility' => 'required|alpha|in:public,private,draft'
|
||||
'visibility' => 'required|alpha|in:public,private,draft',
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$collection = Collection::whereProfileId($profile->id)->findOrFail($id);
|
||||
|
@ -99,6 +93,7 @@ class CollectionController extends Controller
|
|||
$collection->visibility = $request->input('visibility');
|
||||
$collection->published_at = now();
|
||||
$collection->save();
|
||||
|
||||
return CollectionService::setCollection($collection->id, $collection);
|
||||
}
|
||||
|
||||
|
@ -126,7 +121,7 @@ class CollectionController extends Controller
|
|||
|
||||
$this->validate($request, [
|
||||
'collection_id' => 'required|int|min:1|exists:collections,id',
|
||||
'post_id' => 'required|int|min:1'
|
||||
'post_id' => 'required|int|min:1',
|
||||
]);
|
||||
|
||||
$profileId = $request->user()->profile_id;
|
||||
|
@ -153,7 +148,7 @@ class CollectionController extends Controller
|
|||
abort(400, 'You can only add '.$max.' posts per collection');
|
||||
}
|
||||
|
||||
$status = Status::whereScope('public')
|
||||
$status = Status::whereIn('scope', ['public', 'unlisted'])
|
||||
->whereProfileId($profileId)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video'])
|
||||
->findOrFail($postId);
|
||||
|
@ -161,22 +156,18 @@ class CollectionController extends Controller
|
|||
$item = CollectionItem::firstOrCreate([
|
||||
'collection_id' => $collection->id,
|
||||
'object_type' => 'App\Status',
|
||||
'object_id' => $status->id
|
||||
'object_id' => $status->id,
|
||||
], [
|
||||
'order' => $count,
|
||||
]);
|
||||
|
||||
CollectionService::addItem(
|
||||
$collection->id,
|
||||
$status->id,
|
||||
$count
|
||||
);
|
||||
CollectionService::deleteCollection($collection->id);
|
||||
|
||||
$collection->updated_at = now();
|
||||
$collection->save();
|
||||
CollectionService::setCollection($collection->id, $collection);
|
||||
|
||||
return StatusService::get($status->id);
|
||||
return StatusService::get($status->id, false);
|
||||
}
|
||||
|
||||
public function getCollection(Request $request, $id)
|
||||
|
@ -226,10 +217,10 @@ class CollectionController extends Controller
|
|||
|
||||
return collect($items)
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function ($item) {
|
||||
return $item && isset($item['account'], $item['media_attachments']);
|
||||
return $item && ($item['visibility'] == 'public' || $item['visibility'] == 'unlisted') && isset($item['account'], $item['media_attachments']);
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
@ -284,7 +275,7 @@ class CollectionController extends Controller
|
|||
abort_if(! $request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'collection_id' => 'required|int|min:1|exists:collections,id',
|
||||
'post_id' => 'required|int|min:1'
|
||||
'post_id' => 'required|int|min:1',
|
||||
]);
|
||||
|
||||
$profileId = $request->user()->profile_id;
|
||||
|
@ -298,7 +289,7 @@ class CollectionController extends Controller
|
|||
abort(400, 'You cannot delete the only post of a collection!');
|
||||
}
|
||||
|
||||
$status = Status::whereScope('public')
|
||||
$status = Status::whereIn('scope', ['public', 'unlisted'])
|
||||
->whereIn('type', ['photo', 'photo:album', 'video'])
|
||||
->findOrFail($postId);
|
||||
|
||||
|
@ -323,4 +314,31 @@ class CollectionController extends Controller
|
|||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function getSelfCollections(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$user = $request->user();
|
||||
$pid = $user->profile_id;
|
||||
|
||||
$profile = AccountService::get($pid, true);
|
||||
if (! $profile || ! isset($profile['id'])) {
|
||||
return response()->json([], 404);
|
||||
}
|
||||
|
||||
return Collection::whereProfileId($pid)
|
||||
->orderByDesc('id')
|
||||
->paginate(9)
|
||||
->map(function ($collection) {
|
||||
$c = CollectionService::getCollection($collection->id);
|
||||
$c['items'] = collect(CollectionService::getItems($collection->id))
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id, false);
|
||||
})->filter()->values();
|
||||
|
||||
return $c;
|
||||
})
|
||||
->filter()
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,23 +2,18 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Cache;
|
||||
|
||||
use App\Comment;
|
||||
use App\Jobs\CommentPipeline\CommentPipeline;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\UserFilter;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use App\UserFilter;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use Purify;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
|
@ -34,8 +29,8 @@ class CommentController extends Controller
|
|||
}
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'),
|
||||
'sensitive' => 'nullable|boolean'
|
||||
'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'),
|
||||
'sensitive' => 'nullable|boolean',
|
||||
]);
|
||||
$comment = $request->input('comment');
|
||||
$statusId = $request->input('item');
|
||||
|
@ -60,13 +55,14 @@ class CommentController extends Controller
|
|||
}
|
||||
|
||||
$reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) {
|
||||
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
|
||||
|
||||
$scope = $profile->is_private == true ? 'private' : 'public';
|
||||
$autolink = Autolink::create()->autolink($comment);
|
||||
$reply = new Status();
|
||||
$reply = new Status;
|
||||
$reply->profile_id = $profile->id;
|
||||
$reply->is_nsfw = $nsfw;
|
||||
$reply->caption = e($comment);
|
||||
$reply->rendered = $autolink;
|
||||
$reply->caption = Purify::clean($comment);
|
||||
$reply->rendered = $defaultCaption;
|
||||
$reply->in_reply_to_id = $status->id;
|
||||
$reply->in_reply_to_profile_id = $status->profile_id;
|
||||
$reply->scope = $scope;
|
||||
|
@ -81,9 +77,9 @@ class CommentController extends Controller
|
|||
CommentPipeline::dispatch($status, $reply);
|
||||
|
||||
if ($request->ajax()) {
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$entity = new Fractal\Resource\Item($reply, new StatusTransformer());
|
||||
$fractal = new Fractal\Manager;
|
||||
$fractal->setSerializer(new ArraySerializer);
|
||||
$entity = new Fractal\Resource\Item($reply, new StatusTransformer);
|
||||
$entity = $fractal->createData($entity)->toArray();
|
||||
$response = [
|
||||
'code' => 200,
|
||||
|
|
|
@ -2,58 +2,39 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Cache, DB, Storage, URL;
|
||||
use Carbon\Carbon;
|
||||
use App\{
|
||||
Avatar,
|
||||
Collection,
|
||||
CollectionItem,
|
||||
Hashtag,
|
||||
Like,
|
||||
Media,
|
||||
MediaTag,
|
||||
Notification,
|
||||
Profile,
|
||||
Place,
|
||||
Status,
|
||||
UserFilter,
|
||||
UserSetting
|
||||
};
|
||||
use App\Models\Poll;
|
||||
use App\Transformer\Api\{
|
||||
MediaTransformer,
|
||||
MediaDraftTransformer,
|
||||
StatusTransformer,
|
||||
StatusStatelessTransformer
|
||||
};
|
||||
use League\Fractal;
|
||||
use App\Util\Media\Filter;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Jobs\AvatarPipeline\AvatarOptimize;
|
||||
use App\Collection;
|
||||
use App\CollectionItem;
|
||||
use App\Hashtag;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Jobs\VideoPipeline\{
|
||||
VideoOptimize,
|
||||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Jobs\VideoPipeline\VideoThumbnail;
|
||||
use App\Media;
|
||||
use App\MediaTag;
|
||||
use App\Models\Poll;
|
||||
use App\Notification;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\SnowflakeService;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Status;
|
||||
use App\Transformer\Api\MediaTransformer;
|
||||
use App\UserFilter;
|
||||
use App\Util\Media\Filter;
|
||||
use App\Util\Media\License;
|
||||
use Image;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Purify;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class ComposeController extends Controller
|
||||
{
|
||||
|
@ -62,8 +43,8 @@ class ComposeController extends Controller
|
|||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
$this->fractal = new Fractal\Manager;
|
||||
$this->fractal->setSerializer(new ArraySerializer);
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
|
@ -87,11 +68,12 @@ class ComposeController extends Controller
|
|||
'max:'.config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24'
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$user = $request->user();
|
||||
$profile = $user->profile;
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-upload:'.$user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
|
@ -103,21 +85,22 @@ class ComposeController extends Controller
|
|||
|
||||
abort_if($limitReached == true, 429);
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
|
||||
abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
|
||||
|
@ -129,17 +112,18 @@ class ComposeController extends Controller
|
|||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
$media = new Media();
|
||||
$media = new Media;
|
||||
$media->status_id = null;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->caption = '';
|
||||
$media->mime = $mime;
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
$media->version = 3;
|
||||
$media->version = '3';
|
||||
$media->save();
|
||||
|
||||
$preview_url = $media->url().'?v='.time();
|
||||
|
@ -162,11 +146,16 @@ class ComposeController extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $preview_url;
|
||||
$res['url'] = $url;
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
|
@ -184,6 +173,7 @@ class ComposeController extends Controller
|
|||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-updates:'.$user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
|
@ -211,10 +201,12 @@ class ComposeController extends Controller
|
|||
$dir = implode('/', $fragments);
|
||||
$path = $photo->storePubliclyAs($dir, $name);
|
||||
$res = [
|
||||
'url' => $media->url() . '?v=' . time()
|
||||
'url' => $media->url().'?v='.time(),
|
||||
];
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
Cache::forget($limitKey);
|
||||
UserStorageService::recalculateUpdateStorageUsed($request->user()->id);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -223,18 +215,22 @@ class ComposeController extends Controller
|
|||
abort_if(! $request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1|exists:media,id'
|
||||
'id' => 'required|integer|min:1|exists:media,id',
|
||||
]);
|
||||
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$media = Media::whereNull('status_id')
|
||||
->whereUserId(Auth::id())
|
||||
->findOrFail($request->input('id'));
|
||||
|
||||
MediaStorageService::delete($media, true);
|
||||
|
||||
UserStorageService::recalculateUpdateStorageUsed($request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'msg' => 'Successfully deleted',
|
||||
'code' => 200
|
||||
'code' => 200,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -243,7 +239,7 @@ class ComposeController extends Controller
|
|||
abort_if(! $request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:50'
|
||||
'q' => 'required|string|min:1|max:50',
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
@ -255,6 +251,10 @@ class ComposeController extends Controller
|
|||
$q = mb_substr($q, 1);
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$blocked = UserFilter::whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->whereFilterableId($request->user()->profile_id)
|
||||
|
@ -273,7 +273,7 @@ class ComposeController extends Controller
|
|||
'id' => (string) $r->id,
|
||||
'name' => $r->username,
|
||||
'privacy' => true,
|
||||
'avatar' => $r->avatarUrl()
|
||||
'avatar' => $r->avatarUrl(),
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -286,9 +286,11 @@ class ComposeController extends Controller
|
|||
|
||||
$this->validate($request, [
|
||||
'status_id' => 'required',
|
||||
'profile_id' => 'required'
|
||||
'profile_id' => 'required',
|
||||
]);
|
||||
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$user = $request->user();
|
||||
$status_id = $request->input('status_id');
|
||||
$profile_id = (int) $request->input('profile_id');
|
||||
|
@ -317,8 +319,9 @@ class ComposeController extends Controller
|
|||
{
|
||||
abort_if(! $request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|max:100'
|
||||
'q' => 'required|string|max:100',
|
||||
]);
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $pid, 400);
|
||||
$q = e($request->input('q'));
|
||||
|
@ -339,12 +342,13 @@ class ComposeController extends Controller
|
|||
->map(function ($place) {
|
||||
return [
|
||||
'id' => $place->place_id,
|
||||
'count' => $place->pc
|
||||
'count' => $place->pc,
|
||||
];
|
||||
})
|
||||
->unique('id')
|
||||
->values();
|
||||
}
|
||||
|
||||
return Status::selectRaw('id, place_id, count(place_id) as pc')
|
||||
->whereNotNull('place_id')
|
||||
->where('id', '>', $minId)
|
||||
|
@ -358,7 +362,7 @@ class ComposeController extends Controller
|
|||
->map(function ($place) {
|
||||
return [
|
||||
'id' => $place->place_id,
|
||||
'count' => $place->pc
|
||||
'count' => $place->pc,
|
||||
];
|
||||
});
|
||||
});
|
||||
|
@ -381,11 +385,12 @@ class ComposeController extends Controller
|
|||
'id' => $r->id,
|
||||
'name' => $r->name,
|
||||
'country' => $r->country,
|
||||
'url' => url('/discover/places/' . $r->id . '/' . $r->slug)
|
||||
'url' => url('/discover/places/'.$r->id.'/'.$r->slug),
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
|
@ -394,9 +399,11 @@ class ComposeController extends Controller
|
|||
abort_if(! $request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:2|max:50'
|
||||
'q' => 'required|string|min:2|max:50',
|
||||
]);
|
||||
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
if (Str::of($q)->startsWith('@')) {
|
||||
|
@ -420,6 +427,7 @@ class ComposeController extends Controller
|
|||
->get()
|
||||
->map(function ($profile) {
|
||||
$username = $profile->domain ? substr($profile->username, 1) : $profile->username;
|
||||
|
||||
return [
|
||||
'key' => '@'.str_limit($username, 30),
|
||||
'value' => $username,
|
||||
|
@ -434,9 +442,11 @@ class ComposeController extends Controller
|
|||
abort_if(! $request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:2|max:50'
|
||||
'q' => 'required|string|min:2|max:50',
|
||||
]);
|
||||
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
$results = Hashtag::select('slug')
|
||||
|
@ -448,7 +458,7 @@ class ComposeController extends Controller
|
|||
->map(function ($tag) {
|
||||
return [
|
||||
'key' => '#'.$tag->slug,
|
||||
'value' => $tag->slug
|
||||
'value' => $tag->slug,
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -458,7 +468,7 @@ class ComposeController extends Controller
|
|||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
|
||||
'media.*' => 'required',
|
||||
'media.*.id' => 'required|integer|min:1',
|
||||
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
|
||||
|
@ -475,6 +485,8 @@ class ComposeController extends Controller
|
|||
// 'optimize_media' => 'nullable'
|
||||
]);
|
||||
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
if (config('costar.enabled') == true) {
|
||||
$blockedKeywords = config('costar.keyword.block');
|
||||
if ($blockedKeywords !== null && $request->caption) {
|
||||
|
@ -487,22 +499,22 @@ class ComposeController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$user = $request->user();
|
||||
$profile = $user->profile;
|
||||
|
||||
$limitKey = 'compose:rate-limit:store:'.$user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
|
||||
$dailyLimit = Status::whereProfileId($user->profile_id)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->where('created_at', '>', now()->subDays(1))
|
||||
->count();
|
||||
// $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
|
||||
// $dailyLimit = Status::whereProfileId($user->profile_id)
|
||||
// ->whereNull('in_reply_to_id')
|
||||
// ->whereNull('reblog_of_id')
|
||||
// ->where('created_at', '>', now()->subDays(1))
|
||||
// ->count();
|
||||
|
||||
return $dailyLimit >= 1000;
|
||||
});
|
||||
// return $dailyLimit >= 1000;
|
||||
// });
|
||||
|
||||
abort_if($limitReached == true, 429);
|
||||
// abort_if($limitReached == true, 429);
|
||||
|
||||
$license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null;
|
||||
|
||||
|
@ -558,8 +570,9 @@ class ComposeController extends Controller
|
|||
$status->cw_summary = $request->input('spoiler_text');
|
||||
}
|
||||
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
|
||||
$status->caption = strip_tags($request->input('caption')) ?? $defaultCaption;
|
||||
$status->rendered = $defaultCaption;
|
||||
$status->scope = 'draft';
|
||||
$status->visibility = 'draft';
|
||||
$status->profile_id = $profile->id;
|
||||
|
@ -602,9 +615,9 @@ class ComposeController extends Controller
|
|||
CollectionItem::firstOrCreate([
|
||||
'collection_id' => $collection->id,
|
||||
'object_type' => 'App\Status',
|
||||
'object_id' => $status->id
|
||||
'object_id' => $status->id,
|
||||
], [
|
||||
'order' => $count
|
||||
'order' => $count,
|
||||
]);
|
||||
|
||||
CollectionService::addItem(
|
||||
|
@ -624,7 +637,6 @@ class ComposeController extends Controller
|
|||
Cache::forget('_api:statuses:recent_9:'.$profile->id);
|
||||
Cache::forget('profile:status_count:'.$profile->id);
|
||||
Cache::forget('status:transformer:media:attachments:'.$status->id);
|
||||
Cache::forget($user->storageUsedKey());
|
||||
Cache::forget('profile:embed:'.$status->profile_id);
|
||||
Cache::forget($limitKey);
|
||||
|
||||
|
@ -635,7 +647,7 @@ class ComposeController extends Controller
|
|||
{
|
||||
abort_unless(config('exp.top'), 404);
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
|
||||
'place' => 'nullable',
|
||||
|
@ -643,6 +655,8 @@ class ComposeController extends Controller
|
|||
'tagged' => 'nullable',
|
||||
]);
|
||||
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
if (config('costar.enabled') == true) {
|
||||
$blockedKeywords = config('costar.keyword.block');
|
||||
if ($blockedKeywords !== null && $request->caption) {
|
||||
|
@ -655,13 +669,14 @@ class ComposeController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$user = $request->user();
|
||||
$profile = $user->profile;
|
||||
$visibility = $request->input('visibility');
|
||||
$status = new Status;
|
||||
$place = $request->input('place');
|
||||
$cw = $request->input('cw');
|
||||
$tagged = $request->input('tagged');
|
||||
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
|
||||
|
||||
if ($place && is_array($place)) {
|
||||
$status->place_id = $place['id'];
|
||||
|
@ -671,7 +686,8 @@ class ComposeController extends Controller
|
|||
$status->comments_disabled = (bool) $request->input('comments_disabled');
|
||||
}
|
||||
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->caption = $request->filled('caption') ? strip_tags($request->caption) : $defaultCaption;
|
||||
$status->rendered = $defaultCaption;
|
||||
$status->profile_id = $profile->id;
|
||||
$entities = [];
|
||||
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
|
||||
|
@ -680,14 +696,13 @@ class ComposeController extends Controller
|
|||
$status->visibility = $visibility;
|
||||
$status->scope = $visibility;
|
||||
$status->type = 'text';
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$status->entities = json_encode(array_merge([
|
||||
'timg' => [
|
||||
'version' => 0,
|
||||
'bg_id' => 1,
|
||||
'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
|
||||
'length' => strlen($status->caption),
|
||||
]
|
||||
],
|
||||
], $entities), JSON_UNESCAPED_SLASHES);
|
||||
$status->save();
|
||||
|
||||
|
@ -706,7 +721,6 @@ class ComposeController extends Controller
|
|||
MediaTagService::sendNotification($mt);
|
||||
}
|
||||
|
||||
|
||||
Cache::forget('user:account:id:'.$profile->user_id);
|
||||
Cache::forget('_api:statuses:recent_9:'.$profile->id);
|
||||
Cache::forget('profile:status_count:'.$profile->id);
|
||||
|
@ -717,16 +731,18 @@ class ComposeController extends Controller
|
|||
public function mediaProcessingCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1'
|
||||
'id' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$media = Media::whereUserId($request->user()->id)
|
||||
->whereNull('status_id')
|
||||
->findOrFail($request->input('id'));
|
||||
|
||||
if (config('pixelfed.media_fast_process')) {
|
||||
return [
|
||||
'finished' => true
|
||||
'finished' => true,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -736,26 +752,28 @@ class ComposeController extends Controller
|
|||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
case 'video/mp4':
|
||||
$finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
|
||||
$finished = (bool) config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
|
||||
break;
|
||||
|
||||
default:
|
||||
# code...
|
||||
// code...
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
'finished' => $finished
|
||||
'finished' => $finished,
|
||||
];
|
||||
}
|
||||
|
||||
public function composeSettings(Request $request)
|
||||
{
|
||||
$uid = $request->user()->id;
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$default = [
|
||||
'default_license' => 1,
|
||||
'media_descriptions' => false,
|
||||
'max_altext_length' => config_cache('pixelfed.max_altext_length')
|
||||
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
|
||||
];
|
||||
$settings = AccountService::settings($uid);
|
||||
if (isset($settings['other']) && isset($settings['other']['scope'])) {
|
||||
|
@ -770,27 +788,26 @@ class ComposeController extends Controller
|
|||
public function createPoll(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private',
|
||||
'comments_disabled' => 'nullable',
|
||||
'expiry' => 'required|in:60,360,1440,10080',
|
||||
'pollOptions' => 'required|array|min:1|max:4'
|
||||
'pollOptions' => 'required|array|min:1|max:4',
|
||||
]);
|
||||
|
||||
abort(404);
|
||||
abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
|
||||
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
abort_if(Status::whereType('poll')
|
||||
->whereProfileId($request->user()->profile_id)
|
||||
->whereCaption($request->input('caption'))
|
||||
->where('created_at', '>', now()->subDays(2))
|
||||
->exists()
|
||||
, 422, 'Duplicate detected.');
|
||||
->exists(), 422, 'Duplicate detected.');
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $request->user()->profile_id;
|
||||
$status->caption = $request->input('caption');
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$status->visibility = 'draft';
|
||||
$status->scope = 'draft';
|
||||
$status->type = 'poll';
|
||||
|
|
|
@ -50,4 +50,15 @@ class ContactController extends Controller
|
|||
|
||||
return redirect()->back()->with('status', 'Success - Your message has been sent to admins.');
|
||||
}
|
||||
|
||||
public function showAdminResponse(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$uid = $request->user()->id;
|
||||
$contact = Contact::whereUserId($uid)
|
||||
->whereNotNull('response')
|
||||
->whereNotNull('responded_at')
|
||||
->findOrFail($id);
|
||||
return view('site.contact.admin-response', compact('contact'));
|
||||
}
|
||||
}
|
||||
|
|
399
app/Http/Controllers/CuratedRegisterController.php
Normal file
399
app/Http/Controllers/CuratedRegisterController.php
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -2,27 +2,29 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth, Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{
|
||||
DirectMessage,
|
||||
Media,
|
||||
Notification,
|
||||
Profile,
|
||||
Status,
|
||||
User,
|
||||
UserFilter,
|
||||
UserSetting
|
||||
};
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\DirectMessage;
|
||||
use App\Jobs\DirectPipeline\DirectDeletePipeline;
|
||||
use App\Jobs\DirectPipeline\DirectDeliverPipeline;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\Media;
|
||||
use App\Models\Conversation;
|
||||
use App\Notification;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Status;
|
||||
use App\UserFilter;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DirectMessageController extends Controller
|
||||
{
|
||||
|
@ -35,10 +37,14 @@ class DirectMessageController extends Controller
|
|||
{
|
||||
$this->validate($request, [
|
||||
'a' => 'nullable|string|in:inbox,sent,filtered',
|
||||
'page' => 'nullable|integer|min:1|max:99'
|
||||
'page' => 'nullable|integer|min:1|max:99',
|
||||
]);
|
||||
|
||||
$profile = $request->user()->profile_id;
|
||||
$user = $request->user();
|
||||
if ($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id)) {
|
||||
return [];
|
||||
}
|
||||
$profile = $user->profile_id;
|
||||
$action = $request->input('a', 'inbox');
|
||||
$page = $request->input('page');
|
||||
|
||||
|
@ -68,7 +74,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->author->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
] : [
|
||||
'id' => (string) $r->to_id,
|
||||
'name' => $r->recipient->name,
|
||||
|
@ -79,7 +85,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->recipient->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
];
|
||||
})->values();
|
||||
}
|
||||
|
@ -108,7 +114,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->author->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
] : [
|
||||
'id' => (string) $r->to_id,
|
||||
'name' => $r->recipient->name,
|
||||
|
@ -119,7 +125,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->recipient->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
@ -149,7 +155,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->author->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
] : [
|
||||
'id' => (string) $r->to_id,
|
||||
'name' => $r->recipient->name,
|
||||
|
@ -160,7 +166,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->recipient->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
@ -190,7 +196,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->author->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
] : [
|
||||
'id' => (string) $r->to_id,
|
||||
'name' => $r->recipient->name,
|
||||
|
@ -201,7 +207,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->recipient->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
@ -230,7 +236,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->author->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
] : [
|
||||
'id' => (string) $r->to_id,
|
||||
'name' => $r->recipient->name,
|
||||
|
@ -241,7 +247,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->recipient->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
@ -271,7 +277,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->author->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
] : [
|
||||
'id' => (string) $r->to_id,
|
||||
'name' => $r->recipient->name,
|
||||
|
@ -282,7 +288,7 @@ class DirectMessageController extends Controller
|
|||
'domain' => $r->recipient->domain,
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => $r->status->caption,
|
||||
'messages' => []
|
||||
'messages' => [],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
@ -296,10 +302,15 @@ class DirectMessageController extends Controller
|
|||
$this->validate($request, [
|
||||
'to_id' => 'required',
|
||||
'message' => 'required|string|min:1|max:500',
|
||||
'type' => 'required|in:text,emoji'
|
||||
'type' => 'required|in:text,emoji',
|
||||
]);
|
||||
|
||||
$profile = $request->user()->profile;
|
||||
$user = $request->user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
|
||||
if (! $user->is_admin) {
|
||||
abort_if($user->created_at->gt(now()->subHours(72)), 400, 'You need to wait a bit before you can DM another account');
|
||||
}
|
||||
$profile = $user->profile;
|
||||
$recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
|
||||
|
||||
abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
|
||||
|
@ -318,7 +329,6 @@ class DirectMessageController extends Controller
|
|||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = $msg;
|
||||
$status->rendered = $msg;
|
||||
$status->visibility = 'direct';
|
||||
$status->scope = 'direct';
|
||||
$status->in_reply_to_profile_id = $recipient->id;
|
||||
|
@ -335,13 +345,13 @@ class DirectMessageController extends Controller
|
|||
Conversation::updateOrInsert(
|
||||
[
|
||||
'to_id' => $recipient->id,
|
||||
'from_id' => $profile->id
|
||||
'from_id' => $profile->id,
|
||||
],
|
||||
[
|
||||
'type' => $dm->type,
|
||||
'status_id' => $status->id,
|
||||
'dm_id' => $dm->id,
|
||||
'is_hidden' => $hidden
|
||||
'is_hidden' => $hidden,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -351,7 +361,7 @@ class DirectMessageController extends Controller
|
|||
$dm->meta = [
|
||||
'domain' => parse_url($msg, PHP_URL_HOST),
|
||||
'local' => parse_url($msg, PHP_URL_HOST) ==
|
||||
parse_url(config('app.url'), PHP_URL_HOST)
|
||||
parse_url(config('app.url'), PHP_URL_HOST),
|
||||
];
|
||||
$dm->save();
|
||||
}
|
||||
|
@ -364,7 +374,7 @@ class DirectMessageController extends Controller
|
|||
->exists();
|
||||
|
||||
if ($recipient->domain == null && $hidden == false && ! $nf) {
|
||||
$notification = new Notification();
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $recipient->id;
|
||||
$notification->actor_id = $profile->id;
|
||||
$notification->action = 'dm';
|
||||
|
@ -387,7 +397,7 @@ class DirectMessageController extends Controller
|
|||
'media' => null,
|
||||
'timeAgo' => $dm->created_at->diffForHumans(null, null, true),
|
||||
'seen' => $dm->read_at != null,
|
||||
'meta' => $dm->meta
|
||||
'meta' => $dm->meta,
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
|
@ -396,9 +406,14 @@ class DirectMessageController extends Controller
|
|||
public function thread(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'pid' => 'required'
|
||||
'pid' => 'required',
|
||||
'max_id' => 'sometimes|integer',
|
||||
'min_id' => 'sometimes|integer',
|
||||
]);
|
||||
$uid = $request->user()->profile_id;
|
||||
$user = $request->user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$uid = $user->profile_id;
|
||||
$pid = $request->input('pid');
|
||||
$max_id = $request->input('max_id');
|
||||
$min_id = $request->input('min_id');
|
||||
|
@ -408,29 +423,33 @@ class DirectMessageController extends Controller
|
|||
if ($min_id) {
|
||||
$res = DirectMessage::select('*')
|
||||
->where('id', '>', $min_id)
|
||||
->where(function($q) use($pid,$uid) {
|
||||
return $q->where([['from_id',$pid],['to_id',$uid]
|
||||
])->orWhere([['from_id',$uid],['to_id',$pid]]);
|
||||
->where(function ($query) use ($pid, $uid) {
|
||||
$query->where('from_id', $pid)->where('to_id', $uid);
|
||||
})->orWhere(function ($query) use ($pid, $uid) {
|
||||
$query->where('from_id', $uid)->where('to_id', $pid);
|
||||
})
|
||||
->latest()
|
||||
->orderBy('id', 'asc')
|
||||
->take(8)
|
||||
->get();
|
||||
->get()
|
||||
->reverse();
|
||||
} elseif ($max_id) {
|
||||
$res = DirectMessage::select('*')
|
||||
->where('id', '<', $max_id)
|
||||
->where(function($q) use($pid,$uid) {
|
||||
return $q->where([['from_id',$pid],['to_id',$uid]
|
||||
])->orWhere([['from_id',$uid],['to_id',$pid]]);
|
||||
->where(function ($query) use ($pid, $uid) {
|
||||
$query->where('from_id', $pid)->where('to_id', $uid);
|
||||
})->orWhere(function ($query) use ($pid, $uid) {
|
||||
$query->where('from_id', $uid)->where('to_id', $pid);
|
||||
})
|
||||
->latest()
|
||||
->orderBy('id', 'desc')
|
||||
->take(8)
|
||||
->get();
|
||||
} else {
|
||||
$res = DirectMessage::where(function($q) use($pid,$uid) {
|
||||
return $q->where([['from_id',$pid],['to_id',$uid]
|
||||
])->orWhere([['from_id',$uid],['to_id',$pid]]);
|
||||
$res = DirectMessage::where(function ($query) use ($pid, $uid) {
|
||||
$query->where('from_id', $pid)->where('to_id', $uid);
|
||||
})->orWhere(function ($query) use ($pid, $uid) {
|
||||
$query->where('from_id', $uid)->where('to_id', $pid);
|
||||
})
|
||||
->latest()
|
||||
->orderBy('id', 'desc')
|
||||
->take(8)
|
||||
->get();
|
||||
}
|
||||
|
@ -446,30 +465,32 @@ class DirectMessageController extends Controller
|
|||
'type' => $s->type,
|
||||
'text' => $s->status->caption,
|
||||
'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null,
|
||||
'carousel' => MediaService::get($s->status_id),
|
||||
'created_at' => $s->created_at->format('c'),
|
||||
'timeAgo' => $s->created_at->diffForHumans(null, null, true),
|
||||
'seen' => $s->read_at != null,
|
||||
'reportId' => (string) $s->status_id,
|
||||
'meta' => json_decode($s->meta,true)
|
||||
'meta' => json_decode($s->meta, true),
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
$filters = UserFilterService::mutes($uid);
|
||||
|
||||
$w = [
|
||||
'id' => (string) $r->id,
|
||||
'name' => $r->name,
|
||||
'username' => $r->username,
|
||||
'avatar' => $r->avatarUrl(),
|
||||
'url' => $r->url(),
|
||||
'muted' => UserFilter::whereUserId($uid)
|
||||
->whereFilterableId($r->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('dm.mute')
|
||||
->first() ? true : false,
|
||||
'muted' => in_array($r->id, $filters),
|
||||
'isLocal' => (bool) ! $r->domain,
|
||||
'domain' => $r->domain,
|
||||
'created_at' => $r->created_at->format('c'),
|
||||
'updated_at' => $r->updated_at->format('c'),
|
||||
'timeAgo' => $r->created_at->diffForHumans(null, true, true),
|
||||
'lastMessage' => '',
|
||||
'messages' => $res
|
||||
'messages' => $res,
|
||||
];
|
||||
|
||||
return response()->json($w, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
@ -478,7 +499,7 @@ class DirectMessageController extends Controller
|
|||
public function delete(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required'
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$sid = $request->input('id');
|
||||
|
@ -500,6 +521,8 @@ class DirectMessageController extends Controller
|
|||
if ($recipient['local'] == false) {
|
||||
$dmc = $dm;
|
||||
$this->remoteDelete($dmc);
|
||||
} else {
|
||||
StatusDelete::dispatch($status)->onQueue('high');
|
||||
}
|
||||
|
||||
if (Conversation::whereStatusId($sid)->count()) {
|
||||
|
@ -514,7 +537,7 @@ class DirectMessageController extends Controller
|
|||
'updated_at' => $latest->updated_at,
|
||||
'status_id' => $latest->status_id,
|
||||
'type' => $latest->type,
|
||||
'is_hidden' => false
|
||||
'is_hidden' => false,
|
||||
]);
|
||||
|
||||
Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id])
|
||||
|
@ -522,36 +545,39 @@ class DirectMessageController extends Controller
|
|||
'updated_at' => $latest->updated_at,
|
||||
'status_id' => $latest->status_id,
|
||||
'type' => $latest->type,
|
||||
'is_hidden' => false
|
||||
'is_hidden' => false,
|
||||
]);
|
||||
} else {
|
||||
Conversation::where([
|
||||
'status_id' => $sid,
|
||||
'to_id' => $dm->from_id,
|
||||
'from_id' => $dm->to_id
|
||||
'from_id' => $dm->to_id,
|
||||
])->delete();
|
||||
|
||||
Conversation::where([
|
||||
'status_id' => $sid,
|
||||
'from_id' => $dm->from_id,
|
||||
'to_id' => $dm->to_id
|
||||
'to_id' => $dm->to_id,
|
||||
])->delete();
|
||||
}
|
||||
}
|
||||
|
||||
StatusService::del($status->id, true);
|
||||
|
||||
$status->delete();
|
||||
$dm->delete();
|
||||
$status->forceDeleteQuietly();
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function get(Request $request, $id)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$dm = DirectMessage::whereStatusId($id)->firstOrFail();
|
||||
abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404);
|
||||
|
||||
return response()->json($dm, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
|
@ -565,10 +591,11 @@ class DirectMessageController extends Controller
|
|||
'max:'.config_cache('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
'to_id' => 'required'
|
||||
'to_id' => 'required',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
|
||||
$profile = $user->profile;
|
||||
$recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
|
||||
abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
|
||||
|
@ -583,16 +610,19 @@ class DirectMessageController extends Controller
|
|||
$hidden = false;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
|
@ -608,13 +638,12 @@ class DirectMessageController extends Controller
|
|||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = null;
|
||||
$status->rendered = null;
|
||||
$status->visibility = 'direct';
|
||||
$status->scope = 'direct';
|
||||
$status->in_reply_to_profile_id = $recipient->id;
|
||||
$status->save();
|
||||
|
||||
$media = new Media();
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
|
@ -638,16 +667,20 @@ class DirectMessageController extends Controller
|
|||
Conversation::updateOrInsert(
|
||||
[
|
||||
'to_id' => $recipient->id,
|
||||
'from_id' => $profile->id
|
||||
'from_id' => $profile->id,
|
||||
],
|
||||
[
|
||||
'type' => $dm->type,
|
||||
'status_id' => $status->id,
|
||||
'dm_id' => $dm->id,
|
||||
'is_hidden' => $hidden
|
||||
'is_hidden' => $hidden,
|
||||
]
|
||||
);
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
if ($recipient->domain) {
|
||||
$this->remoteDeliver($dm);
|
||||
}
|
||||
|
@ -656,7 +689,7 @@ class DirectMessageController extends Controller
|
|||
'id' => $dm->id,
|
||||
'reportId' => (string) $dm->status_id,
|
||||
'type' => $dm->type,
|
||||
'url' => $media->url()
|
||||
'url' => $media->url(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -667,6 +700,11 @@ class DirectMessageController extends Controller
|
|||
'remote' => 'nullable',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
if ($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = $request->input('q');
|
||||
$r = $request->input('remote', false);
|
||||
|
||||
|
@ -703,13 +741,14 @@ class DirectMessageController extends Controller
|
|||
->get()
|
||||
->map(function ($r) {
|
||||
$acct = AccountService::get($r->id);
|
||||
|
||||
return [
|
||||
'local' => (bool) ! $r->domain,
|
||||
'id' => (string) $r->id,
|
||||
'name' => $r->username,
|
||||
'privacy' => true,
|
||||
'avatar' => $r->avatarUrl(),
|
||||
'account' => $acct
|
||||
'account' => $acct,
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -720,11 +759,13 @@ class DirectMessageController extends Controller
|
|||
{
|
||||
$this->validate($request, [
|
||||
'pid' => 'required',
|
||||
'sid' => 'required'
|
||||
'sid' => 'required',
|
||||
]);
|
||||
|
||||
$pid = $request->input('pid');
|
||||
$sid = $request->input('sid');
|
||||
$user = $request->user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$dms = DirectMessage::whereToId($request->user()->profile_id)
|
||||
->whereFromId($pid)
|
||||
|
@ -743,9 +784,11 @@ class DirectMessageController extends Controller
|
|||
public function mute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required'
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
|
||||
$fid = $request->input('id');
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
|
@ -754,7 +797,7 @@ class DirectMessageController extends Controller
|
|||
'user_id' => $pid,
|
||||
'filterable_id' => $fid,
|
||||
'filterable_type' => 'App\Profile',
|
||||
'filter_type' => 'dm.mute'
|
||||
'filter_type' => 'dm.mute',
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -764,9 +807,12 @@ class DirectMessageController extends Controller
|
|||
public function unmute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required'
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
$fid = $request->input('id');
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
|
@ -785,15 +831,22 @@ class DirectMessageController extends Controller
|
|||
{
|
||||
$profile = $dm->author;
|
||||
$url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
|
||||
$status = $dm->status;
|
||||
|
||||
if (! $status) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = [
|
||||
[
|
||||
'type' => 'Mention',
|
||||
'href' => $dm->recipient->permalink(),
|
||||
'name' => $dm->recipient->emailUrl(),
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
|
||||
|
||||
$body = [
|
||||
'@context' => [
|
||||
'https://w3id.org/security/v1',
|
||||
|
@ -809,7 +862,7 @@ class DirectMessageController extends Controller
|
|||
'id' => $dm->status->url(),
|
||||
'type' => 'Note',
|
||||
'summary' => null,
|
||||
'content' => $dm->status->rendered ?? $dm->status->caption,
|
||||
'content' => $content,
|
||||
'inReplyTo' => null,
|
||||
'published' => $dm->status->created_at->toAtomString(),
|
||||
'url' => $dm->status->url(),
|
||||
|
@ -826,10 +879,10 @@ class DirectMessageController extends Controller
|
|||
];
|
||||
})->toArray(),
|
||||
'tag' => $tags,
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
Helpers::sendSignedObject($profile, $url, $body);
|
||||
DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high');
|
||||
}
|
||||
|
||||
public function remoteDelete($dm)
|
||||
|
@ -843,16 +896,15 @@ class DirectMessageController extends Controller
|
|||
],
|
||||
'id' => $dm->status->permalink('#delete'),
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
],
|
||||
'type' => 'Delete',
|
||||
'actor' => $dm->status->profile->permalink(),
|
||||
'object' => [
|
||||
'id' => $dm->status->url(),
|
||||
'type' => 'Tombstone'
|
||||
]
|
||||
'type' => 'Tombstone',
|
||||
],
|
||||
];
|
||||
|
||||
Helpers::sendSignedObject($profile, $url, $body);
|
||||
DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,48 +2,51 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\{
|
||||
DiscoverCategory,
|
||||
Follower,
|
||||
Hashtag,
|
||||
HashtagFollow,
|
||||
Instance,
|
||||
Like,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
UserFilter
|
||||
};
|
||||
use Auth, DB, Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Hashtag;
|
||||
use App\Instance;
|
||||
use App\Like;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminShadowFilterService;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\HashtagService;
|
||||
use App\Services\Internal\BeagleService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\ReblogService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\TrendingHashtagService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Status;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DiscoverController extends Controller
|
||||
{
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(! Auth::check() && config('instance.discover.public') == false, 403);
|
||||
|
||||
return view('discover.home');
|
||||
}
|
||||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
if ($request->user()) {
|
||||
return redirect('/i/web/hashtag/'.$hashtag.'?src=pd');
|
||||
}
|
||||
abort_if(! config('instance.discover.tags.is_public') && ! Auth::check(), 403);
|
||||
|
||||
$tag = Hashtag::whereName($hashtag)
|
||||
->orWhere('slug', $hashtag)
|
||||
->where('is_banned', '!=', true)
|
||||
->firstOrFail();
|
||||
$tagCount = StatusHashtagService::count($tag->id);
|
||||
$tagCount = $tag->cached_count ?? 0;
|
||||
|
||||
return view('discover.tags.show', compact('tag', 'tagCount'));
|
||||
}
|
||||
|
||||
|
@ -54,7 +57,7 @@ class DiscoverController extends Controller
|
|||
|
||||
$this->validate($request, [
|
||||
'hashtag' => 'required|string|min:1|max:124',
|
||||
'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3)
|
||||
'page' => 'nullable|integer|min:1|max:'.($user ? 29 : 3),
|
||||
]);
|
||||
|
||||
$page = $request->input('page') ?? '1';
|
||||
|
@ -75,7 +78,7 @@ class DiscoverController extends Controller
|
|||
}
|
||||
$res['hashtag'] = [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url()
|
||||
'url' => $hashtag->url(),
|
||||
];
|
||||
if ($user) {
|
||||
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||
|
@ -84,18 +87,21 @@ class DiscoverController extends Controller
|
|||
$tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']);
|
||||
$tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']);
|
||||
$tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']);
|
||||
|
||||
return $tag;
|
||||
})
|
||||
->filter(function ($tag) {
|
||||
if (! StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
} else {
|
||||
if ($page != 1) {
|
||||
$res['tags'] = [];
|
||||
|
||||
return $res;
|
||||
}
|
||||
$key = 'discover:tags:public_feed:'.$hashtag->id.':page:'.$page;
|
||||
|
@ -105,6 +111,7 @@ class DiscoverController extends Controller
|
|||
if (! $tag['status']['local']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
|
@ -114,10 +121,12 @@ class DiscoverController extends Controller
|
|||
if (! StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -144,12 +153,13 @@ class DiscoverController extends Controller
|
|||
$ttls = [
|
||||
1 => 1500,
|
||||
31 => 14400,
|
||||
365 => 86400
|
||||
365 => 86400,
|
||||
];
|
||||
$key = ':api:discover:trending:v2.12:range:'.$days;
|
||||
|
||||
$ids = Cache::remember($key, $ttls[$days], function () use ($days) {
|
||||
$min_id = SnowflakeService::byDate(now()->subDays($days));
|
||||
|
||||
return DB::table('statuses')
|
||||
->select(
|
||||
'id',
|
||||
|
@ -165,7 +175,7 @@ class DiscoverController extends Controller
|
|||
->whereIn('type', [
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video'
|
||||
'video',
|
||||
])
|
||||
->whereIsNsfw(false)
|
||||
->orderBy('likes_count', 'desc')
|
||||
|
@ -192,6 +202,7 @@ class DiscoverController extends Controller
|
|||
abort_if(! $request->user(), 403);
|
||||
|
||||
$res = TrendingHashtagService::getTrending();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -236,6 +247,7 @@ class DiscoverController extends Controller
|
|||
->map(function ($id) {
|
||||
$status = StatusService::get($id, false);
|
||||
$status['favourited'] = true;
|
||||
|
||||
return $status;
|
||||
})
|
||||
->filter(function ($post) {
|
||||
|
@ -277,6 +289,7 @@ class DiscoverController extends Controller
|
|||
if ($cc) {
|
||||
return is_string($cc) ? json_decode($cc, true) : $cc;
|
||||
}
|
||||
|
||||
return [
|
||||
'hashtags' => [
|
||||
'enabled' => false,
|
||||
|
@ -293,8 +306,8 @@ class DiscoverController extends Controller
|
|||
'server' => [
|
||||
'enabled' => false,
|
||||
'mode' => 'allowlist',
|
||||
'domains' => []
|
||||
]
|
||||
'domains' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -321,12 +334,14 @@ class DiscoverController extends Controller
|
|||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function enabledFeatures(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return $this->config();
|
||||
}
|
||||
|
||||
|
@ -355,6 +370,7 @@ class DiscoverController extends Controller
|
|||
if (! Instance::whereDomain($domain)->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
$parts = array_slice($parts, 0, 10);
|
||||
|
@ -362,6 +378,54 @@ class DiscoverController extends Controller
|
|||
$res['server']['domains'] = $d;
|
||||
}
|
||||
ConfigCacheService::put('config.discover.features', json_encode($res));
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function discoverAccountsPopular(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () {
|
||||
return DB::table('profiles')
|
||||
->where('is_private', false)
|
||||
->whereNull('status')
|
||||
->orderByDesc('profiles.followers_count')
|
||||
->limit(30)
|
||||
->get();
|
||||
});
|
||||
$filters = UserFilterService::filters($pid);
|
||||
$asf = AdminShadowFilterService::getHideFromPublicFeedsList();
|
||||
$ids = $ids->map(function ($profile) {
|
||||
return AccountService::get($profile->id, true);
|
||||
})
|
||||
->filter(function ($profile) {
|
||||
return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked'];
|
||||
})
|
||||
->filter(function ($profile) use ($pid) {
|
||||
return $profile['id'] != $pid;
|
||||
})
|
||||
->filter(function ($profile) use ($pid) {
|
||||
return ! FollowerService::follows($pid, $profile['id'], true);
|
||||
})
|
||||
->filter(function ($profile) use ($asf) {
|
||||
return ! in_array($profile['id'], $asf);
|
||||
})
|
||||
->filter(function ($profile) use ($filters) {
|
||||
return ! in_array($profile['id'], $filters);
|
||||
})
|
||||
->take(16)
|
||||
->values();
|
||||
|
||||
return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function discoverNetworkTrending(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return BeagleService::getDiscoverPosts();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,40 +2,25 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\InboxPipeline\{
|
||||
DeleteWorker,
|
||||
InboxWorker,
|
||||
InboxValidator
|
||||
};
|
||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||
use App\{
|
||||
AccountLog,
|
||||
Like,
|
||||
Profile,
|
||||
Status,
|
||||
User
|
||||
};
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use App\Util\ActivityPub\{
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Outbox
|
||||
};
|
||||
use Zttp\Zttp;
|
||||
use App\Jobs\InboxPipeline\DeleteWorker;
|
||||
use App\Jobs\InboxPipeline\InboxValidator;
|
||||
use App\Jobs\InboxPipeline\InboxWorker;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Status;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
public function nodeinfoWellKnown()
|
||||
{
|
||||
abort_if(! config('federation.nodeinfo.enabled'), 404);
|
||||
|
||||
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
@ -43,6 +28,7 @@ class FederationController extends Controller
|
|||
public function nodeinfo()
|
||||
{
|
||||
abort_if(! config('federation.nodeinfo.enabled'), 404);
|
||||
|
||||
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
@ -59,28 +45,64 @@ class FederationController extends Controller
|
|||
$resource = $request->input('resource');
|
||||
$domain = config('pixelfed.domain.app');
|
||||
|
||||
if(config('federation.activitypub.sharedInbox') &&
|
||||
$resource == 'acct:' . $domain . '@' . $domain) {
|
||||
// Instance Actor
|
||||
if (
|
||||
config('federation.activitypub.sharedInbox') &&
|
||||
$resource == 'acct:'.$domain.'@'.$domain
|
||||
) {
|
||||
$res = [
|
||||
'subject' => 'acct:'.$domain.'@'.$domain,
|
||||
'aliases' => [
|
||||
'https://' . $domain . '/i/actor'
|
||||
'https://'.$domain.'/i/actor',
|
||||
],
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => 'https://' . $domain . '/site/kb/instance-actor'
|
||||
'href' => 'https://'.$domain.'/site/kb/instance-actor',
|
||||
],
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => 'https://' . $domain . '/i/actor'
|
||||
]
|
||||
]
|
||||
'href' => 'https://'.$domain.'/i/actor',
|
||||
],
|
||||
[
|
||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template' => 'https://'.$domain.'/authorize_interaction?uri={uri}',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
if (str_starts_with($resource, 'https://')) {
|
||||
if (str_starts_with($resource, 'https://'.$domain.'/users/')) {
|
||||
$username = str_replace('https://'.$domain.'/users/', '', $resource);
|
||||
if (strlen($username) > 15) {
|
||||
return response('', 400);
|
||||
}
|
||||
$stripped = str_replace(['_', '.', '-'], '', $username);
|
||||
if (! ctype_alnum($stripped)) {
|
||||
return response('', 400);
|
||||
}
|
||||
$key = 'federation:webfinger:sha256:url-username:'.$username;
|
||||
if ($cached = Cache::get($key)) {
|
||||
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$profile = Profile::whereUsername($username)->first();
|
||||
if (! $profile || $profile->status !== null || $profile->domain) {
|
||||
return response('', 400);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
Cache::put($key, $webfinger, 1209600);
|
||||
|
||||
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
} else {
|
||||
return response('', 400);
|
||||
}
|
||||
}
|
||||
$hash = hash('sha256', $resource);
|
||||
$key = 'federation:webfinger:sha256:'.$hash;
|
||||
if ($cached = Cache::get($key)) {
|
||||
|
@ -94,8 +116,8 @@ class FederationController extends Controller
|
|||
return response('', 400);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
|
||||
if(!$profile || $profile->status !== null) {
|
||||
$profile = Profile::whereUsername($username)->first();
|
||||
if (! $profile || $profile->status !== null || $profile->domain) {
|
||||
return response('', 400);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
|
@ -117,18 +139,21 @@ class FederationController extends Controller
|
|||
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
if (! $request->wantsJson()) {
|
||||
return redirect('/'.$username);
|
||||
}
|
||||
|
||||
$id = AccountService::usernameToId($username);
|
||||
abort_if(! $id, 404);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(! $account || ! isset($account['statuses_count']), 404);
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox',
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
'totalItems' => $account['statuses_count'] ?? 0,
|
||||
];
|
||||
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
|
@ -136,7 +161,7 @@ class FederationController extends Controller
|
|||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! config('federation.activitypub.inbox'), 404);
|
||||
|
||||
$headers = $request->headers->all();
|
||||
|
@ -158,6 +183,7 @@ class FederationController extends Controller
|
|||
if ($obj['object']['type'] === 'Person') {
|
||||
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -165,27 +191,30 @@ class FederationController extends Controller
|
|||
if ($obj['object']['type'] === 'Tombstone') {
|
||||
if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
} elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! config('federation.activitypub.sharedInbox'), 404);
|
||||
|
||||
$headers = $request->headers->all();
|
||||
|
@ -210,6 +239,7 @@ class FederationController extends Controller
|
|||
if ($obj['object']['type'] === 'Person') {
|
||||
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -217,50 +247,59 @@ class FederationController extends Controller
|
|||
if ($obj['object']['type'] === 'Tombstone') {
|
||||
if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
} elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
$id = AccountService::usernameToId($username);
|
||||
abort_if(! $id, 404);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(! $account || ! isset($account['following_count']), 404);
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
'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)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
$id = AccountService::usernameToId($username);
|
||||
abort_if(! $id, 404);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(! $account || ! isset($account['followers_count']), 404);
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $account['followers_count'] ?? 0,
|
||||
];
|
||||
|
||||
return response()->json($obj);
|
||||
return response()->json($obj)->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
}
|
||||
|
|
671
app/Http/Controllers/GroupController.php
Normal file
671
app/Http/Controllers/GroupController.php
Normal file
|
@ -0,0 +1,671 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Instance;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupBlock;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupLimit;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupReport;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Storage;
|
||||
|
||||
class GroupController extends GroupFederationController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
abort_unless(config('groups.enabled'), 404);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function show(Request $request, $id, $path = false)
|
||||
{
|
||||
$group = Group::find($id);
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return $this->showGroupObject($group);
|
||||
}
|
||||
|
||||
return view('layouts.spa', compact('id', 'path'));
|
||||
}
|
||||
|
||||
public function showStatus(Request $request, $gid, $sid)
|
||||
{
|
||||
$group = Group::find($gid);
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if ($group->is_private) {
|
||||
abort_if(! $request->user(), 404);
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
}
|
||||
|
||||
$gp = GroupPost::whereGroupId($gid)
|
||||
->findOrFail($sid);
|
||||
|
||||
return view('layouts.spa', compact('group', 'gp'));
|
||||
}
|
||||
|
||||
public function getGroup(Request $request, $id)
|
||||
{
|
||||
$group = Group::whereNull('status')->findOrFail($id);
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
$group = $this->toJson($group, $pid);
|
||||
|
||||
return response()->json($group, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showStatusLikes(Request $request, $id, $sid)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$user = $request->user();
|
||||
$pid = $user->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
$status = GroupPost::whereGroupId($id)->findOrFail($sid);
|
||||
$likes = GroupLike::whereStatusId($sid)
|
||||
->cursorPaginate(10)
|
||||
->map(function ($l) use ($group) {
|
||||
$account = AccountService::get($l->profile_id);
|
||||
$account['url'] = "/groups/{$group->id}/user/{$account['id']}";
|
||||
|
||||
return $account;
|
||||
})
|
||||
->filter(function ($l) {
|
||||
return $l && isset($l['id']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $likes;
|
||||
}
|
||||
|
||||
public function groupSettings(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
return view('groups.settings', compact('group'));
|
||||
}
|
||||
|
||||
public function joinGroup(Request $request, $id)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if($group->isMember($pid), 404);
|
||||
|
||||
if (! $request->user()->is_admin) {
|
||||
abort_if(GroupService::getRejoinTimeout($group->id, $pid), 422, 'Cannot re-join this group for 24 hours after leaving or cancelling a request to join');
|
||||
}
|
||||
|
||||
$member = new GroupMember;
|
||||
$member->group_id = $group->id;
|
||||
$member->profile_id = $pid;
|
||||
$member->role = 'member';
|
||||
$member->local_group = true;
|
||||
$member->local_profile = true;
|
||||
$member->join_request = $group->is_private;
|
||||
$member->save();
|
||||
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:joined',
|
||||
null,
|
||||
GroupMember::class,
|
||||
$member->id
|
||||
);
|
||||
|
||||
$group = $this->toJson($group, $pid);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
public function updateGroup(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'description' => 'nullable|max:500',
|
||||
'membership' => 'required|in:all,local,private',
|
||||
'avatar' => 'nullable',
|
||||
'header' => 'nullable',
|
||||
'discoverable' => 'required',
|
||||
'activitypub' => 'required',
|
||||
'is_nsfw' => 'required',
|
||||
'category' => 'required|string|in:'.implode(',', GroupService::categories()),
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::whereProfileId($pid)->findOrFail($id);
|
||||
$member = GroupMember::whereGroupId($group->id)->whereProfileId($pid)->firstOrFail();
|
||||
|
||||
abort_if($member->role != 'founder', 403, 'Invalid group permission');
|
||||
|
||||
$metadata = $group->metadata;
|
||||
$len = $group->is_private ? 12 : 4;
|
||||
|
||||
if ($request->hasFile('avatar')) {
|
||||
$avatar = $request->file('avatar');
|
||||
|
||||
if ($avatar) {
|
||||
if (isset($metadata['avatar']) &&
|
||||
isset($metadata['avatar']['path']) &&
|
||||
Storage::exists($metadata['avatar']['path'])
|
||||
) {
|
||||
Storage::delete($metadata['avatar']['path']);
|
||||
}
|
||||
|
||||
$fileName = 'avatar_'.strtolower(str_random($len)).'.'.$avatar->extension();
|
||||
$path = $avatar->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
|
||||
$url = url(Storage::url($path));
|
||||
$metadata['avatar'] = [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->hasFile('header')) {
|
||||
$header = $request->file('header');
|
||||
|
||||
if ($header) {
|
||||
if (isset($metadata['header']) &&
|
||||
isset($metadata['header']['path']) &&
|
||||
Storage::exists($metadata['header']['path'])
|
||||
) {
|
||||
Storage::delete($metadata['header']['path']);
|
||||
}
|
||||
|
||||
$fileName = 'header_'.strtolower(str_random($len)).'.'.$header->extension();
|
||||
$path = $header->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
|
||||
$url = url(Storage::url($path));
|
||||
$metadata['header'] = [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$cat = GroupService::categoryById($group->category_id);
|
||||
if ($request->category !== $cat['name']) {
|
||||
$group->category_id = GroupCategory::whereName($request->category)->first()->id;
|
||||
}
|
||||
|
||||
$changes = null;
|
||||
$group->description = e($request->input('description', null));
|
||||
$group->is_private = $request->input('membership') == 'private';
|
||||
$group->local_only = $request->input('membership') == 'local';
|
||||
$group->activitypub = $request->input('activitypub') == 'true';
|
||||
$group->discoverable = $request->input('discoverable') == 'true';
|
||||
$group->is_nsfw = $request->input('is_nsfw') == 'true';
|
||||
$group->metadata = $metadata;
|
||||
if ($group->isDirty()) {
|
||||
$changes = $group->getDirty();
|
||||
}
|
||||
$group->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:settings:updated',
|
||||
$changes
|
||||
);
|
||||
|
||||
GroupService::del($group->id);
|
||||
|
||||
$res = $this->toJson($group, $pid);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function toJson($group, $pid = false)
|
||||
{
|
||||
return GroupService::get($group->id, $pid);
|
||||
}
|
||||
|
||||
public function groupLeave(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
|
||||
|
||||
abort_if(! $group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
|
||||
GroupService::del($group->id);
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::setRejoinTimeout($group->id, $pid);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function cancelJoinRequest(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
|
||||
abort_if($group->isMember($pid), 422, 'Cannot cancel approved join request, please leave group instead.');
|
||||
|
||||
GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
|
||||
GroupService::del($group->id);
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::setRejoinTimeout($group->id, $pid);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function metaBlockSearch(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
|
||||
switch ($type) {
|
||||
case 'instance':
|
||||
$res = Instance::whereDomain($item)->first();
|
||||
if ($res) {
|
||||
abort_if(GroupBlock::whereGroupId($group->id)->whereInstanceId($res->id)->exists(), 400);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$res = Profile::whereUsername($item)->first();
|
||||
if ($res) {
|
||||
abort_if(GroupBlock::whereGroupId($group->id)->whereProfileId($res->id)->exists(), 400);
|
||||
}
|
||||
if ($res->user_id != null) {
|
||||
abort_if(User::whereIsAdmin(true)->whereId($res->user_id)->exists(), 400);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json((bool) $res, ($res ? 200 : 404));
|
||||
}
|
||||
|
||||
public function reportCreate(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
|
||||
$id = $request->input('id');
|
||||
$type = $request->input('type');
|
||||
$types = [
|
||||
// original 3
|
||||
'spam',
|
||||
'sensitive',
|
||||
'abusive',
|
||||
|
||||
// new
|
||||
'underage',
|
||||
'violence',
|
||||
'copyright',
|
||||
'impersonation',
|
||||
'scam',
|
||||
'terrorism',
|
||||
];
|
||||
|
||||
$gp = GroupPost::whereGroupId($group->id)->find($id);
|
||||
abort_if(! $gp, 422, 'Cannot report an invalid or deleted post');
|
||||
abort_if(! in_array($type, $types), 422, 'Invalid report type');
|
||||
abort_if($gp->profile_id === $pid, 422, 'Cannot report your own post');
|
||||
abort_if(
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereProfileId($pid)
|
||||
->whereItemType(GroupPost::class)
|
||||
->whereItemId($id)
|
||||
->exists(),
|
||||
422,
|
||||
'You already reported this'
|
||||
);
|
||||
|
||||
$report = new GroupReport();
|
||||
$report->group_id = $group->id;
|
||||
$report->profile_id = $pid;
|
||||
$report->type = $type;
|
||||
$report->item_type = GroupPost::class;
|
||||
$report->item_id = $id;
|
||||
$report->open = true;
|
||||
$report->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:report:create',
|
||||
[
|
||||
'type' => $type,
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $gp->status_id,
|
||||
'profile_id' => $gp->profile_id,
|
||||
'username' => optional(AccountService::get($gp->profile_id))['acct'],
|
||||
'gpid' => $gp->id,
|
||||
'url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response([200]);
|
||||
}
|
||||
|
||||
public function reportAction(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:cw,delete,ignore',
|
||||
'id' => 'required|string',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$id = $request->input('id');
|
||||
|
||||
$report = GroupReport::whereGroupId($group->id)
|
||||
->findOrFail($id);
|
||||
$status = Status::findOrFail($report->item_id);
|
||||
$gp = GroupPost::whereGroupId($group->id)
|
||||
->whereStatusId($status->id)
|
||||
->firstOrFail();
|
||||
|
||||
switch ($action) {
|
||||
case 'cw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereItemType($report->item_type)
|
||||
->whereItemId($report->item_id)
|
||||
->update(['open' => false]);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:moderation:action',
|
||||
[
|
||||
'type' => 'cw',
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response()->json([200]);
|
||||
break;
|
||||
|
||||
case 'ignore':
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereItemType($report->item_type)
|
||||
->whereItemId($report->item_id)
|
||||
->update(['open' => false]);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:moderation:action',
|
||||
[
|
||||
'type' => 'ignore',
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response()->json([200]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemberInteractionLimits(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$profile_id = $request->input('profile_id');
|
||||
abort_if(! $group->isMember($profile_id), 404);
|
||||
$limits = GroupService::getInteractionLimits($group->id, $profile_id);
|
||||
|
||||
return response()->json($limits);
|
||||
}
|
||||
|
||||
public function updateMemberInteractionLimits(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|exists:profiles,id',
|
||||
'can_post' => 'required',
|
||||
'can_comment' => 'required',
|
||||
'can_like' => 'required',
|
||||
]);
|
||||
|
||||
$member = $request->input('profile_id');
|
||||
$can_post = $request->input('can_post');
|
||||
$can_comment = $request->input('can_comment');
|
||||
$can_like = $request->input('can_like');
|
||||
$account = AccountService::get($member);
|
||||
|
||||
abort_if(! $account, 422, 'Invalid profile');
|
||||
abort_if(! $group->isMember($member), 422, 'Invalid profile');
|
||||
|
||||
$limit = GroupLimit::firstOrCreate([
|
||||
'profile_id' => $member,
|
||||
'group_id' => $group->id,
|
||||
]);
|
||||
|
||||
if ($limit->wasRecentlyCreated) {
|
||||
abort_if(GroupLimit::whereGroupId($group->id)->count() >= 25, 422, 'limit_reached');
|
||||
}
|
||||
|
||||
$previousLimits = $limit->limits;
|
||||
|
||||
$limit->limits = [
|
||||
'can_post' => $can_post,
|
||||
'can_comment' => $can_comment,
|
||||
'can_like' => $can_like,
|
||||
];
|
||||
$limit->save();
|
||||
|
||||
GroupService::clearInteractionLimits($group->id, $member);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:member-limits:updated',
|
||||
[
|
||||
'profile_id' => $account['id'],
|
||||
'username' => $account['username'],
|
||||
'previousLimits' => $previousLimits,
|
||||
'newLimits' => $limit->limits,
|
||||
],
|
||||
GroupLimit::class,
|
||||
$limit->id
|
||||
);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function showProfile(Request $request, $id, $pid)
|
||||
{
|
||||
$group = Group::find($id);
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function showProfileByUsername(Request $request, $id, $pid)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
if (! $request->user()) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
$group = Group::find($id);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if (! $group->isMember($cid)) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
$profile = Profile::whereUsername($pid)->first();
|
||||
|
||||
if (! $group->isMember($profile->id)) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
if ($profile) {
|
||||
$url = url("/groups/{$id}/user/{$profile->id}");
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
abort(404, 'Invalid username');
|
||||
}
|
||||
|
||||
public function groupInviteLanding(Request $request, $id)
|
||||
{
|
||||
abort(404, 'Not yet implemented');
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
return view('groups.invite', compact('group'));
|
||||
}
|
||||
|
||||
public function groupShortLinkRedirect(Request $request, $hid)
|
||||
{
|
||||
$gid = HashidService::decode($hid);
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
return redirect($group->url());
|
||||
}
|
||||
|
||||
public function groupInviteClaim(Request $request, $id)
|
||||
{
|
||||
$group = GroupService::get($id);
|
||||
abort_if(! $group || empty($group), 404);
|
||||
|
||||
return view('groups.invite-claim', compact('group'));
|
||||
}
|
||||
|
||||
public function groupMemberInviteCheck(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
$exists = GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists();
|
||||
|
||||
return response()->json([
|
||||
'gid' => $id,
|
||||
'can_join' => (bool) $exists,
|
||||
]);
|
||||
}
|
||||
|
||||
public function groupMemberInviteAccept(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
abort_if(! GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists(), 422);
|
||||
|
||||
$gm = new GroupMember;
|
||||
$gm->group_id = $id;
|
||||
$gm->profile_id = $pid;
|
||||
$gm->role = 'member';
|
||||
$gm->local_group = $group->local;
|
||||
$gm->local_profile = true;
|
||||
$gm->join_request = false;
|
||||
$gm->save();
|
||||
|
||||
GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->delete();
|
||||
GroupService::del($id);
|
||||
GroupService::delSelf($id, $pid);
|
||||
|
||||
return ['next_url' => $group->url()];
|
||||
}
|
||||
|
||||
public function groupMemberInviteDecline(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
return ['next_url' => '/'];
|
||||
}
|
||||
}
|
107
app/Http/Controllers/GroupFederationController.php
Normal file
107
app/Http/Controllers/GroupFederationController.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\InstanceActor;
|
||||
use App\Services\MediaService;
|
||||
use App\Status;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class GroupFederationController extends Controller
|
||||
{
|
||||
public function getGroupObject(Request $request, $id)
|
||||
{
|
||||
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id);
|
||||
$res = $this->showGroupObject($group);
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showGroupObject($group)
|
||||
{
|
||||
return Cache::remember('ap:groups:object:'.$group->id, 3600, function () use ($group) {
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $group->url(),
|
||||
'inbox' => $group->permalink('/inbox'),
|
||||
'name' => $group->name,
|
||||
'outbox' => $group->permalink('/outbox'),
|
||||
'summary' => $group->description,
|
||||
'type' => 'Group',
|
||||
'attributedTo' => [
|
||||
'type' => 'Person',
|
||||
'id' => $group->admin->permalink(),
|
||||
],
|
||||
// 'endpoints' => [
|
||||
// 'sharedInbox' => config('app.url') . '/f/inbox'
|
||||
// ],
|
||||
'preferredUsername' => 'gid_'.$group->id,
|
||||
'publicKey' => [
|
||||
'id' => $group->permalink('#main-key'),
|
||||
'owner' => $group->permalink(),
|
||||
'publicKeyPem' => InstanceActor::first()->public_key,
|
||||
],
|
||||
'url' => $group->permalink(),
|
||||
];
|
||||
|
||||
if ($group->metadata && isset($group->metadata['avatar'])) {
|
||||
$res['icon'] = [
|
||||
'type' => 'Image',
|
||||
'url' => $group->metadata['avatar']['url'],
|
||||
];
|
||||
}
|
||||
|
||||
if ($group->metadata && isset($group->metadata['header'])) {
|
||||
$res['image'] = [
|
||||
'type' => 'Image',
|
||||
'url' => $group->metadata['header']['url'],
|
||||
];
|
||||
}
|
||||
ksort($res);
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public function getStatusObject(Request $request, $gid, $sid)
|
||||
{
|
||||
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($gid);
|
||||
$gp = GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
$status = Status::findOrFail($gp->status_id);
|
||||
// permission check
|
||||
$content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $gp->url(),
|
||||
|
||||
'type' => 'Note',
|
||||
|
||||
'summary' => null,
|
||||
'content' => $content,
|
||||
'inReplyTo' => null,
|
||||
|
||||
'published' => $status->created_at->toAtomString(),
|
||||
'url' => $gp->url(),
|
||||
'attributedTo' => $status->profile->permalink(),
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
$group->permalink('/followers'),
|
||||
],
|
||||
'cc' => [],
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'attachment' => MediaService::activitypub($status->id),
|
||||
'target' => [
|
||||
'type' => 'Collection',
|
||||
'id' => $group->permalink('/wall'),
|
||||
'attributedTo' => $group->permalink(),
|
||||
],
|
||||
];
|
||||
|
||||
// ksort($res);
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
10
app/Http/Controllers/GroupPostController.php
Normal file
10
app/Http/Controllers/GroupPostController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GroupPostController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
83
app/Http/Controllers/Groups/CreateGroupsController.php
Normal file
83
app/Http/Controllers/Groups/CreateGroupsController.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
|
||||
class CreateGroupsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function checkCreatePermission(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$config = GroupService::config();
|
||||
if($request->user()->is_admin) {
|
||||
$allowed = true;
|
||||
} else {
|
||||
$max = $config['limits']['user']['create']['max'];
|
||||
$allowed = Group::whereProfileId($pid)->count() <= $max;
|
||||
}
|
||||
|
||||
return ['permission' => (bool) $allowed];
|
||||
}
|
||||
|
||||
public function storeGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'description' => 'nullable|max:500',
|
||||
'membership' => 'required|in:public,private,local'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$config = GroupService::config();
|
||||
abort_if($config['limits']['user']['create']['new'] == false && $request->user()->is_admin == false, 422, 'Invalid operation');
|
||||
$max = $config['limits']['user']['create']['max'];
|
||||
// abort_if(Group::whereProfileId($pid)->count() <= $max, 422, 'Group limit reached');
|
||||
|
||||
$group = new Group;
|
||||
$group->profile_id = $pid;
|
||||
$group->name = $request->input('name');
|
||||
$group->description = $request->input('description', null);
|
||||
$group->is_private = $request->input('membership') == 'private';
|
||||
$group->local_only = $request->input('membership') == 'local';
|
||||
$group->metadata = $request->input('configuration');
|
||||
$group->save();
|
||||
|
||||
GroupService::log($group->id, $pid, 'group:created');
|
||||
|
||||
$member = new GroupMember;
|
||||
$member->group_id = $group->id;
|
||||
$member->profile_id = $pid;
|
||||
$member->role = 'founder';
|
||||
$member->local_group = true;
|
||||
$member->local_profile = true;
|
||||
$member->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:joined',
|
||||
null,
|
||||
GroupMember::class,
|
||||
$member->id
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $group->id,
|
||||
'url' => $group->url()
|
||||
];
|
||||
}
|
||||
}
|
353
app/Http/Controllers/Groups/GroupsAdminController.php
Normal file
353
app/Http/Controllers/Groups/GroupsAdminController.php
Normal file
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupBlock;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupReport;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
|
||||
class GroupsAdminController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getAdminTabs(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
abort_if($pid !== $group->profile_id, 404);
|
||||
|
||||
$reqs = GroupMember::whereGroupId($group->id)->whereJoinRequest(true)->count();
|
||||
$mods = GroupReport::whereGroupId($group->id)->whereOpen(true)->count();
|
||||
$tabs = [
|
||||
'moderation_count' => $mods > 99 ? '99+' : $mods,
|
||||
'request_count' => $reqs > 99 ? '99+' : $reqs
|
||||
];
|
||||
|
||||
return response()->json($tabs);
|
||||
}
|
||||
|
||||
public function getInteractionLogs(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$logs = GroupInteraction::whereGroupId($id)
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->map(function($log) use($group) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'profile' => GroupAccountService::get($group->id, $log->profile_id),
|
||||
'type' => $log->type,
|
||||
'metadata' => $log->metadata,
|
||||
'created_at' => $log->created_at->format('c')
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($logs, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getBlocks(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$blocks = [
|
||||
'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->take(3)->pluck('name'),
|
||||
'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->take(3)->pluck('name'),
|
||||
'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->take(3)->pluck('name')
|
||||
];
|
||||
|
||||
return response()->json($blocks, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function exportBlocks(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$blocks = [
|
||||
'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->pluck('name'),
|
||||
'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->pluck('name'),
|
||||
'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->pluck('name')
|
||||
];
|
||||
|
||||
$blocks['_created_at'] = now()->format('c');
|
||||
$blocks['_version'] = '1.0.0';
|
||||
ksort($blocks);
|
||||
|
||||
return response()->streamDownload(function() use($blocks) {
|
||||
echo json_encode($blocks, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
});
|
||||
}
|
||||
|
||||
public function addBlock(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required',
|
||||
'type' => 'required|in:instance,user,moderate'
|
||||
]);
|
||||
|
||||
$item = $request->input('item');
|
||||
$type = $request->input('type');
|
||||
|
||||
switch($type) {
|
||||
case 'instance':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->instance_id = $instance->id;
|
||||
$gb->name = $instance->domain;
|
||||
$gb->is_user = false;
|
||||
$gb->moderated = false;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:block:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$profile = Profile::whereUsername($item)->first();
|
||||
abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->profile_id = $profile->id;
|
||||
$gb->name = $profile->username;
|
||||
$gb->is_user = true;
|
||||
$gb->moderated = false;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:block:user',
|
||||
[
|
||||
'username' => $profile->username,
|
||||
'domain' => $profile->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'moderate':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->instance_id = $instance->id;
|
||||
$gb->name = $instance->domain;
|
||||
$gb->is_user = false;
|
||||
$gb->moderated = true;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:moderate:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
default:
|
||||
return response()->json([], 422, []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function undoBlock(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required',
|
||||
'type' => 'required|in:instance,user,moderate'
|
||||
]);
|
||||
|
||||
$item = $request->input('item');
|
||||
$type = $request->input('type');
|
||||
|
||||
switch($type) {
|
||||
case 'instance':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereInstanceId($instance->id)
|
||||
->whereModerated(false)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:unblock:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$profile = Profile::whereUsername($item)->first();
|
||||
abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereProfileId($profile->id)
|
||||
->whereIsUser(true)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:unblock:user',
|
||||
[
|
||||
'username' => $profile->username,
|
||||
'domain' => $profile->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'moderate':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereInstanceId($instance->id)
|
||||
->whereModerated(true)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:moderate:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
default:
|
||||
return response()->json([], 422, []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getReportList(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$scope = $request->input('scope', 'open');
|
||||
|
||||
$list = GroupReport::selectRaw('id, profile_id, item_type, item_id, type, created_at, count(*) as total')
|
||||
->whereGroupId($group->id)
|
||||
->groupBy('item_id')
|
||||
->when($scope == 'open', function($query, $scope) {
|
||||
return $query->whereOpen(true);
|
||||
})
|
||||
->latest()
|
||||
->simplePaginate(10)
|
||||
->map(function($report) use($group) {
|
||||
$res = [
|
||||
'id' => (string) $report->id,
|
||||
'profile' => GroupAccountService::get($group->id, $report->profile_id),
|
||||
'type' => $report->type,
|
||||
'created_at' => $report->created_at->format('c'),
|
||||
'total_count' => $report->total
|
||||
];
|
||||
|
||||
if($report->item_type === GroupPost::class) {
|
||||
$res['status'] = GroupPostService::get($group->id, $report->item_id);
|
||||
}
|
||||
|
||||
return $res;
|
||||
});
|
||||
return response()->json($list, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
}
|
84
app/Http/Controllers/Groups/GroupsApiController.php
Normal file
84
app/Http/Controllers/Groups/GroupsApiController.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupMember;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
|
||||
class GroupsApiController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
protected function toJson($group, $pid = false)
|
||||
{
|
||||
return GroupService::get($group->id, $pid);
|
||||
}
|
||||
|
||||
public function getConfig(Request $request)
|
||||
{
|
||||
return GroupService::config();
|
||||
}
|
||||
|
||||
public function getGroupAccount(Request $request, $gid, $pid)
|
||||
{
|
||||
$res = GroupAccountService::get($gid, $pid);
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function getGroupCategories(Request $request)
|
||||
{
|
||||
$res = GroupService::categories();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupsByCategory(Request $request)
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$category = GroupCategory::whereName($name)->firstOrFail();
|
||||
$groups = Group::whereCategoryId($category->id)
|
||||
->simplePaginate(6)
|
||||
->map(function($group) {
|
||||
return GroupService::get($group->id);
|
||||
})
|
||||
->filter(function($group) {
|
||||
return $group;
|
||||
})
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getRecommendedGroups(Request $request)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getSelfGroups(Request $request)
|
||||
{
|
||||
$selfOnly = $request->input('self') == true;
|
||||
$memberOnly = $request->input('member') == true;
|
||||
$pid = $request->user()->profile_id;
|
||||
$res = GroupMember::whereProfileId($request->user()->profile_id)
|
||||
->when($selfOnly, function($q, $selfOnly) {
|
||||
return $q->whereRole('founder');
|
||||
})
|
||||
->when($memberOnly, function($q, $memberOnly) {
|
||||
return $q->whereRole('member');
|
||||
})
|
||||
->simplePaginate(4)
|
||||
->map(function($member) use($pid) {
|
||||
$group = $member->group;
|
||||
return $this->toJson($group, $pid);
|
||||
});
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
361
app/Http/Controllers/Groups/GroupsCommentController.php
Normal file
361
app/Http/Controllers/Groups/GroupsCommentController.php
Normal file
|
@ -0,0 +1,361 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupCommentService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupComment;
|
||||
use Purify;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Jobs\GroupsPipeline\ImageResizePipeline;
|
||||
use App\Jobs\GroupsPipeline\ImageS3UploadPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewPostPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewCommentPipeline;
|
||||
use App\Jobs\GroupsPipeline\DeleteCommentPipeline;
|
||||
|
||||
class GroupsCommentController extends Controller
|
||||
{
|
||||
public function getComments(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required',
|
||||
'cid' => 'sometimes',
|
||||
'limit' => 'nullable|integer|min:3|max:10'
|
||||
]);
|
||||
|
||||
$pid = optional($request->user())->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$cid = $request->has('cid') && $request->input('cid') == 1;
|
||||
$limit = $request->input('limit', 3);
|
||||
$maxId = $request->input('max_id', 0);
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if($group->is_private && !$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$status = $cid ? GroupComment::findOrFail($sid) : GroupPost::findOrFail($sid);
|
||||
|
||||
abort_if($status->group_id != $group->id, 400, 'Invalid group');
|
||||
|
||||
$replies = GroupComment::whereGroupId($group->id)
|
||||
->whereStatusId($status->id)
|
||||
->orderByDesc('id')
|
||||
->when($maxId, function($query, $maxId) {
|
||||
return $query->where('id', '<', $maxId);
|
||||
})
|
||||
->take($limit)
|
||||
->get()
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupCommentService::get($gp['group_id'], $gp['id']);
|
||||
$status['reply_count'] = $gp['reply_count'];
|
||||
$status['url'] = $gp->url();
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$gp['profile_id']}");
|
||||
return $status;
|
||||
});
|
||||
|
||||
return $replies->toArray();
|
||||
}
|
||||
|
||||
public function storeComment(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required|exists:groups,id',
|
||||
'sid' => 'required|exists:group_posts,id',
|
||||
'cid' => 'sometimes',
|
||||
'content' => 'required|string|min:1|max:1500'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$cid = $request->input('cid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$caption = e($request->input('content'));
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
|
||||
|
||||
$parent = $cid == 1 ?
|
||||
GroupComment::findOrFail($sid) :
|
||||
GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
// $autolink = Purify::clean(Autolink::create()->autolink($caption));
|
||||
// $autolink = str_replace('/discover/tags/', '/groups/' . $gid . '/topics/', $autolink);
|
||||
|
||||
$status = new GroupComment;
|
||||
$status->group_id = $group->id;
|
||||
$status->profile_id = $pid;
|
||||
$status->status_id = $parent->id;
|
||||
$status->caption = Purify::clean($caption);
|
||||
$status->visibility = 'public';
|
||||
$status->is_nsfw = false;
|
||||
$status->local = true;
|
||||
$status->save();
|
||||
|
||||
NewCommentPipeline::dispatch($parent, $status)->onQueue('groups');
|
||||
// todo: perform in job
|
||||
$parent->reply_count = $parent->reply_count ? $parent->reply_count + $parent->reply_count : 1;
|
||||
$parent->save();
|
||||
GroupPostService::del($parent->group_id, $parent->id);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:comment:created',
|
||||
[
|
||||
'type' => 'group:post:comment',
|
||||
'status_id' => $status->id
|
||||
],
|
||||
GroupPost::class,
|
||||
$status->id
|
||||
);
|
||||
|
||||
//GroupCommentPipeline::dispatch($parent, $status, $gp);
|
||||
//NewStatusPipeline::dispatch($status, $gp);
|
||||
//GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
|
||||
// todo: perform in job
|
||||
$s = GroupCommentService::get($status->group_id, $status->id);
|
||||
|
||||
$s['pf_type'] = 'text';
|
||||
$s['visibility'] = 'public';
|
||||
$s['url'] = $status->url();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function storeCommentPhoto(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required|exists:groups,id',
|
||||
'sid' => 'required|exists:group_posts,id',
|
||||
'photo' => 'required|image'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$caption = $request->input('content');
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
$parent = GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
|
||||
$status = new GroupComment;
|
||||
$status->status_id = $parent->id;
|
||||
$status->group_id = $group->id;
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = Purify::clean($caption);
|
||||
$status->visibility = 'draft';
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
|
||||
$photo = $request->file('photo');
|
||||
$storagePath = GroupMediaService::path($group->id, $pid, $status->id);
|
||||
$storagePath = 'public/g/' . $group->id . '/p/' . $parent->id;
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
|
||||
$media = new GroupMedia();
|
||||
$media->group_id = $group->id;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $photo->getMimeType();
|
||||
$media->save();
|
||||
|
||||
ImageResizePipeline::dispatchSync($media);
|
||||
ImageS3UploadPipeline::dispatchSync($media);
|
||||
|
||||
// $gp = new GroupPost;
|
||||
// $gp->group_id = $group->id;
|
||||
// $gp->profile_id = $pid;
|
||||
// $gp->type = 'reply:photo';
|
||||
// $gp->status_id = $status->id;
|
||||
// $gp->in_reply_to_id = $parent->id;
|
||||
// $gp->save();
|
||||
|
||||
// GroupService::log(
|
||||
// $group->id,
|
||||
// $pid,
|
||||
// 'group:comment:created',
|
||||
// [
|
||||
// 'type' => $gp->type,
|
||||
// 'status_id' => $status->id
|
||||
// ],
|
||||
// GroupPost::class,
|
||||
// $gp->id
|
||||
// );
|
||||
|
||||
// todo: perform in job
|
||||
// $parent->reply_count = Status::whereInReplyToId($parent->id)->count();
|
||||
// $parent->save();
|
||||
// StatusService::del($parent->id);
|
||||
// GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
|
||||
// delay response while background job optimizes media
|
||||
// sleep(5);
|
||||
|
||||
// todo: perform in job
|
||||
$s = GroupCommentService::get($status->group_id, $status->id);
|
||||
|
||||
// $s['pf_type'] = 'text';
|
||||
// $s['visibility'] = 'public';
|
||||
// $s['url'] = $gp->url();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function deleteComment(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1',
|
||||
'gid' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$gp = GroupComment::whereGroupId($group->id)->findOrFail($request->input('id'));
|
||||
abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403);
|
||||
|
||||
$parent = GroupPost::find($gp->status_id);
|
||||
abort_if(!$parent, 422, 'Invalid parent');
|
||||
|
||||
DeleteCommentPipeline::dispatch($parent, $gp)->onQueue('groups');
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:status:deleted',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $gp->id,
|
||||
],
|
||||
GroupComment::class,
|
||||
$gp->id
|
||||
);
|
||||
$gp->delete();
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect('/groups/feed');
|
||||
}
|
||||
}
|
||||
|
||||
public function likePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group || $gid != $group['id'], 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupCommentService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::firstOrCreate([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'comment_id' => $sid,
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated) {
|
||||
// update parent post like count
|
||||
$parent = GroupComment::find($sid);
|
||||
abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count + 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::add($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupCommentService::del($gid, $sid);
|
||||
$count++;
|
||||
GroupService::log(
|
||||
$gid,
|
||||
$pid,
|
||||
'group:like',
|
||||
null,
|
||||
GroupLike::class,
|
||||
$like->id
|
||||
);
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function unlikePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group || $gid != $group['id'], 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupCommentService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::where([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'comment_id' => $sid,
|
||||
])->first();
|
||||
|
||||
if($like) {
|
||||
$like->delete();
|
||||
$parent = GroupComment::find($sid);
|
||||
abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count - 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::remove($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupCommentService::del($gid, $sid);
|
||||
$count--;
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
57
app/Http/Controllers/Groups/GroupsDiscoverController.php
Normal file
57
app/Http/Controllers/Groups/GroupsDiscoverController.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
|
||||
class GroupsDiscoverController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getDiscoverPopular(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$groups = Group::orderByDesc('member_count')
|
||||
->take(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return GroupService::get($id);
|
||||
})
|
||||
->filter(function($id) {
|
||||
return $id;
|
||||
})
|
||||
->take(6)
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getDiscoverNew(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$groups = Group::latest()
|
||||
->take(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return GroupService::get($id);
|
||||
})
|
||||
->filter(function($id) {
|
||||
return $id;
|
||||
})
|
||||
->take(6)
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
}
|
188
app/Http/Controllers/Groups/GroupsFeedController.php
Normal file
188
app/Http/Controllers/Groups/GroupsFeedController.php
Normal file
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\Groups\GroupFeedService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\RelationshipService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupInvitation;
|
||||
|
||||
class GroupsFeedController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getSelfFeed(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$limit = $request->input('limit', 5);
|
||||
$page = $request->input('page');
|
||||
$initial = $request->has('initial');
|
||||
|
||||
if($initial) {
|
||||
$res = Cache::remember('groups:self:feed:' . $pid, 900, function() use($pid) {
|
||||
return $this->getSelfFeedV0($pid, 5, null);
|
||||
});
|
||||
} else {
|
||||
abort_if($page && $page > 5, 422);
|
||||
$res = $this->getSelfFeedV0($pid, $limit, $page);
|
||||
}
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
protected function getSelfFeedV0($pid, $limit, $page)
|
||||
{
|
||||
return GroupPost::join('group_members', 'group_posts.group_id', 'group_members.group_id')
|
||||
->select('group_posts.*', 'group_members.group_id', 'group_members.profile_id')
|
||||
->where('group_members.profile_id', $pid)
|
||||
->whereIn('group_posts.type', ['text', 'photo', 'video'])
|
||||
->orderByDesc('group_posts.id')
|
||||
->limit($limit)
|
||||
// ->pluck('group_posts.status_id')
|
||||
->simplePaginate($limit)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = url("/groups/{$gp['group_id']}/p/{$gp['id']}");
|
||||
$status['group'] = GroupService::get($gp['group_id']);
|
||||
$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
|
||||
return $status;
|
||||
});
|
||||
}
|
||||
|
||||
public function getGroupProfileFeed(Request $request, $id, $pid)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$feed = GroupPost::whereGroupId($id)
|
||||
->whereProfileId($pid)
|
||||
->latest()
|
||||
->paginate(3)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
$status['account']['url'] = "/groups/{$gp['group_id']}/user/{$status['account']['id']}";
|
||||
|
||||
return $status;
|
||||
})
|
||||
->filter(function($status) {
|
||||
return $status;
|
||||
});
|
||||
|
||||
return $feed;
|
||||
}
|
||||
|
||||
public function getGroupFeed(Request $request, $id)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$user = $request->user();
|
||||
$pid = optional($user)->profile_id ?? false;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
$max = $request->input('max_id');
|
||||
$limit = $request->limit ?? 3;
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
// $posts = GroupPost::whereGroupId($group->id)
|
||||
// ->when($maxId, function($q, $maxId) {
|
||||
// return $q->where('status_id', '<', $maxId);
|
||||
// })
|
||||
// ->whereNull('in_reply_to_id')
|
||||
// ->orderByDesc('status_id')
|
||||
// ->simplePaginate($limit)
|
||||
// ->map(function($gp) use($pid) {
|
||||
// $status = StatusService::get($gp['status_id'], false);
|
||||
// if(!$status) {
|
||||
// return false;
|
||||
// }
|
||||
// $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']);
|
||||
// $status['favourites_count'] = LikeService::count($gp['status_id']);
|
||||
// $status['pf_type'] = $gp['type'];
|
||||
// $status['visibility'] = 'public';
|
||||
// $status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
// $status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
|
||||
// return $status;
|
||||
// })->filter(function($status) {
|
||||
// return $status;
|
||||
// });
|
||||
// return $posts;
|
||||
|
||||
Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() use($id) {
|
||||
if(GroupFeedService::count($id) == 0) {
|
||||
GroupFeedService::warmCache($id, true, 400);
|
||||
}
|
||||
});
|
||||
|
||||
if ($max) {
|
||||
$feed = GroupFeedService::getRankedMaxId($id, $max, $limit);
|
||||
} else {
|
||||
$feed = GroupFeedService::get($id, 0, $limit);
|
||||
}
|
||||
|
||||
$res = collect($feed)
|
||||
->map(function($k) use($user, $id) {
|
||||
$status = GroupPostService::get($id, $k);
|
||||
if($status && $user) {
|
||||
$pid = $user->profile_id;
|
||||
$sid = $status['account']['id'];
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $status['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($status['id']);
|
||||
$status['relationship'] = $pid == $sid ? [] : RelationshipService::get($pid, $sid);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
214
app/Http/Controllers/Groups/GroupsMemberController.php
Normal file
214
app/Http/Controllers/Groups/GroupsMemberController.php
Normal file
|
@ -0,0 +1,214 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupMember;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
use App\Services\Groups\GroupHashtagService;
|
||||
use App\Jobs\GroupsPipeline\MemberJoinApprovedPipeline;
|
||||
use App\Jobs\GroupsPipeline\MemberJoinRejectedPipeline;
|
||||
|
||||
class GroupsMemberController extends Controller
|
||||
{
|
||||
public function getGroupMembers(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'limit' => 'nullable|integer|min:3|max:10'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$members = GroupMember::whereGroupId($gid)
|
||||
->whereJoinRequest(false)
|
||||
->simplePaginate(10)
|
||||
->map(function($member) use($pid) {
|
||||
$account = AccountService::get($member['profile_id']);
|
||||
$account['role'] = $member['role'];
|
||||
$account['joined'] = $member['created_at'];
|
||||
$account['following'] = $pid != $member['profile_id'] ?
|
||||
FollowerService::follows($pid, $member['profile_id']) :
|
||||
null;
|
||||
$account['url'] = url("/groups/{$member->group_id}/user/{$member['profile_id']}");
|
||||
return $account;
|
||||
});
|
||||
|
||||
return response()->json($members->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupMemberJoinRequests(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
return GroupMember::whereGroupId($group->id)
|
||||
->whereJoinRequest(true)
|
||||
->whereNull('rejected_at')
|
||||
->paginate(10)
|
||||
->map(function($member) {
|
||||
return AccountService::get($member->profile_id);
|
||||
});
|
||||
}
|
||||
|
||||
public function handleGroupMemberJoinRequest(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
$mid = $request->input('pid');
|
||||
abort_if($group->isMember($mid), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'pid' => 'required',
|
||||
'action' => 'required|in:approve,reject'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
||||
$member = GroupMember::whereGroupId($group->id)
|
||||
->whereProfileId($mid)
|
||||
->firstOrFail();
|
||||
|
||||
if($action == 'approve') {
|
||||
MemberJoinApprovedPipeline::dispatch($member)->onQueue('groups');
|
||||
} else if ($action == 'reject') {
|
||||
MemberJoinRejectedPipeline::dispatch($member)->onQueue('groups');
|
||||
}
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function getGroupMember(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'pid' => 'required'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$member_id = $request->input('pid');
|
||||
$member = GroupMember::whereGroupId($gid)
|
||||
->whereProfileId($member_id)
|
||||
->firstOrFail();
|
||||
|
||||
$account = GroupAccountService::get($group->id, $member['profile_id']);
|
||||
$account['role'] = $member['role'];
|
||||
$account['joined'] = $member['created_at'];
|
||||
$account['following'] = $pid != $member['profile_id'] ?
|
||||
FollowerService::follows($pid, $member['profile_id']) :
|
||||
null;
|
||||
$account['url'] = url("/groups/{$gid}/user/{$member_id}");
|
||||
|
||||
return response()->json($account, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupMemberCommonIntersections(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
// $this->validate($request, [
|
||||
// 'gid' => 'required',
|
||||
// 'pid' => 'required'
|
||||
// ]);
|
||||
|
||||
$gid = $request->input('gid');
|
||||
$pid = $request->input('pid');
|
||||
|
||||
if($pid === $cid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($cid), 404);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$self = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr')
|
||||
->whereProfileId($cid)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('countr')
|
||||
->take(20)
|
||||
->pluck('hashtag_id');
|
||||
$user = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr')
|
||||
->whereProfileId($pid)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('countr')
|
||||
->take(20)
|
||||
->pluck('hashtag_id');
|
||||
|
||||
$topics = $self->intersect($user)
|
||||
->values()
|
||||
->shuffle()
|
||||
->take(3)
|
||||
->map(function($id) use($group) {
|
||||
$tag = GroupHashtagService::get($id);
|
||||
$tag['url'] = url("/groups/{$group->id}/topics/{$tag['slug']}?src=upt");
|
||||
return $tag;
|
||||
});
|
||||
|
||||
// $friends = DB::table('followers as u')
|
||||
// ->join('followers as s', 'u.following_id', '=', 's.following_id')
|
||||
// ->where('s.profile_id', $cid)
|
||||
// ->where('u.profile_id', $pid)
|
||||
// ->inRandomOrder()
|
||||
// ->take(10)
|
||||
// ->pluck('s.following_id')
|
||||
// ->map(function($id) use($gid) {
|
||||
// $res = AccountService::get($id);
|
||||
// $res['url'] = url("/groups/{$gid}/user/{$id}");
|
||||
// return $res;
|
||||
// });
|
||||
$mutualGroups = GroupService::mutualGroups($cid, $pid, [$gid]);
|
||||
|
||||
$mutualFriends = collect(FollowerService::mutualIds($cid, $pid))
|
||||
->map(function($id) use($gid) {
|
||||
$res = AccountService::get($id);
|
||||
if(GroupService::isMember($gid, $id)) {
|
||||
$res['url'] = url("/groups/{$gid}/user/{$id}");
|
||||
} else if(!$res['local']) {
|
||||
$res['url'] = url("/i/web/profile/_/{$id}");
|
||||
}
|
||||
return $res;
|
||||
});
|
||||
$mutualFriendsCount = FollowerService::mutualCount($cid, $pid);
|
||||
|
||||
$res = [
|
||||
'groups_count' => $mutualGroups['count'],
|
||||
'groups' => $mutualGroups['groups'],
|
||||
'topics' => $topics,
|
||||
'friends_count' => $mutualFriendsCount,
|
||||
'friends' => $mutualFriends,
|
||||
];
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
31
app/Http/Controllers/Groups/GroupsMetaController.php
Normal file
31
app/Http/Controllers/Groups/GroupsMetaController.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
|
||||
class GroupsMetaController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function deleteGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$group->status = "delete";
|
||||
$group->save();
|
||||
GroupService::del($group->id);
|
||||
return [200];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Notification;
|
||||
|
||||
class GroupsNotificationsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function selfGlobalNotifications(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$res = Notification::whereProfileId($pid)
|
||||
->where('action', 'like', 'group%')
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->map(function($n) {
|
||||
$res = [
|
||||
'id' => $n->id,
|
||||
'type' => $n->action,
|
||||
'account' => AccountService::get($n->actor_id),
|
||||
'object' => [
|
||||
'id' => $n->item_id,
|
||||
'type' => last(explode('\\', $n->item_type)),
|
||||
],
|
||||
'created_at' => $n->created_at->format('c')
|
||||
];
|
||||
|
||||
if($res['object']['type'] == 'Status' || in_array($n->action, ['group:comment'])) {
|
||||
$res['status'] = StatusService::get($n->item_id, false);
|
||||
$res['group'] = GroupService::get($res['status']['gid']);
|
||||
}
|
||||
|
||||
if($res['object']['type'] == 'Group') {
|
||||
$res['group'] = GroupService::get($n->item_id);
|
||||
}
|
||||
|
||||
return $res;
|
||||
});
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
420
app/Http/Controllers/Groups/GroupsPostController.php
Normal file
420
app/Http/Controllers/Groups/GroupsPostController.php
Normal file
|
@ -0,0 +1,420 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupFeedService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Jobs\GroupsPipeline\ImageResizePipeline;
|
||||
use App\Jobs\GroupsPipeline\ImageS3UploadPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewPostPipeline;
|
||||
|
||||
class GroupsPostController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function storePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'group_id' => 'required|exists:groups,id',
|
||||
'caption' => 'sometimes|string|max:'.config_cache('pixelfed.max_caption_length', 500),
|
||||
'pollOptions' => 'sometimes|array|min:1|max:4'
|
||||
]);
|
||||
|
||||
$group = Group::findOrFail($request->input('group_id'));
|
||||
$pid = $request->user()->profile_id;
|
||||
$caption = $request->input('caption');
|
||||
$type = $request->input('type', 'text');
|
||||
|
||||
abort_if(!GroupService::canPost($group->id, $pid), 422, 'You cannot create new posts at this time');
|
||||
|
||||
if($type == 'text') {
|
||||
abort_if(strlen(e($caption)) == 0, 403);
|
||||
}
|
||||
|
||||
$gp = new GroupPost;
|
||||
$gp->group_id = $group->id;
|
||||
$gp->profile_id = $pid;
|
||||
$gp->caption = e($caption);
|
||||
$gp->type = $type;
|
||||
$gp->visibility = 'draft';
|
||||
$gp->save();
|
||||
|
||||
$status = $gp;
|
||||
|
||||
NewPostPipeline::dispatchSync($gp);
|
||||
|
||||
// NewStatusPipeline::dispatch($status, $gp);
|
||||
|
||||
if($type == 'poll') {
|
||||
// Polls not supported yet
|
||||
// $poll = new Poll;
|
||||
// $poll->status_id = $status->id;
|
||||
// $poll->profile_id = $status->profile_id;
|
||||
// $poll->poll_options = $request->input('pollOptions');
|
||||
// $poll->expires_at = now()->addMinutes($request->input('expiry'));
|
||||
// $poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
|
||||
// return 0;
|
||||
// })->toArray();
|
||||
// $poll->save();
|
||||
// sleep(5);
|
||||
}
|
||||
if($type == 'photo') {
|
||||
$photo = $request->file('photo');
|
||||
$storagePath = GroupMediaService::path($group->id, $pid, $status->id);
|
||||
// $storagePath = 'public/g/' . $group->id . '/p/' . $status->id;
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
// $hash = \hash_file('sha256', $photo);
|
||||
|
||||
$media = new GroupMedia();
|
||||
$media->group_id = $group->id;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $photo->getMimeType();
|
||||
$media->save();
|
||||
|
||||
// Bus::chain([
|
||||
// new ImageResizePipeline($media),
|
||||
// new ImageS3UploadPipeline($media),
|
||||
// ])->dispatch($media);
|
||||
|
||||
ImageResizePipeline::dispatchSync($media);
|
||||
ImageS3UploadPipeline::dispatchSync($media);
|
||||
// ImageOptimize::dispatch($media);
|
||||
// delay response while background job optimizes media
|
||||
// sleep(5);
|
||||
}
|
||||
if($type == 'video') {
|
||||
$video = $request->file('video');
|
||||
$storagePath = 'public/g/' . $group->id . '/p/' . $status->id;
|
||||
$path = $video->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $video);
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->user_id = $request->user()->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $video->getSize();
|
||||
$media->mime = $video->getMimeType();
|
||||
$media->save();
|
||||
|
||||
VideoThumbnail::dispatch($media);
|
||||
sleep(15);
|
||||
}
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:status:created',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $status->id
|
||||
],
|
||||
GroupPost::class,
|
||||
$gp->id
|
||||
);
|
||||
|
||||
$s = GroupPostService::get($status->group_id, $status->id);
|
||||
GroupFeedService::add($group->id, $gp->id);
|
||||
Cache::forget('groups:self:feed:' . $pid);
|
||||
|
||||
$s['pf_type'] = $type;
|
||||
$s['visibility'] = 'public';
|
||||
$s['url'] = $gp->url();
|
||||
|
||||
if($type == 'poll') {
|
||||
$s['poll'] = PollService::get($status->id);
|
||||
}
|
||||
|
||||
$group->last_active_at = now();
|
||||
$group->save();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function deletePost(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1',
|
||||
'gid' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$gp = GroupPost::whereGroupId($status->group_id)->findOrFail($request->input('id'));
|
||||
abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403);
|
||||
$cached = GroupPostService::get($status->group_id, $status->id);
|
||||
|
||||
if($cached) {
|
||||
$cached = collect($cached)->filter(function($r, $k) {
|
||||
return in_array($k, [
|
||||
'id',
|
||||
'sensitive',
|
||||
'pf_type',
|
||||
'media_attachments',
|
||||
'content_text',
|
||||
'created_at'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
GroupService::log(
|
||||
$status->group_id,
|
||||
$request->user()->profile_id,
|
||||
'group:status:deleted',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $status->id,
|
||||
'original' => $cached
|
||||
],
|
||||
GroupPost::class,
|
||||
$gp->id
|
||||
);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
// if($status->profile_id != $user->profile->id &&
|
||||
// $user->is_admin == true &&
|
||||
// $status->uri == null
|
||||
// ) {
|
||||
// $media = $status->media;
|
||||
|
||||
// $ai = new AccountInterstitial;
|
||||
// $ai->user_id = $status->profile->user_id;
|
||||
// $ai->type = 'post.removed';
|
||||
// $ai->view = 'account.moderation.post.removed';
|
||||
// $ai->item_type = 'App\Status';
|
||||
// $ai->item_id = $status->id;
|
||||
// $ai->has_media = (bool) $media->count();
|
||||
// $ai->blurhash = $media->count() ? $media->first()->blurhash : null;
|
||||
// $ai->meta = json_encode([
|
||||
// 'caption' => $status->caption,
|
||||
// 'created_at' => $status->created_at,
|
||||
// 'type' => $status->type,
|
||||
// 'url' => $status->url(),
|
||||
// 'is_nsfw' => $status->is_nsfw,
|
||||
// 'scope' => $status->scope,
|
||||
// 'reblog' => $status->reblog_of_id,
|
||||
// 'likes_count' => $status->likes_count,
|
||||
// 'reblogs_count' => $status->reblogs_count,
|
||||
// ]);
|
||||
// $ai->save();
|
||||
|
||||
// $u = $status->profile->user;
|
||||
// $u->has_interstitial = true;
|
||||
// $u->save();
|
||||
// }
|
||||
|
||||
if($status->in_reply_to_id) {
|
||||
$parent = GroupPost::find($status->in_reply_to_id);
|
||||
if($parent) {
|
||||
$parent->reply_count = GroupPost::whereInReplyToId($parent->id)->count();
|
||||
$parent->save();
|
||||
GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
}
|
||||
}
|
||||
|
||||
GroupPostService::del($group->id, $gp->id);
|
||||
GroupFeedService::del($group->id, $gp->id);
|
||||
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
|
||||
// Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect($user->url());
|
||||
}
|
||||
}
|
||||
|
||||
public function likePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group, 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupPostService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::firstOrCreate([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'status_id' => $sid,
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated) {
|
||||
// update parent post like count
|
||||
$parent = GroupPost::whereGroupId($gid)->find($sid);
|
||||
abort_if(!$parent, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count + 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::add($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupPostService::del($gid, $sid);
|
||||
$count++;
|
||||
GroupService::log(
|
||||
$gid,
|
||||
$pid,
|
||||
'group:like',
|
||||
null,
|
||||
GroupLike::class,
|
||||
$like->id
|
||||
);
|
||||
}
|
||||
// if (GroupLike::whereGroupId($gid)->whereStatusId($sid)->whereProfileId($pid)->exists()) {
|
||||
// $like = GroupLike::whereProfileId($pid)->whereStatusId($sid)->firstOrFail();
|
||||
// // UnlikePipeline::dispatch($like);
|
||||
// $count = $gp->likes_count - 1;
|
||||
// $action = 'group:unlike';
|
||||
// } else {
|
||||
// $count = $gp->likes_count;
|
||||
// $like = GroupLike::firstOrCreate([
|
||||
// 'group_id' => $gid,
|
||||
// 'profile_id' => $pid,
|
||||
// 'status_id' => $sid
|
||||
// ]);
|
||||
// if($like->wasRecentlyCreated == true) {
|
||||
// $count++;
|
||||
// $gp->likes_count = $count;
|
||||
// $like->save();
|
||||
// $gp->save();
|
||||
// // LikePipeline::dispatch($like);
|
||||
// $action = 'group:like';
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// Cache::forget('status:'.$status->id.':likedby:userid:'.$request->user()->id);
|
||||
// StatusService::del($status->id);
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function unlikePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group, 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupPostService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::where([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'status_id' => $sid,
|
||||
])->first();
|
||||
|
||||
if($like) {
|
||||
$like->delete();
|
||||
$parent = GroupPost::whereGroupId($gid)->find($sid);
|
||||
abort_if(!$parent, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count - 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::remove($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupPostService::del($gid, $sid);
|
||||
$count--;
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getGroupMedia(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'type' => 'required|in:photo,video'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$type = $request->input('type');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$media = GroupPost::whereGroupId($gid)
|
||||
->whereType($type)
|
||||
->latest()
|
||||
->simplePaginate(20)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
return $status;
|
||||
})->filter(function($status) {
|
||||
return $status;
|
||||
});
|
||||
|
||||
return response()->json($media->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
221
app/Http/Controllers/Groups/GroupsSearchController.php
Normal file
221
app/Http/Controllers/Groups/GroupsSearchController.php
Normal file
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\Groups\GroupActivityPubService;
|
||||
|
||||
class GroupsSearchController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function inviteFriendsToGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'uids' => 'required',
|
||||
'g' => 'required',
|
||||
]);
|
||||
$uid = $request->input('uids');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(
|
||||
GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->count() >= 20,
|
||||
422,
|
||||
'Invite limit reached'
|
||||
);
|
||||
|
||||
$profiles = collect($uid)
|
||||
->map(function($u) {
|
||||
return Profile::find($u);
|
||||
})
|
||||
->filter(function($u) use($pid) {
|
||||
return $u &&
|
||||
$u->id != $pid &&
|
||||
isset($u->id) &&
|
||||
Follower::whereFollowingId($pid)
|
||||
->whereProfileId($u->id)
|
||||
->exists();
|
||||
})
|
||||
->filter(function($u) use($group, $pid) {
|
||||
return GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->whereToProfileId($u->id)
|
||||
->exists() == false;
|
||||
})
|
||||
->each(function($u) use($gid, $pid) {
|
||||
$gi = new GroupInvitation;
|
||||
$gi->group_id = $gid;
|
||||
$gi->from_profile_id = $pid;
|
||||
$gi->to_profile_id = $u->id;
|
||||
$gi->to_local = true;
|
||||
$gi->from_local = $u->domain == null;
|
||||
$gi->save();
|
||||
// GroupMemberInvite::dispatch($gi);
|
||||
});
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function searchFriendsToInvite(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$res = Profile::where('username', 'like', "%{$q}%")
|
||||
->whereNull('profiles.domain')
|
||||
->join('followers', 'profiles.id', '=', 'followers.profile_id')
|
||||
->where('followers.following_id', $pid)
|
||||
->take(10)
|
||||
->get()
|
||||
->filter(function($p) use($group) {
|
||||
return $group->isMember($p->profile_id) == false;
|
||||
})
|
||||
->filter(function($p) use($group, $pid) {
|
||||
return GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->whereToProfileId($p->profile_id)
|
||||
->exists() == false;
|
||||
})
|
||||
->map(function($gm) use ($gid) {
|
||||
$a = AccountService::get($gm->profile_id);
|
||||
return [
|
||||
'id' => (string) $gm->profile_id,
|
||||
'username' => $a['acct'],
|
||||
'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search")
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function searchGlobalResults(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:140',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
|
||||
if(str_starts_with($q, 'https://')) {
|
||||
$res = Helpers::getSignedFetch($q);
|
||||
if($res && $res = json_decode($res, true)) {
|
||||
|
||||
}
|
||||
if($res && isset($res['type']) && in_array($res['type'], ['Group', 'Note', 'Page'])) {
|
||||
if($res['type'] === 'Group') {
|
||||
return GroupActivityPubService::fetchGroup($q, true);
|
||||
}
|
||||
$resp = GroupActivityPubService::fetchGroupPost($q, true);
|
||||
$resp['name'] = 'Group Post';
|
||||
$resp['url'] = '/groups/' . $resp['group_id'] . '/p/' . $resp['id'];
|
||||
return [$resp];
|
||||
}
|
||||
}
|
||||
return Group::whereNull('status')
|
||||
->where('name', 'like', '%' . $q . '%')
|
||||
->orderBy('id')
|
||||
->take(10)
|
||||
->pluck('id')
|
||||
->map(function($group) {
|
||||
return GroupService::get($group);
|
||||
});
|
||||
}
|
||||
|
||||
public function searchLocalAutocomplete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$res = GroupMember::whereGroupId($gid)
|
||||
->join('profiles', 'group_members.profile_id', '=', 'profiles.id')
|
||||
->where('profiles.username', 'like', "%{$q}%")
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function($gm) use ($gid) {
|
||||
$a = AccountService::get($gm->profile_id);
|
||||
return [
|
||||
'username' => $a['username'],
|
||||
'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search")
|
||||
];
|
||||
});
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function searchAddRecent(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$key = 'groups:search:recent:'.$gid.':pid:'.$pid;
|
||||
$ttl = now()->addDays(14);
|
||||
$res = Cache::get($key);
|
||||
if(!$res) {
|
||||
$val = json_encode([$q]);
|
||||
} else {
|
||||
$ex = collect(json_decode($res))
|
||||
->prepend($q)
|
||||
->unique('value')
|
||||
->slice(0, 3)
|
||||
->values()
|
||||
->all();
|
||||
$val = json_encode($ex);
|
||||
}
|
||||
Cache::put($key, $val, $ttl);
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function searchGetRecent(Request $request)
|
||||
{
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
$key = 'groups:search:recent:'.$gid.':pid:'.$pid;
|
||||
return Cache::get($key);
|
||||
}
|
||||
}
|
133
app/Http/Controllers/Groups/GroupsTopicController.php
Normal file
133
app/Http/Controllers/Groups/GroupsTopicController.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupPost;
|
||||
|
||||
class GroupsTopicController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function groupTopics(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$posts = GroupPostHashtag::join('group_hashtags', 'group_hashtags.id', '=', 'group_post_hashtags.hashtag_id')
|
||||
->selectRaw('group_hashtags.*, group_post_hashtags.*, count(group_post_hashtags.hashtag_id) as ht_count')
|
||||
->where('group_post_hashtags.group_id', $gid)
|
||||
->orderByDesc('ht_count')
|
||||
->limit(10)
|
||||
->pluck('group_post_hashtags.hashtag_id', 'ht_count')
|
||||
->map(function($id, $key) use ($gid) {
|
||||
$tag = GroupHashtag::find($id);
|
||||
return [
|
||||
'hid' => $id,
|
||||
'name' => $tag->name,
|
||||
'url' => url("/groups/{$gid}/topics/{$tag->slug}"),
|
||||
'count' => $key
|
||||
];
|
||||
})->values();
|
||||
|
||||
return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function groupTopicTag(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'name' => 'required'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$name = $request->input('name');
|
||||
$hashtag = GroupHashtag::whereName($name)->first();
|
||||
|
||||
if(!$hashtag) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// $posts = GroupPost::whereGroupId($gid)
|
||||
// ->select('status_hashtags.*', 'group_posts.*')
|
||||
// ->where('status_hashtags.hashtag_id', $hashtag->id)
|
||||
// ->join('status_hashtags', 'group_posts.status_id', '=', 'status_hashtags.status_id')
|
||||
// ->orderByDesc('group_posts.status_id')
|
||||
// ->simplePaginate($limit)
|
||||
// ->map(function($gp) use($pid) {
|
||||
// $status = StatusService::get($gp['status_id'], false);
|
||||
// if(!$status) {
|
||||
// return false;
|
||||
// }
|
||||
// $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']);
|
||||
// $status['favourites_count'] = LikeService::count($gp['status_id']);
|
||||
// $status['pf_type'] = $gp['type'];
|
||||
// $status['visibility'] = 'public';
|
||||
// $status['url'] = $gp->url();
|
||||
// return $status;
|
||||
// });
|
||||
|
||||
$posts = GroupPostHashtag::whereGroupId($gid)
|
||||
->whereHashtagId($hashtag->id)
|
||||
->orderByDesc('id')
|
||||
->simplePaginate($limit)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['status_id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['status_id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['status_id']);
|
||||
$status['pf_type'] = $status['pf_type'];
|
||||
$status['visibility'] = 'public';
|
||||
return $status;
|
||||
});
|
||||
|
||||
return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showTopicFeed(Request $request, $gid, $tag)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
$gid = $group->id;
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
return view('groups.topic-feed', compact('gid', 'tag'));
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ trait Instagram
|
|||
{
|
||||
public function instagram()
|
||||
{
|
||||
if(config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
return view('settings.import.instagram.home');
|
||||
|
@ -25,6 +25,9 @@ trait Instagram
|
|||
|
||||
public function instagramStart(Request $request)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$completed = ImportJob::whereProfileId(Auth::user()->profile->id)
|
||||
->whereService('instagram')
|
||||
->whereNotNull('completed_at')
|
||||
|
@ -38,6 +41,9 @@ trait Instagram
|
|||
|
||||
protected function instagramRedirectOrNew()
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$exists = ImportJob::whereProfileId($profile->id)
|
||||
->whereService('instagram')
|
||||
|
@ -61,6 +67,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepOne(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
|
@ -72,6 +81,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepOneStore(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$max = 'max:' . config('pixelfed.import.instagram.limits.size');
|
||||
$this->validate($request, [
|
||||
'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max,
|
||||
|
@ -114,6 +126,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepTwo(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
|
@ -125,6 +140,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepTwoStore(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$this->validate($request, [
|
||||
'media' => 'required|file|max:1000'
|
||||
]);
|
||||
|
@ -150,6 +168,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepThree(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereService('instagram')
|
||||
|
@ -162,6 +183,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepThreeStore(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
try {
|
||||
|
|
|
@ -83,6 +83,17 @@ class ImportPostController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
public function formatHashtags($val = false)
|
||||
{
|
||||
if(!$val || !strlen($val)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$groupedHashtagRegex = '/#\w+(?=#)/';
|
||||
|
||||
return preg_replace($groupedHashtagRegex, '$0 ', $val);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_unless(config('import.instagram.enabled'), 404);
|
||||
|
@ -128,11 +139,11 @@ class ImportPostController extends Controller
|
|||
$ip->media = $c->map(function($m) {
|
||||
return [
|
||||
'uri' => $m['uri'],
|
||||
'title' => $m['title'],
|
||||
'title' => $this->formatHashtags($m['title']),
|
||||
'creation_timestamp' => $m['creation_timestamp']
|
||||
];
|
||||
})->toArray();
|
||||
$ip->caption = $c->count() > 1 ? $file['title'] : $ip->media[0]['title'];
|
||||
$ip->caption = $c->count() > 1 ? $this->formatHashtags($file['title']) : $this->formatHashtags($ip->media[0]['title']);
|
||||
$ip->filename = last(explode('/', $ip->media[0]['uri']));
|
||||
$ip->metadata = $c->map(function($m) {
|
||||
return [
|
||||
|
@ -168,7 +179,7 @@ class ImportPostController extends Controller
|
|||
'required',
|
||||
'file',
|
||||
$mimes,
|
||||
'max:' . config('pixelfed.max_photo_size')
|
||||
'max:' . config_cache('pixelfed.max_photo_size')
|
||||
]
|
||||
]);
|
||||
|
||||
|
|
|
@ -2,48 +2,29 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Bookmark,
|
||||
DirectMessage,
|
||||
DiscoverCategory,
|
||||
Hashtag,
|
||||
Follower,
|
||||
Like,
|
||||
Media,
|
||||
MediaTag,
|
||||
Notification,
|
||||
Profile,
|
||||
StatusHashtag,
|
||||
Status,
|
||||
User,
|
||||
UserFilter,
|
||||
};
|
||||
use Auth,Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Carbon\Carbon;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
StatusTransformer,
|
||||
// StatusMediaContainerTransformer,
|
||||
};
|
||||
use App\Util\Media\Filter;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\AccountInterstitial;
|
||||
use App\Bookmark;
|
||||
use App\DirectMessage;
|
||||
use App\DiscoverCategory;
|
||||
use App\Follower;
|
||||
use App\Jobs\ModPipeline\HandleSpammerPipeline;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Profile;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Services\DiscoverService;
|
||||
use App\Services\ModLogService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\DiscoverService;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Status; // StatusMediaContainerTransformer,
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use App\User;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Validation\Rule;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class InternalApiController extends Controller
|
||||
{
|
||||
|
@ -52,8 +33,8 @@ class InternalApiController extends Controller
|
|||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
$this->fractal = new Fractal\Manager;
|
||||
$this->fractal->setSerializer(new ArraySerializer);
|
||||
}
|
||||
|
||||
// deprecated v2 compose api
|
||||
|
@ -63,10 +44,7 @@ class InternalApiController extends Controller
|
|||
}
|
||||
|
||||
// deprecated
|
||||
public function discover(Request $request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
public function discover(Request $request) {}
|
||||
|
||||
public function discoverPosts(Request $request)
|
||||
{
|
||||
|
@ -84,6 +62,7 @@ class InternalApiController extends Controller
|
|||
})
|
||||
->take(12)
|
||||
->values();
|
||||
|
||||
return response()->json(compact('posts'));
|
||||
}
|
||||
|
||||
|
@ -110,7 +89,7 @@ class InternalApiController extends Controller
|
|||
public function statusReplies(Request $request, int $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|int|min:1|max:6'
|
||||
'limit' => 'nullable|int|min:1|max:6',
|
||||
]);
|
||||
$parent = Status::whereScope('public')->findOrFail($id);
|
||||
$limit = $request->input('limit') ?? 3;
|
||||
|
@ -118,16 +97,13 @@ class InternalApiController extends Controller
|
|||
->orderBy('created_at', 'desc')
|
||||
->take($limit)
|
||||
->get();
|
||||
$resource = new Fractal\Resource\Collection($children, new StatusTransformer());
|
||||
$resource = new Fractal\Resource\Collection($children, new StatusTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function stories(Request $request)
|
||||
{
|
||||
|
||||
}
|
||||
public function stories(Request $request) {}
|
||||
|
||||
public function discoverCategories(Request $request)
|
||||
{
|
||||
|
@ -136,9 +112,10 @@ class InternalApiController extends Controller
|
|||
return [
|
||||
'name' => $item->name,
|
||||
'url' => $item->url(),
|
||||
'thumb' => $item->thumb()
|
||||
'thumb' => $item->thumb(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
|
@ -153,15 +130,15 @@ class InternalApiController extends Controller
|
|||
'addcw',
|
||||
'remcw',
|
||||
'unlist',
|
||||
'spammer'
|
||||
])
|
||||
'spammer',
|
||||
]),
|
||||
],
|
||||
'item_id' => 'required|integer|min:1',
|
||||
'item_type' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::in(['profile', 'status'])
|
||||
]
|
||||
Rule::in(['profile', 'status']),
|
||||
],
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
@ -184,7 +161,7 @@ class InternalApiController extends Controller
|
|||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'cw',
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -229,7 +206,7 @@ class InternalApiController extends Controller
|
|||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'remove_cw',
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -255,7 +232,7 @@ class InternalApiController extends Controller
|
|||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'unlist',
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -299,7 +276,7 @@ class InternalApiController extends Controller
|
|||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'spammer',
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
@ -307,6 +284,7 @@ class InternalApiController extends Controller
|
|||
}
|
||||
|
||||
StatusService::del($status->id, true);
|
||||
|
||||
return ['msg' => 200];
|
||||
}
|
||||
|
||||
|
@ -331,6 +309,7 @@ class InternalApiController extends Controller
|
|||
if ($status) {
|
||||
BookmarkService::add($pid, $status['id']);
|
||||
}
|
||||
|
||||
return $status;
|
||||
})
|
||||
->filter(function ($bookmark) {
|
||||
|
@ -350,7 +329,7 @@ class InternalApiController extends Controller
|
|||
'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
|
||||
'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
|
||||
'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|min:1|max:24'
|
||||
'limit' => 'nullable|integer|min:1|max:24',
|
||||
]);
|
||||
|
||||
$profile = Profile::whereNull('status')->findOrFail($id);
|
||||
|
@ -369,17 +348,19 @@ class InternalApiController extends Controller
|
|||
$pid = Auth::user()->profile->id;
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
|
||||
$visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : [];
|
||||
} else {
|
||||
if (Auth::check()) {
|
||||
$pid = Auth::user()->profile->id;
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
|
||||
$visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
|
||||
} else {
|
||||
$visibility = ['public', 'unlisted'];
|
||||
}
|
||||
|
@ -391,7 +372,6 @@ class InternalApiController extends Controller
|
|||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
|
@ -411,7 +391,7 @@ class InternalApiController extends Controller
|
|||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
|
@ -431,6 +411,7 @@ class InternalApiController extends Controller
|
|||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$exists = Redis::sismember('email:manual', $pid);
|
||||
|
||||
return view('account.email.request_verification', compact('exists'));
|
||||
}
|
||||
|
||||
|
@ -438,6 +419,7 @@ class InternalApiController extends Controller
|
|||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
Redis::sadd('email:manual', $pid);
|
||||
|
||||
return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Http\Resources\DirectoryProfile;
|
||||
use App\Profile;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LandingController extends Controller
|
||||
{
|
||||
|
@ -15,7 +14,7 @@ class LandingController extends Controller
|
|||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config_cache('instance.landing.show_directory') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
}
|
||||
|
@ -26,14 +25,14 @@ class LandingController extends Controller
|
|||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config_cache('instance.landing.show_explore') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_explore') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function getDirectoryApi(Request $request)
|
||||
{
|
||||
abort_if(config_cache('instance.landing.show_directory') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return DirectoryProfile::collection(
|
||||
Profile::whereNull('domain')
|
||||
|
|
|
@ -2,14 +2,15 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Media;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
//return view('settings.drive.index');
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function composeUpdate(Request $request, $id)
|
||||
|
@ -19,7 +20,7 @@ class MediaController extends Controller
|
|||
|
||||
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
|
||||
{
|
||||
abort_if(!config_cache('pixelfed.cloud_storage'), 404);
|
||||
abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404);
|
||||
$path = 'public/m/_v2/'.$pid.'/'.$mhash.'/'.$uhash.'/'.$f;
|
||||
$media = Media::whereProfileId($pid)
|
||||
->whereMediaPath($path)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue