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>
|
||||
|
|
|
@ -18,8 +18,7 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke
|
|||
protected function getExtraParams(AccessTokenEntityInterface $accessToken)
|
||||
{
|
||||
return [
|
||||
'created_at' => time(),
|
||||
'scope' => 'read write follow push'
|
||||
'created_at' => time(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
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
|
||||
{
|
||||
|
@ -29,20 +29,20 @@ class FetchMissingMediaMimeType extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
foreach(Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) {
|
||||
foreach (Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) {
|
||||
$res = Http::retry(2, 100, throw: false)->head($media->remote_url);
|
||||
|
||||
if(!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) {
|
||||
if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$media->mime = $res->header('content-type');
|
||||
|
||||
if($res->hasHeader('content-length')) {
|
||||
if ($res->hasHeader('content-length')) {
|
||||
$media->size = $res->header('content-length');
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ class FetchMissingMediaMimeType extends Command
|
|||
|
||||
MediaService::del($media->status_id);
|
||||
StatusService::del($media->status_id);
|
||||
$this->info('mid:'.$media->id . ' (' . $res->header('content-type') . ':' . $res->header('content-length') . ' bytes)');
|
||||
$this->info('mid:'.$media->id.' ('.$res->header('content-type').':'.$res->header('content-length').' bytes)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
@ -35,23 +34,24 @@ class TransformImports extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(!config('import.instagram.enabled')) {
|
||||
if (! config('import.instagram.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get();
|
||||
|
||||
if(!$ips->count()) {
|
||||
if (! $ips->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($ips as $ip) {
|
||||
foreach ($ips as $ip) {
|
||||
$id = $ip->user_id;
|
||||
$pid = $ip->profile_id;
|
||||
$profile = Profile::find($pid);
|
||||
if(!$profile) {
|
||||
if (! $profile) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -63,34 +63,43 @@ class TransformImports extends Command
|
|||
->where('creation_day', $ip->creation_day)
|
||||
->exists();
|
||||
|
||||
if($exists == true) {
|
||||
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();
|
||||
|
||||
if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Storage::exists('imports/'.$id.'/'.$ip->filename) === false) {
|
||||
ImportService::clearAttempts($profile->id);
|
||||
ImportService::getPostCount($profile->id, true);
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$missingMedia = false;
|
||||
foreach($ip->media as $ipm) {
|
||||
foreach ($ip->media as $ipm) {
|
||||
$fileName = last(explode('/', $ipm['uri']));
|
||||
$og = 'imports/' . $id . '/' . $fileName;
|
||||
if(!Storage::exists($og)) {
|
||||
$og = 'imports/'.$id.'/'.$fileName;
|
||||
if (! Storage::exists($og)) {
|
||||
$missingMedia = true;
|
||||
}
|
||||
}
|
||||
|
||||
if($missingMedia === true) {
|
||||
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';
|
||||
|
@ -107,20 +115,21 @@ class TransformImports extends Command
|
|||
$status->created_at = now()->parse($ip->creation_date);
|
||||
$status->save();
|
||||
|
||||
foreach($ip->media as $ipm) {
|
||||
foreach ($ip->media as $ipm) {
|
||||
$fileName = last(explode('/', $ipm['uri']));
|
||||
$ext = last(explode('.', $fileName));
|
||||
$basePath = MediaPathService::get($profile);
|
||||
$og = 'imports/' . $id . '/' . $fileName;
|
||||
if(!Storage::exists($og)) {
|
||||
$og = 'imports/'.$id.'/'.$fileName;
|
||||
if (! Storage::exists($og)) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
$size = Storage::size($og);
|
||||
$mime = Storage::mimeType($og);
|
||||
$newFile = Str::random(40) . '.' . $ext;
|
||||
$np = $basePath . '/' . $newFile;
|
||||
$newFile = Str::random(40).'.'.$ext;
|
||||
$np = $basePath.'/'.$newFile;
|
||||
Storage::move($og, $np);
|
||||
$media = new Media;
|
||||
$media->profile_id = $pid;
|
||||
|
|
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,30 +19,39 @@ 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);
|
||||
if (config('import.instagram.enabled')) {
|
||||
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
|
||||
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
|
||||
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
|
||||
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
|
||||
|
||||
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
|
||||
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
|
||||
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
|
||||
$schedule->command('app:instance-update-total-local-posts')->twiceDailyAt(1, 13, 45)->onOneServer();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,16 +3,31 @@
|
|||
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);
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function adminUrl()
|
||||
{
|
||||
return url('/i/admin/messages/show/' . $this->id);
|
||||
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
|
||||
{
|
||||
|
@ -41,64 +31,67 @@ trait AdminDirectoryController
|
|||
$res['countries'] = collect((new ISO3166)->all())->pluck('name');
|
||||
$res['admins'] = User::whereIsAdmin(true)
|
||||
->where('2fa_enabled', true)
|
||||
->get()->map(function($user) {
|
||||
return [
|
||||
'uid' => (string) $user->id,
|
||||
'pid' => (string) $user->profile_id,
|
||||
'username' => $user->username,
|
||||
'created_at' => $user->created_at
|
||||
];
|
||||
});
|
||||
->get()->map(function ($user) {
|
||||
return [
|
||||
'uid' => (string) $user->id,
|
||||
'pid' => (string) $user->profile_id,
|
||||
'username' => $user->username,
|
||||
'created_at' => $user->created_at,
|
||||
];
|
||||
});
|
||||
$config = ConfigCache::whereK('pixelfed.directory')->first();
|
||||
if($config) {
|
||||
if ($config) {
|
||||
$data = $config->v ? json_decode($config->v, true) : [];
|
||||
$res = array_merge($res, $data);
|
||||
}
|
||||
|
||||
if(empty($res['summary'])) {
|
||||
if (empty($res['summary'])) {
|
||||
$summary = ConfigCache::whereK('app.short_description')->pluck('v');
|
||||
$res['summary'] = $summary ? $summary[0] : null;
|
||||
}
|
||||
|
||||
if(isset($res['banner_image']) && !empty($res['banner_image'])) {
|
||||
if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
|
||||
$res['banner_image'] = url(Storage::url($res['banner_image']));
|
||||
}
|
||||
|
||||
if(isset($res['favourite_posts'])) {
|
||||
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
|
||||
if (isset($res['favourite_posts'])) {
|
||||
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
|
||||
$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')) {
|
||||
$testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true))
|
||||
->map(function($t) {
|
||||
if (config_cache('pixelfed.directory.testimonials')) {
|
||||
$testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
|
||||
->map(function ($t) {
|
||||
return [
|
||||
'profile' => AccountService::get($t['profile_id']),
|
||||
'body' => $t['body']
|
||||
'body' => $t['body'],
|
||||
];
|
||||
});
|
||||
$res['testimonials'] = $testimonials;
|
||||
|
@ -107,8 +100,8 @@ trait AdminDirectoryController
|
|||
$validator = Validator::make($res['feature_config'], [
|
||||
'media_types' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
|
||||
$fail('You must enable image/jpeg and image/png support.');
|
||||
}
|
||||
},
|
||||
|
@ -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 &&
|
||||
|
@ -145,11 +138,11 @@ trait AdminDirectoryController
|
|||
foreach (new \DirectoryIterator($path) as $io) {
|
||||
$name = $io->getFilename();
|
||||
$skip = ['vendor'];
|
||||
if($io->isDot() || in_array($name, $skip)) {
|
||||
if ($io->isDot() || in_array($name, $skip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if($io->isDir()) {
|
||||
if ($io->isDir()) {
|
||||
$langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
|
||||
}
|
||||
}
|
||||
|
@ -158,25 +151,26 @@ trait AdminDirectoryController
|
|||
$res['primary_locale'] = config('app.locale');
|
||||
|
||||
$submissionState = Http::withoutVerifying()
|
||||
->post('https://pixelfed.org/api/v1/directory/check-submission', [
|
||||
'domain' => config('pixelfed.domain.app')
|
||||
]);
|
||||
->post('https://pixelfed.org/api/v1/directory/check-submission', [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
]);
|
||||
|
||||
$res['submission_state'] = $submissionState->json();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function validVal($res, $val, $count = false, $minLen = false)
|
||||
{
|
||||
if(!isset($res[$val])) {
|
||||
if (! isset($res[$val])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($count) {
|
||||
if ($count) {
|
||||
return count($res[$val]) >= $count;
|
||||
}
|
||||
|
||||
if($minLen) {
|
||||
if ($minLen) {
|
||||
return strlen($res[$val]) >= $minLen;
|
||||
}
|
||||
|
||||
|
@ -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) : [];
|
||||
|
@ -207,27 +201,28 @@ trait AdminDirectoryController
|
|||
$res['contact_email'] = $request->input('contact_email');
|
||||
$res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
|
||||
|
||||
if($request->filled('location')) {
|
||||
if ($request->filled('location')) {
|
||||
$exists = (new ISO3166)->name($request->location);
|
||||
if($exists) {
|
||||
if ($exists) {
|
||||
$res['location'] = $request->input('location');
|
||||
}
|
||||
}
|
||||
|
||||
if($request->hasFile('banner_image')) {
|
||||
if ($request->hasFile('banner_image')) {
|
||||
collect(Storage::files('public/headers'))
|
||||
->filter(function($name) {
|
||||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png'
|
||||
];
|
||||
return !in_array($name, $protected);
|
||||
})
|
||||
->each(function($name) {
|
||||
Storage::delete($name);
|
||||
});
|
||||
$path = $request->file('banner_image')->store('public/headers');
|
||||
->filter(function ($name) {
|
||||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
|
||||
return ! in_array($name, $protected);
|
||||
})
|
||||
->each(function ($name) {
|
||||
Storage::delete($name);
|
||||
});
|
||||
$path = $request->file('banner_image')->storePublicly('public/headers');
|
||||
$res['banner_image'] = $path;
|
||||
ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
|
||||
|
||||
|
@ -239,9 +234,10 @@ trait AdminDirectoryController
|
|||
|
||||
ConfigCacheService::put('pixelfed.directory', $config->v);
|
||||
$updated = json_decode($config->v, true);
|
||||
if(isset($updated['banner_image'])) {
|
||||
if (isset($updated['banner_image'])) {
|
||||
$updated['banner_image'] = url(Storage::url($updated['banner_image']));
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
|
@ -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,13 +262,14 @@ 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' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
|
||||
$fail('You must enable image/jpeg and image/png support.');
|
||||
}
|
||||
},
|
||||
|
@ -282,10 +280,10 @@ trait AdminDirectoryController
|
|||
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
|
||||
'max_album_length' => 'required|integer|min:4|max:20',
|
||||
'account_deletion' => 'required|accepted',
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000'
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000',
|
||||
]);
|
||||
|
||||
if(!$validator->validate()) {
|
||||
if (! $validator->validate()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
|
@ -294,6 +292,7 @@ trait AdminDirectoryController
|
|||
|
||||
$data = (new PixelfedDirectoryController())->buildListing();
|
||||
$res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
@ -301,7 +300,7 @@ trait AdminDirectoryController
|
|||
{
|
||||
$bannerImage = ConfigCache::whereK('app.banner_image')->first();
|
||||
$directory = ConfigCache::whereK('pixelfed.directory')->first();
|
||||
if(!$bannerImage && !$directory || empty($directory->v)) {
|
||||
if (! $bannerImage && ! $directory || empty($directory->v)) {
|
||||
return;
|
||||
}
|
||||
$directoryArr = json_decode($directory->v, true);
|
||||
|
@ -309,12 +308,12 @@ trait AdminDirectoryController
|
|||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png'
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
if(!$path || in_array($path, $protected)) {
|
||||
if (! $path || in_array($path, $protected)) {
|
||||
return;
|
||||
}
|
||||
if(Storage::exists($directoryArr['banner_image'])) {
|
||||
if (Storage::exists($directoryArr['banner_image'])) {
|
||||
Storage::delete($directoryArr['banner_image']);
|
||||
}
|
||||
|
||||
|
@ -325,12 +324,13 @@ trait AdminDirectoryController
|
|||
$bannerImage->save();
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
ConfigCacheService::put('pixelfed.directory', $directory);
|
||||
|
||||
return $bannerImage->v;
|
||||
}
|
||||
|
||||
public function directoryGetPopularPosts(Request $request)
|
||||
{
|
||||
$ids = Cache::remember('admin:api:popular_posts', 86400, function() {
|
||||
$ids = Cache::remember('admin:api:popular_posts', 86400, function () {
|
||||
return Status::whereLocal(true)
|
||||
->whereScope('public')
|
||||
->whereType('photo')
|
||||
|
@ -340,21 +340,21 @@ trait AdminDirectoryController
|
|||
->pluck('id');
|
||||
});
|
||||
|
||||
$res = $ids->map(function($id) {
|
||||
$res = $ids->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function directoryGetAddPostByIdSearch(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|integer'
|
||||
'q' => 'required|integer',
|
||||
]);
|
||||
|
||||
$id = $request->input('q');
|
||||
|
@ -377,11 +377,12 @@ trait AdminDirectoryController
|
|||
$profile_id = $request->input('profile_id');
|
||||
$testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail();
|
||||
$existing = collect(json_decode($testimonials->v, true))
|
||||
->filter(function($t) use($profile_id) {
|
||||
->filter(function ($t) use ($profile_id) {
|
||||
return $t['profile_id'] !== $profile_id;
|
||||
})
|
||||
->values();
|
||||
ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
|
@ -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,18 +433,19 @@ trait AdminDirectoryController
|
|||
$user = User::whereProfileId($profile_id)->firstOrFail();
|
||||
|
||||
$configCache = ConfigCache::firstOrCreate([
|
||||
'k' => 'pixelfed.directory.testimonials'
|
||||
'k' => 'pixelfed.directory.testimonials',
|
||||
]);
|
||||
|
||||
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
|
||||
|
||||
$updated = $testimonials->map(function($t) use($profile_id, $body) {
|
||||
if($t['profile_id'] == $profile_id) {
|
||||
$updated = $testimonials->map(function ($t) use ($profile_id, $body) {
|
||||
if ($t['profile_id'] == $profile_id) {
|
||||
$t['body'] = $body;
|
||||
}
|
||||
|
||||
return $t;
|
||||
})
|
||||
->values();
|
||||
->values();
|
||||
|
||||
$configCache->v = json_encode($updated);
|
||||
$configCache->save();
|
||||
|
|
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
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
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,91 +2,94 @@
|
|||
|
||||
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')
|
||||
->latest()
|
||||
->simplePaginate(6)
|
||||
->map(function($report) {
|
||||
->map(function ($report) {
|
||||
$r = [
|
||||
'id' => $report->id,
|
||||
'type' => $report->type,
|
||||
'item_id' => $report->item_id,
|
||||
'item_type' => $report->item_type,
|
||||
'created_at' => $report->created_at
|
||||
'created_at' => $report->created_at,
|
||||
];
|
||||
if($report->item_type === 'App\\Status') {
|
||||
if ($report->item_type === 'App\\Status') {
|
||||
$status = StatusService::get($report->item_id, false);
|
||||
if(!$status) {
|
||||
if (! $status) {
|
||||
return;
|
||||
}
|
||||
|
||||
$r['status'] = $status;
|
||||
|
||||
if($status['in_reply_to_id']) {
|
||||
if ($status['in_reply_to_id']) {
|
||||
$r['parent'] = StatusService::get($status['in_reply_to_id'], false);
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
|
@ -114,18 +119,19 @@ class AdminApiController extends Controller
|
|||
$user = $appeal->user;
|
||||
$profile = $user->profile;
|
||||
|
||||
if($action == 'dismiss') {
|
||||
if ($action == 'dismiss') {
|
||||
$appeal->is_spam = true;
|
||||
$appeal->appeal_handled_at = $now;
|
||||
$appeal->save();
|
||||
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $profile->id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $profile->id);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$profile->id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$profile->id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'delete-post') {
|
||||
if ($action == 'delete-post') {
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->is_spam = true;
|
||||
$appeal->save();
|
||||
|
@ -140,10 +146,11 @@ class AdminApiController extends Controller
|
|||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
StatusDelete::dispatch($appeal->status)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'delete-account') {
|
||||
if ($action == 'delete-account') {
|
||||
abort_if($user->is_admin, 400, 'Cannot delete an admin account.');
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->is_spam = true;
|
||||
|
@ -159,22 +166,24 @@ class AdminApiController extends Controller
|
|||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
DeleteAccountPipeline::dispatch($appeal->user)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'dismiss-all') {
|
||||
if ($action == 'dismiss-all') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereNull('appeal_handled_at')
|
||||
->whereUserId($appeal->user_id)
|
||||
->update(['appeal_handled_at' => $now, 'is_spam' => true]);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'approve') {
|
||||
if ($action == 'approve') {
|
||||
$status = $appeal->status;
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
|
@ -190,29 +199,30 @@ class AdminApiController extends Controller
|
|||
Notification::whereAction('autospam.warning')
|
||||
->whereProfileId($appeal->user->profile_id)
|
||||
->get()
|
||||
->each(function($n) use($appeal) {
|
||||
->each(function ($n) use ($appeal) {
|
||||
NotificationService::del($appeal->user->profile_id, $n->id);
|
||||
$n->forceDelete();
|
||||
});
|
||||
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'approve-all') {
|
||||
if ($action == 'approve-all') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereNull('appeal_handled_at')
|
||||
->whereUserId($appeal->user_id)
|
||||
->get()
|
||||
->each(function($report) use($meta) {
|
||||
->each(function ($report) use ($meta) {
|
||||
$report->is_spam = false;
|
||||
$report->appeal_handled_at = now();
|
||||
$report->save();
|
||||
$status = Status::find($report->item_id);
|
||||
if($status) {
|
||||
if ($status) {
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
|
@ -223,14 +233,15 @@ class AdminApiController extends Controller
|
|||
Notification::whereAction('autospam.warning')
|
||||
->whereProfileId($report->user->profile_id)
|
||||
->get()
|
||||
->each(function($n) use($report) {
|
||||
->each(function ($n) use ($report) {
|
||||
NotificationService::del($report->user->profile_id, $n->id);
|
||||
$n->forceDelete();
|
||||
});
|
||||
});
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -239,42 +250,48 @@ 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')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(6)
|
||||
->map(function($report) {
|
||||
->map(function ($report) {
|
||||
$r = [
|
||||
'id' => $report->id,
|
||||
'type' => $report->type,
|
||||
'message' => $report->message,
|
||||
'object_id' => $report->object_id,
|
||||
'object_type' => $report->object_type,
|
||||
'created_at' => $report->created_at
|
||||
'created_at' => $report->created_at,
|
||||
];
|
||||
|
||||
if($report->profile_id) {
|
||||
if ($report->profile_id) {
|
||||
$r['reported_by_account'] = AccountService::get($report->profile_id, true);
|
||||
}
|
||||
|
||||
if($report->object_type === 'App\\Status') {
|
||||
if ($report->object_type === 'App\\Status') {
|
||||
$status = StatusService::get($report->object_id, false);
|
||||
if(!$status) {
|
||||
if (! $status) {
|
||||
return;
|
||||
}
|
||||
|
||||
$r['status'] = $status;
|
||||
|
||||
if($status['in_reply_to_id']) {
|
||||
if (isset($status['in_reply_to_id'])) {
|
||||
$r['parent'] = StatusService::get($status['in_reply_to_id'], false);
|
||||
}
|
||||
}
|
||||
|
||||
if($report->object_type === 'App\\Profile') {
|
||||
$r['account'] = AccountService::get($report->object_id, false);
|
||||
if ($report->object_type === 'App\\Profile') {
|
||||
$acct = AccountService::get($report->object_id, true);
|
||||
if ($acct) {
|
||||
$r['account'] = $acct;
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
})
|
||||
->filter()
|
||||
|
@ -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'
|
||||
'action' => 'required|string',
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
@ -299,10 +318,10 @@ class AdminApiController extends Controller
|
|||
$actions = [
|
||||
'ignore',
|
||||
'cw',
|
||||
'unlist'
|
||||
'unlist',
|
||||
];
|
||||
|
||||
if (!in_array($action, $actions)) {
|
||||
if (! in_array($action, $actions)) {
|
||||
return abort(403);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
->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 = [
|
||||
|
@ -405,76 +431,84 @@ class AdminApiController extends Controller
|
|||
|
||||
$key = $request->input('key');
|
||||
$value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
abort_if(!in_array($key, $allowedKeys), 400, 'Invalid cache key.');
|
||||
abort_if(! in_array($key, $allowedKeys), 400, 'Invalid cache key.');
|
||||
|
||||
ConfigCacheService::put($key, $value);
|
||||
|
||||
return collect([
|
||||
return collect([
|
||||
[
|
||||
'name' => 'ActivityPub Federation',
|
||||
'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
|
||||
'key' => 'federation.activitypub.enabled'
|
||||
'key' => 'federation.activitypub.enabled',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Open Registration',
|
||||
'description' => 'Allow new account registrations.',
|
||||
'key' => 'pixelfed.open_registration'
|
||||
'key' => 'pixelfed.open_registration',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Stories',
|
||||
'description' => 'Enable the ephemeral Stories feature.',
|
||||
'key' => 'instance.stories.enabled'
|
||||
'key' => 'instance.stories.enabled',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Require Email Verification',
|
||||
'description' => 'Require new accounts to verify their email address.',
|
||||
'key' => 'pixelfed.enforce_email_verification'
|
||||
'key' => 'pixelfed.enforce_email_verification',
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'AutoSpam Detection',
|
||||
'description' => 'Detect and remove spam from public timelines.',
|
||||
'key' => 'pixelfed.bouncer.enabled'
|
||||
'key' => 'pixelfed.bouncer.enabled',
|
||||
],
|
||||
])
|
||||
->map(function($s) {
|
||||
$s['state'] = (bool) config_cache($s['key']);
|
||||
return $s;
|
||||
});
|
||||
->map(function ($s) {
|
||||
$s['state'] = (bool) config_cache($s['key']);
|
||||
|
||||
return $s;
|
||||
});
|
||||
}
|
||||
|
||||
public function getUsers(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 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',
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
$res = User::whereNull('status')
|
||||
->when($q, function($query, $q) {
|
||||
return $query->where('username', 'like', '%' . $q . '%');
|
||||
->when($q, function ($query, $q) {
|
||||
return $query->where('username', 'like', '%'.$q.'%');
|
||||
})
|
||||
->orderBy('id', $sort)
|
||||
->cursorPaginate(10);
|
||||
|
||||
return AdminUser::collection($res);
|
||||
}
|
||||
|
||||
public function getUser(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$id = $request->input('user_id');
|
||||
$key = 'pf-admin-api:getUser:byId:' . $id;
|
||||
if($request->has('refresh')) {
|
||||
$key = 'pf-admin-api:getUser:byId:'.$id;
|
||||
if ($request->has('refresh')) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
return Cache::remember($key, 86400, function() use($id) {
|
||||
|
||||
return Cache::remember($key, 86400, function () use ($id) {
|
||||
$user = User::findOrFail($id);
|
||||
$profile = $user->profile;
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
|
@ -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');
|
||||
|
@ -513,8 +549,8 @@ class AdminApiController extends Controller
|
|||
|
||||
abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
|
||||
|
||||
if($action === 'delete') {
|
||||
if(config('pixelfed.account_deletion') == false) {
|
||||
if ($action === 'delete') {
|
||||
if (config('pixelfed.account_deletion') == false) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
|
@ -542,7 +578,7 @@ class AdminApiController extends Controller
|
|||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
NetworkTimelineService::deleteByProfileId($profile->id);
|
||||
|
||||
if($profile->user_id) {
|
||||
if ($profile->user_id) {
|
||||
DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
|
||||
DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
|
||||
$user->email = $user->id;
|
||||
|
@ -561,11 +597,12 @@ class AdminApiController extends Controller
|
|||
AccountService::del($profile->id);
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 200,
|
||||
'msg' => 'deleted',
|
||||
];
|
||||
} else if($action === 'refresh_stats') {
|
||||
} elseif ($action === 'refresh_stats') {
|
||||
$profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count();
|
||||
$profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
|
||||
$statusCount = Status::whereProfileId($user->profile_id)
|
||||
|
@ -575,7 +612,7 @@ class AdminApiController extends Controller
|
|||
->count();
|
||||
$profile->status_count = $statusCount;
|
||||
$profile->save();
|
||||
} else if($action === 'verify_email') {
|
||||
} elseif ($action === 'verify_email') {
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
|
||||
|
@ -587,11 +624,11 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => 'Manually verified email address',
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
} else if($action === 'unlisted') {
|
||||
} elseif ($action === 'unlisted') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
|
@ -600,13 +637,13 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->unlisted = !$profile->unlisted;
|
||||
$profile->unlisted = ! $profile->unlisted;
|
||||
$profile->save();
|
||||
} else if($action === 'cw') {
|
||||
} elseif ($action === 'cw') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
|
@ -615,13 +652,13 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->cw = !$profile->cw;
|
||||
$profile->cw = ! $profile->cw;
|
||||
$profile->save();
|
||||
} else if($action === 'no_autolink') {
|
||||
} elseif ($action === 'no_autolink') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
|
@ -630,11 +667,11 @@ class AdminApiController extends Controller
|
|||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->no_autolink = !$profile->no_autolink;
|
||||
$profile->no_autolink = ! $profile->no_autolink;
|
||||
$profile->save();
|
||||
} else {
|
||||
$profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
|
@ -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',
|
||||
|
@ -684,19 +723,19 @@ class AdminApiController extends Controller
|
|||
$sortBy = $request->input('sort_by', 'id');
|
||||
$filter = $request->input('filter');
|
||||
|
||||
$res = Instance::when($q, function($query, $q) {
|
||||
return $query->where('domain', 'like', '%' . $q . '%');
|
||||
})
|
||||
->when($filter, function($query, $filter) {
|
||||
if($filter === 'all') {
|
||||
$res = Instance::when($q, function ($query, $q) {
|
||||
return $query->where('domain', 'like', '%'.$q.'%');
|
||||
})
|
||||
->when($filter, function ($query, $filter) {
|
||||
if ($filter === 'all') {
|
||||
return $query;
|
||||
} else {
|
||||
return $query->where($filter, true);
|
||||
}
|
||||
})
|
||||
->when($sortBy, function($query, $sortBy) use($sort) {
|
||||
->when($sortBy, function ($query, $sortBy) use ($sort) {
|
||||
return $query->orderBy($sortBy, $sort);
|
||||
}, function($query) {
|
||||
}, function ($query) {
|
||||
return $query->orderBy('id', 'desc');
|
||||
})
|
||||
->cursorPaginate(10)
|
||||
|
@ -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,49 +805,51 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getAllStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin === 1, 404);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 404);
|
||||
|
||||
if($request->has('refresh')) {
|
||||
abort_unless($request->user()->is_admin === 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
if ($request->has('refresh')) {
|
||||
Cache::forget('admin-api:instance-all-stats-v1');
|
||||
}
|
||||
|
||||
return Cache::remember('admin-api:instance-all-stats-v1', 1209600, function() {
|
||||
return Cache::remember('admin-api:instance-all-stats-v1', 1209600, function () {
|
||||
$days = range(1, 7);
|
||||
$res = [
|
||||
'cached_at' => now()->format('c'),
|
||||
];
|
||||
$minStatusId = SnowflakeService::byDate(now()->subDays(7));
|
||||
|
||||
foreach($days as $day) {
|
||||
foreach ($days as $day) {
|
||||
$label = now()->subDays($day)->format('D');
|
||||
$labelShort = substr($label, 0, 1);
|
||||
$res['users']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => User::whereDate('created_at', now()->subDays($day))->count()
|
||||
'count' => User::whereDate('created_at', now()->subDays($day))->count(),
|
||||
];
|
||||
|
||||
$res['posts']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count()
|
||||
'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count(),
|
||||
];
|
||||
|
||||
$res['instances']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => Instance::whereDate('created_at', now()->subDays($day))->count()
|
||||
'count' => Instance::whereDate('created_at', now()->subDays($day))->count(),
|
||||
];
|
||||
}
|
||||
|
||||
$res['users']['total'] = DB::table('users')->count();
|
||||
$res['users']['min'] = collect($res['users']['days'])->min('count');
|
||||
$res['users']['max'] = collect($res['users']['days'])->max('count');
|
||||
$res['users']['change'] = collect($res['users']['days'])->sum('count');;
|
||||
$res['users']['change'] = collect($res['users']['days'])->sum('count');
|
||||
$res['posts']['total'] = DB::table('statuses')->whereNull('uri')->count();
|
||||
$res['posts']['min'] = collect($res['posts']['days'])->min('count');
|
||||
$res['posts']['max'] = collect($res['posts']['days'])->max('count');
|
||||
|
|
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
File diff suppressed because it is too large
Load diff
|
@ -2,319 +2,337 @@
|
|||
|
||||
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 = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function instance(Request $request)
|
||||
{
|
||||
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
||||
if(config_cache('instance.admin.pid')) {
|
||||
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
|
||||
}
|
||||
$admin = User::whereIsAdmin(true)->first();
|
||||
return $admin && isset($admin->profile_id) ?
|
||||
AccountService::getMastodon($admin->profile_id, true) :
|
||||
null;
|
||||
});
|
||||
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
||||
if (config_cache('instance.admin.pid')) {
|
||||
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
|
||||
}
|
||||
$admin = User::whereIsAdmin(true)->first();
|
||||
|
||||
$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
|
||||
return config_cache('app.rules') ?
|
||||
collect(json_decode(config_cache('app.rules'), true))
|
||||
->map(function($rule, $key) {
|
||||
$id = $key + 1;
|
||||
return [
|
||||
'id' => "{$id}",
|
||||
'text' => $rule
|
||||
];
|
||||
})
|
||||
->toArray() : [];
|
||||
});
|
||||
return $admin && isset($admin->profile_id) ?
|
||||
AccountService::getMastodon($admin->profile_id, true) :
|
||||
null;
|
||||
});
|
||||
|
||||
$res = [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'version' => config('pixelfed.version'),
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly()
|
||||
]
|
||||
],
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
|
||||
]
|
||||
],
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => 'wss://' . config('pixelfed.domain.app'),
|
||||
'status' => null
|
||||
],
|
||||
'accounts' => [
|
||||
'max_featured_tags' => 0,
|
||||
],
|
||||
'statuses' => [
|
||||
'max_characters' => (int) config('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 4,
|
||||
'max_characters_per_option' => 50,
|
||||
'min_expiration' => 300,
|
||||
'max_expiration' => 2629746,
|
||||
],
|
||||
'translation' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
'registrations' => [
|
||||
'enabled' => (bool) config_cache('pixelfed.open_registration'),
|
||||
'approval_required' => false,
|
||||
'message' => null
|
||||
],
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact
|
||||
],
|
||||
'rules' => $rules
|
||||
];
|
||||
$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
|
||||
return config_cache('app.rules') ?
|
||||
collect(json_decode(config_cache('app.rules'), true))
|
||||
->map(function ($rule, $key) {
|
||||
$id = $key + 1;
|
||||
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
return [
|
||||
'id' => "{$id}",
|
||||
'text' => $rule,
|
||||
];
|
||||
})
|
||||
->toArray() : [];
|
||||
});
|
||||
|
||||
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use ($contact, $rules) {
|
||||
return [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'version' => '3.5.3 (compatible; Pixelfed '.config('pixelfed.version').')',
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly(),
|
||||
],
|
||||
],
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
],
|
||||
],
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => null,
|
||||
'status' => null,
|
||||
],
|
||||
'vapid' => [
|
||||
'public_key' => config('webpush.vapid.public_key'),
|
||||
],
|
||||
'accounts' => [
|
||||
'max_featured_tags' => 0,
|
||||
],
|
||||
'statuses' => [
|
||||
'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23,
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400,
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 0,
|
||||
'max_characters_per_option' => 0,
|
||||
'min_expiration' => 0,
|
||||
'max_expiration' => 0,
|
||||
],
|
||||
'translation' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
'registrations' => [
|
||||
'enabled' => null,
|
||||
'approval_required' => false,
|
||||
'message' => null,
|
||||
'url' => null,
|
||||
],
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact,
|
||||
],
|
||||
'rules' => $rules,
|
||||
];
|
||||
});
|
||||
|
||||
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v2/search
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
/**
|
||||
* GET /api/v2/search
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:100',
|
||||
'account_id' => 'nullable|string',
|
||||
'max_id' => 'nullable|string',
|
||||
'min_id' => 'nullable|string',
|
||||
'type' => 'nullable|in:accounts,hashtags,statuses',
|
||||
'exclude_unreviewed' => 'nullable',
|
||||
'resolve' => 'nullable',
|
||||
'limit' => 'nullable|integer|max:40',
|
||||
'offset' => 'nullable|integer',
|
||||
'following' => 'nullable'
|
||||
]);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:100',
|
||||
'account_id' => 'nullable|string',
|
||||
'max_id' => 'nullable|string',
|
||||
'min_id' => 'nullable|string',
|
||||
'type' => 'nullable|in:accounts,hashtags,statuses',
|
||||
'exclude_unreviewed' => 'nullable',
|
||||
'resolve' => 'nullable',
|
||||
'limit' => 'nullable|integer|max:40',
|
||||
'offset' => 'nullable|integer',
|
||||
'following' => 'nullable',
|
||||
]);
|
||||
|
||||
$mastodonMode = !$request->has('_pe');
|
||||
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
|
||||
}
|
||||
if ($request->user()->has_roles && ! UserRoleService::can('can-view-discover', $request->user()->id)) {
|
||||
return [
|
||||
'accounts' => [],
|
||||
'hashtags' => [],
|
||||
'statuses' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v2/streaming/config
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getWebsocketConfig()
|
||||
{
|
||||
return config('broadcasting.default') === 'pusher' ? [
|
||||
'host' => config('broadcasting.connections.pusher.options.host'),
|
||||
'port' => config('broadcasting.connections.pusher.options.port'),
|
||||
'key' => config('broadcasting.connections.pusher.key'),
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster')
|
||||
] : [];
|
||||
}
|
||||
$mastodonMode = ! $request->has('_pe');
|
||||
|
||||
/**
|
||||
* POST /api/v2/media
|
||||
*
|
||||
*
|
||||
* @return MediaTransformer
|
||||
*/
|
||||
public function mediaUploadV2(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'file.*' => [
|
||||
'required_without:file',
|
||||
'mimetypes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'file' => [
|
||||
'required_without:file.*',
|
||||
'mimetypes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
|
||||
'replace_id' => 'sometimes'
|
||||
]);
|
||||
/**
|
||||
* GET /api/v2/streaming/config
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getWebsocketConfig()
|
||||
{
|
||||
return config('broadcasting.default') === 'pusher' ? [
|
||||
'host' => config('broadcasting.connections.pusher.options.host'),
|
||||
'port' => config('broadcasting.connections.pusher.options.port'),
|
||||
'key' => config('broadcasting.connections.pusher.key'),
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster'),
|
||||
] : [];
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
/**
|
||||
* POST /api/v2/media
|
||||
*
|
||||
*
|
||||
* @return MediaTransformer
|
||||
*/
|
||||
public function mediaUploadV2(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
if($user->last_active_at == null) {
|
||||
return [];
|
||||
}
|
||||
$this->validate($request, [
|
||||
'file.*' => [
|
||||
'required_without:file',
|
||||
'mimetypes:'.config_cache('pixelfed.media_types'),
|
||||
'max:'.config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'file' => [
|
||||
'required_without:file.*',
|
||||
'mimetypes:'.config_cache('pixelfed.media_types'),
|
||||
'max:'.config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
'description' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
|
||||
'replace_id' => 'sometimes',
|
||||
]);
|
||||
|
||||
if(empty($request->file('file'))) {
|
||||
return response('', 422);
|
||||
}
|
||||
$user = $request->user();
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
|
||||
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
|
||||
if ($user->last_active_at == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $dailyLimit >= 1250;
|
||||
});
|
||||
abort_if($limitReached == true, 429);
|
||||
if (empty($request->file('file'))) {
|
||||
return response('', 422);
|
||||
}
|
||||
|
||||
$profile = $user->profile;
|
||||
$limitKey = 'compose:rate-limit:media-upload:'.$user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
|
||||
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
return $dailyLimit >= 1250;
|
||||
});
|
||||
abort_if($limitReached == true, 429);
|
||||
|
||||
$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;
|
||||
$profile = $user->profile;
|
||||
|
||||
$photo = $request->file('file');
|
||||
$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;
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$license = null;
|
||||
$mime = $photo->getMimeType();
|
||||
$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;
|
||||
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
||||
if($settings && !empty($settings->compose_settings)) {
|
||||
$compose = $settings->compose_settings;
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$license = null;
|
||||
$mime = $photo->getMimeType();
|
||||
|
||||
if(isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
if ($settings && ! empty($settings->compose_settings)) {
|
||||
$compose = $settings->compose_settings;
|
||||
|
||||
if($request->has('replace_id')) {
|
||||
$rpid = $request->input('replace_id');
|
||||
$removeMedia = Media::whereNull('status_id')
|
||||
->whereUserId($user->id)
|
||||
->whereProfileId($profile->id)
|
||||
->where('created_at', '>', now()->subHours(2))
|
||||
->find($rpid);
|
||||
if($removeMedia) {
|
||||
MediaDeletePipeline::dispatch($removeMedia)
|
||||
->onQueue('mmo')
|
||||
->delay(now()->addMinutes(15));
|
||||
}
|
||||
}
|
||||
if (isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = null;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $mime;
|
||||
$media->caption = $request->input('description');
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
if($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
break;
|
||||
if ($request->has('replace_id')) {
|
||||
$rpid = $request->input('replace_id');
|
||||
$removeMedia = Media::whereNull('status_id')
|
||||
->whereUserId($user->id)
|
||||
->whereProfileId($profile->id)
|
||||
->where('created_at', '>', now()->subHours(2))
|
||||
->find($rpid);
|
||||
if ($removeMedia) {
|
||||
MediaDeletePipeline::dispatch($removeMedia)
|
||||
->onQueue('mmo')
|
||||
->delay(now()->addMinutes(15));
|
||||
}
|
||||
}
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media)->onQueue('mmo');
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
}
|
||||
$media = new Media();
|
||||
$media->status_id = null;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $mime;
|
||||
$media->caption = $request->input('description');
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
if ($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $media->url(). '?v=' . time();
|
||||
$res['url'] = null;
|
||||
return $this->json($res, 202);
|
||||
}
|
||||
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();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $media->url().'?v='.time();
|
||||
$res['url'] = null;
|
||||
|
||||
return $this->json($res, 202);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,9 +135,10 @@ 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) {
|
||||
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,230 +3,239 @@
|
|||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\EmailService;
|
||||
use App\User;
|
||||
use Purify;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\BouncerService;
|
||||
use Purify;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles the registration of new users as well as their
|
||||
| validation and creation. By default this controller uses a trait to
|
||||
| provide this functionality without requiring any additional code.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles the registration of new users as well as their
|
||||
| validation and creation. By default this controller uses a trait to
|
||||
| provide this functionality without requiring any additional code.
|
||||
|
|
||||
*/
|
||||
|
||||
use RegistersUsers;
|
||||
use RegistersUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after registration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/i/web';
|
||||
/**
|
||||
* Where to redirect users after registration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/i/web';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
public function getRegisterToken()
|
||||
{
|
||||
return \Cache::remember('pf:register:rt', 900, function() {
|
||||
return str_random(40);
|
||||
});
|
||||
}
|
||||
public function getRegisterToken()
|
||||
{
|
||||
return \Cache::remember('pf:register:rt', 900, function () {
|
||||
return str_random(40);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
if(config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
$data['email'] = strtolower($data['email']);
|
||||
}
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
*
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
public function validator(array $data)
|
||||
{
|
||||
if (config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
$data['email'] = strtolower($data['email']);
|
||||
}
|
||||
|
||||
$usernameRules = [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
$usernameRules = [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if(ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
if (ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if(($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
if (($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
if (! ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
if (! ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if(!ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if (! ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
];
|
||||
if (! preg_match('/[a-zA-Z]/', $value)) {
|
||||
return $fail('Username is invalid. Must contain at least one alphabetical character.');
|
||||
}
|
||||
|
||||
$emailRules = [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
];
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
$rt = [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if($value !== $this->getRegisterToken()) {
|
||||
return $fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
];
|
||||
$emailRules = [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if ($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
$rules = [
|
||||
'agecheck' => 'required|accepted',
|
||||
'rt' => $rt,
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'username' => $usernameRules,
|
||||
'email' => $emailRules,
|
||||
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
|
||||
];
|
||||
$rt = [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value !== $this->getRegisterToken()) {
|
||||
return $fail('Something went wrong');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
if(config('captcha.enabled') || config('captcha.active.register')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
}
|
||||
$rules = [
|
||||
'agecheck' => 'required|accepted',
|
||||
'rt' => $rt,
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'username' => $usernameRules,
|
||||
'email' => $emailRules,
|
||||
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
|
||||
];
|
||||
|
||||
return Validator::make($data, $rules);
|
||||
}
|
||||
if ((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return \App\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
{
|
||||
if(config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
$data['email'] = strtolower($data['email']);
|
||||
}
|
||||
return Validator::make($data, $rules);
|
||||
}
|
||||
|
||||
return User::create([
|
||||
'name' => Purify::clean($data['name']),
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'app_register_ip' => request()->ip()
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
*
|
||||
*
|
||||
* @return \App\User
|
||||
*/
|
||||
public function create(array $data)
|
||||
{
|
||||
if (config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
$data['email'] = strtolower($data['email']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application registration form.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function showRegistrationForm()
|
||||
{
|
||||
if(config_cache('pixelfed.open_registration')) {
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||
}
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if($hasLimit) {
|
||||
$limit = config('pixelfed.max_users');
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
if($limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
}
|
||||
abort_if($limit <= $count, 404);
|
||||
return view('auth.register');
|
||||
} else {
|
||||
return view('auth.register');
|
||||
}
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
return User::create([
|
||||
'name' => Purify::clean($data['name']),
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'app_register_ip' => request()->ip(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a registration request for the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
abort_if(config_cache('pixelfed.open_registration') == false, 400);
|
||||
/**
|
||||
* Show the application registration form.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function showRegistrationForm()
|
||||
{
|
||||
if ((bool) config_cache('pixelfed.open_registration')) {
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||
}
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if ($hasLimit) {
|
||||
$limit = config('pixelfed.max_users');
|
||||
$count = User::where(function ($q) {
|
||||
return $q->whereNull('status')->orWhereNotIn('status', ['deleted', 'delete']);
|
||||
})->count();
|
||||
if ($limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
}
|
||||
abort_if($limit <= $count, 404);
|
||||
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
return view('auth.register');
|
||||
} else {
|
||||
return view('auth.register');
|
||||
}
|
||||
} else {
|
||||
if ((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
|
||||
return redirect('/auth/sign_up');
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if($hasLimit) {
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
/**
|
||||
* Handle a registration request for the application.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
abort_if(config_cache('pixelfed.open_registration') == false, 400);
|
||||
|
||||
if($limit && $limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
}
|
||||
}
|
||||
if (config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if ($hasLimit) {
|
||||
$count = User::where(function ($q) {
|
||||
return $q->whereNull('status')->orWhereNotIn('status', ['deleted', 'delete']);
|
||||
})->count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
|
||||
$this->validator($request->all())->validate();
|
||||
if ($limit && $limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
}
|
||||
}
|
||||
|
||||
event(new Registered($user = $this->create($request->all())));
|
||||
$this->validator($request->all())->validate();
|
||||
|
||||
$this->guard()->login($user);
|
||||
event(new Registered($user = $this->create($request->all())));
|
||||
|
||||
return $this->registered($request, $user)
|
||||
?: redirect($this->redirectPath());
|
||||
}
|
||||
$this->guard()->login($user);
|
||||
|
||||
return $this->registered($request, $user)
|
||||
?: redirect($this->redirectPath());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,65 +3,62 @@
|
|||
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
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
$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);
|
||||
|
||||
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 ($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($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);
|
||||
$exists->delete();
|
||||
if ($request->ajax()) {
|
||||
return ['code' => 200, 'msg' => 'Bookmark removed!'];
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
return ['code' => 200, 'msg' => 'Bookmark removed!'];
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
|
||||
}
|
||||
}
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $user->profile_id]
|
||||
);
|
||||
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
);
|
||||
if (! $bookmark->wasRecentlyCreated) {
|
||||
BookmarkService::del($user->profile_id, $status->id);
|
||||
$bookmark->delete();
|
||||
} else {
|
||||
BookmarkService::add($user->profile_id, $status->id);
|
||||
}
|
||||
|
||||
if (!$bookmark->wasRecentlyCreated) {
|
||||
BookmarkService::del($profile->id, $status->id);
|
||||
$bookmark->delete();
|
||||
} else {
|
||||
BookmarkService::add($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,72 +2,65 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use App\{
|
||||
Collection,
|
||||
CollectionItem,
|
||||
Profile,
|
||||
Status
|
||||
};
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Collection;
|
||||
use App\CollectionItem;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CollectionController extends Controller
|
||||
{
|
||||
public function create(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
abort_if(! Auth::check(), 403);
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$collection = Collection::firstOrCreate([
|
||||
'profile_id' => $profile->id,
|
||||
'published_at' => null
|
||||
'published_at' => null,
|
||||
]);
|
||||
$collection->visibility = 'draft';
|
||||
$collection->save();
|
||||
|
||||
return view('collection.create', compact('collection'));
|
||||
}
|
||||
|
||||
public function show(Request $request, int $id)
|
||||
{
|
||||
$user = $request->user();
|
||||
$collection = CollectionService::getCollection($id);
|
||||
abort_if(!$collection, 404);
|
||||
if($collection['published_at'] == null || $collection['visibility'] != 'public') {
|
||||
abort_if(!$user, 404);
|
||||
if($user->profile_id != $collection['pid']) {
|
||||
if(!$user->is_admin) {
|
||||
abort_if($collection['visibility'] != 'private', 404);
|
||||
abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
return view('collection.show', compact('collection'));
|
||||
$collection = CollectionService::getCollection($id);
|
||||
abort_if(! $collection, 404);
|
||||
if ($collection['published_at'] == null || $collection['visibility'] != 'public') {
|
||||
abort_if(! $user, 404);
|
||||
if ($user->profile_id != $collection['pid']) {
|
||||
if (! $user->is_admin) {
|
||||
abort_if($collection['visibility'] != 'private', 404);
|
||||
abort_if(! FollowerService::follows($user->profile_id, $collection['pid']), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('collection.show', compact('collection'));
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
return $request->all();
|
||||
abort_if(! Auth::check(), 403);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function store(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'title' => 'nullable|max:50',
|
||||
'description' => 'nullable|max:500',
|
||||
'visibility' => 'nullable|string|in:public,private,draft'
|
||||
'title' => 'nullable|max:50',
|
||||
'description' => 'nullable|max:500',
|
||||
'visibility' => 'nullable|string|in:public,private,draft',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
@ -78,20 +71,21 @@ class CollectionController extends Controller
|
|||
$collection->save();
|
||||
|
||||
CollectionService::deleteCollection($id);
|
||||
|
||||
return CollectionService::setCollection($collection->id, $collection);
|
||||
}
|
||||
|
||||
public function publish(Request $request, int $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'title' => 'nullable|max:50',
|
||||
'description' => 'nullable|max:500',
|
||||
'visibility' => 'required|alpha|in:public,private,draft'
|
||||
'title' => 'nullable|max:50',
|
||||
'description' => 'nullable|max:500',
|
||||
'visibility' => 'required|alpha|in:public,private,draft',
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$collection = Collection::whereProfileId($profile->id)->findOrFail($id);
|
||||
if($collection->items()->count() == 0) {
|
||||
if ($collection->items()->count() == 0) {
|
||||
abort(404);
|
||||
}
|
||||
$collection->title = strip_tags($request->input('title'));
|
||||
|
@ -99,12 +93,13 @@ class CollectionController extends Controller
|
|||
$collection->visibility = $request->input('visibility');
|
||||
$collection->published_at = now();
|
||||
$collection->save();
|
||||
|
||||
return CollectionService::setCollection($collection->id, $collection);
|
||||
}
|
||||
|
||||
public function delete(Request $request, int $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user(), 403);
|
||||
$user = $request->user();
|
||||
|
||||
$collection = Collection::whereProfileId($user->profile_id)->findOrFail($id);
|
||||
|
@ -113,7 +108,7 @@ class CollectionController extends Controller
|
|||
|
||||
CollectionService::deleteCollection($id);
|
||||
|
||||
if($request->wantsJson()) {
|
||||
if ($request->wantsJson()) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
@ -122,11 +117,11 @@ class CollectionController extends Controller
|
|||
|
||||
public function storeId(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'collection_id' => 'required|int|min:1|exists:collections,id',
|
||||
'post_id' => 'required|int|min:1'
|
||||
'post_id' => 'required|int|min:1',
|
||||
]);
|
||||
|
||||
$profileId = $request->user()->profile_id;
|
||||
|
@ -136,155 +131,151 @@ class CollectionController extends Controller
|
|||
$collection = Collection::whereProfileId($profileId)->findOrFail($collectionId);
|
||||
$count = $collection->items()->count();
|
||||
|
||||
if($count) {
|
||||
if ($count) {
|
||||
CollectionItem::whereCollectionId($collection->id)
|
||||
->get()
|
||||
->filter(function($col) {
|
||||
->filter(function ($col) {
|
||||
return StatusService::get($col->object_id, false) == null;
|
||||
})
|
||||
->each(function($col) use($collectionId) {
|
||||
->each(function ($col) use ($collectionId) {
|
||||
CollectionService::removeItem($collectionId, $col->object_id);
|
||||
$col->delete();
|
||||
});
|
||||
}
|
||||
|
||||
$max = config('pixelfed.max_collection_length');
|
||||
if($count >= $max) {
|
||||
if ($count >= $max) {
|
||||
abort(400, 'You can only add '.$max.' posts per collection');
|
||||
}
|
||||
|
||||
$status = Status::whereScope('public')
|
||||
$status = Status::whereIn('scope', ['public', 'unlisted'])
|
||||
->whereProfileId($profileId)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video'])
|
||||
->findOrFail($postId);
|
||||
|
||||
$item = CollectionItem::firstOrCreate([
|
||||
'collection_id' => $collection->id,
|
||||
'object_type' => 'App\Status',
|
||||
'object_id' => $status->id
|
||||
],[
|
||||
'order' => $count,
|
||||
'object_type' => 'App\Status',
|
||||
'object_id' => $status->id,
|
||||
], [
|
||||
'order' => $count,
|
||||
]);
|
||||
|
||||
CollectionService::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)
|
||||
{
|
||||
$user = $request->user();
|
||||
$collection = CollectionService::getCollection($id);
|
||||
$user = $request->user();
|
||||
$collection = CollectionService::getCollection($id);
|
||||
|
||||
if(!$collection) {
|
||||
if (! $collection) {
|
||||
return response()->json([], 404);
|
||||
}
|
||||
|
||||
if($collection['published_at'] == null || $collection['visibility'] != 'public') {
|
||||
abort_unless($user, 404);
|
||||
if($user->profile_id != $collection['pid']) {
|
||||
if(!$user->is_admin) {
|
||||
abort_if($collection['visibility'] != 'private', 404);
|
||||
abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($collection['published_at'] == null || $collection['visibility'] != 'public') {
|
||||
abort_unless($user, 404);
|
||||
if ($user->profile_id != $collection['pid']) {
|
||||
if (! $user->is_admin) {
|
||||
abort_if($collection['visibility'] != 'private', 404);
|
||||
abort_if(! FollowerService::follows($user->profile_id, $collection['pid']), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
public function getItems(Request $request, int $id)
|
||||
{
|
||||
$user = $request->user();
|
||||
$collection = CollectionService::getCollection($id);
|
||||
$user = $request->user();
|
||||
$collection = CollectionService::getCollection($id);
|
||||
|
||||
if(!$collection) {
|
||||
if (! $collection) {
|
||||
return response()->json([], 404);
|
||||
}
|
||||
|
||||
if($collection['published_at'] == null || $collection['visibility'] != 'public') {
|
||||
abort_unless($user, 404);
|
||||
if($user->profile_id != $collection['pid']) {
|
||||
if(!$user->is_admin) {
|
||||
abort_if($collection['visibility'] != 'private', 404);
|
||||
abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($collection['published_at'] == null || $collection['visibility'] != 'public') {
|
||||
abort_unless($user, 404);
|
||||
if ($user->profile_id != $collection['pid']) {
|
||||
if (! $user->is_admin) {
|
||||
abort_if($collection['visibility'] != 'private', 404);
|
||||
abort_if(! FollowerService::follows($user->profile_id, $collection['pid']), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
$page = $request->input('page') ?? 1;
|
||||
$start = $page == 1 ? 0 : ($page * 10 - 10);
|
||||
$end = $start + 10;
|
||||
$items = CollectionService::getItems($id, $start, $end);
|
||||
|
||||
return collect($items)
|
||||
->map(function($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($item) {
|
||||
return $item && isset($item['account'], $item['media_attachments']);
|
||||
})
|
||||
->values();
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function ($item) {
|
||||
return $item && ($item['visibility'] == 'public' || $item['visibility'] == 'unlisted') && isset($item['account'], $item['media_attachments']);
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
public function getUserCollections(Request $request, int $id)
|
||||
{
|
||||
$user = $request->user();
|
||||
$pid = $user ? $user->profile_id : null;
|
||||
$follows = false;
|
||||
$visibility = ['public'];
|
||||
$user = $request->user();
|
||||
$pid = $user ? $user->profile_id : null;
|
||||
$follows = false;
|
||||
$visibility = ['public'];
|
||||
|
||||
$profile = AccountService::get($id, true);
|
||||
if(!$profile || !isset($profile['id'])) {
|
||||
if (! $profile || ! isset($profile['id'])) {
|
||||
return response()->json([], 404);
|
||||
}
|
||||
|
||||
if($pid) {
|
||||
$follows = FollowerService::follows($pid, $profile['id']);
|
||||
if ($pid) {
|
||||
$follows = FollowerService::follows($pid, $profile['id']);
|
||||
}
|
||||
|
||||
if($profile['locked']) {
|
||||
abort_if(!$pid, 404);
|
||||
if(!$user->is_admin) {
|
||||
abort_if($profile['id'] != $pid && $follows == false, 404);
|
||||
if ($profile['locked']) {
|
||||
abort_if(! $pid, 404);
|
||||
if (! $user->is_admin) {
|
||||
abort_if($profile['id'] != $pid && $follows == false, 404);
|
||||
}
|
||||
}
|
||||
|
||||
$owner = $pid ? $pid == $profile['id'] : false;
|
||||
|
||||
if($follows) {
|
||||
$visibility = ['public', 'private'];
|
||||
if ($follows) {
|
||||
$visibility = ['public', 'private'];
|
||||
}
|
||||
|
||||
if($pid && $pid == $profile['id']) {
|
||||
$visibility = ['public', 'private', 'draft'];
|
||||
if ($pid && $pid == $profile['id']) {
|
||||
$visibility = ['public', 'private', 'draft'];
|
||||
}
|
||||
|
||||
return Collection::whereProfileId($profile['id'])
|
||||
->whereIn('visibility', $visibility)
|
||||
->when(!$owner, function($q, $owner) {
|
||||
return $q->whereNotNull('published_at');
|
||||
})
|
||||
->whereIn('visibility', $visibility)
|
||||
->when(! $owner, function ($q, $owner) {
|
||||
return $q->whereNotNull('published_at');
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->paginate(9)
|
||||
->map(function($collection) {
|
||||
return CollectionService::getCollection($collection->id);
|
||||
});
|
||||
->map(function ($collection) {
|
||||
return CollectionService::getCollection($collection->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function deleteId(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(! $request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'collection_id' => 'required|int|min:1|exists:collections,id',
|
||||
'post_id' => 'required|int|min:1'
|
||||
'post_id' => 'required|int|min:1',
|
||||
]);
|
||||
|
||||
$profileId = $request->user()->profile_id;
|
||||
|
@ -294,11 +285,11 @@ class CollectionController extends Controller
|
|||
$collection = Collection::whereProfileId($profileId)->findOrFail($collectionId);
|
||||
$count = $collection->items()->count();
|
||||
|
||||
if($count == 1) {
|
||||
if ($count == 1) {
|
||||
abort(400, 'You cannot delete the only post of a collection!');
|
||||
}
|
||||
|
||||
$status = Status::whereScope('public')
|
||||
$status = Status::whereIn('scope', ['public', 'unlisted'])
|
||||
->whereIn('type', ['photo', 'photo:album', 'video'])
|
||||
->findOrFail($postId);
|
||||
|
||||
|
@ -312,7 +303,7 @@ class CollectionController extends Controller
|
|||
CollectionItem::whereCollectionId($collection->id)
|
||||
->orderBy('created_at')
|
||||
->get()
|
||||
->each(function($item, $index) {
|
||||
->each(function ($item, $index) {
|
||||
$item->order = $index;
|
||||
$item->save();
|
||||
});
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -33,9 +28,9 @@ class CommentController extends Controller
|
|||
abort(403);
|
||||
}
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'),
|
||||
'sensitive' => 'nullable|boolean'
|
||||
'item' => 'required|integer|min:1',
|
||||
'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'),
|
||||
'sensitive' => 'nullable|boolean',
|
||||
]);
|
||||
$comment = $request->input('comment');
|
||||
$statusId = $request->input('item');
|
||||
|
@ -45,7 +40,7 @@ class CommentController extends Controller
|
|||
$profile = $user->profile;
|
||||
$status = Status::findOrFail($statusId);
|
||||
|
||||
if($status->comments_disabled == true) {
|
||||
if ($status->comments_disabled == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -55,18 +50,19 @@ class CommentController extends Controller
|
|||
->whereFilterableId($profile->id)
|
||||
->exists();
|
||||
|
||||
if($filtered == true) {
|
||||
if ($filtered == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reply = DB::transaction(function() use($comment, $status, $profile, $nsfw) {
|
||||
$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,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -2,366 +2,430 @@
|
|||
|
||||
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 home(Request $request)
|
||||
{
|
||||
abort_if(! Auth::check() && config('instance.discover.public') == false, 403);
|
||||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
|
||||
return view('discover.home');
|
||||
}
|
||||
|
||||
$tag = Hashtag::whereName($hashtag)
|
||||
->orWhere('slug', $hashtag)
|
||||
->where('is_banned', '!=', true)
|
||||
->firstOrFail();
|
||||
$tagCount = StatusHashtagService::count($tag->id);
|
||||
return view('discover.tags.show', compact('tag', 'tagCount'));
|
||||
}
|
||||
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);
|
||||
|
||||
public function getHashtags(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(!config('instance.discover.tags.is_public') && !$user, 403);
|
||||
$tag = Hashtag::whereName($hashtag)
|
||||
->orWhere('slug', $hashtag)
|
||||
->where('is_banned', '!=', true)
|
||||
->firstOrFail();
|
||||
$tagCount = $tag->cached_count ?? 0;
|
||||
|
||||
$this->validate($request, [
|
||||
'hashtag' => 'required|string|min:1|max:124',
|
||||
'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3)
|
||||
]);
|
||||
return view('discover.tags.show', compact('tag', 'tagCount'));
|
||||
}
|
||||
|
||||
$page = $request->input('page') ?? '1';
|
||||
$end = $page > 1 ? $page * 9 : 0;
|
||||
$tag = $request->input('hashtag');
|
||||
public function getHashtags(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(! config('instance.discover.tags.is_public') && ! $user, 403);
|
||||
|
||||
if(config('database.default') === 'pgsql') {
|
||||
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
|
||||
} else {
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
}
|
||||
$this->validate($request, [
|
||||
'hashtag' => 'required|string|min:1|max:124',
|
||||
'page' => 'nullable|integer|min:1|max:'.($user ? 29 : 3),
|
||||
]);
|
||||
|
||||
if($hashtag->is_banned == true) {
|
||||
return [];
|
||||
}
|
||||
if($user) {
|
||||
$res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id);
|
||||
}
|
||||
$res['hashtag'] = [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url()
|
||||
];
|
||||
if($user) {
|
||||
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||
$res['tags'] = collect($tags)
|
||||
->map(function($tag) use($user) {
|
||||
$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;
|
||||
$tags = Cache::remember($key, 43200, function() use($hashtag, $page, $end) {
|
||||
return collect(StatusHashtagService::get($hashtag->id, $page, $end))
|
||||
->filter(function($tag) {
|
||||
if(!$tag['status']['local']) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
});
|
||||
$res['tags'] = collect($tags)
|
||||
->filter(function($tag) {
|
||||
if(!StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
$page = $request->input('page') ?? '1';
|
||||
$end = $page > 1 ? $page * 9 : 0;
|
||||
$tag = $request->input('hashtag');
|
||||
|
||||
public function profilesDirectory(Request $request)
|
||||
{
|
||||
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
|
||||
}
|
||||
if (config('database.default') === 'pgsql') {
|
||||
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
|
||||
} else {
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
}
|
||||
|
||||
public function profilesDirectoryApi(Request $request)
|
||||
{
|
||||
return ['error' => 'Temporarily unavailable.'];
|
||||
}
|
||||
if ($hashtag->is_banned == true) {
|
||||
return [];
|
||||
}
|
||||
if ($user) {
|
||||
$res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id);
|
||||
}
|
||||
$res['hashtag'] = [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url(),
|
||||
];
|
||||
if ($user) {
|
||||
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||
$res['tags'] = collect($tags)
|
||||
->map(function ($tag) use ($user) {
|
||||
$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']);
|
||||
|
||||
public function trendingApi(Request $request)
|
||||
{
|
||||
abort_if(config('instance.discover.public') == false && !$request->user(), 403);
|
||||
return $tag;
|
||||
})
|
||||
->filter(function ($tag) {
|
||||
if (! StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'range' => 'nullable|string|in:daily,monthly,yearly',
|
||||
]);
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
} else {
|
||||
if ($page != 1) {
|
||||
$res['tags'] = [];
|
||||
|
||||
$range = $request->input('range');
|
||||
$days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365);
|
||||
$ttls = [
|
||||
1 => 1500,
|
||||
31 => 14400,
|
||||
365 => 86400
|
||||
];
|
||||
$key = ':api:discover:trending:v2.12:range:' . $days;
|
||||
return $res;
|
||||
}
|
||||
$key = 'discover:tags:public_feed:'.$hashtag->id.':page:'.$page;
|
||||
$tags = Cache::remember($key, 43200, function () use ($hashtag, $page, $end) {
|
||||
return collect(StatusHashtagService::get($hashtag->id, $page, $end))
|
||||
->filter(function ($tag) {
|
||||
if (! $tag['status']['local']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ids = Cache::remember($key, $ttls[$days], function() use($days) {
|
||||
$min_id = SnowflakeService::byDate(now()->subDays($days));
|
||||
return DB::table('statuses')
|
||||
->select(
|
||||
'id',
|
||||
'scope',
|
||||
'type',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'created_at'
|
||||
)
|
||||
->where('id', '>', $min_id)
|
||||
->whereNull('uri')
|
||||
->whereScope('public')
|
||||
->whereIn('type', [
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video'
|
||||
])
|
||||
->whereIsNsfw(false)
|
||||
->orderBy('likes_count','desc')
|
||||
->take(30)
|
||||
->pluck('id');
|
||||
});
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
});
|
||||
$res['tags'] = collect($tags)
|
||||
->filter(function ($tag) {
|
||||
if (! StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
$res = $ids->map(function($s) {
|
||||
return StatusService::get($s);
|
||||
})->filter(function($s) use($filtered) {
|
||||
return
|
||||
$s &&
|
||||
!in_array($s['account']['id'], $filtered) &&
|
||||
isset($s['account']);
|
||||
})->values();
|
||||
return $res;
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
public function profilesDirectory(Request $request)
|
||||
{
|
||||
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
|
||||
}
|
||||
|
||||
public function trendingHashtags(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
public function profilesDirectoryApi(Request $request)
|
||||
{
|
||||
return ['error' => 'Temporarily unavailable.'];
|
||||
}
|
||||
|
||||
$res = TrendingHashtagService::getTrending();
|
||||
return $res;
|
||||
}
|
||||
public function trendingApi(Request $request)
|
||||
{
|
||||
abort_if(config('instance.discover.public') == false && ! $request->user(), 403);
|
||||
|
||||
public function trendingPlaces(Request $request)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
$this->validate($request, [
|
||||
'range' => 'nullable|string|in:daily,monthly,yearly',
|
||||
]);
|
||||
|
||||
public function myMemories(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$this->config()['memories']['enabled'], 404);
|
||||
$type = $request->input('type') ?? 'posts';
|
||||
$range = $request->input('range');
|
||||
$days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365);
|
||||
$ttls = [
|
||||
1 => 1500,
|
||||
31 => 14400,
|
||||
365 => 86400,
|
||||
];
|
||||
$key = ':api:discover:trending:v2.12:range:'.$days;
|
||||
|
||||
switch($type) {
|
||||
case 'posts':
|
||||
$res = Status::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->whereNull(['reblog_of_id', 'in_reply_to_id'])
|
||||
->limit(20)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
$ids = Cache::remember($key, $ttls[$days], function () use ($days) {
|
||||
$min_id = SnowflakeService::byDate(now()->subDays($days));
|
||||
|
||||
case 'liked':
|
||||
$res = Like::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->orderByDesc('status_id')
|
||||
->limit(20)
|
||||
->pluck('status_id')
|
||||
->map(function($id) {
|
||||
$status = StatusService::get($id, false);
|
||||
$status['favourited'] = true;
|
||||
return $status;
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
}
|
||||
return DB::table('statuses')
|
||||
->select(
|
||||
'id',
|
||||
'scope',
|
||||
'type',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'created_at'
|
||||
)
|
||||
->where('id', '>', $min_id)
|
||||
->whereNull('uri')
|
||||
->whereScope('public')
|
||||
->whereIn('type', [
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video',
|
||||
])
|
||||
->whereIsNsfw(false)
|
||||
->orderBy('likes_count', 'desc')
|
||||
->take(30)
|
||||
->pluck('id');
|
||||
});
|
||||
|
||||
return $res;
|
||||
}
|
||||
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
|
||||
|
||||
public function accountInsightsPopularPosts(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$this->config()['insights']['enabled'], 404);
|
||||
$posts = Cache::remember('pf:discover:metro2:accinsights:popular:' . $pid, 43200, function() use ($pid) {
|
||||
return Status::whereProfileId($pid)
|
||||
->whereNotNull('likes_count')
|
||||
->orderByDesc('likes_count')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
});
|
||||
$res = $ids->map(function ($s) {
|
||||
return StatusService::get($s);
|
||||
})->filter(function ($s) use ($filtered) {
|
||||
return
|
||||
$s &&
|
||||
! in_array($s['account']['id'], $filtered) &&
|
||||
isset($s['account']);
|
||||
})->values();
|
||||
|
||||
return $posts;
|
||||
}
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function config()
|
||||
{
|
||||
$cc = ConfigCacheService::get('config.discover.features');
|
||||
if($cc) {
|
||||
return is_string($cc) ? json_decode($cc, true) : $cc;
|
||||
}
|
||||
return [
|
||||
'hashtags' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'memories' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'insights' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'friends' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'server' => [
|
||||
'enabled' => false,
|
||||
'mode' => 'allowlist',
|
||||
'domains' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
public function trendingHashtags(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 403);
|
||||
|
||||
public function serverTimeline(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$this->config()['server']['enabled'], 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$domain = $request->input('domain');
|
||||
$config = $this->config();
|
||||
$domains = explode(',', $config['server']['domains']);
|
||||
abort_unless(in_array($domain, $domains), 400);
|
||||
$res = TrendingHashtagService::getTrending();
|
||||
|
||||
$res = Status::whereNotNull('uri')
|
||||
->where('uri', 'like', 'https://' . $domain . '%')
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->orderByDesc('id')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
return $res;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function enabledFeatures(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
return $this->config();
|
||||
}
|
||||
public function trendingPlaces(Request $request)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateFeatures(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user()->is_admin, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$this->validate($request, [
|
||||
'features.friends.enabled' => 'boolean',
|
||||
'features.hashtags.enabled' => 'boolean',
|
||||
'features.insights.enabled' => 'boolean',
|
||||
'features.memories.enabled' => 'boolean',
|
||||
'features.server.enabled' => 'boolean',
|
||||
]);
|
||||
$res = $request->input('features');
|
||||
if($res['server'] && isset($res['server']['domains']) && !empty($res['server']['domains'])) {
|
||||
$parts = explode(',', $res['server']['domains']);
|
||||
$parts = array_filter($parts, function($v) {
|
||||
$len = strlen($v);
|
||||
$pos = strpos($v, '.');
|
||||
$domain = trim($v);
|
||||
if($pos == false || $pos == ($len + 1)) {
|
||||
return false;
|
||||
}
|
||||
if(!Instance::whereDomain($domain)->exists()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
$parts = array_slice($parts, 0, 10);
|
||||
$d = implode(',', array_map('trim', $parts));
|
||||
$res['server']['domains'] = $d;
|
||||
}
|
||||
ConfigCacheService::put('config.discover.features', json_encode($res));
|
||||
return $res;
|
||||
}
|
||||
public function myMemories(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $this->config()['memories']['enabled'], 404);
|
||||
$type = $request->input('type') ?? 'posts';
|
||||
|
||||
switch ($type) {
|
||||
case 'posts':
|
||||
$res = Status::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->whereNull(['reblog_of_id', 'in_reply_to_id'])
|
||||
->limit(20)
|
||||
->pluck('id')
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
|
||||
case 'liked':
|
||||
$res = Like::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->orderByDesc('status_id')
|
||||
->limit(20)
|
||||
->pluck('status_id')
|
||||
->map(function ($id) {
|
||||
$status = StatusService::get($id, false);
|
||||
$status['favourited'] = true;
|
||||
|
||||
return $status;
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function accountInsightsPopularPosts(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $this->config()['insights']['enabled'], 404);
|
||||
$posts = Cache::remember('pf:discover:metro2:accinsights:popular:'.$pid, 43200, function () use ($pid) {
|
||||
return Status::whereProfileId($pid)
|
||||
->whereNotNull('likes_count')
|
||||
->orderByDesc('likes_count')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
});
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
public function config()
|
||||
{
|
||||
$cc = ConfigCacheService::get('config.discover.features');
|
||||
if ($cc) {
|
||||
return is_string($cc) ? json_decode($cc, true) : $cc;
|
||||
}
|
||||
|
||||
return [
|
||||
'hashtags' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'memories' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'insights' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'friends' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'server' => [
|
||||
'enabled' => false,
|
||||
'mode' => 'allowlist',
|
||||
'domains' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function serverTimeline(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
abort_if(! $this->config()['server']['enabled'], 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$domain = $request->input('domain');
|
||||
$config = $this->config();
|
||||
$domains = explode(',', $config['server']['domains']);
|
||||
abort_unless(in_array($domain, $domains), 400);
|
||||
|
||||
$res = Status::whereNotNull('uri')
|
||||
->where('uri', 'like', 'https://'.$domain.'%')
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->orderByDesc('id')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function enabledFeatures(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return $this->config();
|
||||
}
|
||||
|
||||
public function updateFeatures(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
abort_if(! $request->user()->is_admin, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$this->validate($request, [
|
||||
'features.friends.enabled' => 'boolean',
|
||||
'features.hashtags.enabled' => 'boolean',
|
||||
'features.insights.enabled' => 'boolean',
|
||||
'features.memories.enabled' => 'boolean',
|
||||
'features.server.enabled' => 'boolean',
|
||||
]);
|
||||
$res = $request->input('features');
|
||||
if ($res['server'] && isset($res['server']['domains']) && ! empty($res['server']['domains'])) {
|
||||
$parts = explode(',', $res['server']['domains']);
|
||||
$parts = array_filter($parts, function ($v) {
|
||||
$len = strlen($v);
|
||||
$pos = strpos($v, '.');
|
||||
$domain = trim($v);
|
||||
if ($pos == false || $pos == ($len + 1)) {
|
||||
return false;
|
||||
}
|
||||
if (! Instance::whereDomain($domain)->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
$parts = array_slice($parts, 0, 10);
|
||||
$d = implode(',', array_map('trim', $parts));
|
||||
$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,265 +2,304 @@
|
|||
|
||||
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','*');
|
||||
}
|
||||
public function nodeinfoWellKnown()
|
||||
{
|
||||
abort_if(! config('federation.nodeinfo.enabled'), 404);
|
||||
|
||||
public function nodeinfo()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
if (!config('federation.webfinger.enabled') ||
|
||||
!$request->has('resource') ||
|
||||
!$request->filled('resource')
|
||||
) {
|
||||
return response('', 400);
|
||||
}
|
||||
public function nodeinfo()
|
||||
{
|
||||
abort_if(! config('federation.nodeinfo.enabled'), 404);
|
||||
|
||||
$resource = $request->input('resource');
|
||||
$domain = config('pixelfed.domain.app');
|
||||
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
if(config('federation.activitypub.sharedInbox') &&
|
||||
$resource == 'acct:' . $domain . '@' . $domain) {
|
||||
$res = [
|
||||
'subject' => 'acct:' . $domain . '@' . $domain,
|
||||
'aliases' => [
|
||||
'https://' . $domain . '/i/actor'
|
||||
],
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => 'https://' . $domain . '/site/kb/instance-actor'
|
||||
],
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => 'https://' . $domain . '/i/actor'
|
||||
]
|
||||
]
|
||||
];
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$hash = hash('sha256', $resource);
|
||||
$key = 'federation:webfinger:sha256:' . $hash;
|
||||
if($cached = Cache::get($key)) {
|
||||
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
if(strpos($resource, $domain) == false) {
|
||||
return response('', 400);
|
||||
}
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if(empty($parsed) || $parsed['domain'] !== $domain) {
|
||||
return response('', 400);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
|
||||
if(!$profile || $profile->status !== null) {
|
||||
return response('', 400);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
Cache::put($key, $webfinger, 1209600);
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
if (! config('federation.webfinger.enabled') ||
|
||||
! $request->has('resource') ||
|
||||
! $request->filled('resource')
|
||||
) {
|
||||
return response('', 400);
|
||||
}
|
||||
|
||||
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
$resource = $request->input('resource');
|
||||
$domain = config('pixelfed.domain.app');
|
||||
|
||||
public function hostMeta(Request $request)
|
||||
{
|
||||
abort_if(!config('federation.webfinger.enabled'), 404);
|
||||
// Instance Actor
|
||||
if (
|
||||
config('federation.activitypub.sharedInbox') &&
|
||||
$resource == 'acct:'.$domain.'@'.$domain
|
||||
) {
|
||||
$res = [
|
||||
'subject' => 'acct:'.$domain.'@'.$domain,
|
||||
'aliases' => [
|
||||
'https://'.$domain.'/i/actor',
|
||||
],
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => 'https://'.$domain.'/site/kb/instance-actor',
|
||||
],
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => 'https://'.$domain.'/i/actor',
|
||||
],
|
||||
[
|
||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template' => 'https://'.$domain.'/authorize_interaction?uri={uri}',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
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);
|
||||
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
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)) {
|
||||
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
if (strpos($resource, $domain) == false) {
|
||||
return response('', 400);
|
||||
}
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if (empty($parsed) || $parsed['domain'] !== $domain) {
|
||||
return response('', 400);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$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);
|
||||
|
||||
if(!$request->wantsJson()) {
|
||||
return redirect('/' . $username);
|
||||
}
|
||||
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox',
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
public function hostMeta(Request $request)
|
||||
{
|
||||
abort_if(! config('federation.webfinger.enabled'), 404);
|
||||
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
|
||||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.inbox'), 404);
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
if(!$payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if(!isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if(in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if($obj['object']['type'] === 'Person') {
|
||||
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (! $request->wantsJson()) {
|
||||
return redirect('/'.$username);
|
||||
}
|
||||
|
||||
if($obj['object']['type'] === 'Tombstone') {
|
||||
if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
return;
|
||||
}
|
||||
}
|
||||
$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' => $account['statuses_count'] ?? 0,
|
||||
];
|
||||
|
||||
if($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
}
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! config('federation.activitypub.inbox'), 404);
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
if (! $payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if (! isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if (in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
if (isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if ($obj['object']['type'] === 'Person') {
|
||||
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if(!isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if(in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
if ($obj['object']['type'] === 'Tombstone') {
|
||||
if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if($obj['object']['type'] === 'Person') {
|
||||
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
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');
|
||||
}
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
return response()->json($obj);
|
||||
}
|
||||
}
|
||||
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! config('federation.activitypub.sharedInbox'), 404);
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
|
||||
return response()->json($obj);
|
||||
}
|
||||
if (! $payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if (! isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if (in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if ($obj['object']['type'] === 'Person') {
|
||||
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
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' => 'OrderedCollection',
|
||||
'totalItems' => $account['following_count'] ?? 0,
|
||||
];
|
||||
|
||||
return response()->json($obj)->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
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' => 'OrderedCollection',
|
||||
'totalItems' => $account['followers_count'] ?? 0,
|
||||
];
|
||||
|
||||
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,442 +2,424 @@
|
|||
|
||||
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
|
||||
{
|
||||
protected $fractal;
|
||||
protected $fractal;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
}
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->fractal = new Fractal\Manager;
|
||||
$this->fractal->setSerializer(new ArraySerializer);
|
||||
}
|
||||
|
||||
// deprecated v2 compose api
|
||||
public function compose(Request $request)
|
||||
{
|
||||
return redirect('/');
|
||||
}
|
||||
// deprecated v2 compose api
|
||||
public function compose(Request $request)
|
||||
{
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
// deprecated
|
||||
public function discover(Request $request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// deprecated
|
||||
public function discover(Request $request) {}
|
||||
|
||||
public function discoverPosts(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$filters = UserFilterService::filters($pid);
|
||||
$forYou = DiscoverService::getForYou();
|
||||
$posts = $forYou->take(50)->map(function($post) {
|
||||
return StatusService::get($post);
|
||||
})
|
||||
->filter(function($post) use($filters) {
|
||||
return $post &&
|
||||
isset($post['account']) &&
|
||||
isset($post['account']['id']) &&
|
||||
!in_array($post['account']['id'], $filters);
|
||||
})
|
||||
->take(12)
|
||||
->values();
|
||||
return response()->json(compact('posts'));
|
||||
}
|
||||
public function discoverPosts(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$filters = UserFilterService::filters($pid);
|
||||
$forYou = DiscoverService::getForYou();
|
||||
$posts = $forYou->take(50)->map(function ($post) {
|
||||
return StatusService::get($post);
|
||||
})
|
||||
->filter(function ($post) use ($filters) {
|
||||
return $post &&
|
||||
isset($post['account']) &&
|
||||
isset($post['account']['id']) &&
|
||||
! in_array($post['account']['id'], $filters);
|
||||
})
|
||||
->take(12)
|
||||
->values();
|
||||
|
||||
public function directMessage(Request $request, $profileId, $threadId)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return response()->json(compact('posts'));
|
||||
}
|
||||
|
||||
if($profileId != $profile->id) {
|
||||
abort(403);
|
||||
}
|
||||
public function directMessage(Request $request, $profileId, $threadId)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$msg = DirectMessage::whereToId($profile->id)
|
||||
->orWhere('from_id',$profile->id)
|
||||
->findOrFail($threadId);
|
||||
if ($profileId != $profile->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
|
||||
->whereIn('from_id', [$profile->id,$msg->from_id])
|
||||
->orderBy('created_at', 'asc')
|
||||
->paginate(30);
|
||||
$msg = DirectMessage::whereToId($profile->id)
|
||||
->orWhere('from_id', $profile->id)
|
||||
->findOrFail($threadId);
|
||||
|
||||
return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
$thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
|
||||
->whereIn('from_id', [$profile->id, $msg->from_id])
|
||||
->orderBy('created_at', 'asc')
|
||||
->paginate(30);
|
||||
|
||||
public function statusReplies(Request $request, int $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|int|min:1|max:6'
|
||||
]);
|
||||
$parent = Status::whereScope('public')->findOrFail($id);
|
||||
$limit = $request->input('limit') ?? 3;
|
||||
$children = Status::whereInReplyToId($parent->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->take($limit)
|
||||
->get();
|
||||
$resource = new Fractal\Resource\Collection($children, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
public function statusReplies(Request $request, int $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|int|min:1|max:6',
|
||||
]);
|
||||
$parent = Status::whereScope('public')->findOrFail($id);
|
||||
$limit = $request->input('limit') ?? 3;
|
||||
$children = Status::whereInReplyToId($parent->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->take($limit)
|
||||
->get();
|
||||
$resource = new Fractal\Resource\Collection($children, new StatusTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
public function stories(Request $request)
|
||||
{
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
}
|
||||
public function stories(Request $request) {}
|
||||
|
||||
public function discoverCategories(Request $request)
|
||||
{
|
||||
$categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
|
||||
$res = $categories->map(function($item) {
|
||||
return [
|
||||
'name' => $item->name,
|
||||
'url' => $item->url(),
|
||||
'thumb' => $item->thumb()
|
||||
];
|
||||
});
|
||||
return response()->json($res);
|
||||
}
|
||||
public function discoverCategories(Request $request)
|
||||
{
|
||||
$categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
|
||||
$res = $categories->map(function ($item) {
|
||||
return [
|
||||
'name' => $item->name,
|
||||
'url' => $item->url(),
|
||||
'thumb' => $item->thumb(),
|
||||
];
|
||||
});
|
||||
|
||||
public function modAction(Request $request)
|
||||
{
|
||||
abort_unless(Auth::user()->is_admin, 400);
|
||||
$this->validate($request, [
|
||||
'action' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::in([
|
||||
'addcw',
|
||||
'remcw',
|
||||
'unlist',
|
||||
'spammer'
|
||||
])
|
||||
],
|
||||
'item_id' => 'required|integer|min:1',
|
||||
'item_type' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::in(['profile', 'status'])
|
||||
]
|
||||
]);
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
$action = $request->input('action');
|
||||
$item_id = $request->input('item_id');
|
||||
$item_type = $request->input('item_type');
|
||||
public function modAction(Request $request)
|
||||
{
|
||||
abort_unless(Auth::user()->is_admin, 400);
|
||||
$this->validate($request, [
|
||||
'action' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::in([
|
||||
'addcw',
|
||||
'remcw',
|
||||
'unlist',
|
||||
'spammer',
|
||||
]),
|
||||
],
|
||||
'item_id' => 'required|integer|min:1',
|
||||
'item_type' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::in(['profile', 'status']),
|
||||
],
|
||||
]);
|
||||
|
||||
$status = Status::findOrFail($item_id);
|
||||
$author = User::whereProfileId($status->profile_id)->first();
|
||||
abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
|
||||
$action = $request->input('action');
|
||||
$item_id = $request->input('item_id');
|
||||
$item_type = $request->input('item_type');
|
||||
|
||||
switch($action) {
|
||||
case 'addcw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\Status::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'cw',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$status = Status::findOrFail($item_id);
|
||||
$author = User::whereProfileId($status->profile_id)->first();
|
||||
abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
|
||||
|
||||
if($status->uri == null) {
|
||||
$media = $status->media;
|
||||
$ai = new AccountInterstitial;
|
||||
$ai->user_id = $status->profile->user_id;
|
||||
$ai->type = 'post.cw';
|
||||
$ai->view = 'account.moderation.post.cw';
|
||||
$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();
|
||||
switch ($action) {
|
||||
case 'addcw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\Status::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'cw',
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
$u = $status->profile->user;
|
||||
$u->has_interstitial = true;
|
||||
$u->save();
|
||||
}
|
||||
break;
|
||||
if ($status->uri == null) {
|
||||
$media = $status->media;
|
||||
$ai = new AccountInterstitial;
|
||||
$ai->user_id = $status->profile->user_id;
|
||||
$ai->type = 'post.cw';
|
||||
$ai->view = 'account.moderation.post.cw';
|
||||
$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();
|
||||
|
||||
case 'remcw':
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\Status::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'remove_cw',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
if($status->uri == null) {
|
||||
$ai = AccountInterstitial::whereUserId($status->profile->user_id)
|
||||
->whereType('post.cw')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->first();
|
||||
$ai->delete();
|
||||
}
|
||||
break;
|
||||
$u = $status->profile->user;
|
||||
$u->has_interstitial = true;
|
||||
$u->save();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
$status->scope = $status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
PublicTimelineService::del($status->id);
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\Status::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'unlist',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
case 'remcw':
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\Status::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'remove_cw',
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
if ($status->uri == null) {
|
||||
$ai = AccountInterstitial::whereUserId($status->profile->user_id)
|
||||
->whereType('post.cw')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->first();
|
||||
$ai->delete();
|
||||
}
|
||||
break;
|
||||
|
||||
if($status->uri == null) {
|
||||
$media = $status->media;
|
||||
$ai = new AccountInterstitial;
|
||||
$ai->user_id = $status->profile->user_id;
|
||||
$ai->type = 'post.unlist';
|
||||
$ai->view = 'account.moderation.post.unlist';
|
||||
$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();
|
||||
case 'unlist':
|
||||
$status->scope = $status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
PublicTimelineService::del($status->id);
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\Status::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'unlist',
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
$u = $status->profile->user;
|
||||
$u->has_interstitial = true;
|
||||
$u->save();
|
||||
}
|
||||
break;
|
||||
if ($status->uri == null) {
|
||||
$media = $status->media;
|
||||
$ai = new AccountInterstitial;
|
||||
$ai->user_id = $status->profile->user_id;
|
||||
$ai->type = 'post.unlist';
|
||||
$ai->view = 'account.moderation.post.unlist';
|
||||
$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();
|
||||
|
||||
case 'spammer':
|
||||
HandleSpammerPipeline::dispatch($status->profile);
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\User::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'spammer',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
break;
|
||||
}
|
||||
$u = $status->profile->user;
|
||||
$u->has_interstitial = true;
|
||||
$u->save();
|
||||
}
|
||||
break;
|
||||
|
||||
StatusService::del($status->id, true);
|
||||
return ['msg' => 200];
|
||||
}
|
||||
case 'spammer':
|
||||
HandleSpammerPipeline::dispatch($status->profile);
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\User::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'spammer',
|
||||
'message' => 'Success!',
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
break;
|
||||
}
|
||||
|
||||
public function composePost(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
StatusService::del($status->id, true);
|
||||
|
||||
public function bookmarks(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$res = Bookmark::whereProfileId($pid)
|
||||
->orderByDesc('created_at')
|
||||
->simplePaginate(10)
|
||||
->map(function($bookmark) use($pid) {
|
||||
$status = StatusService::get($bookmark->status_id, false);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['bookmarked_at'] = str_replace('+00:00', 'Z', $bookmark->created_at->format(DATE_RFC3339_EXTENDED));
|
||||
return ['msg' => 200];
|
||||
}
|
||||
|
||||
if($status) {
|
||||
BookmarkService::add($pid, $status['id']);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($bookmark) {
|
||||
return $bookmark && isset($bookmark['id']);
|
||||
})
|
||||
->values();
|
||||
public function composePost(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
public function bookmarks(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$res = Bookmark::whereProfileId($pid)
|
||||
->orderByDesc('created_at')
|
||||
->simplePaginate(10)
|
||||
->map(function ($bookmark) use ($pid) {
|
||||
$status = StatusService::get($bookmark->status_id, false);
|
||||
if (! $status) {
|
||||
return false;
|
||||
}
|
||||
$status['bookmarked_at'] = str_replace('+00:00', 'Z', $bookmark->created_at->format(DATE_RFC3339_EXTENDED));
|
||||
|
||||
public function accountStatuses(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'only_media' => 'nullable',
|
||||
'pinned' => 'nullable',
|
||||
'exclude_replies' => 'nullable',
|
||||
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|min:1|max:24'
|
||||
]);
|
||||
if ($status) {
|
||||
BookmarkService::add($pid, $status['id']);
|
||||
}
|
||||
|
||||
$profile = Profile::whereNull('status')->findOrFail($id);
|
||||
return $status;
|
||||
})
|
||||
->filter(function ($bookmark) {
|
||||
return $bookmark && isset($bookmark['id']);
|
||||
})
|
||||
->values();
|
||||
|
||||
$limit = $request->limit ?? 9;
|
||||
$max_id = $request->max_id;
|
||||
$min_id = $request->min_id;
|
||||
$scope = $request->only_media == true ?
|
||||
['photo', 'photo:album', 'video', 'video:album'] :
|
||||
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
if($profile->is_private) {
|
||||
if(!Auth::check()) {
|
||||
return response()->json([]);
|
||||
}
|
||||
$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'] : [];
|
||||
} 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'];
|
||||
} else {
|
||||
$visibility = ['public', 'unlisted'];
|
||||
}
|
||||
}
|
||||
public function accountStatuses(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'only_media' => 'nullable',
|
||||
'pinned' => 'nullable',
|
||||
'exclude_replies' => 'nullable',
|
||||
'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
|
||||
'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
|
||||
'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|min:1|max:24',
|
||||
]);
|
||||
|
||||
$dir = $min_id ? '>' : '<';
|
||||
$id = $min_id ?? $max_id;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'reblogs_count',
|
||||
'scope',
|
||||
'local',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereProfileId($profile->id)
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('visibility', $visibility)
|
||||
->latest()
|
||||
->limit($limit)
|
||||
->get();
|
||||
$profile = Profile::whereNull('status')->findOrFail($id);
|
||||
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
$limit = $request->limit ?? 9;
|
||||
$max_id = $request->max_id;
|
||||
$min_id = $request->min_id;
|
||||
$scope = $request->only_media == true ?
|
||||
['photo', 'photo:album', 'video', 'video:album'] :
|
||||
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
if ($profile->is_private) {
|
||||
if (! Auth::check()) {
|
||||
return response()->json([]);
|
||||
}
|
||||
$pid = Auth::user()->profile->id;
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
|
||||
public function remoteProfile(Request $request, $id)
|
||||
{
|
||||
return redirect('/i/web/profile/' . $id);
|
||||
}
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
$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');
|
||||
|
||||
public function remoteStatus(Request $request, $profileId, $statusId)
|
||||
{
|
||||
return redirect('/i/web/post/' . $statusId);
|
||||
}
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
$visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
|
||||
} else {
|
||||
$visibility = ['public', 'unlisted'];
|
||||
}
|
||||
}
|
||||
|
||||
public function requestEmailVerification(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$exists = Redis::sismember('email:manual', $pid);
|
||||
return view('account.email.request_verification', compact('exists'));
|
||||
}
|
||||
$dir = $min_id ? '>' : '<';
|
||||
$id = $min_id ?? $max_id;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'reblogs_count',
|
||||
'scope',
|
||||
'local',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereProfileId($profile->id)
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('visibility', $visibility)
|
||||
->latest()
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
public function requestEmailVerificationStore(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
Redis::sadd('email:manual', $pid);
|
||||
return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']);
|
||||
}
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer);
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function remoteProfile(Request $request, $id)
|
||||
{
|
||||
return redirect('/i/web/profile/'.$id);
|
||||
}
|
||||
|
||||
public function remoteStatus(Request $request, $profileId, $statusId)
|
||||
{
|
||||
return redirect('/i/web/post/'.$statusId);
|
||||
}
|
||||
|
||||
public function requestEmailVerification(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$exists = Redis::sismember('email:manual', $pid);
|
||||
|
||||
return view('account.email.request_verification', compact('exists'));
|
||||
}
|
||||
|
||||
public function requestEmailVerificationStore(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
Redis::sadd('email:manual', $pid);
|
||||
|
||||
return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,44 +2,43 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Http\Resources\DirectoryProfile;
|
||||
use App\Profile;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LandingController extends Controller
|
||||
{
|
||||
public function directoryRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
if ($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config_cache('instance.landing.show_directory') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function exploreRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
if ($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config_cache('instance.landing.show_explore') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_explore') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function getDirectoryApi(Request $request)
|
||||
{
|
||||
abort_if(config_cache('instance.landing.show_directory') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return DirectoryProfile::collection(
|
||||
Profile::whereNull('domain')
|
||||
->whereIsSuggestable(true)
|
||||
->orderByDesc('updated_at')
|
||||
->cursorPaginate(20)
|
||||
);
|
||||
return DirectoryProfile::collection(
|
||||
Profile::whereNull('domain')
|
||||
->whereIsSuggestable(true)
|
||||
->orderByDesc('updated_at')
|
||||
->cursorPaginate(20)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,30 +2,31 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Media;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
//return view('settings.drive.index');
|
||||
}
|
||||
public function index(Request $request)
|
||||
{
|
||||
//return view('settings.drive.index');
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function composeUpdate(Request $request, $id)
|
||||
{
|
||||
public function composeUpdate(Request $request, $id)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
}
|
||||
|
||||
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
|
||||
{
|
||||
abort_if(!config_cache('pixelfed.cloud_storage'), 404);
|
||||
$path = 'public/m/_v2/' . $pid . '/' . $mhash . '/' . $uhash . '/' . $f;
|
||||
$media = Media::whereProfileId($pid)
|
||||
->whereMediaPath($path)
|
||||
->whereNotNull('cdn_url')
|
||||
->firstOrFail();
|
||||
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
|
||||
{
|
||||
abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404);
|
||||
$path = 'public/m/_v2/'.$pid.'/'.$mhash.'/'.$uhash.'/'.$f;
|
||||
$media = Media::whereProfileId($pid)
|
||||
->whereMediaPath($path)
|
||||
->whereNotNull('cdn_url')
|
||||
->firstOrFail();
|
||||
|
||||
return redirect()->away($media->cdn_url);
|
||||
}
|
||||
return redirect()->away($media->cdn_url);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue