mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-02-03 10:20:46 +00:00
Merge branch 'main' of https://github.com/eufelipemateus/pixelfed into main
This commit is contained in:
commit
cd56fdb841
421 changed files with 33899 additions and 11256 deletions
|
@ -7,7 +7,7 @@ jobs:
|
|||
build:
|
||||
docker:
|
||||
# Specify the version you desire here
|
||||
- image: cimg/php:8.1.12
|
||||
- image: cimg/php:8.2.5
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
@ -29,14 +29,14 @@ jobs:
|
|||
- restore_cache:
|
||||
keys:
|
||||
# "composer.lock" can be used if it is committed to the repo
|
||||
- v1-dependencies-{{ checksum "composer.json" }}
|
||||
- v2-dependencies-{{ checksum "composer.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
- v2-dependencies-
|
||||
|
||||
- run: composer install -n --prefer-dist
|
||||
|
||||
- save_cache:
|
||||
key: composer-v1-{{ checksum "composer.lock" }}
|
||||
key: composer-v2-{{ checksum "composer.lock" }}
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
|
|
7
.ddev/commands/redis/redis-cli
Executable file
7
.ddev/commands/redis/redis-cli
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
#ddev-generated
|
||||
## Description: Run redis-cli inside the redis container
|
||||
## Usage: redis-cli [flags] [args]
|
||||
## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version"
|
||||
|
||||
redis-cli -p 6379 -h redis $@
|
32
.ddev/config.yaml
Normal file
32
.ddev/config.yaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
type: laravel
|
||||
docroot: public
|
||||
php_version: "8.1"
|
||||
webserver_type: nginx-fpm
|
||||
database:
|
||||
type: mariadb
|
||||
version: "10.4"
|
||||
disable_settings_management: true
|
||||
web_environment:
|
||||
- DB_CONNECTION=mysql
|
||||
- DB_HOST=ddev-pixelfed-db
|
||||
- DB_DATABASE=db
|
||||
- DB_USERNAME=db
|
||||
- DB_PASSWORD=db
|
||||
- REDIS_HOST=ddev-pixelfed-redis
|
||||
- MAIL_DRIVER=smtp
|
||||
- MAIL_HOST=localhost
|
||||
- MAIL_PORT=1025
|
||||
- MAIL_USERNAME=null
|
||||
- MAIL_PASSWORD=null
|
||||
- MAIL_ENCRYPTION=null
|
||||
- APP_KEY=placeholder
|
||||
- APP_NAME=PixelfedTest
|
||||
- APP_ENV=local
|
||||
- APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
|
||||
- APP_DEBUG=true
|
||||
- APP_URL=https://pixelfed.ddev.site
|
||||
- APP_DOMAIN=pixelfed.ddev.site
|
||||
- ADMIN_DOMAIN=pixelfed.ddev.site
|
||||
- SESSION_DOMAIN=pixelfed.ddev.site
|
||||
- "TRUST_PROXIES=*"
|
||||
- LOG_CHANNEL=stack
|
14
.ddev/docker-compose.redis.yaml
Normal file
14
.ddev/docker-compose.redis.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
#ddev-generated
|
||||
version: '3.6'
|
||||
services:
|
||||
redis:
|
||||
container_name: ddev-${DDEV_SITENAME}-redis
|
||||
image: redis:6
|
||||
# These labels ensure this service is discoverable by ddev.
|
||||
labels:
|
||||
com.ddev.site-name: ${DDEV_SITENAME}
|
||||
com.ddev.approot: $DDEV_APPROOT
|
||||
volumes:
|
||||
- ".:/mnt/ddev_config"
|
||||
- "./redis:/usr/local/etc/redis"
|
||||
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
8
.ddev/redis/redis.conf
Normal file
8
.ddev/redis/redis.conf
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Redis configuration.
|
||||
# #ddev-generated
|
||||
# Example configuration files for reference:
|
||||
# http://download.redis.io/redis-stable/redis.conf
|
||||
# http://download.redis.io/redis-stable/sentinel.conf
|
||||
|
||||
maxmemory 2048mb
|
||||
maxmemory-policy allkeys-lfu
|
|
@ -2,6 +2,7 @@ root = true
|
|||
|
||||
[*]
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
|
|
@ -101,7 +101,6 @@ NODEINFO=true
|
|||
WEBFINGER=true
|
||||
|
||||
## S3
|
||||
FILESYSTEM_DRIVER=local
|
||||
FILESYSTEM_CLOUD=s3
|
||||
PF_ENABLE_CLOUD=false
|
||||
#AWS_ACCESS_KEY_ID=
|
||||
|
@ -127,8 +126,8 @@ LOG_CHANNEL=stderr
|
|||
## Image
|
||||
IMAGE_DRIVER=imagick
|
||||
|
||||
## Broadcasting
|
||||
BROADCAST_DRIVER=log # log driver for local development
|
||||
## Broadcasting: log driver for local development
|
||||
BROADCAST_DRIVER=log
|
||||
|
||||
## Cache
|
||||
CACHE_DRIVER=redis
|
||||
|
|
|
@ -68,7 +68,6 @@ MAIL_FROM_NAME="Pixelfed"
|
|||
|
||||
## S3 Configuration (Post-Installer)
|
||||
PF_ENABLE_CLOUD=false
|
||||
FILESYSTEM_DRIVER=local
|
||||
FILESYSTEM_CLOUD=s3
|
||||
#AWS_ACCESS_KEY_ID=
|
||||
#AWS_SECRET_ACCESS_KEY=
|
||||
|
|
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
target-branch: "staging"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
versioning-strategy: lockfile-only
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
target-branch: "staging"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
versioning-strategy: lockfile-only
|
125
.github/workflows/build-docker.yml
vendored
Normal file
125
.github/workflows/build-docker.yml
vendored
Normal file
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
name: Build Docker image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-docker.yml
|
||||
- contrib/docker/Dockerfile.apache
|
||||
- contrib/docker/Dockerfile.fpm
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-docker-apache:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.0.0
|
||||
with:
|
||||
dockerfile: contrib/docker/Dockerfile.apache
|
||||
failure-threshold: error
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
secrets: inherit
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Fetch tags
|
||||
uses: docker/metadata-action@v4
|
||||
secrets: inherit
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-apache
|
||||
tags: |
|
||||
type=edge,branch=dev
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: contrib/docker/Dockerfile.apache
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-docker-fpm:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.0.0
|
||||
with:
|
||||
dockerfile: contrib/docker/Dockerfile.fpm
|
||||
failure-threshold: error
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
secrets: inherit
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Fetch tags
|
||||
uses: docker/metadata-action@v4
|
||||
secrets: inherit
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||
flavor: |
|
||||
suffix=-fpm
|
||||
tags: |
|
||||
type=edge,branch=dev
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: contrib/docker/Dockerfile.fpm
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
1
.node-version
Normal file
1
.node-version
Normal file
|
@ -0,0 +1 @@
|
|||
v14.20.1
|
171
CHANGELOG.md
171
CHANGELOG.md
|
@ -1,11 +1,67 @@
|
|||
# Release Notes
|
||||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.4...dev)
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.6...dev)
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.6 (2023-05-03)](https://github.com/pixelfed/pixelfed/compare/v0.11.5...v0.11.6)
|
||||
|
||||
### Added
|
||||
- Add php 8.2 support. Bump laravel version, v9 => v10 ([fb4ac4eb](https://github.com/pixelfed/pixelfed/commit/fb4ac4eb))
|
||||
- New media:fix-nonlocal-driver command. Fixes s3 media created with invalid FILESYSTEM_DRIVER=s3 configuration ([672cccd4](https://github.com/pixelfed/pixelfed/commit/672cccd4))
|
||||
- New landing page design ([09c0032b](https://github.com/pixelfed/pixelfed/commit/09c0032b))
|
||||
- Add cloud ip bans to BouncerService (disabled by default) ([50ab2e20](https://github.com/pixelfed/pixelfed/commit/50ab2e20))
|
||||
- Redesigned Admin Dashboard Reports/Moderation ([c6cc6327](https://github.com/pixelfed/pixelfed/commit/c6cc6327))
|
||||
|
||||
### Fixes
|
||||
- Fixed `violates check constraint "statuses_visibility_check"` bug affecting postgres instances + various api endpoints ([79b6a17e](https://github.com/pixelfed/pixelfed/commit/79b6a17e))
|
||||
- Fixed duplicate hashtags on postgres ([64059cb4](https://github.com/pixelfed/pixelfed/commit/64059cb4))
|
||||
- Fixed custom emoji domain search on postgres. Closes #4333 ([3dac45f3](https://github.com/pixelfed/pixelfed/commit/3dac45f3))
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Controller, fix blocking remote accounts. Closes #4256 ([8e71e0c0](https://github.com/pixelfed/pixelfed/commit/8e71e0c0))
|
||||
- Update ComposeController, fix postgres location search. Closes #4242 and #4239 ([64a4a006](https://github.com/pixelfed/pixelfed/commit/64a4a006))
|
||||
- Update app.js, add title attribute to iframe embeds to comply with accessibility requirements ([4d72b9e3](https://github.com/pixelfed/pixelfed/commit/4d72b9e3))
|
||||
- Update MediaPathService, fix story path ([aebbad96](https://github.com/pixelfed/pixelfed/commit/aebbad96))
|
||||
- Update Story v1.1 api endpoints ([855e9626](https://github.com/pixelfed/pixelfed/commit/855e9626))
|
||||
- Update ApiV1Controller, filter mute/blocks on statuses/context and statuses/replies endpoints ([73aa01e8](https://github.com/pixelfed/pixelfed/commit/73aa01e8))
|
||||
- Update filesystems, store all files as public by default and add default permissions. Fixes #4273, #4275. Closes #3825 ([22da2647](https://github.com/pixelfed/pixelfed/commit/22da2647))
|
||||
- Update Profile model, fix avatar url path generation. Fixes #4041, Fixes #4031, Fixes #3523 ([28bf8649](https://github.com/pixelfed/pixelfed/commit/28bf8649))
|
||||
- Update filesystem config, change FILESYSTEM_DRIVER env variable to DANGEROUSLY_SET_FILESYSTEM_DRIVER and remove from default env configs. Changing the default filesystem should be avoided, use FILESYSTEM_CLOUD for s3 support, otherwise you can break things ([573c88d7](https://github.com/pixelfed/pixelfed/commit/573c88d7))
|
||||
- Update MediaS3GarbageCollector, fix handle ([2eee36cf](https://github.com/pixelfed/pixelfed/commit/2eee36cf))
|
||||
- Update StatusController, allow users to delete replies to posts ([738925c2](https://github.com/pixelfed/pixelfed/commit/738925c2))
|
||||
- Update admin autospam/report email templates, remove image previews ([76be49ac](https://github.com/pixelfed/pixelfed/commit/76be49ac))
|
||||
- Update LandingService, enable landing directory/explore feed by default and move configuration to config/instance.php file ([780f2507](https://github.com/pixelfed/pixelfed/commit/780f2507))
|
||||
- Update ImageOptimizePipeline, improve support for disabling image optimizations ([e76289e4](https://github.com/pixelfed/pixelfed/commit/e76289e4))
|
||||
- Update LandingController, fix config variable names ([b716926b](https://github.com/pixelfed/pixelfed/commit/b716926b))
|
||||
- Update Privacy Settings, add Directory setting ([634c15e4](https://github.com/pixelfed/pixelfed/commit/634c15e4))
|
||||
- Update site config ([6d59dc8e](https://github.com/pixelfed/pixelfed/commit/6d59dc8e))
|
||||
- Update db:raw queries to support laravel v10 ([849e5103](https://github.com/pixelfed/pixelfed/commit/849e5103))
|
||||
- Update RegisterController, store client ip during registration ([d4c967de](https://github.com/pixelfed/pixelfed/commit/d4c967de))
|
||||
- Update ApiV1Controller, fix account blocks. Closes #4304 ([98739139](https://github.com/pixelfed/pixelfed/commit/98739139))
|
||||
- Update RegisterController, improve max_users calculation and add kb page to redirect to if conditions are met ([1bbee6d0](https://github.com/pixelfed/pixelfed/commit/1bbee6d0))
|
||||
- Update SecuritySettings, remove imagick depdency for 2FA qr code generation image ([506f95c6](https://github.com/pixelfed/pixelfed/commit/506f95c6))
|
||||
- Update 2fa checkpoint view design ([86c472ac](https://github.com/pixelfed/pixelfed/commit/86c472ac))
|
||||
- Update sudo mode checkpoint view design ([091e0b2c](https://github.com/pixelfed/pixelfed/commit/091e0b2c))
|
||||
- Update ForgotPasswordController, add captcha support, improve security and a new redesigned view ([f6e7ff64](https://github.com/pixelfed/pixelfed/commit/f6e7ff64))
|
||||
- Update ResetPasswordController, add captcha support, improve security and a new redesigned view ([0ab5b96a](https://github.com/pixelfed/pixelfed/commit/0ab5b96a))
|
||||
- Update Inbox, remove handleCreateActivity logic that rejected posts from accounts without followers ([a93a3efd](https://github.com/pixelfed/pixelfed/commit/a93a3efd))
|
||||
- Update ApiV1Controller and DiscoverController, fix postgres hashtag search ([055aa6b3](https://github.com/pixelfed/pixelfed/commit/055aa6b3))
|
||||
- Update StatusTagsPipeline, deduplicate hashtags on postgres ([867cbc75](https://github.com/pixelfed/pixelfed/commit/867cbc75))
|
||||
- Update SearchApiV2Service, fix postgres hashtag search and prepend wildcard operator to improve results ([6e20d0a6](https://github.com/pixelfed/pixelfed/commit/6e20d0a6))
|
||||
|
||||
## [v0.11.5 (2023-03-25)](https://github.com/pixelfed/pixelfed/compare/v0.11.4...v0.11.5)
|
||||
|
||||
### New Features
|
||||
- Mobile App Registration ([#3829](https://github.com/pixelfed/pixelfed/pull/3829))
|
||||
- Portfolios ([#3705](https://github.com/pixelfed/pixelfed/pull/3705))
|
||||
- Server Directory ([#3762](https://github.com/pixelfed/pixelfed/pull/3762))
|
||||
- Manually verify email address (php artisan user:verifyemail) ([682f5f0f](https://github.com/pixelfed/pixelfed/commit/682f5f0f))
|
||||
- Manually generate in-app registration confirmation links (php artisan user:app-magic-link) ([73eb9e36](https://github.com/pixelfed/pixelfed/commit/73eb9e36))
|
||||
- Optional home feed caching ([3328b367](https://github.com/pixelfed/pixelfed/commit/3328b367))
|
||||
- Admin Invites ([b73ca9a1](https://github.com/pixelfed/pixelfed/commit/b73ca9a1))
|
||||
- Hashtag administration ([84872311](https://github.com/pixelfed/pixelfed/commit/84872311))
|
||||
- Admin report email notifications ([4e1d0ed5](https://github.com/pixelfed/pixelfed/commit/4e1d0ed5))
|
||||
- Add Licenses help page, fixes #4238 ([3c712a70](https://github.com/pixelfed/pixelfed/commit/3c712a70))
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
|
||||
|
@ -26,8 +82,117 @@
|
|||
- Update landing view, add `app.name` and `app.short_description` for better customizability ([bda9d16b](https://github.com/pixelfed/pixelfed/commit/bda9d16b))
|
||||
- Update Profile, fix avatarUrl paths. Fixes #3559 #3634 ([989e4249](https://github.com/pixelfed/pixelfed/commit/989e4249))
|
||||
- Update InboxPipeline, bump request timeout from 5s to 60s ([bb120019](https://github.com/pixelfed/pixelfed/commit/bb120019))
|
||||
- Update web routes, fix missing hom route ([a9f4ddfc](https://github.com/pixelfed/pixelfed/commit/a9f4ddfc))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
- Update web routes, fix missing home route ([a9f4ddfc](https://github.com/pixelfed/pixelfed/commit/a9f4ddfc))
|
||||
- Allow forceHttps to be disabled, fixes #3710 ([a31bdec7](https://github.com/pixelfed/pixelfed/commit/a31bdec7))
|
||||
- Update MediaStorageService, fix size check bug ([319f0ba5](https://github.com/pixelfed/pixelfed/commit/319f0ba5))
|
||||
- Update AvatarSync, fix sync skipping recently fetched avatars by setting last_fetched_at to null before refetching ([a83fc798](https://github.com/pixelfed/pixelfed/commit/a83fc798))
|
||||
- Refactor AvatarStorage to support migrating avatars to cloud storage, fix remote avatar refetching and merge AvatarSync commands and add deprecation notice to avatar:sync command ([223aea47](https://github.com/pixelfed/pixelfed/commit/223aea47))
|
||||
- Update AvatarStorage, improve overview calculations ([733b9fd0](https://github.com/pixelfed/pixelfed/commit/733b9fd0))
|
||||
- Update filesystem config, fix DO Spaces root default ([720b6eb3](https://github.com/pixelfed/pixelfed/commit/720b6eb3))
|
||||
- Update Avatar pipeline, fix cloud storage media_path ([02edd19d](https://github.com/pixelfed/pixelfed/commit/02edd19d))
|
||||
- Update FederationController, add instance actor profile to webfinger ([6e3c8097](https://github.com/pixelfed/pixelfed/commit/6e3c8097))
|
||||
- Update MediaService, add summary attribute for better alt text federation ([a12712cc](https://github.com/pixelfed/pixelfed/commit/a12712cc))
|
||||
- Update AvatarObserver, fix cloud delete bug by checking if cloud storage is enabled ([9f7672f5](https://github.com/pixelfed/pixelfed/commit/9f7672f5))
|
||||
- Update DeleteAccountPipeline, dispatch on low queue ([6eabe07c](https://github.com/pixelfed/pixelfed/commit/6eabe07c))
|
||||
- Update DeleteAccountPipeline, handle flysystem v3 changes by checking files exist before attempting to delete ([23e2998f](https://github.com/pixelfed/pixelfed/commit/23e2998f))
|
||||
- Update FollowerService, use redis sorted sets for follower relations ([356cc277](https://github.com/pixelfed/pixelfed/commit/356cc277))
|
||||
- Update FollowerService, use redis sorted sets for following relations ([f46b01af](https://github.com/pixelfed/pixelfed/commit/f46b01af))
|
||||
- Update PublicApiController, refactor follower/following api endpoints to consume FollowerService instead of querying database ([b39f91b4](https://github.com/pixelfed/pixelfed/commit/b39f91b4))
|
||||
- Update follower/following profile layout, optimized for mobile devices and use FollowerService ([78a5575d](https://github.com/pixelfed/pixelfed/commit/78a5575d))
|
||||
- Update sidebar menu, when clicking on the active feed/timeline buttons force a reload and scroll to top of feed ([78a5575d](https://github.com/pixelfed/pixelfed/commit/78a5575d))
|
||||
- Update InboxPipeline, increase timeout from 60s to 300s ([d1b888b5](https://github.com/pixelfed/pixelfed/commit/d1b888b5))
|
||||
- Update backup config, fixes #3793, #3920, #3931 ([b0c4cc30](https://github.com/pixelfed/pixelfed/commit/b0c4cc30))
|
||||
- Update FederationController, add two new queues (follow, shared) to prioritize follow request handling ([8ba33864](https://github.com/pixelfed/pixelfed/commit/8ba33864))
|
||||
- Dispatch follow accept/reject pipeline jobs to follow queue ([aaed2bf6](https://github.com/pixelfed/pixelfed/commit/aaed2bf6))
|
||||
- Update MediaStorageService, improve support for pleroma .blob avatars ([66226658](https://github.com/pixelfed/pixelfed/commit/66226658))
|
||||
- Update ApiV1Controller, remove min avatar size limit, fixes #3715 ([2b0db812](https://github.com/pixelfed/pixelfed/commit/2b0db812))
|
||||
- Update InboxPipeline, add inbox job queue and separate http sig validation from activity handling ([e6c1604d](https://github.com/pixelfed/pixelfed/commit/e6c1604d))
|
||||
- Update InboxPipeline, dispatch Follow/Accept Follow jobs to follow queue ([f62d2494](https://github.com/pixelfed/pixelfed/commit/f62d2494))
|
||||
- Add MediaS3GarbageCollector command to clear local media after uploaded to S3 disks after 12 hours ([b8c3f153](https://github.com/pixelfed/pixelfed/commit/b8c3f153))
|
||||
- Update MediaS3GarbageCollector command, disable logging by default and optimize huge invocations ([a14af93b](https://github.com/pixelfed/pixelfed/commit/a14af93b))
|
||||
- Update MediaStorageService, clear MediaService and StatusService caches after localToCloud ([de56b0f0](https://github.com/pixelfed/pixelfed/commit/de56b0f0))
|
||||
- Add CloudMediaMigrate command to migrate older local media to cloud storage ([382d00d9](https://github.com/pixelfed/pixelfed/commit/382d00d9))
|
||||
- Update MediaS3GarbageCollector command, handle thumbnail deletion ([95bbcc38](https://github.com/pixelfed/pixelfed/commit/95bbcc38))
|
||||
- Update StatusReplyPipeline, remove expensive reply count re-calculation query ([a2f8aad1](https://github.com/pixelfed/pixelfed/commit/a2f8aad1))
|
||||
- Update CommentPipeline, remove expensive reply count re-calculation query ([b457a446](https://github.com/pixelfed/pixelfed/commit/b457a446))
|
||||
- Update FederationController, improve inbox/sharedInbox delete handling ([2180a2de](https://github.com/pixelfed/pixelfed/commit/2180a2de))
|
||||
- Update HashtagController, improve trending hashtag endpoint ([4873c7dd](https://github.com/pixelfed/pixelfed/commit/4873c7dd))
|
||||
- Fix CustomEmoji, properly handle shortcode updates and delete old copy in case the extension changes ([bc29073a](https://github.com/pixelfed/pixelfed/commit/bc29073a))
|
||||
- Update reply pipelines, restore reply_count logic ([0d780ffb](https://github.com/pixelfed/pixelfed/commit/0d780ffb))
|
||||
- Update StatusTagsPipeline, reject if `type` not set ([91085c45](https://github.com/pixelfed/pixelfed/commit/91085c45))
|
||||
- Update ReplyPipelines, use more efficent reply count calculation ([d4dfa95c](https://github.com/pixelfed/pixelfed/commit/d4dfa95c))
|
||||
- Update StatusDelete pipeline, dispatch async ([257c0949](https://github.com/pixelfed/pixelfed/commit/257c0949))
|
||||
- Update lexer/extractor to handle banned hashtags ([909a8a5a](https://github.com/pixelfed/pixelfed/commit/909a8a5a))
|
||||
- Update FederationController, fix double lock bug ([9fcccca9](https://github.com/pixelfed/pixelfed/commit/9fcccca9))
|
||||
- Update AdminInvite component, fix email regex ([2aea77d3](https://github.com/pixelfed/pixelfed/commit/2aea77d3))
|
||||
- Update database config, use single transaction and skip lock tables for mysql dump ([936f1e7a](https://github.com/pixelfed/pixelfed/commit/936f1e7a))
|
||||
- Update database config, add sticky flag https://laravel.com/docs/9.x/database#the-sticky-option ([10b65980](https://github.com/pixelfed/pixelfed/commit/10b65980))
|
||||
- Update profile audience to filter blocked instances ([e0c3dae3](https://github.com/pixelfed/pixelfed/commit/e0c3dae3))
|
||||
- Update SearchApiV2Service, improve query performance ([4d1f2811](https://github.com/pixelfed/pixelfed/commit/4d1f2811))
|
||||
- Update InstanceService, improve unlisted/banned network post filtering ([a0da6ec3](https://github.com/pixelfed/pixelfed/commit/a0da6ec3))
|
||||
- Update ApiV1DotController, fix inAppRegistrationConfirm logic ([6cfbedd9](https://github.com/pixelfed/pixelfed/commit/6cfbedd9))
|
||||
- Update ApiV1Controller, allow description (alt text) updates after status is published ([869c3ed1](https://github.com/pixelfed/pixelfed/commit/869c3ed1))
|
||||
- Update AdminApiController, fix postgres support ([84fb59d0](https://github.com/pixelfed/pixelfed/commit/84fb59d0))
|
||||
- Update StatusReplyPipeline, fix comment counts ([164aa577](https://github.com/pixelfed/pixelfed/commit/164aa577))
|
||||
- Update ComposeModal, add Alt Text button to caption screen ([4db48188](https://github.com/pixelfed/pixelfed/commit/4db48188))
|
||||
- Update AccountService, fix actor cache invalidation ([498b46f7](https://github.com/pixelfed/pixelfed/commit/498b46f7))
|
||||
- Update SharePipeline, fix share handling and notification generation ([83e1e203](https://github.com/pixelfed/pixelfed/commit/83e1e203))
|
||||
- Update SharePipeline, fix ReblogService and undo handling ([016c6e41](https://github.com/pixelfed/pixelfed/commit/016c6e41))
|
||||
- Update AP Helpers, fix media validation bug that would reject media with alttext/name longer than 255 chars and store remote alt text if set ([a7f58349](https://github.com/pixelfed/pixelfed/commit/a7f58349))
|
||||
- Update MentionPipeline, store non-local mentions ([17149230](https://github.com/pixelfed/pixelfed/commit/17149230))
|
||||
- Update Like model, increase rate limit to 500 likes per day ([ab7676f9](https://github.com/pixelfed/pixelfed/commit/ab7676f9))
|
||||
- Update ComposeController, fix validation issue ([80e6a5a9](https://github.com/pixelfed/pixelfed/commit/80e6a5a9))
|
||||
- Update reply view, fix visibility filtering ([d419af4b](https://github.com/pixelfed/pixelfed/commit/d419af4b))
|
||||
- Update AP helpers, ingest attachments in replies ([c504e643](https://github.com/pixelfed/pixelfed/commit/c504e643))
|
||||
- Update Media model, use cloud filesystem url if enabled instead of cdn_url to easily update S3 media urls ([e6bc57d7](https://github.com/pixelfed/pixelfed/commit/e6bc57d7))
|
||||
- Update ap helpers, fix unset media name bug ([083f506b](https://github.com/pixelfed/pixelfed/commit/083f506b))
|
||||
- Update MediaStorageService, fix improper path ([964c62da](https://github.com/pixelfed/pixelfed/commit/964c62da))
|
||||
- Update ApiV1Controller, fix account statuses and bookmark pagination ([9f66d6b6](https://github.com/pixelfed/pixelfed/commit/9f66d6b6))
|
||||
- Update SearchApiV2Service, improve account search results ([f6a588f9](https://github.com/pixelfed/pixelfed/commit/f6a588f9))
|
||||
- Update profile model, improve avatarUrl fallback ([620ee826](https://github.com/pixelfed/pixelfed/commit/620ee826))
|
||||
- Update ApiV1Controller, use cursor pagination for favourited_by and reblogged_by endpoints ([e1c7e701](https://github.com/pixelfed/pixelfed/commit/e1c7e701))
|
||||
- Update ApiV1Controller, fix favourited_by and reblogged_by follows attribute ([1a130f3e](https://github.com/pixelfed/pixelfed/commit/1a130f3e))
|
||||
- Update notifications component, improve UX with exponential retry and loading state ([937e6d07](https://github.com/pixelfed/pixelfed/commit/937e6d07))
|
||||
- Update likeModal and shareModal components, use new pagination logic and re-add Follow/Unfollow buttons ([b565ead6](https://github.com/pixelfed/pixelfed/commit/b565ead6))
|
||||
- Update profileFeed component, fix pagination ([7cf41628](https://github.com/pixelfed/pixelfed/commit/7cf41628))
|
||||
- Update ApiV1Controller, add BookmarkService logic to bookmark endpoints ([29b1af10](https://github.com/pixelfed/pixelfed/commit/29b1af10))
|
||||
- Update ApiV1Controller, filter conversations without last_status ([e8a6a8c7](https://github.com/pixelfed/pixelfed/commit/e8a6a8c7))
|
||||
- Update ApiV1Controller and BookmarkController, fix api differences and allow unbookmarking regardless of relationship ([e343061a](https://github.com/pixelfed/pixelfed/commit/e343061a))
|
||||
- Update ApiV1Controller, add pixelfed entity support to bookmarks endpoint ([94069db9](https://github.com/pixelfed/pixelfed/commit/94069db9))
|
||||
- Update PostReactions, reduce bookmark timeout to 2s from 5s ([a8094e6c](https://github.com/pixelfed/pixelfed/commit/a8094e6c))
|
||||
- Update CollectionController, fixes #3946 ([abd52f4d](https://github.com/pixelfed/pixelfed/commit/abd52f4d))
|
||||
- Update ComposeController, fix add to collection logic ([9f8957b9](https://github.com/pixelfed/pixelfed/commit/9f8957b9))
|
||||
- Update v1.1 api, add post moderation endpoint ([9bbd6dcd](https://github.com/pixelfed/pixelfed/commit/9bbd6dcd))
|
||||
- Update StatusService, on purge remove from NetworkTimelineService cache ([18940cb2](https://github.com/pixelfed/pixelfed/commit/18940cb2))
|
||||
- Update mute/block logic with admin defined limits and improved filtering to skip deleted accounts ([5b879f01](https://github.com/pixelfed/pixelfed/commit/5b879f01))
|
||||
- Update FollowPipeline, fix followers_count and following_count counters ([6153b620](https://github.com/pixelfed/pixelfed/commit/6153b620))
|
||||
- Update ApiV1Controller, fix media update. Fixes #4196 ([f3164650](https://github.com/pixelfed/pixelfed/commit/f3164650))
|
||||
- Update SearchApiV2Service, fix hashtag search. ([1992b5bc](https://github.com/pixelfed/pixelfed/commit/1992b5bc))
|
||||
- Update ApiV1Controller, allow optional mastodonMode on v2/search endpoint. ([f4a69631](https://github.com/pixelfed/pixelfed/commit/f4a69631))
|
||||
- Update ApiV1Controller, add cursor pagination and pagination link headers to account/{id}/followers and account/{id}/following endpoints with legacy support for `page=` simple pagination ([713aa5fd](https://github.com/pixelfed/pixelfed/commit/713aa5fd))
|
||||
- Update legacy Profile component to use new cursor pagination for following/follower modals ([7a1495e6](https://github.com/pixelfed/pixelfed/commit/7a1495e6))
|
||||
- Update ApiV1Controller, fix link header pagination in /api/v1/statuses/{id}/favourited_by ([adc82eca](https://github.com/pixelfed/pixelfed/commit/adc82eca))
|
||||
- Update ApiV1Controller, fix link header pagination in /api/v1/statuses/{id}/reblogged_by ([e346b675](https://github.com/pixelfed/pixelfed/commit/e346b675))
|
||||
- Update ApiV1Controller, fix following/follower entities, use masto schema by default and update components accordingly ([4716c280](https://github.com/pixelfed/pixelfed/commit/4716c280))
|
||||
- Update FollowerController, remove deprecated /i/follow endpoint ([4739d614](https://github.com/pixelfed/pixelfed/commit/4739d614))
|
||||
- Update queue config, set "after_commit" to true ([304ea956](https://github.com/pixelfed/pixelfed/commit/304ea956))
|
||||
- Update ApiV1Controller, fix home timeline bug ([a8ec8445](https://github.com/pixelfed/pixelfed/commit/a8ec8445))
|
||||
- Update ApiV1Controller, increase home timeline max limit to 100 to fix compatibility with mastoapi ([5cf9ba78](https://github.com/pixelfed/pixelfed/commit/5cf9ba78))
|
||||
- Update ApiV1Controller, preserve album order. Fixes #3708 ([deb26971](https://github.com/pixelfed/pixelfed/commit/deb26971))
|
||||
- Update site config endpoint ([f9be48d6](https://github.com/pixelfed/pixelfed/commit/f9be48d6))
|
||||
- Update Portfolios, add ActivityPub + RSS support, light mode, style customization and more ([5ad0d883](https://github.com/pixelfed/pixelfed/commit/5ad0d883))
|
||||
- Update atom feed, improve cache expiry and fix double encoding bug. Fixes #4121 ([467c9d75](https://github.com/pixelfed/pixelfed/commit/467c9d75))
|
||||
- Update email settings, add dangerzone middleware to prompt for password before you can change your email address. Fixes #4101 ([186ba7f0](https://github.com/pixelfed/pixelfed/commit/186ba7f0))
|
||||
- Update InboxPipelines, improve handling of missing signature validation headers ([419c0fb0](https://github.com/pixelfed/pixelfed/commit/419c0fb0))
|
||||
- Update admin instances dashboard ([ecfc0766](https://github.com/pixelfed/pixelfed/commit/ecfc0766))
|
||||
- Update ap helpers, fix album order bug by setting media order ([871f798c](https://github.com/pixelfed/pixelfed/commit/871f798c))
|
||||
- Update image pipeline, dispatch jobs to mmo queue and add "replace_id" param to v2/media endpoint to dispatch delayed MediaDeletePipeline job for original media id to improve media gc on supported clients ([5a67e9f9](https://github.com/pixelfed/pixelfed/commit/5a67e9f9))
|
||||
- Update admin instance management, improve filtering/sorting and add import/export support ([d5d9500d](https://github.com/pixelfed/pixelfed/commit/d5d9500d))
|
||||
- Update Post component, show state error when status account is null or missing ([e6dc6234](https://github.com/pixelfed/pixelfed/commit/e6dc6234))
|
||||
- Update private profile view, add rel=me support, hide avatar/bio when not logged in and add robots meta tag to block search engine indexing on private profiles ([ab4bb9a0](https://github.com/pixelfed/pixelfed/commit/ab4bb9a0))
|
||||
- Update settings, set maxlength on name and bio inputs. Fixes #4248 ([558700fc](https://github.com/pixelfed/pixelfed/commit/558700fc))
|
||||
- Update api routes, add post method support to /api/v1/accounts/update_credentials to properly handle binary form data (avatars). Fixes #4250 ([1ae19ea5](https://github.com/pixelfed/pixelfed/commit/1ae19ea5))
|
||||
- Update ApiV1Controller, improve timeline account hydration ([4e79c772](https://github.com/pixelfed/pixelfed/commit/4e79c772))
|
||||
|
||||
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)
|
||||
|
||||
|
|
|
@ -11,7 +11,10 @@ class AccountInterstitial extends Model
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['read_at', 'appeal_requested_at'];
|
||||
protected $casts = [
|
||||
'read_at' => 'datetime',
|
||||
'appeal_requested_at' => 'datetime'
|
||||
];
|
||||
|
||||
public const JSON_MESSAGE = 'Please use web browser to proceed.';
|
||||
|
||||
|
|
|
@ -6,7 +6,10 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Activity extends Model
|
||||
{
|
||||
protected $dates = ['processed_at'];
|
||||
protected $casts = [
|
||||
'processed_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected $fillable = ['data', 'to_id', 'from_id', 'object_type'];
|
||||
|
||||
public function toProfile()
|
||||
|
|
|
@ -14,13 +14,13 @@ class Avatar extends Model
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = [
|
||||
'deleted_at',
|
||||
'last_fetched_at',
|
||||
'last_processed_at'
|
||||
protected $casts = [
|
||||
'deleted_at' => 'datetime',
|
||||
'last_fetched_at' => 'datetime',
|
||||
'last_processed_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected $fillable = ['profile_id'];
|
||||
protected $guarded = [];
|
||||
|
||||
protected $visible = [
|
||||
'id',
|
||||
|
|
179
app/Console/Commands/AdminInviteCommand.php
Normal file
179
app/Console/Commands/AdminInviteCommand.php
Normal file
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\AdminInvite;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminInviteCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'admin:invite';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Create an invite link';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Pixelfed Admin Inviter');
|
||||
$this->line(' ');
|
||||
$this->info(' Manage user registration invite links');
|
||||
$this->line(' ');
|
||||
|
||||
$action = $this->choice(
|
||||
'Select an action',
|
||||
[
|
||||
'Create invite',
|
||||
'View invites',
|
||||
'Expire invite',
|
||||
'Cancel'
|
||||
],
|
||||
3
|
||||
);
|
||||
|
||||
switch($action) {
|
||||
case 'Create invite':
|
||||
return $this->create();
|
||||
break;
|
||||
|
||||
case 'View invites':
|
||||
return $this->view();
|
||||
break;
|
||||
|
||||
case 'Expire invite':
|
||||
return $this->expire();
|
||||
break;
|
||||
|
||||
case 'Cancel':
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function create()
|
||||
{
|
||||
$this->info('Create Invite');
|
||||
$this->line('=============');
|
||||
$this->info('Set an optional invite name (only visible to admins)');
|
||||
$name = $this->ask('Invite Name (optional)', 'Untitled Invite');
|
||||
|
||||
$this->info('Set an optional invite description (only visible to admins)');
|
||||
$description = $this->ask('Invite Description (optional)');
|
||||
|
||||
$this->info('Set an optional message to invitees (visible to all)');
|
||||
$message = $this->ask('Invite Message (optional)', 'You\'ve been invited to join');
|
||||
|
||||
$this->info('Set maximum # of invite uses, use 0 for unlimited');
|
||||
$max_uses = $this->ask('Max uses', 1);
|
||||
|
||||
$shouldExpire = $this->choice(
|
||||
'Set an invite expiry date?',
|
||||
[
|
||||
'No - invite never expires',
|
||||
'Yes - expire after 24 hours',
|
||||
'Custom - let me pick an expiry date'
|
||||
],
|
||||
0
|
||||
);
|
||||
switch($shouldExpire) {
|
||||
case 'No - invite never expires':
|
||||
$expires = null;
|
||||
break;
|
||||
|
||||
case 'Yes - expire after 24 hours':
|
||||
$expires = now()->addHours(24);
|
||||
break;
|
||||
|
||||
case 'Custom - let me pick an expiry date':
|
||||
$this->info('Set custom expiry date in days');
|
||||
$customExpiry = $this->ask('Custom Expiry', 14);
|
||||
$expires = now()->addDays($customExpiry);
|
||||
break;
|
||||
}
|
||||
|
||||
$this->info('Skip email verification for invitees?');
|
||||
$skipEmailVerification = $this->choice('Skip email verification', ['No', 'Yes'], 0);
|
||||
|
||||
$invite = new AdminInvite;
|
||||
$invite->name = $name;
|
||||
$invite->description = $description;
|
||||
$invite->message = $message;
|
||||
$invite->max_uses = $max_uses;
|
||||
$invite->skip_email_verification = $skipEmailVerification === 'Yes';
|
||||
$invite->expires_at = $expires;
|
||||
$invite->invite_code = Str::uuid() . Str::random(random_int(1,6));
|
||||
$invite->save();
|
||||
|
||||
$this->info('####################');
|
||||
$this->info('# Invite Generated!');
|
||||
$this->line(' ');
|
||||
$this->info($invite->url());
|
||||
$this->line(' ');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function view()
|
||||
{
|
||||
$this->info('View Invites');
|
||||
$this->line('=============');
|
||||
if(AdminInvite::count() == 0) {
|
||||
$this->line(' ');
|
||||
$this->error('No invites found!');
|
||||
return;
|
||||
}
|
||||
$this->table(
|
||||
['Invite Code', 'Uses Left', 'Expires'],
|
||||
AdminInvite::all(['invite_code', 'max_uses', 'uses', 'expires_at'])->map(function($invite) {
|
||||
return [
|
||||
'invite_code' => $invite->invite_code,
|
||||
'uses_left' => $invite->max_uses ? ($invite->max_uses - $invite->uses) : '∞',
|
||||
'expires_at' => $invite->expires_at ? $invite->expires_at->diffForHumans() : 'never'
|
||||
];
|
||||
})->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
protected function expire()
|
||||
{
|
||||
$token = $this->anticipate('Enter invite code to expire', function($val) {
|
||||
if(!$val || empty($val)) {
|
||||
return [];
|
||||
}
|
||||
return AdminInvite::where('invite_code', 'like', '%' . $val . '%')->pluck('invite_code')->toArray();
|
||||
});
|
||||
|
||||
if(!$token || empty($token)) {
|
||||
$this->error('Invalid invite code');
|
||||
return;
|
||||
}
|
||||
$invite = AdminInvite::whereInviteCode($token)->first();
|
||||
if(!$invite) {
|
||||
$this->error('Invalid invite code');
|
||||
return;
|
||||
}
|
||||
$invite->max_uses = 1;
|
||||
$invite->expires_at = now()->subHours(2);
|
||||
$invite->save();
|
||||
$this->info('Expired the following invite: ' . $invite->url());
|
||||
}
|
||||
}
|
293
app/Console/Commands/AvatarStorage.php
Normal file
293
app/Console/Commands/AvatarStorage.php
Normal file
|
@ -0,0 +1,293 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Avatar;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use Storage;
|
||||
use App\Services\AccountService;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
|
||||
|
||||
class AvatarStorage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'avatar:storage';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Manage avatar storage';
|
||||
|
||||
public $found = 0;
|
||||
public $notFetched = 0;
|
||||
public $fixed = 0;
|
||||
public $missing = 0;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Pixelfed Avatar Storage Manager');
|
||||
$this->line(' ');
|
||||
$segments = [
|
||||
[
|
||||
'Local',
|
||||
Avatar::whereNull('is_remote')->count(),
|
||||
PrettyNumber::size(Avatar::whereNull('is_remote')->sum('size'))
|
||||
],
|
||||
[
|
||||
'Remote',
|
||||
Avatar::whereIsRemote(true)->count(),
|
||||
PrettyNumber::size(Avatar::whereIsRemote(true)->sum('size'))
|
||||
],
|
||||
[
|
||||
'Cached (CDN)',
|
||||
Avatar::whereNotNull('cdn_url')->count(),
|
||||
PrettyNumber::size(Avatar::whereNotNull('cdn_url')->sum('size'))
|
||||
],
|
||||
[
|
||||
'Uncached',
|
||||
Avatar::whereNull('cdn_url')->count(),
|
||||
PrettyNumber::size(Avatar::whereNull('cdn_url')->sum('size'))
|
||||
],
|
||||
[
|
||||
'------------',
|
||||
'----------',
|
||||
'-----'
|
||||
],
|
||||
[
|
||||
'Total',
|
||||
Avatar::count(),
|
||||
PrettyNumber::size(Avatar::sum('size'))
|
||||
],
|
||||
];
|
||||
$this->table(
|
||||
['Segment', 'Count', 'Space Used'],
|
||||
$segments
|
||||
);
|
||||
|
||||
$this->line(' ');
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage')) {
|
||||
$this->info('✅ - Cloud storage configured');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
||||
if(config('instance.avatar.local_to_cloud')) {
|
||||
$this->info('✅ - Store avatars on cloud filesystem');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
|
||||
$disk = Storage::disk(config_cache('filesystems.cloud'));
|
||||
$exists = $disk->exists('cache/avatars/default.jpg');
|
||||
$state = $exists ? '✅' : '❌';
|
||||
$msg = $state . ' - Cloud default avatar exists';
|
||||
$this->info($msg);
|
||||
}
|
||||
|
||||
$options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
|
||||
[
|
||||
'Cancel',
|
||||
'Upload default avatar to cloud',
|
||||
'Move local avatars to cloud',
|
||||
'Re-fetch remote avatars'
|
||||
] : [
|
||||
'Cancel',
|
||||
'Re-fetch remote avatars'
|
||||
];
|
||||
|
||||
$this->missing = Profile::where('created_at', '<', now()->subDays(1))->doesntHave('avatar')->count();
|
||||
if($this->missing != 0) {
|
||||
$options[] = 'Fix missing avatars';
|
||||
}
|
||||
|
||||
$choice = $this->choice(
|
||||
'Select action:',
|
||||
$options,
|
||||
0
|
||||
);
|
||||
|
||||
return $this->handleChoice($choice);
|
||||
}
|
||||
|
||||
protected function handleChoice($id)
|
||||
{
|
||||
switch ($id) {
|
||||
case 'Cancel':
|
||||
return;
|
||||
break;
|
||||
|
||||
case 'Upload default avatar to cloud':
|
||||
return $this->uploadDefaultAvatar();
|
||||
break;
|
||||
|
||||
case 'Move local avatars to cloud':
|
||||
return $this->uploadAvatarsToCloud();
|
||||
break;
|
||||
|
||||
case 'Re-fetch remote avatars':
|
||||
return $this->refetchRemoteAvatars();
|
||||
break;
|
||||
|
||||
case 'Fix missing avatars':
|
||||
return $this->fixMissingAvatars();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function uploadDefaultAvatar()
|
||||
{
|
||||
if(!$this->confirm('Are you sure you want to upload the default avatar to the cloud storage disk?')) {
|
||||
return;
|
||||
}
|
||||
$disk = Storage::disk(config_cache('filesystems.cloud'));
|
||||
$disk->put('cache/avatars/default.jpg', Storage::get('public/avatars/default.jpg'));
|
||||
Avatar::whereMediaPath('public/avatars/default.jpg')->update(['cdn_url' => $disk->url('cache/avatars/default.jpg')]);
|
||||
$this->info('Successfully uploaded default avatar to cloud storage!');
|
||||
$this->info($disk->url('cache/avatars/default.jpg'));
|
||||
}
|
||||
|
||||
protected function uploadAvatarsToCloud()
|
||||
{
|
||||
if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
|
||||
$this->error('Enable cloud storage and avatar cloud storage to perform this action');
|
||||
return;
|
||||
}
|
||||
$confirm = $this->confirm('Are you sure you want to move local avatars to cloud storage?');
|
||||
if(!$confirm) {
|
||||
$this->error('Aborted action');
|
||||
return;
|
||||
}
|
||||
|
||||
$disk = Storage::disk(config_cache('filesystems.cloud'));
|
||||
|
||||
if($disk->missing('cache/avatars/default.jpg')) {
|
||||
$disk->put('cache/avatars/default.jpg', Storage::get('public/avatars/default.jpg'));
|
||||
}
|
||||
|
||||
Avatar::whereNull('is_remote')->chunk(5, function($avatars) use($disk) {
|
||||
foreach($avatars as $avatar) {
|
||||
if($avatar->media_path === 'public/avatars/default.jpg') {
|
||||
$avatar->cdn_url = $disk->url('cache/avatars/default.jpg');
|
||||
$avatar->save();
|
||||
} else {
|
||||
if(!$avatar->media_path || !Str::of($avatar->media_path)->startsWith('public/avatars/')) {
|
||||
continue;
|
||||
}
|
||||
$ext = pathinfo($avatar->media_path, PATHINFO_EXTENSION);
|
||||
$newPath = 'cache/avatars/' . $avatar->profile_id . '/avatar_' . strtolower(Str::random(6)) . '.' . $ext;
|
||||
$existing = Storage::disk('local')->get($avatar->media_path);
|
||||
if(!$existing) {
|
||||
continue;
|
||||
}
|
||||
$newMediaPath = $disk->put($newPath, $existing);
|
||||
$avatar->media_path = $newPath;
|
||||
$avatar->cdn_url = $disk->url($newPath);
|
||||
$avatar->save();
|
||||
}
|
||||
|
||||
Cache::forget('avatar:' . $avatar->profile_id);
|
||||
Cache::forget(AccountService::CACHE_KEY . $avatar->profile_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function refetchRemoteAvatars()
|
||||
{
|
||||
if(!$this->confirm('Are you sure you want to refetch all remote avatars? This could take a while.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
|
||||
$this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
|
||||
return;
|
||||
}
|
||||
|
||||
$count = Profile::has('avatar')
|
||||
->with('avatar')
|
||||
->whereNull('user_id')
|
||||
->count();
|
||||
|
||||
$this->info('Found ' . $count . ' remote avatars to re-fetch');
|
||||
$this->line(' ');
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
|
||||
Profile::has('avatar')
|
||||
->with('avatar')
|
||||
->whereNull('user_id')
|
||||
->chunk(50, function($profiles) use($bar) {
|
||||
foreach($profiles as $profile) {
|
||||
$avatar = $profile->avatar;
|
||||
$avatar->last_fetched_at = null;
|
||||
$avatar->save();
|
||||
RemoteAvatarFetch::dispatch($profile)->onQueue('low');
|
||||
$bar->advance();
|
||||
}
|
||||
});
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info('Finished dispatching avatar refetch jobs!');
|
||||
$this->line(' ');
|
||||
$this->info('This may take a few minutes to complete, you may need to run "php artisan cache:clear" after the jobs are processed.');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
||||
protected function incr($name)
|
||||
{
|
||||
switch($name) {
|
||||
case 'found':
|
||||
$this->found = $this->found + 1;
|
||||
break;
|
||||
|
||||
case 'notFetched':
|
||||
$this->notFetched = $this->notFetched + 1;
|
||||
break;
|
||||
|
||||
case 'fixed':
|
||||
$this->fixed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function fixMissingAvatars()
|
||||
{
|
||||
if(!$this->confirm('Are you sure you want to fix missing avatars?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Found ' . $this->missing . ' accounts with missing profiles');
|
||||
|
||||
Profile::where('created_at', '<', now()->subDays(1))
|
||||
->doesntHave('avatar')
|
||||
->chunk(50, function($profiles) {
|
||||
foreach($profiles as $profile) {
|
||||
Avatar::updateOrCreate([
|
||||
'profile_id' => $profile->id
|
||||
], [
|
||||
'media_path' => 'public/avatars/default.jpg',
|
||||
'is_remote' => $profile->domain == null && $profile->private_key == null
|
||||
]);
|
||||
$this->incr('fixed');
|
||||
}
|
||||
});
|
||||
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info('Fixed ' . $this->fixed . ' accounts with a blank avatar');
|
||||
}
|
||||
}
|
|
@ -48,6 +48,18 @@ class AvatarSync extends Command
|
|||
public function handle()
|
||||
{
|
||||
$this->info('Welcome to the avatar sync manager');
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->error('This command is deprecated and will be removed in a future version');
|
||||
$this->error('You should use the following command instead: ');
|
||||
$this->line(' ');
|
||||
$this->info('php artisan avatar:storage');
|
||||
$this->line(' ');
|
||||
|
||||
$confirm = $this->confirm('Are you sure you want to use this deprecated command even though it is no longer supported?');
|
||||
if(!$confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actions = [
|
||||
'Analyze',
|
||||
|
@ -123,7 +135,7 @@ class AvatarSync extends Command
|
|||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
Profile::chunk(5000, function($profiles) use ($bar) {
|
||||
Profile::chunk(50, function($profiles) use ($bar) {
|
||||
foreach($profiles as $profile) {
|
||||
if($profile->domain == null) {
|
||||
$bar->advance();
|
||||
|
@ -146,41 +158,11 @@ class AvatarSync extends Command
|
|||
|
||||
protected function fetch()
|
||||
{
|
||||
$this->info('Fetching ....');
|
||||
Avatar::whereIsRemote(true)
|
||||
->whereNull('cdn_url')
|
||||
// ->with('profile')
|
||||
->chunk(10, function($avatars) {
|
||||
foreach($avatars as $avatar) {
|
||||
if(!$avatar || !$avatar->profile) {
|
||||
continue;
|
||||
}
|
||||
$url = $avatar->profile->remote_url;
|
||||
if(!$url || !Helpers::validateUrl($url)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$res = Helpers::fetchFromUrl($url);
|
||||
if(
|
||||
!is_array($res) ||
|
||||
!isset($res['@context']) ||
|
||||
!isset($res['icon']) ||
|
||||
!isset($res['icon']['type']) ||
|
||||
!isset($res['icon']['url']) ||
|
||||
!Str::endsWith($res['icon']['url'], ['.png', '.jpg', '.jpeg'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||
continue;
|
||||
} catch(\Illuminate\Http\Client\ConnectionException $e) {
|
||||
continue;
|
||||
}
|
||||
$avatar->remote_url = $res['icon']['url'];
|
||||
$avatar->save();
|
||||
RemoteAvatarFetch::dispatch($avatar->profile);
|
||||
}
|
||||
});
|
||||
$this->error('This action has been deprecated, please run the following command instead:');
|
||||
$this->line(' ');
|
||||
$this->info('php artisan avatar:storage');
|
||||
$this->line(' ');
|
||||
return;
|
||||
}
|
||||
|
||||
protected function fix()
|
||||
|
@ -208,12 +190,10 @@ class AvatarSync extends Command
|
|||
|
||||
protected function sync()
|
||||
{
|
||||
Avatar::whereIsRemote(true)
|
||||
->with('profile')
|
||||
->chunk(10, function($avatars) {
|
||||
foreach($avatars as $avatar) {
|
||||
RemoteAvatarFetch::dispatch($avatar->profile);
|
||||
}
|
||||
});
|
||||
}
|
||||
$this->error('This action has been deprecated, please run the following command instead:');
|
||||
$this->line(' ');
|
||||
$this->info('php artisan avatar:storage');
|
||||
$this->line(' ');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,22 +40,20 @@ class CatchUnoptimizedMedia extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
DB::transaction(function() {
|
||||
Media::whereNull('processed_at')
|
||||
->where('skip_optimize', '!=', true)
|
||||
->whereNull('remote_url')
|
||||
->whereNotNull('status_id')
|
||||
->whereNotNull('media_path')
|
||||
->where('created_at', '>', now()->subHours(1))
|
||||
->whereIn('mime', [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
])
|
||||
->chunk(50, function($medias) {
|
||||
foreach ($medias as $media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
});
|
||||
});
|
||||
Media::whereNull('processed_at')
|
||||
->where('created_at', '>', now()->subHours(1))
|
||||
->where('skip_optimize', '!=', true)
|
||||
->whereNull('remote_url')
|
||||
->whereNotNull('status_id')
|
||||
->whereNotNull('media_path')
|
||||
->whereIn('mime', [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
])
|
||||
->chunk(50, function($medias) {
|
||||
foreach ($medias as $media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
92
app/Console/Commands/CloudMediaMigrate.php
Normal file
92
app/Console/Commands/CloudMediaMigrate.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class CloudMediaMigrate extends Command
|
||||
{
|
||||
public $totalSize = 0;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:migrate2cloud {--limit=200} {--huge}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Move older media to cloud storage';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = config('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->error('Cloud storage not enabled. Exiting...');
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
$hugeMode = $this->option('huge');
|
||||
|
||||
if($limit > 500 && !$hugeMode) {
|
||||
$this->error('Max limit exceeded, use a limit lower than 500 or run again with the --huge flag');
|
||||
return;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($limit);
|
||||
$bar->start();
|
||||
|
||||
Media::whereNot('version', '4')
|
||||
->where('created_at', '<', now()->subDays(2))
|
||||
->whereRemoteMedia(false)
|
||||
->whereNotNull(['status_id', 'profile_id'])
|
||||
->whereNull(['cdn_url', 'replicated_at'])
|
||||
->orderByDesc('size')
|
||||
->take($limit)
|
||||
->get()
|
||||
->each(function($media) use($bar) {
|
||||
if(Storage::disk('local')->exists($media->media_path)) {
|
||||
$this->totalSize = $this->totalSize + $media->size;
|
||||
try {
|
||||
MediaStorageService::store($media);
|
||||
} catch (FileNotFoundException $e) {
|
||||
$this->error('Error migrating media ' . $media->id . ' to cloud storage: ' . $e->getMessage());
|
||||
return;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$this->error('Error migrating media ' . $media->id . ' to cloud storage: ' . $e->getMessage());
|
||||
return;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Error migrating media ' . $media->id . ' to cloud storage: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->info('Finished!');
|
||||
if($this->totalSize) {
|
||||
$this->info('Uploaded ' . PrettyNumber::size($this->totalSize) . ' of media to cloud storage!');
|
||||
$this->line(' ');
|
||||
$this->info('These files are still stored locally, and will be automatically removed.');
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -5,12 +5,36 @@ namespace App\Console\Commands;
|
|||
use Illuminate\Console\Command;
|
||||
|
||||
use App\{
|
||||
Avatar,
|
||||
Bookmark,
|
||||
Collection,
|
||||
DirectMessage,
|
||||
FollowRequest,
|
||||
Follower,
|
||||
HashtagFollow,
|
||||
Like,
|
||||
Media,
|
||||
MediaTag,
|
||||
Mention,
|
||||
Profile,
|
||||
Report,
|
||||
ReportComment,
|
||||
ReportLog,
|
||||
StatusArchived,
|
||||
StatusHashtag,
|
||||
StatusView,
|
||||
Status,
|
||||
User
|
||||
Story,
|
||||
StoryView,
|
||||
User,
|
||||
UserFilter
|
||||
};
|
||||
use App\Models\{
|
||||
Conversation,
|
||||
Portfolio,
|
||||
UserPronoun
|
||||
};
|
||||
use DB, Cache;
|
||||
|
||||
class FixDuplicateProfiles extends Command
|
||||
{
|
||||
|
@ -45,31 +69,186 @@ class FixDuplicateProfiles extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profiles = Profile::selectRaw('count(user_id) as count,user_id')->whereNotNull('user_id')->groupBy('user_id')->orderBy('user_id', 'desc')->get()->where('count', '>', 1);
|
||||
$count = $profiles->count();
|
||||
if($count == 0) {
|
||||
$this->info("No duplicate profiles found!");
|
||||
return;
|
||||
}
|
||||
$this->info("Found {$count} accounts with duplicate profiles...");
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
$duplicates = DB::table('profiles')
|
||||
->whereNull('domain')
|
||||
->select('username', DB::raw('COUNT(*) as "count"'))
|
||||
->groupBy('username')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->pluck('username');
|
||||
|
||||
foreach ($profiles as $profile) {
|
||||
$dup = Profile::whereUserId($profile->user_id)->get();
|
||||
|
||||
if(
|
||||
$dup->first()->username === $dup->last()->username &&
|
||||
$dup->last()->statuses()->count() == 0 &&
|
||||
$dup->last()->followers()->count() == 0 &&
|
||||
$dup->last()->likes()->count() == 0 &&
|
||||
$dup->last()->media()->count() == 0
|
||||
) {
|
||||
$dup->last()->avatar->forceDelete();
|
||||
$dup->last()->forceDelete();
|
||||
foreach($duplicates as $dupe) {
|
||||
$ids = Profile::whereNull('domain')->whereUsername($dupe)->pluck('id');
|
||||
if(!$ids || $ids->count() != 2) {
|
||||
continue;
|
||||
}
|
||||
$bar->advance();
|
||||
$id = $ids->first();
|
||||
$oid = $ids->last();
|
||||
|
||||
$user = User::whereUsername($dupe)->first();
|
||||
if($user) {
|
||||
$user->profile_id = $id;
|
||||
$user->save();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->checkAvatar($id, $oid);
|
||||
$this->checkBookmarks($id, $oid);
|
||||
$this->checkCollections($id, $oid);
|
||||
$this->checkConversations($id, $oid);
|
||||
$this->checkDirectMessages($id, $oid);
|
||||
$this->checkFollowRequest($id, $oid);
|
||||
$this->checkFollowers($id, $oid);
|
||||
$this->checkHashtagFollow($id, $oid);
|
||||
$this->checkLikes($id, $oid);
|
||||
$this->checkMedia($id, $oid);
|
||||
$this->checkMediaTag($id, $oid);
|
||||
$this->checkMention($id, $oid);
|
||||
$this->checkPortfolio($id, $oid);
|
||||
$this->checkReport($id, $oid);
|
||||
$this->checkStatusArchived($id, $oid);
|
||||
$this->checkStatusHashtag($id, $oid);
|
||||
$this->checkStatusView($id, $oid);
|
||||
$this->checkStatus($id, $oid);
|
||||
$this->checkStory($id, $oid);
|
||||
$this->checkStoryView($id, $oid);
|
||||
$this->checkUserFilter($id, $oid);
|
||||
$this->checkUserPronoun($id, $oid);
|
||||
Profile::find($oid)->forceDelete();
|
||||
}
|
||||
$bar->finish();
|
||||
|
||||
Cache::clear();
|
||||
}
|
||||
|
||||
protected function checkAvatar($id, $oid)
|
||||
{
|
||||
Avatar::whereProfileId($oid)->forceDelete();
|
||||
}
|
||||
|
||||
protected function checkBookmarks($id, $oid)
|
||||
{
|
||||
Bookmark::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkCollections($id, $oid)
|
||||
{
|
||||
Collection::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkConversations($id, $oid)
|
||||
{
|
||||
Conversation::whereToId($oid)->update(['to_id' => $id]);
|
||||
Conversation::whereFromId($oid)->update(['from_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkDirectMessages($id, $oid)
|
||||
{
|
||||
DirectMessage::whereToId($oid)->update(['to_id' => $id]);
|
||||
DirectMessage::whereFromId($oid)->update(['from_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkFollowRequest($id, $oid)
|
||||
{
|
||||
FollowRequest::whereFollowerId($oid)->update(['follower_id' => $id]);
|
||||
FollowRequest::whereFollowingId($oid)->update(['following_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkFollowers($id, $oid)
|
||||
{
|
||||
$f = Follower::whereProfileId($oid)->pluck('following_id');
|
||||
foreach($f as $fo) {
|
||||
Follower::updateOrCreate([
|
||||
'profile_id' => $id,
|
||||
'following_id' => $fo
|
||||
]);
|
||||
}
|
||||
$f = Follower::whereFollowingId($oid)->pluck('profile_id');
|
||||
foreach($f as $fo) {
|
||||
Follower::updateOrCreate([
|
||||
'profile_id' => $fo,
|
||||
'following_id' => $id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkHashtagFollow($id, $oid)
|
||||
{
|
||||
HashtagFollow::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkLikes($id, $oid)
|
||||
{
|
||||
Like::whereStatusProfileId($oid)->update(['status_profile_id' => $id]);
|
||||
Like::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkMedia($id, $oid)
|
||||
{
|
||||
Media::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkMediaTag($id, $oid)
|
||||
{
|
||||
MediaTag::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkMention($id, $oid)
|
||||
{
|
||||
Mention::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkPortfolio($id, $oid)
|
||||
{
|
||||
Portfolio::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkReport($id, $oid)
|
||||
{
|
||||
ReportComment::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
ReportLog::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
Report::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatusArchived($id, $oid)
|
||||
{
|
||||
StatusArchived::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatusHashtag($id, $oid)
|
||||
{
|
||||
StatusHashtag::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatusView($id, $oid)
|
||||
{
|
||||
StatusView::whereStatusProfileId($oid)->update(['profile_id' => $id]);
|
||||
StatusView::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStatus($id, $oid)
|
||||
{
|
||||
Status::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStory($id, $oid)
|
||||
{
|
||||
Story::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkStoryView($id, $oid)
|
||||
{
|
||||
StoryView::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkUserFilter($id, $oid)
|
||||
{
|
||||
UserFilter::whereUserId($oid)->update(['user_id' => $id]);
|
||||
UserFilter::whereFilterableType('App\Profile')->whereFilterableId($oid)->update(['filterable_id' => $id]);
|
||||
}
|
||||
|
||||
protected function checkUserPronoun($id, $oid)
|
||||
{
|
||||
UserPronoun::whereProfileId($oid)->update(['profile_id' => $id]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
133
app/Console/Commands/FixMediaDriver.php
Normal file
133
app/Console/Commands/FixMediaDriver.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Media;
|
||||
use League\Flysystem\MountManager;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\MediaPipeline\MediaFixLocalFilesystemCleanupPipeline;
|
||||
|
||||
class FixMediaDriver extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:fix-nonlocal-driver';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Fix filesystem when FILESYSTEM_DRIVER not set to local';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(config('filesystems.default') !== 'local') {
|
||||
$this->error('Invalid default filesystem, set FILESYSTEM_DRIVER=local to proceed');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == false) {
|
||||
$this->error('Cloud storage not enabled, exiting...');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Media Filesystem Fix');
|
||||
$this->info(' =====================');
|
||||
$this->info(' Fix media that was created when FILESYSTEM_DRIVER=local');
|
||||
$this->info(' was not properly set. This command will fix media urls');
|
||||
$this->info(' and optionally optimize/generate thumbnails when applicable,');
|
||||
$this->info(' clean up temporary local media files and clear the app cache');
|
||||
$this->info(' to fix media paths/urls.');
|
||||
$this->info(' ');
|
||||
$this->error(' Remember, FILESYSTEM_DRIVER=local must remain set or you will break things!');
|
||||
|
||||
if(!$this->confirm('Are you sure you want to perform this command?')) {
|
||||
$this->info('Exiting...');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$optimize = $this->choice(
|
||||
'Do you want to optimize media and generate thumbnails? This will store s3 locally and re-upload optimized versions.',
|
||||
['no', 'yes'],
|
||||
1
|
||||
);
|
||||
|
||||
$cloud = Storage::disk(config('filesystems.cloud'));
|
||||
$mountManager = new MountManager([
|
||||
's3' => $cloud->getDriver(),
|
||||
'local' => Storage::disk('local')->getDriver(),
|
||||
]);
|
||||
|
||||
$this->info('Fixing media, this may take a while...');
|
||||
$this->line(' ');
|
||||
$bar = $this->output->createProgressBar(Media::whereNotNull('status_id')->whereNull('cdn_url')->count());
|
||||
$bar->start();
|
||||
|
||||
foreach(Media::whereNotNull('status_id')->whereNull('cdn_url')->lazyById(20) as $media) {
|
||||
if($cloud->exists($media->media_path)) {
|
||||
if($optimize === 'yes') {
|
||||
$mountManager->copy(
|
||||
's3://' . $media->media_path,
|
||||
'local://' . $media->media_path
|
||||
);
|
||||
sleep(1);
|
||||
if(empty($media->original_sha256)) {
|
||||
$hash = \hash_file('sha256', Storage::disk('local')->path($media->media_path));
|
||||
$media->original_sha256 = $hash;
|
||||
$media->save();
|
||||
sleep(1);
|
||||
}
|
||||
if(
|
||||
$media->mime &&
|
||||
in_array($media->mime, [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp'
|
||||
])
|
||||
) {
|
||||
ImageOptimize::dispatch($media);
|
||||
sleep(3);
|
||||
}
|
||||
} else {
|
||||
$media->cdn_url = $cloud->url($media->media_path);
|
||||
$media->save();
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
|
||||
$this->callSilently('cache:clear');
|
||||
|
||||
$this->info('Successfully fixed media paths and cleared cached!');
|
||||
|
||||
if($optimize === 'yes') {
|
||||
MediaFixLocalFilesystemCleanupPipeline::dispatch()->delay(now()->addMinutes(15))->onQueue('default');
|
||||
$this->line(' ');
|
||||
$this->info('A cleanup job has been dispatched to delete media stored locally, it may take a few minutes to process!');
|
||||
}
|
||||
|
||||
$this->line(' ');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ namespace App\Console\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use App\{Media, Status};
|
||||
use Carbon\Carbon;
|
||||
use App\Services\MediaStorageService;
|
||||
|
||||
class MediaGarbageCollector extends Command
|
||||
|
@ -40,11 +39,10 @@ class MediaGarbageCollector extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = 20000;
|
||||
$limit = 500;
|
||||
|
||||
$gc = Media::whereNull('status_id')
|
||||
->where('created_at', '<', Carbon::now()->subHours(12)->toDateTimeString())
|
||||
->orderBy('created_at','asc')
|
||||
->where('created_at', '<', now()->subHours(2)->toDateTimeString())
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
|
|
195
app/Console/Commands/MediaS3GarbageCollector.php
Normal file
195
app/Console/Commands/MediaS3GarbageCollector.php
Normal file
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use App\Status;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\StatusService;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class MediaS3GarbageCollector extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:s3gc {--limit=200} {--huge} {--log-errors}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete (local) media uploads that exist on S3';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = config('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->error('Cloud storage not enabled. Exiting...');
|
||||
return;
|
||||
}
|
||||
|
||||
$deleteEnabled = config('media.delete_local_after_cloud');
|
||||
if(!$deleteEnabled) {
|
||||
$this->error('Delete local storage after cloud upload is not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
$hugeMode = $this->option('huge');
|
||||
$log = $this->option('log-errors');
|
||||
|
||||
if($limit > 2000 && !$hugeMode) {
|
||||
$this->error('Limit exceeded, please use a limit under 2000 or run again with the --huge flag');
|
||||
return;
|
||||
}
|
||||
|
||||
$minId = Media::orderByDesc('id')->where('created_at', '<', now()->subHours(12))->first();
|
||||
|
||||
if(!$minId) {
|
||||
return;
|
||||
} else {
|
||||
$minId = $minId->id;
|
||||
}
|
||||
|
||||
return $hugeMode ?
|
||||
$this->hugeMode($minId, $limit, $log) :
|
||||
$this->regularMode($minId, $limit, $log);
|
||||
}
|
||||
|
||||
protected function regularMode($minId, $limit, $log)
|
||||
{
|
||||
$gc = Media::whereRemoteMedia(false)
|
||||
->whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
|
||||
->whereNot('version', '4')
|
||||
->where('id', '<', $minId)
|
||||
->inRandomOrder()
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
$totalSize = 0;
|
||||
$bar = $this->output->createProgressBar($gc->count());
|
||||
$bar->start();
|
||||
$cloudDisk = Storage::disk(config('filesystems.cloud'));
|
||||
$localDisk = Storage::disk('local');
|
||||
|
||||
foreach($gc as $media) {
|
||||
try {
|
||||
if(
|
||||
$cloudDisk->exists($media->media_path)
|
||||
) {
|
||||
if( $localDisk->exists($media->media_path)) {
|
||||
$localDisk->delete($media->media_path);
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
$totalSize = $totalSize + $media->size;
|
||||
MediaService::del($media->status_id);
|
||||
StatusService::del($media->status_id, false);
|
||||
if($localDisk->exists($media->thumbnail_path)) {
|
||||
$localDisk->delete($media->thumbnail_path);
|
||||
}
|
||||
} else {
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
}
|
||||
} else {
|
||||
if($log) {
|
||||
Log::channel('media')->info('[GC] Local media not properly persisted to cloud storage', ['media_id' => $media->id]);
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
} catch (FileNotFoundException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (\Exception $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->info('Finished!');
|
||||
if($totalSize) {
|
||||
$this->info('Cleared ' . $totalSize . ' bytes of media from local disk!');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function hugeMode($minId, $limit, $log)
|
||||
{
|
||||
$cloudDisk = Storage::disk(config('filesystems.cloud'));
|
||||
$localDisk = Storage::disk('local');
|
||||
|
||||
$bar = $this->output->createProgressBar($limit);
|
||||
$bar->start();
|
||||
|
||||
Media::whereRemoteMedia(false)
|
||||
->whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
|
||||
->whereNot('version', '4')
|
||||
->where('id', '<', $minId)
|
||||
->chunk(50, function($medias) use($cloudDisk, $localDisk, $bar, $log) {
|
||||
foreach($medias as $media) {
|
||||
try {
|
||||
if($cloudDisk->exists($media->media_path)) {
|
||||
if( $localDisk->exists($media->media_path)) {
|
||||
$localDisk->delete($media->media_path);
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
MediaService::del($media->status_id);
|
||||
StatusService::del($media->status_id, false);
|
||||
if($localDisk->exists($media->thumbnail_path)) {
|
||||
$localDisk->delete($media->thumbnail_path);
|
||||
}
|
||||
} else {
|
||||
$media->version = 4;
|
||||
$media->save();
|
||||
}
|
||||
} else {
|
||||
if($log) {
|
||||
Log::channel('media')->info('[GC] Local media not properly persisted to cloud storage', ['media_id' => $media->id]);
|
||||
}
|
||||
}
|
||||
$bar->advance();
|
||||
} catch (FileNotFoundException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
} catch (\Exception $e) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
$this->info('Finished!');
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
|
@ -51,7 +50,7 @@ class StoryGC extends Command
|
|||
protected function archiveExpiredStories()
|
||||
{
|
||||
$stories = Story::whereActive(true)
|
||||
->where('created_at', '<', now()->subHours(24))
|
||||
->where('expires_at', '<', now())
|
||||
->get();
|
||||
|
||||
foreach($stories as $story) {
|
||||
|
@ -79,6 +78,7 @@ class StoryGC extends Command
|
|||
}
|
||||
StoryRotateMedia::dispatch($story)->onQueue('story');
|
||||
StoryService::removeRotateQueue($id);
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,11 @@ class UserAdmin extends Command
|
|||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
|
|
82
app/Console/Commands/UserRegistrationMagicLink.php
Normal file
82
app/Console/Commands/UserRegistrationMagicLink.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\EmailVerification;
|
||||
use App\User;
|
||||
|
||||
class UserRegistrationMagicLink extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:app-magic-link {--username=} {--email=}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Get the app magic link for users who register in-app but have not recieved the confirmation email';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$username = $this->option('username');
|
||||
$email = $this->option('email');
|
||||
if(!$username && !$email) {
|
||||
$this->error('Please provide the username or email as arguments');
|
||||
$this->line(' ');
|
||||
$this->info('Example: ');
|
||||
$this->info('php artisan user:app-magic-link --username=dansup');
|
||||
$this->info('php artisan user:app-magic-link --email=dansup@pixelfed.com');
|
||||
return;
|
||||
}
|
||||
$user = User::when($username, function($q, $username) {
|
||||
return $q->whereUsername($username);
|
||||
})
|
||||
->when($email, function($q, $email) {
|
||||
return $q->whereEmail($email);
|
||||
})
|
||||
->first();
|
||||
|
||||
if(!$user) {
|
||||
$this->error('We cannot find any matching accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
if($user->email_verified_at) {
|
||||
$this->error('User already verified email address');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$user->register_source || $user->register_source !== 'app' || !$user->app_register_token) {
|
||||
$this->error('User did not register via app');
|
||||
return;
|
||||
}
|
||||
|
||||
$verify = EmailVerification::whereUserId($user->id)->first();
|
||||
|
||||
if(!$verify) {
|
||||
$this->error('Cannot find user verification codes');
|
||||
return;
|
||||
}
|
||||
|
||||
$appUrl = 'pixelfed://confirm-account/'. $user->app_register_token . '?rt=' . $verify->random_token;
|
||||
$this->line(' ');
|
||||
$this->info('Magic link found! Copy the following link and send to user');
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info($appUrl);
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -39,7 +39,11 @@ class UserShow extends Command
|
|||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
|
|
|
@ -39,7 +39,11 @@ class UserSuspend extends Command
|
|||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
|
|
|
@ -39,7 +39,11 @@ class UserUnsuspend extends Command
|
|||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
$user = User::whereUsername($id)->orWhere('id', $id)->first();
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
|
|
|
@ -46,7 +46,7 @@ class VideoThumbnail extends Command
|
|||
->take($limit)
|
||||
->get();
|
||||
foreach($videos as $video) {
|
||||
Pipeline::dispatchNow($video);
|
||||
Pipeline::dispatch($video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,13 +25,17 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('media:optimize')->hourly();
|
||||
$schedule->command('media:gc')->hourly();
|
||||
$schedule->command('media:optimize')->hourlyAt(40);
|
||||
$schedule->command('media:gc')->hourlyAt(5);
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('story:gc')->everyFiveMinutes();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3);
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
||||
|
||||
if(config('pixelfed.cloud_storage') && config('media.delete_local_after_cloud')) {
|
||||
$schedule->command('media:s3gc')->hourlyAt(15);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,16 +17,21 @@ use App\{
|
|||
EmailVerification,
|
||||
Follower,
|
||||
FollowRequest,
|
||||
Media,
|
||||
Notification,
|
||||
Profile,
|
||||
User,
|
||||
UserFilter
|
||||
UserDevice,
|
||||
UserFilter,
|
||||
UserSetting
|
||||
};
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\Mastodon\v1\AccountTransformer;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\RelationshipService;
|
||||
use App\Jobs\FollowPipeline\FollowAcceptPipeline;
|
||||
|
@ -39,7 +44,8 @@ class AccountController extends Controller
|
|||
'user.block',
|
||||
];
|
||||
|
||||
const FILTER_LIMIT = 'You cannot block or mute more than 100 accounts';
|
||||
const FILTER_LIMIT_MUTE_TEXT = 'You cannot mute more than ';
|
||||
const FILTER_LIMIT_BLOCK_TEXT = 'You cannot block more than ';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -145,16 +151,17 @@ class AccountController extends Controller
|
|||
public function mute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'type' => 'required|string|in:user',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$count = UserFilterService::muteCount($user->id);
|
||||
abort_if($count >= 100, 422, self::FILTER_LIMIT);
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::muteCount($pid);
|
||||
$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($user->id)->count();
|
||||
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
|
||||
$filterCount = UserFilter::whereUserId($pid)->count();
|
||||
abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||
}
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
|
@ -167,7 +174,7 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
if ($profile->id == $pid) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
@ -177,29 +184,30 @@ class AccountController extends Controller
|
|||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'user_id' => $pid,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'mute',
|
||||
]);
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
UserFilterService::mute($pid, $filterable['id']);
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
return redirect()->back();
|
||||
if($request->wantsJson()) {
|
||||
return response()->json($res);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
public function unmute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'type' => 'required|string|in:user',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$pid = $request->user()->profile_id;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.mute';
|
||||
|
@ -211,7 +219,7 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
if ($profile->id == $pid) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
@ -224,24 +232,21 @@ class AccountController extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('mute')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
UserFilterService::unmute($pid, $filterable['id']);
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json([200]);
|
||||
return response()->json($res);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
|
@ -250,16 +255,16 @@ class AccountController extends Controller
|
|||
public function block(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'type' => 'required|string|in:user',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$count = UserFilterService::blockCount($user->id);
|
||||
abort_if($count >= 100, 422, self::FILTER_LIMIT);
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::blockCount($pid);
|
||||
$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($user->id)->count();
|
||||
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
|
||||
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();
|
||||
abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||
}
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
|
@ -271,41 +276,74 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id || ($profile->user && $profile->user->is_admin == true)) {
|
||||
if ($profile->id == $pid || ($profile->user && $profile->user->is_admin == true)) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
|
||||
Follower::whereProfileId($profile->id)->whereFollowingId($user->id)->delete();
|
||||
Notification::whereProfileId($user->id)->whereActorId($profile->id)->delete();
|
||||
$followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first();
|
||||
if($followed) {
|
||||
$followed->delete();
|
||||
$profile->following_count = Follower::whereProfileId($profile->id)->count();
|
||||
$profile->save();
|
||||
$selfProfile = $request->user()->profile;
|
||||
$selfProfile->followers_count = Follower::whereFollowingId($pid)->count();
|
||||
$selfProfile->save();
|
||||
FollowerService::remove($profile->id, $pid);
|
||||
AccountService::del($pid);
|
||||
AccountService::del($profile->id);
|
||||
}
|
||||
|
||||
$following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first();
|
||||
if($following) {
|
||||
$following->delete();
|
||||
$profile->followers_count = Follower::whereFollowingId($profile->id)->count();
|
||||
$profile->save();
|
||||
$selfProfile = $request->user()->profile;
|
||||
$selfProfile->following_count = Follower::whereProfileId($pid)->count();
|
||||
$selfProfile->save();
|
||||
FollowerService::remove($pid, $profile->pid);
|
||||
AccountService::del($pid);
|
||||
AccountService::del($profile->id);
|
||||
}
|
||||
|
||||
Notification::whereProfileId($pid)
|
||||
->whereActorId($profile->id)
|
||||
->get()
|
||||
->map(function($n) use($pid) {
|
||||
NotificationService::del($pid, $n['id']);
|
||||
$n->forceDelete();
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::firstOrCreate([
|
||||
'user_id' => $user->id,
|
||||
'user_id' => $pid,
|
||||
'filterable_id' => $filterable['id'],
|
||||
'filterable_type' => $filterable['type'],
|
||||
'filter_type' => 'block',
|
||||
]);
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
UserFilterService::block($pid, $filterable['id']);
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
return redirect()->back();
|
||||
if($request->wantsJson()) {
|
||||
return response()->json($res);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
public function unblock(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'type' => 'required|string|in:user',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$pid = $request->user()->profile_id;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.block';
|
||||
|
@ -316,7 +354,7 @@ class AccountController extends Controller
|
|||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
if ($profile->id == $pid) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
@ -330,7 +368,7 @@ class AccountController extends Controller
|
|||
}
|
||||
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
$filter = UserFilter::whereUserId($pid)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('block')
|
||||
|
@ -338,15 +376,16 @@ class AccountController extends Controller
|
|||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
UserFilterService::unblock($pid, $filterable['id']);
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
$res = RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
return redirect()->back();
|
||||
if($request->wantsJson()) {
|
||||
return response()->json($res);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
public function followRequests(Request $request)
|
||||
|
@ -409,7 +448,7 @@ class AccountController extends Controller
|
|||
AccountService::del($profile->id);
|
||||
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowAcceptPipeline::dispatch($followRequest);
|
||||
FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow');
|
||||
} else {
|
||||
FollowPipeline::dispatch($follow);
|
||||
$followRequest->delete();
|
||||
|
@ -418,7 +457,7 @@ class AccountController extends Controller
|
|||
|
||||
case 'reject':
|
||||
if($follower->domain != null && $follower->private_key === null) {
|
||||
FollowRejectPipeline::dispatch($followRequest);
|
||||
FollowRejectPipeline::dispatch($followRequest)->onQueue('follow');
|
||||
} else {
|
||||
$followRequest->delete();
|
||||
}
|
||||
|
@ -466,6 +505,12 @@ class AccountController extends Controller
|
|||
if($trustDevice == true) {
|
||||
$request->session()->put('sudoTrustDevice', 1);
|
||||
}
|
||||
|
||||
//Fix wrong scheme when using reverse proxy
|
||||
if(!str_contains($next, 'https') && config('instance.force_https_urls', true)) {
|
||||
$next = Str::of($next)->replace('http', 'https')->toString();
|
||||
}
|
||||
|
||||
return redirect($next);
|
||||
} else {
|
||||
return redirect()
|
||||
|
@ -513,25 +558,25 @@ class AccountController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
protected function twoFactorBackupCheck($request, $code, User $user)
|
||||
{
|
||||
$backupCodes = $user->{'2fa_backup_codes'};
|
||||
if($backupCodes) {
|
||||
$codes = json_decode($backupCodes, true);
|
||||
foreach ($codes as $c) {
|
||||
if(hash_equals($c, $code)) {
|
||||
$codes = array_flatten(array_diff($codes, [$code]));
|
||||
$user->{'2fa_backup_codes'} = json_encode($codes);
|
||||
$user->save();
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
protected function twoFactorBackupCheck($request, $code, User $user)
|
||||
{
|
||||
$backupCodes = $user->{'2fa_backup_codes'};
|
||||
if($backupCodes) {
|
||||
$codes = json_decode($backupCodes, true);
|
||||
foreach ($codes as $c) {
|
||||
if(hash_equals($c, $code)) {
|
||||
$codes = array_flatten(array_diff($codes, [$code]));
|
||||
$user->{'2fa_backup_codes'} = json_encode($codes);
|
||||
$user->save();
|
||||
$request->session()->push('2fa.session.active', true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function accountRestored(Request $request)
|
||||
{
|
||||
|
|
102
app/Http/Controllers/Admin/AdminHashtagsController.php
Normal file
102
app/Http/Controllers/Admin/AdminHashtagsController.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use App\Http\Resources\AdminHashtag;
|
||||
use App\Services\TrendingHashtagService;
|
||||
|
||||
trait AdminHashtagsController
|
||||
{
|
||||
public function hashtagsHome(Request $request)
|
||||
{
|
||||
return view('admin.hashtags.home');
|
||||
}
|
||||
|
||||
public function hashtagsApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'sometimes|in:banned,nsfw',
|
||||
'sort' => 'sometimes|in:id,name,cached_count,can_search,can_trend,is_banned,is_nsfw',
|
||||
'dir' => 'sometimes|in:asc,desc'
|
||||
]);
|
||||
$action = $request->input('action');
|
||||
$query = $request->input('q');
|
||||
$sort = $request->input('sort');
|
||||
$order = $request->input('dir');
|
||||
|
||||
$hashtags = Hashtag::when($query, function($q, $query) {
|
||||
return $q->where('name', 'like', $query . '%');
|
||||
})
|
||||
->when($sort, function($q, $sort) use($order) {
|
||||
return $q->orderBy($sort, $order);
|
||||
}, function($q) {
|
||||
return $q->orderByDesc('id');
|
||||
})
|
||||
->when($action, function($q, $action) {
|
||||
if($action === 'banned') {
|
||||
return $q->whereIsBanned(true);
|
||||
} else if ($action === 'nsfw') {
|
||||
return $q->whereIsNsfw(true);
|
||||
}
|
||||
})
|
||||
->cursorPaginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return AdminHashtag::collection($hashtags);
|
||||
}
|
||||
|
||||
public function hashtagsStats(Request $request)
|
||||
{
|
||||
$stats = [
|
||||
'total_unique' => Hashtag::count(),
|
||||
'total_posts' => StatusHashtag::count(),
|
||||
'added_14_days' => Hashtag::where('created_at', '>', now()->subDays(14))->count(),
|
||||
'total_banned' => Hashtag::whereIsBanned(true)->count(),
|
||||
'total_nsfw' => Hashtag::whereIsNsfw(true)->count()
|
||||
];
|
||||
|
||||
return response()->json($stats);
|
||||
}
|
||||
|
||||
public function hashtagsGet(Request $request)
|
||||
{
|
||||
return new AdminHashtag(Hashtag::findOrFail($request->input('id')));
|
||||
}
|
||||
|
||||
public function hashtagsUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'name' => 'required',
|
||||
'slug' => 'required',
|
||||
'can_search' => 'required:boolean',
|
||||
'can_trend' => 'required:boolean',
|
||||
'is_nsfw' => 'required:boolean',
|
||||
'is_banned' => 'required:boolean'
|
||||
]);
|
||||
|
||||
$hashtag = Hashtag::whereSlug($request->input('slug'))->findOrFail($request->input('id'));
|
||||
$canTrendPrev = $hashtag->can_trend == null ? true : $hashtag->can_trend;
|
||||
$hashtag->is_banned = $request->input('is_banned');
|
||||
$hashtag->is_nsfw = $request->input('is_nsfw');
|
||||
$hashtag->can_search = $hashtag->is_banned ? false : $request->input('can_search');
|
||||
$hashtag->can_trend = $hashtag->is_banned ? false : $request->input('can_trend');
|
||||
$hashtag->save();
|
||||
|
||||
TrendingHashtagService::refresh();
|
||||
|
||||
return new AdminHashtag($hashtag);
|
||||
}
|
||||
|
||||
public function hashtagsClearTrendingCache(Request $request)
|
||||
{
|
||||
TrendingHashtagService::refresh();
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
|
@ -8,66 +8,13 @@ use Carbon\Carbon;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Services\InstanceService;
|
||||
use App\Http\Resources\AdminInstance;
|
||||
|
||||
trait AdminInstanceController
|
||||
{
|
||||
|
||||
public function instances(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
||||
'filter' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:1',
|
||||
'max:20',
|
||||
Rule::in([
|
||||
'cw',
|
||||
'unlisted',
|
||||
'banned',
|
||||
// 'popular',
|
||||
'new',
|
||||
'all'
|
||||
])
|
||||
],
|
||||
]);
|
||||
if($request->has('q') && $request->filled('q')) {
|
||||
$instances = Instance::where('domain', 'like', '%' . $request->input('q') . '%')->simplePaginate(10);
|
||||
} else if($request->has('filter') && $request->filled('filter')) {
|
||||
switch ($request->filter) {
|
||||
case 'cw':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereAutoCw(true)->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
case 'unlisted':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereUnlisted(true)->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
case 'banned':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereBanned(true)->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
case 'new':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->latest()->simplePaginate(10);
|
||||
break;
|
||||
// case 'popular':
|
||||
// $popular = Profile::selectRaw('*, count(domain) as count')
|
||||
// ->whereNotNull('domain')
|
||||
// ->groupBy('domain')
|
||||
// ->orderByDesc('count')
|
||||
// ->take(10)
|
||||
// ->get()
|
||||
// ->pluck('domain')
|
||||
// ->toArray();
|
||||
// $instances = Instance::whereIn('domain', $popular)->simplePaginate(10);
|
||||
// break;
|
||||
|
||||
default:
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10);
|
||||
}
|
||||
|
||||
return view('admin.instances.home', compact('instances'));
|
||||
return view('admin.instances.home');
|
||||
}
|
||||
|
||||
public function instanceScan(Request $request)
|
||||
|
@ -133,4 +80,223 @@ trait AdminInstanceController
|
|||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function getInstancesStatsApi(Request $request)
|
||||
{
|
||||
return InstanceService::stats();
|
||||
}
|
||||
|
||||
public function getInstancesQueryApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
return AdminInstance::collection(
|
||||
Instance::where('domain', 'like', '%' . $q . '%')
|
||||
->orderByDesc('user_count')
|
||||
->cursorPaginate(10)
|
||||
->withQueryString()
|
||||
);
|
||||
}
|
||||
|
||||
public function getInstancesApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'filter' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:1',
|
||||
'max:20',
|
||||
Rule::in([
|
||||
'cw',
|
||||
'unlisted',
|
||||
'banned',
|
||||
'popular_users',
|
||||
'popular_statuses',
|
||||
'new',
|
||||
'all'
|
||||
])
|
||||
],
|
||||
'sort' => [
|
||||
'sometimes',
|
||||
'string',
|
||||
Rule::in([
|
||||
'id',
|
||||
'domain',
|
||||
'software',
|
||||
'user_count',
|
||||
'status_count',
|
||||
'banned',
|
||||
'auto_cw',
|
||||
'unlisted'
|
||||
])
|
||||
],
|
||||
'dir' => 'sometimes|in:desc,asc'
|
||||
]);
|
||||
$filter = $request->input('filter');
|
||||
$query = $request->input('q');
|
||||
$sortCol = $request->input('sort');
|
||||
$sortDir = $request->input('dir');
|
||||
|
||||
return AdminInstance::collection(Instance::when($query, function($q, $qq) use($query) {
|
||||
return $q->where('domain', 'like', '%' . $query . '%');
|
||||
})
|
||||
->when($filter, function($q, $f) use($filter) {
|
||||
if($filter == 'cw') { return $q->whereAutoCw(true); }
|
||||
if($filter == 'unlisted') { return $q->whereUnlisted(true); }
|
||||
if($filter == 'banned') { return $q->whereBanned(true); }
|
||||
if($filter == 'new') { return $q->orderByDesc('id'); }
|
||||
if($filter == 'popular_users') { return $q->orderByDesc('user_count'); }
|
||||
if($filter == 'popular_statuses') { return $q->orderByDesc('status_count'); }
|
||||
return $q->orderByDesc('id');
|
||||
})
|
||||
->when($sortCol, function($q, $s) use($sortCol, $sortDir, $filter) {
|
||||
if(!in_array($filter, ['popular_users', 'popular_statuses'])) {
|
||||
return $q->whereNotNull($sortCol)->orderBy($sortCol, $sortDir);
|
||||
}
|
||||
}, function($q) use($filter) {
|
||||
if(!$filter || !in_array($filter, ['popular_users', 'popular_statuses'])) {
|
||||
return $q->orderByDesc('id');
|
||||
}
|
||||
})
|
||||
->cursorPaginate(10)
|
||||
->withQueryString());
|
||||
}
|
||||
|
||||
public function postInstanceUpdateApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'banned' => 'boolean',
|
||||
'auto_cw' => 'boolean',
|
||||
'unlisted' => 'boolean',
|
||||
'notes' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$instance = Instance::findOrFail($id);
|
||||
$instance->update($request->only([
|
||||
'banned',
|
||||
'auto_cw',
|
||||
'unlisted',
|
||||
'notes'
|
||||
]));
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function postInstanceCreateNewApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|string',
|
||||
'banned' => 'boolean',
|
||||
'auto_cw' => 'boolean',
|
||||
'unlisted' => 'boolean',
|
||||
'notes' => 'nullable|string|max:500'
|
||||
]);
|
||||
|
||||
$domain = $request->input('domain');
|
||||
|
||||
abort_if(!strpos($domain, '.'), 400, 'Invalid domain');
|
||||
abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain');
|
||||
|
||||
$instance = new Instance;
|
||||
$instance->domain = $request->input('domain');
|
||||
$instance->banned = $request->input('banned');
|
||||
$instance->auto_cw = $request->input('auto_cw');
|
||||
$instance->unlisted = $request->input('unlisted');
|
||||
$instance->manually_added = true;
|
||||
$instance->notes = $request->input('notes');
|
||||
$instance->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function postInstanceRefreshStatsApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required'
|
||||
]);
|
||||
|
||||
$instance = Instance::findOrFail($request->input('id'));
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->status_count = Profile::whereDomain($instance->domain)->leftJoin('statuses', 'profiles.id', '=', 'statuses.profile_id')->count();
|
||||
$instance->save();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function postInstanceDeleteApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required'
|
||||
]);
|
||||
|
||||
$instance = Instance::findOrFail($request->input('id'));
|
||||
$instance->delete();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function downloadBackup(Request $request)
|
||||
{
|
||||
return response()->streamDownload(function () {
|
||||
$json = [
|
||||
'version' => 1,
|
||||
'auto_cw' => Instance::whereAutoCw(true)->pluck('domain')->toArray(),
|
||||
'unlisted' => Instance::whereUnlisted(true)->pluck('domain')->toArray(),
|
||||
'banned' => Instance::whereBanned(true)->pluck('domain')->toArray(),
|
||||
'created_at' => now()->format('c'),
|
||||
];
|
||||
$chk = hash('sha256', json_encode($json, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||
$json['_sha256'] = $chk;
|
||||
echo json_encode($json, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}, 'pixelfed-instances-mod.json');
|
||||
}
|
||||
|
||||
public function importBackup(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'banned' => 'sometimes|array',
|
||||
'auto_cw' => 'sometimes|array',
|
||||
'unlisted' => 'sometimes|array',
|
||||
]);
|
||||
|
||||
$banned = $request->input('banned');
|
||||
$auto_cw = $request->input('auto_cw');
|
||||
$unlisted = $request->input('unlisted');
|
||||
|
||||
foreach($banned as $i) {
|
||||
Instance::updateOrCreate(
|
||||
['domain' => $i],
|
||||
['banned' => true]
|
||||
);
|
||||
}
|
||||
|
||||
foreach($auto_cw as $i) {
|
||||
Instance::updateOrCreate(
|
||||
['domain' => $i],
|
||||
['auto_cw' => true]
|
||||
);
|
||||
}
|
||||
|
||||
foreach($unlisted as $i) {
|
||||
Instance::updateOrCreate(
|
||||
['domain' => $i],
|
||||
['unlisted' => true]
|
||||
);
|
||||
}
|
||||
|
||||
InstanceService::refresh();
|
||||
return [200];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
|
|||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
|
@ -24,6 +25,13 @@ use Illuminate\Validation\Rule;
|
|||
use App\Services\StoryService;
|
||||
use App\Services\ModLogService;
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\Http\Resources\AdminReport;
|
||||
use App\Http\Resources\AdminSpamReport;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
|
||||
trait AdminReportController
|
||||
{
|
||||
|
@ -74,6 +82,9 @@ trait AdminReportController
|
|||
public function showReport(Request $request, $id)
|
||||
{
|
||||
$report = Report::with('status')->findOrFail($id);
|
||||
if($request->has('ref') && $request->input('ref') == 'email') {
|
||||
return redirect('/i/admin/reports?tab=report&id=' . $report->id);
|
||||
}
|
||||
return view('admin.reports.show', compact('report'));
|
||||
}
|
||||
|
||||
|
@ -200,6 +211,9 @@ trait AdminReportController
|
|||
{
|
||||
$appeal = AccountInterstitial::whereType('post.autospam')
|
||||
->findOrFail($id);
|
||||
if($request->has('ref') && $request->input('ref') == 'email') {
|
||||
return redirect('/i/admin/reports?tab=autospam&id=' . $appeal->id);
|
||||
}
|
||||
$meta = json_decode($appeal->meta);
|
||||
return view('admin.reports.show_spam', compact('appeal', 'meta'));
|
||||
}
|
||||
|
@ -601,4 +615,588 @@ trait AdminReportController
|
|||
Redis::del('email:manual-ignored');
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function reportsStats(Request $request)
|
||||
{
|
||||
$stats = [
|
||||
'total' => Report::count(),
|
||||
'open' => Report::whereNull('admin_seen')->count(),
|
||||
'closed' => Report::whereNotNull('admin_seen')->count(),
|
||||
'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
|
||||
'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
|
||||
'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
|
||||
'email_verification_requests' => Redis::scard('email:manual')
|
||||
];
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public function reportsApiAll(Request $request)
|
||||
{
|
||||
$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
|
||||
|
||||
$reports = AdminReport::collection(
|
||||
Report::orderBy('id','desc')
|
||||
->when($filter, function($q, $filter) {
|
||||
return $filter == 'open' ?
|
||||
$q->whereNull('admin_seen') :
|
||||
$q->whereNotNull('admin_seen');
|
||||
})
|
||||
->groupBy(['object_id', 'object_type'])
|
||||
->cursorPaginate(6)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return $reports;
|
||||
}
|
||||
|
||||
public function reportsApiGet(Request $request, $id)
|
||||
{
|
||||
$report = Report::findOrFail($id);
|
||||
return new AdminReport($report);
|
||||
}
|
||||
|
||||
public function reportsApiHandle(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'object_id' => 'required',
|
||||
'object_type' => 'required',
|
||||
'id' => 'required',
|
||||
'action' => 'required|in:ignore,nsfw,unlist,private,delete',
|
||||
'action_type' => 'required|in:post,profile'
|
||||
]);
|
||||
|
||||
$report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id'));
|
||||
|
||||
if($request->input('action_type') === 'profile') {
|
||||
return $this->reportsHandleProfileAction($report, $request->input('action'));
|
||||
} else if($request->input('action_type') === 'post') {
|
||||
return $this->reportsHandleStatusAction($report, $request->input('action'));
|
||||
}
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
protected function reportsHandleProfileAction($report, $action)
|
||||
{
|
||||
switch($action) {
|
||||
case 'ignore':
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'nsfw':
|
||||
if($report->object_type === 'App\Profile') {
|
||||
$profile = Profile::find($report->object_id);
|
||||
} else if($report->object_type === 'App\Status') {
|
||||
$status = Status::find($report->object_id);
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
$profile = Profile::find($status->profile_id);
|
||||
}
|
||||
|
||||
if(!$profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
|
||||
|
||||
$profile->cw = true;
|
||||
$profile->save();
|
||||
|
||||
foreach(Status::whereProfileId($profile->id)->cursor() as $status) {
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
PublicTimelineService::rem($status->id);
|
||||
}
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user(request()->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => 'cw',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'nsfw' => true,
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
if($report->object_type === 'App\Profile') {
|
||||
$profile = Profile::find($report->object_id);
|
||||
} else if($report->object_type === 'App\Status') {
|
||||
$status = Status::find($report->object_id);
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
$profile = Profile::find($status->profile_id);
|
||||
}
|
||||
|
||||
if(!$profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
|
||||
|
||||
$profile->unlisted = true;
|
||||
$profile->save();
|
||||
|
||||
foreach(Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) {
|
||||
$status->scope = 'unlisted';
|
||||
$status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
PublicTimelineService::rem($status->id);
|
||||
}
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user(request()->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => 'unlisted',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
if($report->object_type === 'App\Profile') {
|
||||
$profile = Profile::find($report->object_id);
|
||||
} else if($report->object_type === 'App\Status') {
|
||||
$status = Status::find($report->object_id);
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
$profile = Profile::find($status->profile_id);
|
||||
}
|
||||
|
||||
if(!$profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.');
|
||||
|
||||
$profile->unlisted = true;
|
||||
$profile->save();
|
||||
|
||||
foreach(Status::whereProfileId($profile->id)->cursor() as $status) {
|
||||
$status->scope = 'private';
|
||||
$status->visibility = 'private';
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
PublicTimelineService::rem($status->id);
|
||||
}
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user(request()->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => 'private',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if(config('pixelfed.account_deletion') == false) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if($report->object_type === 'App\Profile') {
|
||||
$profile = Profile::find($report->object_id);
|
||||
} else if($report->object_type === 'App\Status') {
|
||||
$status = Status::find($report->object_id);
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
$profile = Profile::find($status->profile_id);
|
||||
}
|
||||
|
||||
if(!$profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.');
|
||||
|
||||
$ts = now()->addMonth();
|
||||
|
||||
if($profile->user_id) {
|
||||
$user = $profile->user;
|
||||
abort_if($user->is_admin, 403, 'You cannot delete admin accounts.');
|
||||
$user->status = 'delete';
|
||||
$user->delete_after = $ts;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = $ts;
|
||||
$profile->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user(request()->user())
|
||||
->action('admin.user.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
|
||||
if($profile->user_id) {
|
||||
DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
|
||||
DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
|
||||
$user->email = $user->id;
|
||||
$user->password = '';
|
||||
$user->status = 'delete';
|
||||
$user->save();
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = now()->addMonth();
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
DeleteAccountPipeline::dispatch($user)->onQueue('high');
|
||||
} else {
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = now()->addMonth();
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
|
||||
}
|
||||
return [200];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function reportsHandleStatusAction($report, $action)
|
||||
{
|
||||
switch($action) {
|
||||
case 'ignore':
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'nsfw':
|
||||
$status = Status::find($report->object_id);
|
||||
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
|
||||
abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($status->profile_id)
|
||||
->objectId($status->profile_id)
|
||||
->objectType('App\Status::class')
|
||||
->user(request()->user())
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'cw',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'nsfw' => true,
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
$status = Status::find($report->object_id);
|
||||
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
|
||||
abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
|
||||
|
||||
$status->scope = 'private';
|
||||
$status->visibility = 'private';
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
PublicTimelineService::rem($status->id);
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($status->profile_id)
|
||||
->objectId($status->profile_id)
|
||||
->objectType('App\Status::class')
|
||||
->user(request()->user())
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'private',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
$status = Status::find($report->object_id);
|
||||
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
|
||||
abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.');
|
||||
|
||||
if($status->scope === 'public') {
|
||||
$status->scope = 'unlisted';
|
||||
$status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
PublicTimelineService::rem($status->id);
|
||||
}
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($status->profile_id)
|
||||
->objectId($status->profile_id)
|
||||
->objectType('App\Status::class')
|
||||
->user(request()->user())
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'unlist',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$status = Status::find($report->object_id);
|
||||
|
||||
if(!$status) {
|
||||
return [200];
|
||||
}
|
||||
|
||||
$profile = $status->profile;
|
||||
|
||||
abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.');
|
||||
|
||||
StatusService::del($status->id);
|
||||
|
||||
if($profile->user_id != null && $profile->domain == null) {
|
||||
PublicTimelineService::del($status->id);
|
||||
StatusDelete::dispatch($status)->onQueue('high');
|
||||
} else {
|
||||
NetworkTimelineService::del($status->id);
|
||||
DeleteRemoteStatusPipeline::dispatch($status)->onQueue('high');
|
||||
}
|
||||
|
||||
Report::whereObjectId($report->object_id)
|
||||
->whereObjectType($report->object_type)
|
||||
->update([
|
||||
'admin_seen' => now()
|
||||
]);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function reportsApiSpamAll(Request $request)
|
||||
{
|
||||
$tab = $request->input('tab', 'home');
|
||||
|
||||
$appeals = AdminSpamReport::collection(
|
||||
AccountInterstitial::orderBy('id', 'desc')
|
||||
->whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->cursorPaginate(6)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return $appeals;
|
||||
}
|
||||
|
||||
public function reportsApiSpamHandle(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
||||
abort_if(
|
||||
$action === 'delete-profile' &&
|
||||
!config('pixelfed.account_deletion'),
|
||||
404,
|
||||
"Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config."
|
||||
);
|
||||
|
||||
$report = AccountInterstitial::with('user')
|
||||
->whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($request->input('id'));
|
||||
|
||||
$this->reportsHandleSpamAction($report, $action);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id);
|
||||
PublicTimelineService::warmCache(true, 400);
|
||||
return [$action, $report];
|
||||
}
|
||||
|
||||
public function reportsHandleSpamAction($appeal, $action)
|
||||
{
|
||||
$meta = json_decode($appeal->meta);
|
||||
|
||||
if($action == 'mark-read') {
|
||||
$appeal->is_spam = true;
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
}
|
||||
|
||||
if($action == 'mark-not-spam') {
|
||||
$status = $appeal->status;
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
|
||||
$appeal->is_spam = false;
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
|
||||
if($action == 'mark-all-read') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereNull('appeal_handled_at')
|
||||
->whereUserId($appeal->user_id)
|
||||
->update([
|
||||
'appeal_handled_at' => now(),
|
||||
'is_spam' => true
|
||||
]);
|
||||
}
|
||||
|
||||
if($action == 'mark-all-not-spam') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereUserId($appeal->user_id)
|
||||
->get()
|
||||
->each(function($report) use($meta) {
|
||||
$report->is_spam = false;
|
||||
$report->appeal_handled_at = now();
|
||||
$report->save();
|
||||
$status = Status::find($report->item_id);
|
||||
if($status) {
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if($action == 'delete-profile') {
|
||||
$user = User::findOrFail($appeal->user_id);
|
||||
$profile = $user->profile;
|
||||
|
||||
if($user->is_admin == true) {
|
||||
$mid = request()->user()->id;
|
||||
abort_if($user->id < $mid, 403, 'You cannot delete an admin account.');
|
||||
}
|
||||
|
||||
$ts = now()->addMonth();
|
||||
$user->status = 'delete';
|
||||
$profile->status = 'delete';
|
||||
$user->delete_after = $ts;
|
||||
$profile->delete_after = $ts;
|
||||
$user->save();
|
||||
$profile->save();
|
||||
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($user->id)
|
||||
->objectId($user->id)
|
||||
->objectType('App\User::class')
|
||||
->user(request()->user())
|
||||
->action('admin.user.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
Cache::forget('profiles:private');
|
||||
DeleteAccountPipeline::dispatch($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function reportsApiSpamGet(Request $request, $id)
|
||||
{
|
||||
$report = AccountInterstitial::findOrFail($id);
|
||||
return new AdminSpamReport($report);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ trait AdminSettingsController
|
|||
$cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret'));
|
||||
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
|
||||
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null;
|
||||
$jpeg = in_array('image/jpg', $types) ? true : in_array('image/jpeg', $types);
|
||||
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
|
||||
$png = in_array('image/png', $types);
|
||||
$gif = in_array('image/gif', $types);
|
||||
$mp4 = in_array('video/mp4', $types);
|
||||
|
@ -140,7 +140,9 @@ trait AdminSettingsController
|
|||
'show_custom_css' => 'uikit.show_custom.css',
|
||||
'show_custom_js' => 'uikit.show_custom.js',
|
||||
'cloud_storage' => 'pixelfed.cloud_storage',
|
||||
'account_autofollow' => 'account.autofollow'
|
||||
'account_autofollow' => 'account.autofollow',
|
||||
'show_directory' => 'landing.show_directory',
|
||||
'show_explore_feed' => 'landing.show_explore_feed',
|
||||
];
|
||||
|
||||
foreach ($bools as $key => $value) {
|
||||
|
@ -241,16 +243,20 @@ trait AdminSettingsController
|
|||
];
|
||||
switch (config('database.default')) {
|
||||
case 'pgsql':
|
||||
$exp = DB::raw('select version();');
|
||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'Postgres',
|
||||
'version' => explode(' ', DB::select(DB::raw('select version();'))[0]->version)[1]
|
||||
'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
|
||||
];
|
||||
break;
|
||||
|
||||
case 'mysql':
|
||||
$exp = DB::raw('select version()');
|
||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$sys['database'] = [
|
||||
'name' => 'MySQL',
|
||||
'version' => DB::select( DB::raw("select version()") )[0]->{'version()'}
|
||||
'version' => DB::select($expQuery)[0]->{'version()'}
|
||||
];
|
||||
break;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use App\{
|
|||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
Story,
|
||||
User
|
||||
};
|
||||
|
@ -22,6 +23,7 @@ use Illuminate\Support\Facades\Redis;
|
|||
use App\Http\Controllers\Admin\{
|
||||
AdminDirectoryController,
|
||||
AdminDiscoverController,
|
||||
AdminHashtagsController,
|
||||
AdminInstanceController,
|
||||
AdminReportController,
|
||||
// AdminGroupsController,
|
||||
|
@ -43,6 +45,7 @@ class AdminController extends Controller
|
|||
use AdminReportController,
|
||||
AdminDirectoryController,
|
||||
AdminDiscoverController,
|
||||
AdminHashtagsController,
|
||||
// AdminGroupsController,
|
||||
AdminMediaController,
|
||||
AdminSettingsController,
|
||||
|
@ -201,12 +204,6 @@ class AdminController extends Controller
|
|||
return view('admin.apps.home', compact('apps'));
|
||||
}
|
||||
|
||||
public function hashtagsHome(Request $request)
|
||||
{
|
||||
$hashtags = Hashtag::orderByDesc('id')->paginate(10);
|
||||
return view('admin.hashtags.home', compact('hashtags'));
|
||||
}
|
||||
|
||||
public function messagesHome(Request $request)
|
||||
{
|
||||
$messages = Contact::orderByDesc('id')->paginate(10);
|
||||
|
@ -267,6 +264,10 @@ class AdminController extends Controller
|
|||
]);
|
||||
$changed = false;
|
||||
$changedFields = [];
|
||||
$slug = str_slug($request->input('title'));
|
||||
if(Newsroom::whereSlug($slug)->exists()) {
|
||||
$slug = $slug . '-' . str_random(4);
|
||||
}
|
||||
$news = Newsroom::findOrFail($id);
|
||||
$fields = [
|
||||
'title' => 'string',
|
||||
|
@ -284,7 +285,7 @@ class AdminController extends Controller
|
|||
case 'string':
|
||||
if($request->{$field} != $news->{$field}) {
|
||||
if($field == 'title') {
|
||||
$news->slug = str_slug($request->{$field});
|
||||
$news->slug = $slug;
|
||||
}
|
||||
$news->{$field} = $request->{$field};
|
||||
$changed = true;
|
||||
|
@ -330,6 +331,10 @@ class AdminController extends Controller
|
|||
]);
|
||||
$changed = false;
|
||||
$changedFields = [];
|
||||
$slug = str_slug($request->input('title'));
|
||||
if(Newsroom::whereSlug($slug)->exists()) {
|
||||
$slug = $slug . '-' . str_random(4);
|
||||
}
|
||||
$news = new Newsroom();
|
||||
$fields = [
|
||||
'title' => 'string',
|
||||
|
@ -347,7 +352,7 @@ class AdminController extends Controller
|
|||
case 'string':
|
||||
if($request->{$field} != $news->{$field}) {
|
||||
if($field == 'title') {
|
||||
$news->slug = str_slug($request->{$field});
|
||||
$news->slug = $slug;
|
||||
}
|
||||
$news->{$field} = $request->{$field};
|
||||
$changed = true;
|
||||
|
@ -459,7 +464,9 @@ class AdminController extends Controller
|
|||
->where('shortcode', 'like', '%' . $request->input('q') . '%')
|
||||
->orWhere('domain', 'like', '%' . $request->input('q') . '%');
|
||||
if(!$request->has('dups')) {
|
||||
$q = $q->groupBy('shortcode');
|
||||
if(!$pg) {
|
||||
$q = $q->groupBy('shortcode');
|
||||
}
|
||||
}
|
||||
return $q;
|
||||
}
|
||||
|
@ -518,7 +525,7 @@ class AdminController extends Controller
|
|||
->whereShortcode($request->input('shortcode'));
|
||||
})
|
||||
],
|
||||
'emoji' => 'required|file|mimetypes:jpg,png|max:' . (config('federation.custom_emoji.max_size') / 1000)
|
||||
'emoji' => 'required|file|mimes:jpg,png|max:' . (config('federation.custom_emoji.max_size') / 1000)
|
||||
]);
|
||||
|
||||
$emoji = new CustomEmoji;
|
||||
|
@ -527,7 +534,7 @@ class AdminController extends Controller
|
|||
$emoji->save();
|
||||
|
||||
$fileName = $emoji->id . '.' . $request->emoji->extension();
|
||||
$request->emoji->storeAs('public/emoji', $fileName);
|
||||
$request->emoji->storePubliclyAs('public/emoji', $fileName);
|
||||
$emoji->media_path = 'emoji/' . $fileName;
|
||||
$emoji->save();
|
||||
Cache::forget('pf:custom_emoji');
|
||||
|
|
243
app/Http/Controllers/AdminInviteController.php
Normal file
243
app/Http/Controllers/AdminInviteController.php
Normal file
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AdminInvite;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use Purify;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use App\Services\EmailService;
|
||||
use App\Http\Controllers\Auth\RegisterController;
|
||||
|
||||
class AdminInviteController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
abort_if(!config('instance.admin_invites.enabled'), 404);
|
||||
}
|
||||
|
||||
public function index(Request $request, $code)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
return view('invite.admin_invite', compact('code'));
|
||||
}
|
||||
|
||||
public function apiVerifyCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
|
||||
abort_if(!$invite, 404);
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
$res = [
|
||||
'message' => $invite->message,
|
||||
'max_uses' => $invite->max_uses,
|
||||
'sev' => $invite->skip_email_verification
|
||||
];
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function apiUsernameCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
'username' => 'required'
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
|
||||
abort_if(!$invite, 404);
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
|
||||
$usernameRules = [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if(ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if(($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if(!ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
$rules = ['username' => $usernameRules];
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
|
||||
if($validator->fails()) {
|
||||
return response()->json($validator->errors(), 400);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function apiEmailCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
'email' => 'required'
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
|
||||
abort_if(!$invite, 404);
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
|
||||
$emailRules = [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
$rules = ['email' => $emailRules];
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
|
||||
if($validator->fails()) {
|
||||
return response()->json($validator->errors(), 400);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function apiRegister(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required',
|
||||
'username' => [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if(ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if(($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if(!ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'password' => 'required',
|
||||
'password_confirm' => 'required'
|
||||
]);
|
||||
|
||||
$invite = AdminInvite::whereInviteCode($request->input('token'))->firstOrFail();
|
||||
abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite expired');
|
||||
abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
|
||||
|
||||
$invite->uses = $invite->uses + 1;
|
||||
|
||||
event(new Registered($user = User::create([
|
||||
'name' => Purify::clean($request->input('name')) ?? $request->input('username'),
|
||||
'username' => $request->input('username'),
|
||||
'email' => $request->input('email'),
|
||||
'password' => Hash::make($request->input('password')),
|
||||
])));
|
||||
|
||||
sleep(5);
|
||||
|
||||
$invite->used_by = array_merge($invite->used_by ?? [], [[
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username
|
||||
]]);
|
||||
$invite->save();
|
||||
|
||||
if($invite->skip_email_verification) {
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
}
|
||||
|
||||
if(Auth::attempt([
|
||||
'email' => $request->input('email'),
|
||||
'password' => $request->input('password')
|
||||
])) {
|
||||
$request->session()->regenerate();
|
||||
return redirect()->intended('/');
|
||||
} else {
|
||||
return response()->json([], 400);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,114 +5,581 @@ namespace App\Http\Controllers\Api;
|
|||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use Auth, Cache;
|
||||
use Auth, Cache, DB;
|
||||
use Carbon\Carbon;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Instance,
|
||||
Like,
|
||||
Media,
|
||||
Profile,
|
||||
Status
|
||||
Report,
|
||||
Status,
|
||||
User
|
||||
};
|
||||
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminStatsService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Services\ModLogService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Http\Resources\AdminInstance;
|
||||
use App\Http\Resources\AdminUser;
|
||||
|
||||
class AdminApiController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
public function supported(Request $request)
|
||||
{
|
||||
$this->middleware(['auth', 'admin']);
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
return response()->json(['supported' => true]);
|
||||
}
|
||||
|
||||
public function activity(Request $request)
|
||||
public function getStats(Request $request)
|
||||
{
|
||||
$activity = [];
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$limit = request()->input('limit', 20);
|
||||
|
||||
$activity['captions'] = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'rendered',
|
||||
'uri',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'created_at'
|
||||
)->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($limit);
|
||||
|
||||
$activity['comments'] = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'rendered',
|
||||
'uri',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'created_at'
|
||||
)->whereNotNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($limit);
|
||||
|
||||
return response()->json($activity, 200, [], JSON_PRETTY_PRINT);
|
||||
$res = AdminStatsService::summary();
|
||||
$res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->count();
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function moderateStatus(Request $request)
|
||||
public function autospam(Request $request)
|
||||
{
|
||||
abort(400, 'Unpublished API');
|
||||
return;
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$appeals = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->latest()
|
||||
->simplePaginate(6)
|
||||
->map(function($report) {
|
||||
$r = [
|
||||
'id' => $report->id,
|
||||
'type' => $report->type,
|
||||
'item_id' => $report->item_id,
|
||||
'item_type' => $report->item_type,
|
||||
'created_at' => $report->created_at
|
||||
];
|
||||
if($report->item_type === 'App\\Status') {
|
||||
$status = StatusService::get($report->item_id, false);
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
|
||||
$r['status'] = $status;
|
||||
|
||||
if($status['in_reply_to_id']) {
|
||||
$r['parent'] = StatusService::get($status['in_reply_to_id'], false);
|
||||
}
|
||||
}
|
||||
return $r;
|
||||
});
|
||||
|
||||
return $appeals;
|
||||
}
|
||||
|
||||
public function autospamHandle(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string|in:status,profile',
|
||||
'id' => 'required|integer|min:1',
|
||||
'action' => 'required|string|in:cw,unlink,unlist,suspend,delete'
|
||||
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all',
|
||||
'id' => 'required'
|
||||
]);
|
||||
|
||||
$type = $request->input('type');
|
||||
$id = $request->input('id');
|
||||
$action = $request->input('action');
|
||||
$id = $request->input('id');
|
||||
$appeal = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
$now = now();
|
||||
$res = ['status' => 'success'];
|
||||
$meta = json_decode($appeal->meta);
|
||||
|
||||
if ($type == 'status') {
|
||||
$status = Status::findOrFail($id);
|
||||
switch ($action) {
|
||||
case 'cw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
break;
|
||||
case 'unlink':
|
||||
$status->rendered = $status->caption;
|
||||
$status->save();
|
||||
break;
|
||||
case 'unlist':
|
||||
$status->scope = 'unlisted';
|
||||
$status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
break;
|
||||
if($action == 'dismiss') {
|
||||
$appeal->is_spam = true;
|
||||
$appeal->appeal_handled_at = $now;
|
||||
$appeal->save();
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if ($type == 'profile') {
|
||||
$profile = Profile::findOrFail($id);
|
||||
switch ($action) {
|
||||
|
||||
case 'delete':
|
||||
StatusDelete::dispatch($status);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
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 == 'dismiss-all') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereNull('appeal_handled_at')
|
||||
->whereUserId($appeal->user_id)
|
||||
->update(['appeal_handled_at' => $now, 'is_spam' => true]);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'approve') {
|
||||
$status = $appeal->status;
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
|
||||
$appeal->is_spam = false;
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
StatusService::del($status->id);
|
||||
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'approve-all') {
|
||||
AccountInterstitial::whereType('post.autospam')
|
||||
->whereItemType('App\Status')
|
||||
->whereNull('appeal_handled_at')
|
||||
->whereUserId($appeal->user_id)
|
||||
->get()
|
||||
->each(function($report) use($meta) {
|
||||
$report->is_spam = false;
|
||||
$report->appeal_handled_at = now();
|
||||
$report->save();
|
||||
$status = Status::find($report->item_id);
|
||||
if($status) {
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
StatusService::del($status->id, true);
|
||||
}
|
||||
});
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function modReports(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$reports = Report::whereNull('admin_seen')
|
||||
->orderBy('created_at','desc')
|
||||
->paginate(6)
|
||||
->map(function($report) {
|
||||
$r = [
|
||||
'id' => $report->id,
|
||||
'type' => $report->type,
|
||||
'message' => $report->message,
|
||||
'object_id' => $report->object_id,
|
||||
'object_type' => $report->object_type,
|
||||
'created_at' => $report->created_at
|
||||
];
|
||||
|
||||
if($report->profile_id) {
|
||||
$r['reported_by_account'] = AccountService::get($report->profile_id, true);
|
||||
}
|
||||
|
||||
if($report->object_type === 'App\\Status') {
|
||||
$status = StatusService::get($report->object_id, false);
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
|
||||
$r['status'] = $status;
|
||||
|
||||
if($status['in_reply_to_id']) {
|
||||
$r['parent'] = StatusService::get($status['in_reply_to_id'], false);
|
||||
}
|
||||
}
|
||||
|
||||
if($report->object_type === 'App\\Profile') {
|
||||
$r['account'] = AccountService::get($report->object_id, false);
|
||||
}
|
||||
return $r;
|
||||
})
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
return $reports;
|
||||
}
|
||||
|
||||
public function modReportHandle(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|string',
|
||||
'id' => 'required'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$id = $request->input('id');
|
||||
|
||||
$actions = [
|
||||
'ignore',
|
||||
'cw',
|
||||
'unlist'
|
||||
];
|
||||
|
||||
if (!in_array($action, $actions)) {
|
||||
return abort(403);
|
||||
}
|
||||
|
||||
$report = Report::findOrFail($id);
|
||||
$item = $report->reported();
|
||||
$report->admin_seen = now();
|
||||
|
||||
switch ($action) {
|
||||
case 'ignore':
|
||||
$report->not_interested = true;
|
||||
break;
|
||||
|
||||
case 'cw':
|
||||
Cache::forget('status:thumb:'.$item->id);
|
||||
$item->is_nsfw = true;
|
||||
$item->save();
|
||||
$report->nsfw = true;
|
||||
StatusService::del($item->id, true);
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
$item->visibility = 'unlisted';
|
||||
$item->save();
|
||||
StatusService::del($item->id, true);
|
||||
break;
|
||||
|
||||
default:
|
||||
$report->admin_seen = null;
|
||||
break;
|
||||
}
|
||||
|
||||
$report->save();
|
||||
Cache::forget('admin-dash:reports:list-cache');
|
||||
Cache::forget('admin:dashboard:home:data:v0:15min');
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
public function getConfiguration(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
return collect([
|
||||
[
|
||||
'name' => 'ActivityPub Federation',
|
||||
'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
|
||||
'key' => 'federation.activitypub.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Open Registration',
|
||||
'description' => 'Allow new account registrations.',
|
||||
'key' => 'pixelfed.open_registration'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Stories',
|
||||
'description' => 'Enable the ephemeral Stories feature.',
|
||||
'key' => 'instance.stories.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Require Email Verification',
|
||||
'description' => 'Require new accounts to verify their email address.',
|
||||
'key' => 'pixelfed.enforce_email_verification'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'AutoSpam Detection',
|
||||
'description' => 'Detect and remove spam from public timelines.',
|
||||
'key' => 'pixelfed.bouncer.enabled'
|
||||
],
|
||||
])
|
||||
->map(function($s) {
|
||||
$s['state'] = (bool) config_cache($s['key']);
|
||||
return $s;
|
||||
});
|
||||
}
|
||||
|
||||
public function updateConfiguration(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
$this->validate($request, [
|
||||
'key' => 'required',
|
||||
'value' => 'required'
|
||||
]);
|
||||
|
||||
$allowedKeys = [
|
||||
'federation.activitypub.enabled',
|
||||
'pixelfed.open_registration',
|
||||
'instance.stories.enabled',
|
||||
'pixelfed.enforce_email_verification',
|
||||
'pixelfed.bouncer.enabled',
|
||||
];
|
||||
|
||||
$key = $request->input('key');
|
||||
$value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
abort_if(!in_array($key, $allowedKeys), 400, 'Invalid cache key.');
|
||||
|
||||
ConfigCacheService::put($key, $value);
|
||||
|
||||
return collect([
|
||||
[
|
||||
'name' => 'ActivityPub Federation',
|
||||
'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
|
||||
'key' => 'federation.activitypub.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Open Registration',
|
||||
'description' => 'Allow new account registrations.',
|
||||
'key' => 'pixelfed.open_registration'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Stories',
|
||||
'description' => 'Enable the ephemeral Stories feature.',
|
||||
'key' => 'instance.stories.enabled'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'Require Email Verification',
|
||||
'description' => 'Require new accounts to verify their email address.',
|
||||
'key' => 'pixelfed.enforce_email_verification'
|
||||
],
|
||||
|
||||
[
|
||||
'name' => 'AutoSpam Detection',
|
||||
'description' => 'Detect and remove spam from public timelines.',
|
||||
'key' => 'pixelfed.bouncer.enabled'
|
||||
],
|
||||
])
|
||||
->map(function($s) {
|
||||
$s['state'] = (bool) config_cache($s['key']);
|
||||
return $s;
|
||||
});
|
||||
}
|
||||
|
||||
public function getUsers(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
$q = $request->input('q');
|
||||
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
$res = User::whereNull('status')
|
||||
->when($q, function($query, $q) {
|
||||
return $query->where('username', 'like', '%' . $q . '%');
|
||||
})
|
||||
->orderBy('id', $sort)
|
||||
->cursorPaginate(10);
|
||||
return AdminUser::collection($res);
|
||||
}
|
||||
|
||||
public function getUser(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$id = $request->input('user_id');
|
||||
$user = User::findOrFail($id);
|
||||
$profile = $user->profile;
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
return (new AdminUser($user))->additional(['meta' => [
|
||||
'account' => $account,
|
||||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
]]);
|
||||
}
|
||||
|
||||
public function userAdminAction(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email',
|
||||
'value' => 'sometimes'
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$user = User::findOrFail($id);
|
||||
$profile = Profile::findOrFail($user->profile_id);
|
||||
$action = $request->input('action');
|
||||
|
||||
abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
|
||||
|
||||
if($action === 'refresh_stats') {
|
||||
$profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count();
|
||||
$profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
|
||||
$statusCount = Status::whereProfileId($user->profile_id)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', ['public', 'unlisted', 'private'])
|
||||
->count();
|
||||
$profile->status_count = $statusCount;
|
||||
$profile->save();
|
||||
} else if($action === 'verify_email') {
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($user->id)
|
||||
->objectId($user->id)
|
||||
->objectType('App\User::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => 'Manually verified email address',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
} else {
|
||||
$profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
$profile->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($user->id)
|
||||
->objectId($user->id)
|
||||
->objectType('App\User::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
}
|
||||
|
||||
AccountService::del($user->profile_id);
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
|
||||
return (new AdminUser($user))->additional(['meta' => [
|
||||
'account' => $account,
|
||||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
]]);
|
||||
}
|
||||
|
||||
public function instances(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'sometimes',
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
'sort_by' => 'sometimes|in:id,status_count,user_count,domain',
|
||||
'filter' => 'sometimes|in:all,unlisted,auto_cw,banned',
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
$sortBy = $request->input('sort_by', 'id');
|
||||
$filter = $request->input('filter');
|
||||
|
||||
$res = Instance::when($q, function($query, $q) {
|
||||
return $query->where('domain', 'like', '%' . $q . '%');
|
||||
})
|
||||
->when($filter, function($query, $filter) {
|
||||
if($filter === 'all') {
|
||||
return $query;
|
||||
} else {
|
||||
return $query->where($filter, true);
|
||||
}
|
||||
})
|
||||
->when($sortBy, function($query, $sortBy) use($sort) {
|
||||
return $query->orderBy($sortBy, $sort);
|
||||
}, function($query) {
|
||||
return $query->orderBy('id', 'desc');
|
||||
})
|
||||
->cursorPaginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return AdminInstance::collection($res);
|
||||
}
|
||||
|
||||
public function getInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$id = $request->input('id');
|
||||
$res = Instance::findOrFail($id);
|
||||
|
||||
return new AdminInstance($res);
|
||||
}
|
||||
|
||||
public function moderateInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'key' => 'required|in:unlisted,auto_cw,banned',
|
||||
'value' => 'required'
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$key = $request->input('key');
|
||||
$value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
$res = Instance::findOrFail($id);
|
||||
$res->{$key} = $value;
|
||||
$res->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
NetworkTimelineService::warmCache(true);
|
||||
|
||||
return new AdminInstance($res);
|
||||
}
|
||||
|
||||
public function refreshInstanceStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
]);
|
||||
|
||||
$id = $request->input('id');
|
||||
$instance = Instance::findOrFail($id);
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->status_count = Profile::whereDomain($instance->domain)->leftJoin('statuses', 'profiles.id', '=', 'statuses.profile_id')->count();
|
||||
$instance->save();
|
||||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -96,89 +96,6 @@ class BaseApiController extends Controller
|
|||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accounts(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$profile = Profile::findOrFail($id);
|
||||
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowers(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$profile = Profile::findOrFail($id);
|
||||
$followers = $profile->followers;
|
||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowing(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$profile = Profile::findOrFail($id);
|
||||
$following = $profile->following;
|
||||
$resource = new Fractal\Resource\Collection($following, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountStatuses(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'only_media' => 'nullable',
|
||||
'pinned' => 'nullable',
|
||||
'exclude_replies' => 'nullable',
|
||||
'max_id' => 'nullable|integer|min:1',
|
||||
'since_id' => 'nullable|integer|min:1',
|
||||
'min_id' => 'nullable|integer|min:1',
|
||||
'limit' => 'nullable|integer|min:1|max:24'
|
||||
]);
|
||||
$limit = $request->limit ?? 20;
|
||||
$max_id = $request->max_id ?? false;
|
||||
$min_id = $request->min_id ?? false;
|
||||
$since_id = $request->since_id ?? false;
|
||||
$only_media = $request->only_media ?? false;
|
||||
$user = Auth::user();
|
||||
$account = Profile::whereNull('status')->findOrFail($id);
|
||||
$statuses = $account->statuses()->getQuery();
|
||||
if($only_media == true) {
|
||||
$statuses = $statuses
|
||||
->whereIn('scope', ['public','unlisted'])
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id');
|
||||
}
|
||||
if($id == $account->id && !$max_id && !$min_id && !$since_id) {
|
||||
$statuses = $statuses->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
} else if($since_id) {
|
||||
$statuses = $statuses->where('id', '>', $since_id)
|
||||
->orderBy('id', 'DESC')
|
||||
->paginate($limit);
|
||||
} else if($min_id) {
|
||||
$statuses = $statuses->where('id', '>', $min_id)
|
||||
->orderBy('id', 'ASC')
|
||||
->paginate($limit);
|
||||
} else if($max_id) {
|
||||
$statuses = $statuses->where('id', '<', $max_id)
|
||||
->orderBy('id', 'DESC')
|
||||
->paginate($limit);
|
||||
} else {
|
||||
$statuses = $statuses->whereScope('public')->orderBy('id', 'desc')->paginate($limit);
|
||||
}
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function avatarUpdate(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
@ -195,7 +112,7 @@ class BaseApiController extends Controller
|
|||
$name = $path['name'];
|
||||
$public = $path['storage'];
|
||||
$currentAvatar = storage_path('app/'.$profile->avatar->media_path);
|
||||
$loc = $request->file('upload')->storeAs($public, $name);
|
||||
$loc = $request->file('upload')->storePubliclyAs($public, $name);
|
||||
|
||||
$avatar = Avatar::whereProfileId($profile->id)->firstOrFail();
|
||||
$opath = $avatar->media_path;
|
||||
|
@ -215,21 +132,6 @@ class BaseApiController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
public function showTempMedia(Request $request, $profileId, $mediaId, $timestamp)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
|
||||
public function uploadMedia(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
|
||||
public function deleteMedia(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
|
||||
public function verifyCredentials(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
@ -242,21 +144,6 @@ class BaseApiController extends Controller
|
|||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function drafts(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$medias = Media::whereUserId($user->id)
|
||||
->whereNull('status_id')
|
||||
->latest()
|
||||
->take(13)
|
||||
->get();
|
||||
$resource = new Fractal\Resource\Collection($medias, new MediaDraftTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function accountLikes(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
|
|
@ -14,16 +14,9 @@ use Auth, Cache;
|
|||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Util\Site\Config;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\SuggestionService;
|
||||
|
||||
class ApiController extends BaseApiController
|
||||
{
|
||||
// todo: deprecate and remove
|
||||
public function hydrateLikes(Request $request)
|
||||
{
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function siteConfiguration(Request $request)
|
||||
{
|
||||
return response()->json(Config::get());
|
||||
|
@ -31,79 +24,6 @@ class ApiController extends BaseApiController
|
|||
|
||||
public function userRecommendations(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
abort_if(!config('exp.rec'), 400);
|
||||
|
||||
$id = Auth::user()->profile->id;
|
||||
|
||||
$following = Cache::remember('profile:following:'.$id, now()->addHours(12), function() use ($id) {
|
||||
return Follower::whereProfileId($id)->pluck('following_id')->toArray();
|
||||
});
|
||||
array_push($following, $id);
|
||||
$ids = SuggestionService::get();
|
||||
$filters = UserFilter::whereUserId($id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')->toArray();
|
||||
$following = array_merge($following, $filters);
|
||||
|
||||
$key = config('cache.prefix').':api:local:exp:rec:'.$id;
|
||||
$ttl = (int) Redis::ttl($key);
|
||||
|
||||
if($request->filled('refresh') == true && (290 > $ttl) == true) {
|
||||
Cache::forget('api:local:exp:rec:'.$id);
|
||||
}
|
||||
|
||||
$res = Cache::remember('api:local:exp:rec:'.$id, now()->addMinutes(5), function() use($id, $following, $ids) {
|
||||
return Profile::select(
|
||||
'id',
|
||||
'username'
|
||||
)
|
||||
->whereNotIn('id', $following)
|
||||
->whereIn('id', $ids)
|
||||
->whereIsPrivate(0)
|
||||
->whereNull('status')
|
||||
->whereNull('domain')
|
||||
->inRandomOrder()
|
||||
->take(3)
|
||||
->get()
|
||||
->map(function($item, $key) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'avatar' => $item->avatarUrl(),
|
||||
'username' => $item->username,
|
||||
'message' => 'Recommended for You'
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
return response()->json($res->all());
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function composeLocationSearch(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|max:100'
|
||||
]);
|
||||
$q = filter_var($request->input('q'), FILTER_SANITIZE_STRING);
|
||||
$hash = hash('sha256', $q);
|
||||
$key = 'search:location:id:' . $hash;
|
||||
$places = Cache::remember($key, now()->addMinutes(15), function() use($q) {
|
||||
$q = '%' . $q . '%';
|
||||
return Place::where('name', 'like', $q)
|
||||
->take(80)
|
||||
->get()
|
||||
->map(function($r) {
|
||||
return [
|
||||
'id' => $r->id,
|
||||
'name' => $r->name,
|
||||
'country' => $r->country,
|
||||
'url' => $r->url()
|
||||
];
|
||||
});
|
||||
});
|
||||
return $places;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ namespace App\Http\Controllers\Auth;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
use App\Services\BouncerService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
|
@ -29,4 +31,74 @@ class ForgotPasswordController extends Controller
|
|||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the form to request a password reset link.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showLinkRequestForm()
|
||||
{
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_logins')) {
|
||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||
}
|
||||
|
||||
usleep(random_int(100000, 300000));
|
||||
|
||||
return view('auth.passwords.email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the email for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function validateEmail(Request $request)
|
||||
{
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_logins')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
usleep(random_int(100000, 3000000));
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
$rules = [
|
||||
'email' => 'required|email',
|
||||
'h-captcha-response' => 'required|captcha'
|
||||
];
|
||||
} else {
|
||||
$rules = [
|
||||
'email' => 'required|email'
|
||||
];
|
||||
}
|
||||
|
||||
$request->validate($rules, [
|
||||
'h-captcha-response' => 'Failed to validate the captcha.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a failed password reset link.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function sendResetLinkFailedResponse(Request $request, $response)
|
||||
{
|
||||
if ($request->wantsJson()) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans($response)],
|
||||
]);
|
||||
}
|
||||
|
||||
return back()
|
||||
->withInput($request->only('email'))
|
||||
->withErrors([
|
||||
'email' => trans($response),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\AccountLog;
|
|||
use App\Http\Controllers\Controller;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use App\Services\BouncerService;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
|
@ -42,6 +43,15 @@ class LoginController extends Controller
|
|||
$this->middleware('guest')->except('logout');
|
||||
}
|
||||
|
||||
public function showLoginForm()
|
||||
{
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_logins')) {
|
||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||
}
|
||||
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user login request.
|
||||
*
|
||||
|
@ -51,6 +61,10 @@ class LoginController extends Controller
|
|||
*/
|
||||
public function validateLogin($request)
|
||||
{
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_logins')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
$this->username() => 'required|email',
|
||||
'password' => 'required|string|min:6',
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\User;
|
||||
use Purify;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
@ -11,6 +12,7 @@ use Illuminate\Support\Facades\Validator;
|
|||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\BouncerService;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
|
@ -157,10 +159,11 @@ class RegisterController extends Controller
|
|||
}
|
||||
|
||||
return User::create([
|
||||
'name' => $data['name'],
|
||||
'name' => Purify::clean($data['name']),
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'app_register_ip' => request()->ip()
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -172,9 +175,16 @@ class RegisterController extends Controller
|
|||
public function showRegistrationForm()
|
||||
{
|
||||
if(config_cache('pixelfed.open_registration')) {
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||
}
|
||||
$limit = config('pixelfed.max_users');
|
||||
if($limit) {
|
||||
abort_if($limit <= User::count(), 404);
|
||||
$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');
|
||||
|
@ -194,11 +204,15 @@ class RegisterController extends Controller
|
|||
{
|
||||
abort_if(config_cache('pixelfed.open_registration') == false, 400);
|
||||
|
||||
$count = User::count();
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
|
||||
if(false == config_cache('pixelfed.open_registration') || $limit && $limit <= $count) {
|
||||
return abort(403);
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
}
|
||||
|
||||
$this->validator($request->all())->validate();
|
||||
|
|
|
@ -4,6 +4,10 @@ namespace App\Http\Controllers\Auth;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\BouncerService;
|
||||
use Illuminate\Validation\Rules;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
|
@ -36,4 +40,123 @@ class ResetPasswordController extends Controller
|
|||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password reset validation rules.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
usleep(random_int(100000, 3000000));
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
return [
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => ['required', 'confirmed', 'max:72', Rules\Password::defaults()],
|
||||
'h-captcha-response' => ['required' ,'filled', 'captcha']
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => ['required', 'confirmed', 'max:72', Rules\Password::defaults()],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password reset validation error messages.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function validationErrorMessages()
|
||||
{
|
||||
return [
|
||||
'password.max' => 'Passwords should not exceed 72 characters.',
|
||||
'h-captcha-response.required' => 'Failed to validate the captcha.',
|
||||
'h-captcha-response.filled' => 'Failed to validate the captcha.',
|
||||
'h-captcha-response.captcha' => 'Failed to validate the captcha.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the password reset view for the given token.
|
||||
*
|
||||
* If no token is present, display the link request form.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showResetForm(Request $request)
|
||||
{
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_logins')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
usleep(random_int(100000, 300000));
|
||||
|
||||
$token = $request->route()->parameter('token');
|
||||
|
||||
return view('auth.passwords.reset')->with(
|
||||
['token' => $token, 'email' => $request->email]
|
||||
);
|
||||
}
|
||||
|
||||
public function reset(Request $request)
|
||||
{
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_logins')) {
|
||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
$request->validate($this->rules(), $this->validationErrorMessages());
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$response = $this->broker()->reset(
|
||||
$this->credentials($request), function ($user, $password) {
|
||||
$this->resetPassword($user, $password);
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
return $response == Password::PASSWORD_RESET
|
||||
? $this->sendResetResponse($request, $response)
|
||||
: $this->sendResetFailedResponse($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password reset credentials from the request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
protected function credentials(Request $request)
|
||||
{
|
||||
return $request->only(
|
||||
'email', 'password', 'password_confirmation', 'token'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a failed password reset.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
|
||||
*/
|
||||
protected function sendResetFailedResponse(Request $request, $response)
|
||||
{
|
||||
if ($request->wantsJson()) {
|
||||
throw ValidationException::withMessages(['email' => [trans($response)]]);
|
||||
}
|
||||
return redirect()->back()
|
||||
->withInput($request->only('email'))
|
||||
->withErrors(['email' => [trans($response)]]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class AvatarController extends Controller
|
|||
$dir = $path['root'];
|
||||
$name = $path['name'];
|
||||
$public = $path['storage'];
|
||||
$loc = $request->file('avatar')->storeAs($public, $name);
|
||||
$loc = $request->file('avatar')->storePubliclyAs($public, $name);
|
||||
|
||||
$avatar = Avatar::firstOrNew(['profile_id' => $profile->id]);
|
||||
$currentAvatar = $avatar->recentlyCreated ? null : storage_path('app/'.$profile->avatar->media_path);
|
||||
|
|
|
@ -7,40 +7,61 @@ use App\Status;
|
|||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Services\FollowerService;
|
||||
|
||||
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'));
|
||||
$profile = Auth::user()->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
);
|
||||
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 (!$bookmark->wasRecentlyCreated) {
|
||||
BookmarkService::del($profile->id, $status->id);
|
||||
$bookmark->delete();
|
||||
} else {
|
||||
BookmarkService::add($profile->id, $status->id);
|
||||
}
|
||||
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()) {
|
||||
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
||||
} else {
|
||||
$response = redirect()->back();
|
||||
}
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,7 +135,6 @@ class CollectionController extends Controller
|
|||
|
||||
$collection = Collection::whereProfileId($profileId)->findOrFail($collectionId);
|
||||
$count = $collection->items()->count();
|
||||
CollectionService::deleteCollection($collection->id);
|
||||
|
||||
if($count) {
|
||||
CollectionItem::whereCollectionId($collection->id)
|
||||
|
|
|
@ -41,6 +41,7 @@ use App\Jobs\VideoPipeline\{
|
|||
VideoThumbnail
|
||||
};
|
||||
use App\Services\AccountService;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
|
@ -75,13 +76,16 @@ class ComposeController extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'file.*' => function() {
|
||||
return [
|
||||
'required',
|
||||
'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'),
|
||||
],
|
||||
'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'
|
||||
]);
|
||||
|
@ -119,7 +123,7 @@ class ComposeController extends Controller
|
|||
abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->store($storagePath);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$mime = $photo->getMimeType();
|
||||
|
||||
|
@ -145,11 +149,11 @@ class ComposeController extends Controller
|
|||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
case 'image/webp':
|
||||
ImageOptimize::dispatch($media);
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
break;
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media);
|
||||
VideoThumbnail::dispatch($media)->onQueue('mmo');
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
|
@ -205,11 +209,11 @@ class ComposeController extends Controller
|
|||
$name = last($fragments);
|
||||
array_pop($fragments);
|
||||
$dir = implode('/', $fragments);
|
||||
$path = $photo->storeAs($dir, $name);
|
||||
$path = $photo->storePubliclyAs($dir, $name);
|
||||
$res = [
|
||||
'url' => $media->url() . '?v=' . time()
|
||||
];
|
||||
ImageOptimize::dispatch($media);
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
Cache::forget($limitKey);
|
||||
return $res;
|
||||
}
|
||||
|
@ -317,14 +321,30 @@ class ComposeController extends Controller
|
|||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$pid, 400);
|
||||
$q = filter_var($request->input('q'), FILTER_SANITIZE_STRING);
|
||||
$hash = hash('sha256', $q);
|
||||
$key = 'pf:search:location:v1:id:' . $hash;
|
||||
$popular = Cache::remember('pf:search:location:v1:popular', 86400, function() {
|
||||
if(config('database.default') != 'mysql') {
|
||||
return [];
|
||||
}
|
||||
$q = e($request->input('q'));
|
||||
|
||||
$popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() {
|
||||
$minId = SnowflakeService::byDate(now()->subDays(290));
|
||||
if(config('database.default') == 'pgsql') {
|
||||
return Status::selectRaw('id, place_id, count(place_id) as pc')
|
||||
->whereNotNull('place_id')
|
||||
->where('id', '>', $minId)
|
||||
->orderByDesc('pc')
|
||||
->groupBy(['place_id', 'id'])
|
||||
->limit(400)
|
||||
->get()
|
||||
->filter(function($post) {
|
||||
return $post;
|
||||
})
|
||||
->map(function($place) {
|
||||
return [
|
||||
'id' => $place->place_id,
|
||||
'count' => $place->pc
|
||||
];
|
||||
})
|
||||
->unique('id')
|
||||
->values();
|
||||
}
|
||||
return Status::selectRaw('id, place_id, count(place_id) as pc')
|
||||
->whereNotNull('place_id')
|
||||
->where('id', '>', $minId)
|
||||
|
@ -342,30 +362,30 @@ class ComposeController extends Controller
|
|||
];
|
||||
});
|
||||
});
|
||||
$places = Cache::remember($key, 900, function() use($q, $popular) {
|
||||
$q = '%' . $q . '%';
|
||||
return DB::table('places')
|
||||
->where('name', 'like', $q)
|
||||
->limit((strlen($q) > 5 ? 360 : 180))
|
||||
->get()
|
||||
->sortByDesc(function($place, $key) use($popular) {
|
||||
return $popular->filter(function($p) use($place) {
|
||||
return $p['id'] == $place->id;
|
||||
})->map(function($p) use($place) {
|
||||
return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
|
||||
})->values();
|
||||
})
|
||||
->map(function($r) {
|
||||
return [
|
||||
'id' => $r->id,
|
||||
'name' => $r->name,
|
||||
'country' => $r->country,
|
||||
'url' => url('/discover/places/' . $r->id . '/' . $r->slug)
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
});
|
||||
$q = '%' . $q . '%';
|
||||
$wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like';
|
||||
|
||||
$places = DB::table('places')
|
||||
->where('name', $wildcard, $q)
|
||||
->limit((strlen($q) > 5 ? 360 : 30))
|
||||
->get()
|
||||
->sortByDesc(function($place, $key) use($popular) {
|
||||
return $popular->filter(function($p) use($place) {
|
||||
return $p['id'] == $place->id;
|
||||
})->map(function($p) use($place) {
|
||||
return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
|
||||
})->values();
|
||||
})
|
||||
->map(function($r) {
|
||||
return [
|
||||
'id' => $r->id,
|
||||
'name' => $r->name,
|
||||
'country' => $r->country,
|
||||
'url' => url('/discover/places/' . $r->id . '/' . $r->slug)
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
return $places;
|
||||
}
|
||||
|
||||
|
@ -508,12 +528,7 @@ class ComposeController extends Controller
|
|||
$m->license = $license;
|
||||
$m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
|
||||
$m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
|
||||
// if($optimize_media == false) {
|
||||
// $m->skip_optimize = true;
|
||||
// ImageThumbnail::dispatch($m);
|
||||
// } else {
|
||||
// ImageOptimize::dispatch($m);
|
||||
// }
|
||||
|
||||
if($cw == true || $profile->cw == true) {
|
||||
$m->is_nsfw = $cw;
|
||||
$status->is_nsfw = $cw;
|
||||
|
@ -546,6 +561,7 @@ class ComposeController extends Controller
|
|||
$status->caption = strip_tags($request->caption);
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$status->scope = 'draft';
|
||||
$status->visibility = 'draft';
|
||||
$status->profile_id = $profile->id;
|
||||
$status->save();
|
||||
|
||||
|
@ -582,13 +598,24 @@ class ComposeController extends Controller
|
|||
$collections = Collection::whereProfileId($profile->id)
|
||||
->find($request->input('collections'))
|
||||
->each(function($collection) use($status) {
|
||||
$count = $collection->items()->count();
|
||||
CollectionItem::firstOrCreate([
|
||||
'collection_id' => $collection->id,
|
||||
'object_type' => 'App\Status',
|
||||
'object_id' => $status->id
|
||||
], [
|
||||
'order' => $collection->items()->count()
|
||||
'order' => $count
|
||||
]);
|
||||
|
||||
CollectionService::addItem(
|
||||
$collection->id,
|
||||
$status->id,
|
||||
$count
|
||||
);
|
||||
|
||||
$collection->updated_at = now();
|
||||
$collection->save();
|
||||
CollectionService::setCollection($collection->id, $collection);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use Illuminate\Http\Request;
|
|||
use Auth;
|
||||
use App\Contact;
|
||||
use App\Jobs\ContactPipeline\ContactPipeline;
|
||||
use App\Rules\MaxMultiLine;
|
||||
|
||||
class ContactController extends Controller
|
||||
{
|
||||
|
@ -21,7 +22,7 @@ class ContactController extends Controller
|
|||
abort_if(!Auth::check(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'message' => 'required|string|min:5|max:500',
|
||||
'message' => ['required', 'string', 'min:5', new MaxMultiLine('500')],
|
||||
'request_response' => 'string|max:3'
|
||||
]);
|
||||
|
||||
|
@ -45,7 +46,7 @@ class ContactController extends Controller
|
|||
$contact->response = '';
|
||||
$contact->save();
|
||||
|
||||
ContactPipeline::dispatchNow($contact);
|
||||
ContactPipeline::dispatch($contact);
|
||||
|
||||
return redirect()->back()->with('status', 'Success - Your message has been sent to admins.');
|
||||
}
|
||||
|
|
|
@ -602,7 +602,7 @@ class DirectMessageController extends Controller
|
|||
}
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2) . Str::random(8);
|
||||
$path = $photo->store($storagePath);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
|
|
@ -24,6 +24,7 @@ use App\Services\ReblogService;
|
|||
use App\Services\StatusHashtagService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\TrendingHashtagService;
|
||||
use App\Services\UserFilterService;
|
||||
|
||||
class DiscoverController extends Controller
|
||||
|
@ -40,6 +41,7 @@ class DiscoverController extends Controller
|
|||
|
||||
$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'));
|
||||
|
@ -52,14 +54,22 @@ class DiscoverController extends Controller
|
|||
|
||||
$this->validate($request, [
|
||||
'hashtag' => 'required|string|min:1|max:124',
|
||||
'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 10)
|
||||
'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3)
|
||||
]);
|
||||
|
||||
$page = $request->input('page') ?? '1';
|
||||
$end = $page > 1 ? $page * 9 : 0;
|
||||
$tag = $request->input('hashtag');
|
||||
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
if(config('database.default') === 'pgsql') {
|
||||
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
|
||||
} else {
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
}
|
||||
|
||||
if($hashtag->is_banned == true) {
|
||||
return [];
|
||||
}
|
||||
if($user) {
|
||||
$res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id);
|
||||
}
|
||||
|
@ -181,23 +191,7 @@ class DiscoverController extends Controller
|
|||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$res = Cache::remember('api:discover:v1.1:trending:hashtags', 3600, function() {
|
||||
return StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
|
||||
->groupBy('hashtag_id')
|
||||
->orderBy('total','desc')
|
||||
->where('created_at', '>', now()->subDays(90))
|
||||
->take(9)
|
||||
->get()
|
||||
->map(function($h) {
|
||||
$hashtag = $h->hashtag;
|
||||
return [
|
||||
'id' => $hashtag->id,
|
||||
'total' => $h->total,
|
||||
'name' => '#'.$hashtag->name,
|
||||
'url' => $hashtag->url()
|
||||
];
|
||||
});
|
||||
});
|
||||
$res = TrendingHashtagService::getTrending();
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ use App\Util\ActivityPub\{
|
|||
Outbox
|
||||
};
|
||||
use Zttp\Zttp;
|
||||
use App\Services\InstanceService;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
|
@ -56,12 +57,35 @@ class FederationController extends Controller
|
|||
}
|
||||
|
||||
$resource = $request->input('resource');
|
||||
$domain = config('pixelfed.domain.app');
|
||||
|
||||
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);
|
||||
}
|
||||
$domain = config('pixelfed.domain.app');
|
||||
if(strpos($resource, $domain) == false) {
|
||||
return response('', 400);
|
||||
}
|
||||
|
@ -94,20 +118,18 @@ class FederationController extends Controller
|
|||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.outbox'), 404);
|
||||
|
||||
// $profile = Profile::whereNull('domain')
|
||||
// ->whereNull('status')
|
||||
// ->whereIsPrivate(false)
|
||||
// ->whereUsername($username)
|
||||
// ->firstOrFail();
|
||||
if(!$request->wantsJson()) {
|
||||
return redirect('/' . $username);
|
||||
}
|
||||
|
||||
// $key = 'ap:outbox:latest_10:pid:' . $profile->id;
|
||||
// $ttl = now()->addMinutes(15);
|
||||
// $res = Cache::remember($key, $ttl, function() use($profile) {
|
||||
// return Outbox::get($profile);
|
||||
// });
|
||||
$res = [];
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox',
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
|
@ -119,38 +141,43 @@ class FederationController extends Controller
|
|||
|
||||
$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(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if(!isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
usleep(5000);
|
||||
$lockKey = 'pf:ap:del-lock:' . hash('sha256', $obj['id']);
|
||||
if( isset($obj['actor']) &&
|
||||
isset($obj['object']) &&
|
||||
isset($obj['id']) &&
|
||||
is_string($obj['id']) &&
|
||||
is_string($obj['actor']) &&
|
||||
is_string($obj['object']) &&
|
||||
$obj['actor'] == $obj['object']
|
||||
) {
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Cache::put($lockKey, 1, 3600);
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
return;
|
||||
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
if(!isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
usleep(5000);
|
||||
$lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $obj['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
return;
|
||||
}
|
||||
Cache::put($lockKey, 1, 3600);
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
|
@ -163,29 +190,47 @@ class FederationController extends Controller
|
|||
|
||||
$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(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if(!isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
$lockKey = 'pf:ap:del-lock:' . hash('sha256', $obj['id']);
|
||||
if( isset($obj['actor']) &&
|
||||
isset($obj['object']) &&
|
||||
isset($obj['id']) &&
|
||||
is_string($obj['id']) &&
|
||||
is_string($obj['actor']) &&
|
||||
is_string($obj['object']) &&
|
||||
$obj['actor'] == $obj['object']
|
||||
) {
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Cache::put($lockKey, 1, 3600);
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
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('high');
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -194,15 +239,6 @@ class FederationController extends Controller
|
|||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
|
||||
if($profile->status != null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
|
@ -217,15 +253,6 @@ class FederationController extends Controller
|
|||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
|
||||
if($profile->status != null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
|
|
|
@ -23,109 +23,7 @@ class FollowerController extends Controller
|
|||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|string',
|
||||
'force' => 'nullable|boolean',
|
||||
]);
|
||||
$force = (bool) $request->input('force', true);
|
||||
$item = (int) $request->input('item');
|
||||
$url = $this->handleFollowRequest($item, $force);
|
||||
if($request->wantsJson() == true) {
|
||||
return response()->json(200);
|
||||
} else {
|
||||
return redirect($url);
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleFollowRequest($item, $force)
|
||||
{
|
||||
$user = Auth::user()->profile;
|
||||
|
||||
$target = Profile::where('id', '!=', $user->id)->whereNull('status')->findOrFail($item);
|
||||
$private = (bool) $target->is_private;
|
||||
$remote = (bool) $target->domain;
|
||||
$blocked = UserFilter::whereUserId($target->id)
|
||||
->whereFilterType('block')
|
||||
->whereFilterableId($user->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->exists();
|
||||
|
||||
if($blocked == true) {
|
||||
abort(400, 'You cannot follow this user.');
|
||||
}
|
||||
|
||||
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->exists();
|
||||
|
||||
if($private == true && $isFollowing == 0) {
|
||||
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
|
||||
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
|
||||
}
|
||||
|
||||
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
||||
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
||||
}
|
||||
|
||||
$follow = FollowRequest::firstOrCreate([
|
||||
'follower_id' => $user->id,
|
||||
'following_id' => $target->id
|
||||
]);
|
||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
|
||||
FollowerService::add($user->id, $target->id);
|
||||
} elseif ($private == false && $isFollowing == 0) {
|
||||
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
|
||||
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
|
||||
}
|
||||
|
||||
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
||||
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
||||
}
|
||||
$follower = new Follower();
|
||||
$follower->profile_id = $user->id;
|
||||
$follower->following_id = $target->id;
|
||||
$follower->save();
|
||||
|
||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
FollowerService::add($user->id, $target->id);
|
||||
FollowPipeline::dispatch($follower);
|
||||
} else {
|
||||
if($force == true) {
|
||||
$request = FollowRequest::whereFollowerId($user->id)->whereFollowingId($target->id)->exists();
|
||||
$follower = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->exists();
|
||||
if($remote == true && $request && !$follower) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
if($remote == true && $follower) {
|
||||
$this->sendUndoFollow($user, $target);
|
||||
}
|
||||
Follower::whereProfileId($user->id)
|
||||
->whereFollowingId($target->id)
|
||||
->delete();
|
||||
FollowerService::remove($user->id, $target->id);
|
||||
}
|
||||
}
|
||||
|
||||
Cache::forget('profile:following:'.$target->id);
|
||||
Cache::forget('profile:followers:'.$target->id);
|
||||
Cache::forget('profile:following:'.$user->id);
|
||||
Cache::forget('profile:followers:'.$user->id);
|
||||
Cache::forget('api:local:exp:rec:'.$user->id);
|
||||
Cache::forget('user:account:id:'.$target->user_id);
|
||||
Cache::forget('user:account:id:'.$user->user_id);
|
||||
Cache::forget('px:profile:followers-v1.3:'.$user->id);
|
||||
Cache::forget('px:profile:followers-v1.3:'.$target->id);
|
||||
Cache::forget('px:profile:following-v1.3:'.$user->id);
|
||||
Cache::forget('px:profile:following-v1.3:'.$target->id);
|
||||
Cache::forget('profile:follower_count:'.$target->id);
|
||||
Cache::forget('profile:follower_count:'.$user->id);
|
||||
Cache::forget('profile:following_count:'.$target->id);
|
||||
Cache::forget('profile:following_count:'.$user->id);
|
||||
|
||||
return $target->url();
|
||||
abort(422, 'Deprecated API Endpoint, use /api/v1/accounts/{id}/follow or /api/v1/accounts/{id}/unfollow instead.');
|
||||
}
|
||||
|
||||
public function sendFollow($user, $target)
|
||||
|
|
|
@ -93,7 +93,7 @@ trait Instagram
|
|||
continue;
|
||||
}
|
||||
$storagePath = "import/{$job->uuid}";
|
||||
$path = $v->store($storagePath);
|
||||
$path = $v->storePublicly($storagePath);
|
||||
DB::transaction(function() use ($profile, $job, $path, $original) {
|
||||
$data = new ImportData;
|
||||
$data->profile_id = $profile->id;
|
||||
|
@ -141,7 +141,7 @@ trait Instagram
|
|||
return abort(500);
|
||||
}
|
||||
$storagePath = "import/{$job->uuid}";
|
||||
$path = $media->store($storagePath);
|
||||
$path = $media->storePublicly($storagePath);
|
||||
$job->media_json = $path;
|
||||
$job->stage = 3;
|
||||
$job->save();
|
||||
|
|
45
app/Http/Controllers/LandingController.php
Normal file
45
app/Http/Controllers/LandingController.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Http\Resources\DirectoryProfile;
|
||||
|
||||
class LandingController extends Controller
|
||||
{
|
||||
public function directoryRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function exploreRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config('instance.landing.show_explore') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function getDirectoryApi(Request $request)
|
||||
{
|
||||
abort_if(config('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return DirectoryProfile::collection(
|
||||
Profile::whereNull('domain')
|
||||
->whereIsSuggestable(true)
|
||||
->orderByDesc('updated_at')
|
||||
->cursorPaginate(20)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,10 @@ class PlaceController extends Controller
|
|||
|
||||
public function show(Request $request, $id, $slug)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'sometimes|max:10'
|
||||
]);
|
||||
|
||||
$place = Place::whereSlug($slug)->findOrFail($id);
|
||||
$posts = Status::wherePlaceId($place->id)
|
||||
->whereNull('uri')
|
||||
|
|
|
@ -13,6 +13,10 @@ use App\Services\StatusService;
|
|||
|
||||
class PortfolioController extends Controller
|
||||
{
|
||||
const RSS_FEED_KEY = 'pf:portfolio:rss-feed:';
|
||||
const CACHED_FEED_KEY = 'pf:portfolio:cached-feed:';
|
||||
const RECENT_FEED_KEY = 'pf:portfolio:recent-feed:';
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('portfolio.index');
|
||||
|
@ -60,11 +64,11 @@ class PortfolioController extends Controller
|
|||
$user = AccountService::get($post['account']['id']);
|
||||
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
||||
|
||||
if($user['locked'] || $portfolio->active != true) {
|
||||
if(!$portfolio || $user['locked'] || $portfolio->active != true) {
|
||||
return view('portfolio.404');
|
||||
}
|
||||
|
||||
if(!$post || $post['visibility'] != 'public' || $post['pf_type'] != 'photo' || $user['id'] != $post['account']['id']) {
|
||||
if(!$post || $post['visibility'] != 'public' || !in_array($post['pf_type'], ['photo', 'photo:album']) || $user['id'] != $post['account']['id']) {
|
||||
return view('portfolio.404');
|
||||
}
|
||||
|
||||
|
@ -117,7 +121,7 @@ class PortfolioController extends Controller
|
|||
$this->validate($request, [
|
||||
'profile_source' => 'required|in:recent,custom',
|
||||
'layout' => 'required|in:grid,masonry',
|
||||
'layout_container' => 'required|in:fixed,fluid'
|
||||
'layout_container' => 'required|in:fixed,fluid',
|
||||
]);
|
||||
|
||||
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
||||
|
@ -140,6 +144,7 @@ class PortfolioController extends Controller
|
|||
$portfolio->show_bio = $request->input('show_bio') === 'on';
|
||||
$portfolio->profile_layout = $request->input('layout');
|
||||
$portfolio->profile_container = $request->input('layout_container');
|
||||
$portfolio->metadata = $metadata;
|
||||
$portfolio->save();
|
||||
|
||||
return redirect('/' . $request->user()->username);
|
||||
|
@ -167,20 +172,28 @@ class PortfolioController extends Controller
|
|||
}
|
||||
|
||||
protected function getCustomFeed($portfolio) {
|
||||
if(!$portfolio->metadata['posts']) {
|
||||
if(!isset($portfolio->metadata['posts']) || !$portfolio->metadata['posts']) {
|
||||
return response()->json([], 400);
|
||||
}
|
||||
|
||||
return collect($portfolio->metadata['posts'])->map(function($p) {
|
||||
return StatusService::get($p);
|
||||
})
|
||||
->filter(function($p) {
|
||||
return $p && isset($p['account']);
|
||||
})->values();
|
||||
$feed = Cache::remember(self::CACHED_FEED_KEY . $portfolio->profile_id, 86400, function() use($portfolio) {
|
||||
return collect($portfolio->metadata['posts'])->map(function($p) {
|
||||
return StatusService::get($p);
|
||||
})
|
||||
->filter(function($p) {
|
||||
return $p && isset($p['account']);
|
||||
});
|
||||
});
|
||||
|
||||
if($portfolio->metadata && isset($portfolio->metadata['feed_order']) && $portfolio->metadata['feed_order'] === 'recent') {
|
||||
return $feed->reverse()->values();
|
||||
} else {
|
||||
return $feed->values();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRecentFeed($id) {
|
||||
$media = Cache::remember('portfolio:recent-feed:' . $id, 3600, function() use($id) {
|
||||
$media = Cache::remember(self::RECENT_FEED_KEY . $id, 3600, function() use($id) {
|
||||
return DB::table('media')
|
||||
->whereProfileId($id)
|
||||
->whereNotNull('status_id')
|
||||
|
@ -215,6 +228,14 @@ class PortfolioController extends Controller
|
|||
}
|
||||
|
||||
return $res->map(function($p) {
|
||||
$metadata = $p->metadata;
|
||||
$bgColor = $metadata && isset($metadata['background_color']) ? $metadata['background_color'] : '#000000';
|
||||
$textColor = $metadata && isset($metadata['text_color']) ? $metadata['text_color'] : '#d4d4d8';
|
||||
$rssEnabled = $metadata && isset($metadata['rss_enabled']) ? $metadata['rss_enabled'] : false;
|
||||
$rssButton = $metadata && isset($metadata['show_rss_button']) ? $metadata['show_rss_button'] : false;
|
||||
$colorScheme = $metadata && isset($metadata['color_scheme']) ? $metadata['color_scheme'] : 'dark';
|
||||
$feedOrder = $metadata && isset($metadata['feed_order']) ? $metadata['feed_order'] : 'oldest';
|
||||
|
||||
return [
|
||||
'url' => $p->url(),
|
||||
'pid' => (string) $p->profile_id,
|
||||
|
@ -228,6 +249,13 @@ class PortfolioController extends Controller
|
|||
'show_bio' => (bool) $p->show_bio,
|
||||
'profile_layout' => $p->profile_layout,
|
||||
'profile_source' => $p->profile_source,
|
||||
'color_scheme' => $colorScheme,
|
||||
'background_color' => $bgColor,
|
||||
'text_color' => $textColor,
|
||||
'show_profile_button' => true,
|
||||
'rss_enabled' => $rssEnabled,
|
||||
'show_rss_button' => $rssButton,
|
||||
'feed_order' => $feedOrder,
|
||||
'metadata' => $p->metadata
|
||||
];
|
||||
})->first();
|
||||
|
@ -248,8 +276,13 @@ class PortfolioController extends Controller
|
|||
if(!$p) {
|
||||
return [];
|
||||
}
|
||||
$metadata = $p->metadata;
|
||||
|
||||
return [
|
||||
$rssEnabled = $metadata && isset($metadata['rss_enabled']) ? $metadata['rss_enabled'] : false;
|
||||
$rssButton = $metadata && isset($metadata['show_rss_button']) ? $metadata['show_rss_button'] : false;
|
||||
$profileButton = $metadata && isset($metadata['show_profile_button']) ? $metadata['show_profile_button'] : false;
|
||||
|
||||
$res = [
|
||||
'url' => $p->url(),
|
||||
'show_captions' => (bool) $p->show_captions,
|
||||
'show_license' => (bool) $p->show_license,
|
||||
|
@ -259,8 +292,27 @@ class PortfolioController extends Controller
|
|||
'show_avatar' => (bool) $p->show_avatar,
|
||||
'show_bio' => (bool) $p->show_bio,
|
||||
'profile_layout' => $p->profile_layout,
|
||||
'profile_source' => $p->profile_source
|
||||
'profile_source' => $p->profile_source,
|
||||
'show_profile_button' => $profileButton,
|
||||
'rss_enabled' => $rssEnabled,
|
||||
'show_rss_button' => $rssButton,
|
||||
];
|
||||
|
||||
if($rssEnabled) {
|
||||
$res['rss_feed_url'] = $p->permalink('.rss');
|
||||
}
|
||||
|
||||
if($p->metadata) {
|
||||
if(isset($p->metadata['background_color'])) {
|
||||
$res['background_color'] = $p->metadata['background_color'];
|
||||
}
|
||||
|
||||
if(isset($p->metadata['text_color'])) {
|
||||
$res['text_color'] = $p->metadata['text_color'];
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function storeSettings(Request $request)
|
||||
|
@ -268,11 +320,99 @@ class PortfolioController extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'profile_layout' => 'sometimes|in:grid,masonry,album'
|
||||
'active' => 'sometimes|boolean',
|
||||
'show_captions' => 'sometimes|boolean',
|
||||
'show_license' => 'sometimes|boolean',
|
||||
'show_location' => 'sometimes|boolean',
|
||||
'show_timestamp' => 'sometimes|boolean',
|
||||
'show_link' => 'sometimes|boolean',
|
||||
'show_avatar' => 'sometimes|boolean',
|
||||
'show_bio' => 'sometimes|boolean',
|
||||
'profile_layout' => 'sometimes|in:grid,masonry,album',
|
||||
'profile_source' => 'sometimes|in:recent,custom',
|
||||
'color_scheme' => 'sometimes|in:light,dark,custom',
|
||||
'show_profile_button' => 'sometimes|boolean',
|
||||
'rss_enabled' => 'sometimes|boolean',
|
||||
'show_rss_button' => 'sometimes|boolean',
|
||||
'feed_order' => 'sometimes|in:oldest,recent',
|
||||
'background_color' => [
|
||||
'sometimes',
|
||||
'nullable',
|
||||
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
|
||||
],
|
||||
'text_color' => [
|
||||
'sometimes',
|
||||
'nullable',
|
||||
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
|
||||
],
|
||||
]);
|
||||
|
||||
$res = Portfolio::whereUserId($request->user()->id)
|
||||
->update($request->only([
|
||||
$res = Portfolio::whereUserId($request->user()->id)->firstOrFail();
|
||||
$pid = $request->user()->profile_id;
|
||||
$metadata = $res->metadata;
|
||||
$clearFeedCache = false;
|
||||
|
||||
if($request->has('color_scheme')) {
|
||||
$metadata['color_scheme'] = $request->input('color_scheme');
|
||||
}
|
||||
|
||||
if($request->has('background_color')) {
|
||||
$metadata['background_color'] = $request->input('background_color');
|
||||
$bgc = $request->background_color;
|
||||
if($bgc && $bgc !== '#000000') {
|
||||
$metadata['color_scheme'] = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
if($request->has('text_color')) {
|
||||
$metadata['text_color'] = $request->input('text_color');
|
||||
$txc = $request->text_color;
|
||||
if($txc && $txc !== '#d4d4d8') {
|
||||
$metadata['color_scheme'] = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
if($request->has('show_profile_button')) {
|
||||
$metadata['show_profile_button'] = $request->input('show_profile_button');
|
||||
}
|
||||
|
||||
if($request->has('rss_enabled')) {
|
||||
$metadata['rss_enabled'] = $request->input('rss_enabled');
|
||||
}
|
||||
|
||||
if($request->has('show_rss_button')) {
|
||||
$metadata['show_rss_button'] = $metadata['rss_enabled'] ? $request->input('show_rss_button') : false;
|
||||
}
|
||||
|
||||
if($request->has('feed_order')) {
|
||||
$metadata['feed_order'] = $request->input('feed_order');
|
||||
}
|
||||
|
||||
if(isset($metadata['background_color']) || isset($metadata['text_color'])) {
|
||||
$bgc = isset($metadata['background_color']) ? $metadata['background_color'] : null;
|
||||
$txc = isset($metadata['text_color']) ? $metadata['text_color'] : null;
|
||||
|
||||
if((!$bgc || $bgc == '#000000') && (!$txc || $txc === '#d4d4d8') && $request->color_scheme != 'light') {
|
||||
$metadata['color_scheme'] = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
if($request->has('color_scheme') && $request->color_scheme === 'light') {
|
||||
$metadata['background_color'] = '#ffffff';
|
||||
$metadata['text_color'] = '#000000';
|
||||
$metadata['color_scheme'] = 'light';
|
||||
}
|
||||
|
||||
if($request->metadata !== $metadata) {
|
||||
$res->metadata = $metadata;
|
||||
$res->save();
|
||||
}
|
||||
|
||||
if($request->profile_layout != $res->profile_layout) {
|
||||
$clearFeedCache = true;
|
||||
}
|
||||
|
||||
$res->update($request->only([
|
||||
'active',
|
||||
'show_captions',
|
||||
'show_license',
|
||||
|
@ -285,7 +425,11 @@ class PortfolioController extends Controller
|
|||
'profile_source'
|
||||
]));
|
||||
|
||||
Cache::forget('portfolio:recent-feed:' . $request->user()->profile_id);
|
||||
Cache::forget(self::RECENT_FEED_KEY . $pid);
|
||||
|
||||
if($clearFeedCache) {
|
||||
Cache::forget(self::RSS_FEED_KEY . $pid);
|
||||
}
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
@ -295,7 +439,7 @@ class PortfolioController extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'ids' => 'required|array|max:24'
|
||||
'ids' => 'required|array|max:100'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
@ -308,11 +452,117 @@ class PortfolioController extends Controller
|
|||
->findOrFail($ids);
|
||||
|
||||
$p = Portfolio::whereProfileId($pid)->firstOrFail();
|
||||
$p->metadata = ['posts' => $ids];
|
||||
$metadata = $p->metadata;
|
||||
$metadata['posts'] = $ids;
|
||||
$p->metadata = $metadata;
|
||||
$p->save();
|
||||
|
||||
Cache::forget('portfolio:recent-feed:' . $pid);
|
||||
Cache::forget(self::RECENT_FEED_KEY . $pid);
|
||||
Cache::forget(self::RSS_FEED_KEY . $pid);
|
||||
Cache::forget(self::CACHED_FEED_KEY . $pid);
|
||||
|
||||
return $request->ids;
|
||||
}
|
||||
|
||||
public function getRssFeed(Request $request, $username)
|
||||
{
|
||||
$user = User::whereUsername($username)->first();
|
||||
|
||||
if(!$user) {
|
||||
return view('portfolio.404');
|
||||
}
|
||||
|
||||
$portfolio = Portfolio::whereUserId($user->id)->where('active', 1)->firstOrFail();
|
||||
|
||||
$metadata = $portfolio->metadata;
|
||||
|
||||
abort_if(!$metadata || !isset($metadata['rss_enabled']), 404);
|
||||
abort_unless($metadata['rss_enabled'], 404);
|
||||
|
||||
$account = AccountService::get($user->profile_id);
|
||||
$portfolioUrl = $portfolio->url();
|
||||
$portfolioLayout = $portfolio->profile_layout;
|
||||
|
||||
if(!isset($metadata['posts']) || !count($metadata['posts'])) {
|
||||
$feed = [];
|
||||
} else {
|
||||
$feed = Cache::remember(
|
||||
self::RSS_FEED_KEY . $user->profile_id,
|
||||
43200,
|
||||
function() use($portfolio, $portfolioUrl, $portfolioLayout) {
|
||||
return collect($portfolio->metadata['posts'])->map(function($post) {
|
||||
return StatusService::get($post);
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->map(function($post, $idx) use($portfolioLayout, $portfolioUrl) {
|
||||
$ts = now()->parse($post['created_at']);
|
||||
$url = $portfolioLayout == 'album' ? $portfolioUrl . '?slide=' . ($idx + 1) : $portfolioUrl . '/' . $post['id'];
|
||||
return [
|
||||
'title' => 'Post by ' . $post['account']['username'] . ' on ' . $ts->format('D, d M Y'),
|
||||
'description' => $post['content_text'],
|
||||
'pubDate' => date('D, d M Y H:i:s ', strtotime($post['created_at'])) . 'GMT',
|
||||
'url' => $url
|
||||
];
|
||||
})
|
||||
->reverse()
|
||||
->take(10)
|
||||
->toArray();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$now = date('D, d M Y H:i:s ') . 'GMT';
|
||||
|
||||
return response()
|
||||
->view('portfolio.rss_feed', compact('account', 'now', 'feed', 'portfolioUrl'), 200)
|
||||
->header('Content-Type', 'text/xml');
|
||||
return response($feed)->withHeaders(['Content-Type' => 'text/xml']);
|
||||
}
|
||||
|
||||
|
||||
public function getApFeed(Request $request, $username)
|
||||
{
|
||||
$user = User::whereUsername($username)->first();
|
||||
|
||||
if(!$user) {
|
||||
return view('portfolio.404');
|
||||
}
|
||||
|
||||
$portfolio = Portfolio::whereUserId($user->id)->where('active', 1)->firstOrFail();
|
||||
$metadata = $portfolio->metadata;
|
||||
$baseUrl = config('app.url');
|
||||
$page = $request->input('page');
|
||||
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $portfolio->permalink('.json'),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => isset($metadata['posts']) ? count($metadata['posts']) : 0,
|
||||
];
|
||||
|
||||
if($request->has('page')) {
|
||||
$start = $page == 1 ? 0 : ($page * 10 - 10);
|
||||
$res['id'] = $portfolio->permalink('.json?page=' . $page);
|
||||
$res['type'] = 'OrderedCollectionPage';
|
||||
$res['next'] = $portfolio->permalink('.json?page=' . $page + 1);
|
||||
$res['partOf'] = $portfolio->permalink('.json');
|
||||
$res['orderedItems'] = collect($metadata['posts'])->slice($start)->take(10)->map(function($p) {
|
||||
return StatusService::get($p);
|
||||
})
|
||||
->filter()
|
||||
->map(function($p) {
|
||||
return $p['url'];
|
||||
})
|
||||
->values();
|
||||
|
||||
if(!$res['orderedItems'] || $res['orderedItems']->count() != 10) {
|
||||
unset($res['next']);
|
||||
}
|
||||
} else {
|
||||
$res['first'] = $portfolio->permalink('.json?page=1');
|
||||
}
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)
|
||||
->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,7 +207,7 @@ class ProfileController extends Controller
|
|||
|
||||
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
|
||||
|
||||
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 86400, function() use($pid, $profile) {
|
||||
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 43200, function() use($pid, $profile) {
|
||||
$items = DB::table('statuses')
|
||||
->whereProfileId($pid)
|
||||
->whereVisibility('public')
|
||||
|
|
|
@ -62,36 +62,6 @@ class PublicApiController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
protected function getLikes($status)
|
||||
{
|
||||
if(false == Auth::check()) {
|
||||
return [];
|
||||
} else {
|
||||
$profile = Auth::user()->profile;
|
||||
if($profile->status) {
|
||||
return [];
|
||||
}
|
||||
$likes = $status->likedBy()->orderBy('created_at','desc')->paginate(10);
|
||||
$collection = new Fractal\Resource\Collection($likes, new AccountTransformer());
|
||||
return $this->fractal->createData($collection)->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getShares($status)
|
||||
{
|
||||
if(false == Auth::check()) {
|
||||
return [];
|
||||
} else {
|
||||
$profile = Auth::user()->profile;
|
||||
if($profile->status) {
|
||||
return [];
|
||||
}
|
||||
$shares = $status->sharedBy()->orderBy('created_at','desc')->paginate(10);
|
||||
$collection = new Fractal\Resource\Collection($shares, new AccountTransformer());
|
||||
return $this->fractal->createData($collection)->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatus(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
@ -216,41 +186,6 @@ class PublicApiController extends Controller
|
|||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function statusLikes(Request $request, $username, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$status = Status::findOrFail($id);
|
||||
$this->scopeCheck($status->profile, $status);
|
||||
$page = $request->input('page');
|
||||
if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
|
||||
return response()->json([
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
$likes = $this->getLikes($status);
|
||||
return response()->json([
|
||||
'data' => $likes
|
||||
]);
|
||||
}
|
||||
|
||||
public function statusShares(Request $request, $username, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->findOrFail($id);
|
||||
$this->scopeCheck($profile, $status);
|
||||
$page = $request->input('page');
|
||||
if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
|
||||
return response()->json([
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
$shares = $this->getShares($status);
|
||||
return response()->json([
|
||||
'data' => $shares
|
||||
]);
|
||||
}
|
||||
|
||||
protected function scopeCheck(Profile $profile, Status $status)
|
||||
{
|
||||
if($profile->is_private == true && Auth::check() == false) {
|
||||
|
@ -307,6 +242,7 @@ class PublicApiController extends Controller
|
|||
$user = $request->user();
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
|
||||
if(config('exp.cached_public_timeline') == false) {
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
|
@ -322,6 +258,9 @@ class PublicApiController extends Controller
|
|||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereLocal(true)
|
||||
->when($hideNsfw, function($q, $hideNsfw) {
|
||||
return $q->where('is_nsfw', false);
|
||||
})
|
||||
->whereScope('public')
|
||||
->orderBy('id', 'desc')
|
||||
->limit($limit)
|
||||
|
@ -365,6 +304,9 @@ class PublicApiController extends Controller
|
|||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereLocal(true)
|
||||
->when($hideNsfw, function($q, $hideNsfw) {
|
||||
return $q->where('is_nsfw', false);
|
||||
})
|
||||
->whereScope('public')
|
||||
->orderBy('id', 'desc')
|
||||
->limit($limit)
|
||||
|
@ -447,48 +389,25 @@ class PublicApiController extends Controller
|
|||
$user = $request->user();
|
||||
|
||||
$key = 'user:last_active_at:id:'.$user->id;
|
||||
$ttl = now()->addMinutes(20);
|
||||
Cache::remember($key, $ttl, function() use($user) {
|
||||
if(Cache::get($key) == null) {
|
||||
$user->last_active_at = now();
|
||||
$user->save();
|
||||
return;
|
||||
});
|
||||
Cache::put($key, true, 43200);
|
||||
}
|
||||
|
||||
$pid = $user->profile_id;
|
||||
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
|
||||
$following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
|
||||
if($recentFeed == true) {
|
||||
$key = 'profile:home-timeline-cursor:'.$user->id;
|
||||
$ttl = now()->addMinutes(30);
|
||||
$min = Cache::remember($key, $ttl, function() use($pid) {
|
||||
$res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first();
|
||||
return $res ? $res->status_id : null;
|
||||
});
|
||||
}
|
||||
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
$types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
|
||||
// $types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'text'];
|
||||
|
||||
$textOnlyReplies = false;
|
||||
|
||||
if(config('exp.top')) {
|
||||
$textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
|
||||
$textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
|
||||
|
||||
if($textOnlyPosts) {
|
||||
array_push($types, 'text');
|
||||
}
|
||||
}
|
||||
|
||||
if(config('exp.polls') == true) {
|
||||
array_push($types, 'poll');
|
||||
}
|
||||
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
$id = $min ?? $max;
|
||||
|
@ -513,7 +432,7 @@ class PublicApiController extends Controller
|
|||
'updated_at'
|
||||
)
|
||||
->whereIn('type', $types)
|
||||
->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
|
||||
->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
|
||||
return $q->whereNull('in_reply_to_id');
|
||||
})
|
||||
->where('id', $dir, $id)
|
||||
|
@ -523,10 +442,14 @@ class PublicApiController extends Controller
|
|||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
try {
|
||||
$status = StatusService::get($s->id, false);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
|
||||
|
@ -568,10 +491,14 @@ class PublicApiController extends Controller
|
|||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id, false);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$status = StatusService::get($s->id, false);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
|
||||
|
@ -608,6 +535,7 @@ class PublicApiController extends Controller
|
|||
$amin = SnowflakeService::byDate(now()->subDays(config('federation.network_timeline_days_falloff')));
|
||||
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
|
||||
|
||||
if(config('instance.timeline.network.cached') == false) {
|
||||
if($min || $max) {
|
||||
|
@ -620,7 +548,10 @@ class PublicApiController extends Controller
|
|||
'scope',
|
||||
'created_at',
|
||||
)
|
||||
->where('id', $dir, $id)
|
||||
->where('id', $dir, $id)
|
||||
->when($hideNsfw, function($q, $hideNsfw) {
|
||||
return $q->where('is_nsfw', false);
|
||||
})
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
|
@ -648,6 +579,9 @@ class PublicApiController extends Controller
|
|||
)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->when($hideNsfw, function($q, $hideNsfw) {
|
||||
return $q->where('is_nsfw', false);
|
||||
})
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
|
@ -730,86 +664,6 @@ class PublicApiController extends Controller
|
|||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowers(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(!$account, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if(AccountService::hiddenFollowers($id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if($request->has('page') && $request->page >= 5) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$res = DB::table('followers')
|
||||
->select('id', 'profile_id', 'following_id')
|
||||
->whereFollowingId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->simplePaginate(10)
|
||||
->map(function($follower) {
|
||||
return AccountService::get($follower->profile_id);
|
||||
})
|
||||
->filter(function($account) {
|
||||
return $account && isset($account['id']);
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowing(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(!$account, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if(AccountService::hiddenFollowing($id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if($request->has('page') && $request->page >= 5) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$res = DB::table('followers')
|
||||
->select('id', 'profile_id', 'following_id')
|
||||
->whereProfileId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->simplePaginate(10)
|
||||
->map(function($follower) {
|
||||
return AccountService::get($follower->following_id);
|
||||
})
|
||||
->filter(function($account) {
|
||||
return $account && isset($account['id']);
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountStatuses(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Status;
|
|||
use App\User;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
|
@ -165,6 +166,10 @@ class ReportController extends Controller
|
|||
$report->message = e($request->input('msg'));
|
||||
$report->save();
|
||||
|
||||
if(config('instance.reports.email.enabled')) {
|
||||
ReportNotifyAdminViaEmail::dispatch($report)->onQueue('default');
|
||||
}
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(200);
|
||||
} else {
|
||||
|
|
|
@ -17,11 +17,11 @@ use Mail;
|
|||
use Purify;
|
||||
use App\Mail\PasswordChange;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\PronounService;
|
||||
|
||||
trait HomeSettings
|
||||
{
|
||||
|
||||
public function home()
|
||||
{
|
||||
$id = Auth::user()->profile->id;
|
||||
|
@ -40,7 +40,7 @@ trait HomeSettings
|
|||
public function homeUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
|
||||
'website' => 'nullable|url',
|
||||
'language' => 'nullable|string|min:2|max:5',
|
||||
|
@ -99,10 +99,10 @@ trait HomeSettings
|
|||
}
|
||||
|
||||
if ($changes === true) {
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
$user->save();
|
||||
$profile->save();
|
||||
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
AccountService::del($profile->id);
|
||||
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,10 @@ trait PrivacySettings
|
|||
'show_profile_follower_count',
|
||||
'show_profile_following_count',
|
||||
];
|
||||
|
||||
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
|
||||
$profile->save();
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
if ($field == 'is_private') {
|
||||
|
|
|
@ -16,7 +16,7 @@ use Carbon\Carbon;
|
|||
use Illuminate\Http\Request;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
|
@ -56,13 +56,15 @@ trait SecuritySettings
|
|||
$key,
|
||||
500
|
||||
);
|
||||
|
||||
|
||||
$writer = new Writer(
|
||||
new ImageRenderer(
|
||||
new RendererStyle(400),
|
||||
new ImagickImageBackEnd()
|
||||
new SvgImageBackEnd()
|
||||
)
|
||||
);
|
||||
$qrcode = base64_encode($writer->writeString($qrcode));
|
||||
$qrcode = $writer->writeString($qrcode);
|
||||
$user->{'2fa_secret'} = $key;
|
||||
$user->{'2fa_backup_codes'} = json_encode($backups);
|
||||
$user->save();
|
||||
|
|
|
@ -23,6 +23,7 @@ use App\Http\Controllers\Settings\{
|
|||
};
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
|
@ -136,6 +137,8 @@ class SettingsController extends Controller
|
|||
abort_if($user->is_admin, 403);
|
||||
$profile = $user->profile;
|
||||
$ts = Carbon::now()->addMonth();
|
||||
$user->email = $user->id;
|
||||
$user->password = '';
|
||||
$user->status = 'delete';
|
||||
$profile->status = 'delete';
|
||||
$user->delete_after = $ts;
|
||||
|
@ -143,8 +146,9 @@ class SettingsController extends Controller
|
|||
$user->save();
|
||||
$profile->save();
|
||||
Cache::forget('profiles:private');
|
||||
AccountService::del($profile->id);
|
||||
Auth::logout();
|
||||
DeleteAccountPipeline::dispatch($user)->onQueue('high');
|
||||
DeleteAccountPipeline::dispatch($user)->onQueue('low');
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
|
|
|
@ -219,13 +219,23 @@ class StatusController extends Controller
|
|||
$u->save();
|
||||
}
|
||||
|
||||
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
|
||||
Cache::forget('profile:status_count:' . $status->profile_id);
|
||||
Cache::forget('profile:embed:' . $status->profile_id);
|
||||
StatusService::del($status->id, true);
|
||||
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
|
||||
if($status->in_reply_to_id) {
|
||||
$parent = Status::find($status->in_reply_to_id);
|
||||
if($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) {
|
||||
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
|
||||
Cache::forget('profile:status_count:' . $status->profile_id);
|
||||
Cache::forget('profile:embed:' . $status->profile_id);
|
||||
StatusService::del($status->id, true);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) {
|
||||
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
|
||||
Cache::forget('profile:status_count:' . $status->profile_id);
|
||||
Cache::forget('profile:embed:' . $status->profile_id);
|
||||
StatusService::del($status->id, true);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatchNow($status);
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
|
||||
if($request->wantsJson()) {
|
||||
|
|
360
app/Http/Controllers/Stories/StoryApiV1Controller.php
Normal file
360
app/Http/Controllers/Stories/StoryApiV1Controller.php
Normal file
|
@ -0,0 +1,360 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Stories;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Models\Conversation;
|
||||
use App\DirectMessage;
|
||||
use App\Notification;
|
||||
use App\Story;
|
||||
use App\Status;
|
||||
use App\StoryView;
|
||||
use App\Jobs\StoryPipeline\StoryDelete;
|
||||
use App\Jobs\StoryPipeline\StoryFanout;
|
||||
use App\Jobs\StoryPipeline\StoryReplyDeliver;
|
||||
use App\Jobs\StoryPipeline\StoryViewDeliver;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryApiV1Controller extends Controller
|
||||
{
|
||||
public function carousel(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
if(config('database.default') == 'pgsql') {
|
||||
$s = Story::select('stories.*', 'followers.following_id')
|
||||
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
|
||||
->where('followers.profile_id', $pid)
|
||||
->where('stories.active', true)
|
||||
->get();
|
||||
} else {
|
||||
$s = Story::select('stories.*', 'followers.following_id')
|
||||
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
|
||||
->where('followers.profile_id', $pid)
|
||||
->where('stories.active', true)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
$nodes = $s->map(function($s) use($pid) {
|
||||
$profile = AccountService::get($s->profile_id, true);
|
||||
if(!$profile || !isset($profile['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (string) $s->id,
|
||||
'pid' => (string) $s->profile_id,
|
||||
'type' => $s->type,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'duration' => $s->duration ?? 3,
|
||||
'seen' => StoryService::hasSeen($pid, $s->id),
|
||||
'created_at' => $s->created_at->format('c')
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->groupBy('pid')
|
||||
->map(function($item) use($pid) {
|
||||
$profile = AccountService::get($item[0]['pid'], true);
|
||||
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
|
||||
url("/i/rs/{$profile['id']}");
|
||||
return [
|
||||
'id' => 'pfs:' . $profile['id'],
|
||||
'user' => [
|
||||
'id' => (string) $profile['id'],
|
||||
'username' => $profile['username'],
|
||||
'username_acct' => $profile['acct'],
|
||||
'avatar' => $profile['avatar'],
|
||||
'local' => $profile['local'],
|
||||
'is_author' => $profile['id'] == $pid
|
||||
],
|
||||
'nodes' => $item,
|
||||
'url' => $url,
|
||||
'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
|
||||
];
|
||||
})
|
||||
->sortBy('seen')
|
||||
->values();
|
||||
|
||||
$res = [
|
||||
'self' => [],
|
||||
'nodes' => $nodes,
|
||||
];
|
||||
|
||||
if(Story::whereProfileId($pid)->whereActive(true)->exists()) {
|
||||
$selfStories = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->get()
|
||||
->map(function($s) use($pid) {
|
||||
return [
|
||||
'id' => (string) $s->id,
|
||||
'type' => $s->type,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'duration' => $s->duration,
|
||||
'seen' => true,
|
||||
'created_at' => $s->created_at->format('c')
|
||||
];
|
||||
})
|
||||
->sortBy('id')
|
||||
->values();
|
||||
$selfProfile = AccountService::get($pid, true);
|
||||
$res['self'] = [
|
||||
'user' => [
|
||||
'id' => (string) $selfProfile['id'],
|
||||
'username' => $selfProfile['acct'],
|
||||
'avatar' => $selfProfile['avatar'],
|
||||
'local' => $selfProfile['local'],
|
||||
'is_author' => true
|
||||
],
|
||||
|
||||
'nodes' => $selfStories,
|
||||
];
|
||||
}
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function add(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimetypes:image/jpeg,image/png,video/mp4',
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
'duration' => 'sometimes|integer|min:0|max:30'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$count = Story::whereProfileId($user->profile_id)
|
||||
->whereActive(true)
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
|
||||
if($count >= Story::MAX_PER_DAY) {
|
||||
abort(418, 'You have reached your limit for new Stories today.');
|
||||
}
|
||||
|
||||
$photo = $request->file('file');
|
||||
$path = $this->storeMedia($photo, $user);
|
||||
|
||||
$story = new Story();
|
||||
$story->duration = $request->input('duration', 3);
|
||||
$story->profile_id = $user->profile_id;
|
||||
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
|
||||
$story->mime = $photo->getMimeType();
|
||||
$story->path = $path;
|
||||
$story->local = true;
|
||||
$story->size = $photo->getSize();
|
||||
$story->bearcap_token = str_random(64);
|
||||
$story->expires_at = now()->addMinutes(1440);
|
||||
$story->save();
|
||||
|
||||
$url = $story->path;
|
||||
|
||||
$res = [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully added',
|
||||
'media_id' => (string) $story->id,
|
||||
'media_url' => url(Storage::url($url)) . '?v=' . time(),
|
||||
'media_type' => $story->type
|
||||
];
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function publish(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'media_id' => 'required',
|
||||
'duration' => 'required|integer|min:0|max:30',
|
||||
'can_reply' => 'required|boolean',
|
||||
'can_react' => 'required|boolean'
|
||||
]);
|
||||
|
||||
$id = $request->input('media_id');
|
||||
$user = $request->user();
|
||||
$story = Story::whereProfileId($user->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
$story->active = true;
|
||||
$story->duration = $request->input('duration', 10);
|
||||
$story->can_reply = $request->input('can_reply');
|
||||
$story->can_react = $request->input('can_react');
|
||||
$story->save();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
StoryFanout::dispatch($story)->onQueue('story');
|
||||
StoryService::addRotateQueue($story->id);
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully published',
|
||||
];
|
||||
}
|
||||
|
||||
public function delete(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$story = Story::whereProfileId($user->profile_id)
|
||||
->findOrFail($id);
|
||||
$story->active = false;
|
||||
$story->save();
|
||||
|
||||
StoryDelete::dispatch($story)->onQueue('story');
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully deleted'
|
||||
];
|
||||
}
|
||||
|
||||
public function viewed(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|min:1',
|
||||
]);
|
||||
$id = $request->input('id');
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
|
||||
$story = Story::with('profile')
|
||||
->findOrFail($id);
|
||||
$exp = $story->expires_at;
|
||||
|
||||
$profile = $story->profile;
|
||||
|
||||
if($story->profile_id == $authed->id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
abort_if(!$publicOnly, 403);
|
||||
|
||||
$v = StoryView::firstOrCreate([
|
||||
'story_id' => $id,
|
||||
'profile_id' => $authed->id
|
||||
]);
|
||||
|
||||
if($v->wasRecentlyCreated) {
|
||||
Story::findOrFail($story->id)->increment('view_count');
|
||||
|
||||
if($story->local == false) {
|
||||
StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
|
||||
}
|
||||
}
|
||||
|
||||
Cache::forget('stories:recent:by_id:' . $authed->id);
|
||||
StoryService::addSeen($authed->id, $story->id);
|
||||
return ['code' => 200];
|
||||
}
|
||||
|
||||
public function comment(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'sid' => 'required',
|
||||
'caption' => 'required|string'
|
||||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$text = $request->input('caption');
|
||||
|
||||
$story = Story::findOrFail($request->input('sid'));
|
||||
|
||||
abort_if(!$story->can_reply, 422);
|
||||
|
||||
$status = new Status;
|
||||
$status->type = 'story:reply';
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = $text;
|
||||
$status->rendered = $text;
|
||||
$status->scope = 'direct';
|
||||
$status->visibility = 'direct';
|
||||
$status->in_reply_to_profile_id = $story->profile_id;
|
||||
$status->entities = json_encode([
|
||||
'story_id' => $story->id
|
||||
]);
|
||||
$status->save();
|
||||
|
||||
$dm = new DirectMessage;
|
||||
$dm->to_id = $story->profile_id;
|
||||
$dm->from_id = $pid;
|
||||
$dm->type = 'story:comment';
|
||||
$dm->status_id = $status->id;
|
||||
$dm->meta = json_encode([
|
||||
'story_username' => $story->profile->username,
|
||||
'story_actor_username' => $request->user()->username,
|
||||
'story_id' => $story->id,
|
||||
'story_media_url' => url(Storage::url($story->path)),
|
||||
'caption' => $text
|
||||
]);
|
||||
$dm->save();
|
||||
|
||||
Conversation::updateOrInsert(
|
||||
[
|
||||
'to_id' => $story->profile_id,
|
||||
'from_id' => $pid
|
||||
],
|
||||
[
|
||||
'type' => 'story:comment',
|
||||
'status_id' => $status->id,
|
||||
'dm_id' => $dm->id,
|
||||
'is_hidden' => false
|
||||
]
|
||||
);
|
||||
|
||||
if($story->local) {
|
||||
$n = new Notification;
|
||||
$n->profile_id = $dm->to_id;
|
||||
$n->actor_id = $dm->from_id;
|
||||
$n->item_id = $dm->id;
|
||||
$n->item_type = 'App\DirectMessage';
|
||||
$n->action = 'story:comment';
|
||||
$n->message = "{$request->user()->username} commented on story";
|
||||
$n->rendered = "{$request->user()->username} commented on story";
|
||||
$n->save();
|
||||
} else {
|
||||
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Sent!'
|
||||
];
|
||||
}
|
||||
|
||||
protected function storeMedia($photo, $user)
|
||||
{
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'video/mp4'
|
||||
]) == false) {
|
||||
abort(400, 'Invalid media type');
|
||||
return;
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::story($user->profile);
|
||||
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
|
||||
return $path;
|
||||
}
|
||||
}
|
|
@ -111,7 +111,7 @@ class StoryComposeController extends Controller
|
|||
}
|
||||
|
||||
$storagePath = MediaPathService::story($user->profile);
|
||||
$path = $photo->storeAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
|
||||
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
|
||||
if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) {
|
||||
$fpath = storage_path('app/' . $path);
|
||||
$img = Intervention::make($fpath);
|
||||
|
|
|
@ -19,6 +19,7 @@ class Kernel extends HttpKernel
|
|||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -42,7 +43,6 @@ class Kernel extends HttpKernel
|
|||
|
||||
'api' => [
|
||||
'bindings',
|
||||
\Fruitcake\Cors\HandleCors::class,
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class EmailVerificationCheck
|
|||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($request->user() &&
|
||||
config_cache('pixelfed.enforce_email_verification') &&
|
||||
config('pixelfed.enforce_email_verification') &&
|
||||
is_null($request->user()->email_verified_at) &&
|
||||
!$request->is(
|
||||
'i/auth/*',
|
||||
|
|
29
app/Http/Resources/AdminHashtag.php
Normal file
29
app/Http/Resources/AdminHashtag.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class AdminHashtag extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'can_trend' => $this->can_trend === null ? true : (bool) $this->can_trend,
|
||||
'can_search' => $this->can_search === null ? true : (bool) $this->can_search,
|
||||
'is_nsfw' => (bool) $this->is_nsfw,
|
||||
'is_banned' => (bool) $this->is_banned,
|
||||
'cached_count' => $this->cached_count ?? 0,
|
||||
'created_at' => $this->created_at
|
||||
];
|
||||
}
|
||||
}
|
34
app/Http/Resources/AdminInstance.php
Normal file
34
app/Http/Resources/AdminInstance.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class AdminInstance extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'domain' => $this->domain,
|
||||
'software' => $this->software,
|
||||
'unlisted' => (bool) $this->unlisted,
|
||||
'auto_cw' => (bool) $this->auto_cw,
|
||||
'banned' => (bool) $this->banned,
|
||||
'user_count' => $this->user_count,
|
||||
'status_count' => $this->status_count,
|
||||
'last_crawled_at' => $this->last_crawled_at,
|
||||
'notes' => $this->notes,
|
||||
'base_domain' => $this->base_domain,
|
||||
'ban_subdomains' => $this->ban_subdomains,
|
||||
'actors_last_synced_at' => $this->actors_last_synced_at,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
}
|
||||
}
|
38
app/Http/Resources/AdminReport.php
Normal file
38
app/Http/Resources/AdminReport.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class AdminReport extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$res = [
|
||||
'id' => $this->id,
|
||||
'reporter' => AccountService::get($this->profile_id, true),
|
||||
'type' => $this->type,
|
||||
'object_id' => (string) $this->object_id,
|
||||
'object_type' => $this->object_type,
|
||||
'reported' => AccountService::get($this->reported_profile_id, true),
|
||||
'status' => null,
|
||||
'reporter_message' => $this->message,
|
||||
'admin_seen_at' => $this->admin_seen,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
|
||||
if($this->object_id && $this->object_type === 'App\Status') {
|
||||
$res['status'] = StatusService::get($this->object_id, false);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
33
app/Http/Resources/AdminSpamReport.php
Normal file
33
app/Http/Resources/AdminSpamReport.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class AdminSpamReport extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$res = [
|
||||
'id' => $this->id,
|
||||
'type' => $this->type,
|
||||
'status' => null,
|
||||
'read_at' => $this->read_at,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
|
||||
if($this->item_id && $this->item_type === 'App\Status') {
|
||||
$res['status'] = StatusService::get($this->item_id, false);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
44
app/Http/Resources/AdminUser.php
Normal file
44
app/Http/Resources/AdminUser.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class AdminUser extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$account = AccountService::get($this->profile_id, true);
|
||||
|
||||
$res = [
|
||||
'id' => $this->id,
|
||||
'profile_id' => $this->profile_id,
|
||||
'name' => $this->name,
|
||||
'username' => $this->username,
|
||||
'is_admin' => (bool) $this->is_admin,
|
||||
'email_verified_at' => $this->email_verified_at,
|
||||
'two_factor_enabled' => (bool) $this->{'2fa_enabled'},
|
||||
'register_source' => $this->register_source,
|
||||
'last_active_at' => $this->last_active_at,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
|
||||
if($account) {
|
||||
$res['avatar'] = $account['avatar'];
|
||||
$res['bio'] = $account['note_text'];
|
||||
$res['statuses_count'] = $account['statuses_count'];
|
||||
$res['following_count'] = $account['following_count'];
|
||||
$res['followers_count'] = $account['followers_count'];
|
||||
$res['is_private'] = $account['locked'];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
37
app/Http/Resources/DirectoryProfile.php
Normal file
37
app/Http/Resources/DirectoryProfile.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Cache;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class DirectoryProfile extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$account = AccountService::get($this->id, true);
|
||||
if(!$account) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$url = url($this->username);
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'username' => $this->username,
|
||||
'url' => $url,
|
||||
'avatar' => $account['avatar'],
|
||||
'following_count' => $account['following_count'],
|
||||
'followers_count' => $account['followers_count'],
|
||||
'statuses_count' => $account['statuses_count'],
|
||||
'bio' => $account['note_text']
|
||||
];
|
||||
}
|
||||
}
|
76
app/Http/Resources/StatusStateless.php
Normal file
76
app/Http/Resources/StatusStateless.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Cache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\StatusLabelService;
|
||||
use App\Services\StatusMentionService;
|
||||
use App\Services\PollService;
|
||||
use App\Models\CustomEmoji;
|
||||
|
||||
class StatusStateless extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$status = $this;
|
||||
$taggedPeople = MediaTagService::get($status->id);
|
||||
$poll = $status->type === 'poll' ? PollService::get($status->id) : null;
|
||||
|
||||
return [
|
||||
'_v' => 1,
|
||||
'id' => (string) $status->id,
|
||||
//'gid' => $status->group_id ? (string) $status->group_id : null,
|
||||
'shortcode' => HashidService::encode($status->id),
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null,
|
||||
'reblog' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'content_text' => $status->caption,
|
||||
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
'emojis' => CustomEmoji::scan($status->caption),
|
||||
'reblogs_count' => $status->reblogs_count ?? 0,
|
||||
'favourites_count' => $status->likes_count ?? 0,
|
||||
'reblogged' => null,
|
||||
'favourited' => null,
|
||||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary ?? '',
|
||||
'visibility' => $status->scope ?? $status->visibility,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
],
|
||||
'language' => null,
|
||||
'mentions' => StatusMentionService::get($status->id),
|
||||
'pf_type' => $status->type ?? $status->setType(),
|
||||
'reply_count' => (int) $status->reply_count,
|
||||
'comments_disabled' => (bool) $status->comments_disabled,
|
||||
'thread' => false,
|
||||
'replies' => [],
|
||||
'parent' => [],
|
||||
'place' => $status->place,
|
||||
'local' => (bool) $status->local,
|
||||
'taggedPeople' => $taggedPeople,
|
||||
'liked_by' => LikeService::likedBy($status),
|
||||
'media_attachments' => MediaService::get($status->id),
|
||||
'account' => AccountService::get($status->profile_id, true),
|
||||
'tags' => StatusHashtagService::statusTags($status->id),
|
||||
'poll' => $poll
|
||||
];
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Instance extends Model
|
||||
{
|
||||
protected $fillable = ['domain'];
|
||||
protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes'];
|
||||
|
||||
public function profiles()
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@ use Illuminate\Queue\InteractsWithQueue;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
use Image as Intervention;
|
||||
use Storage;
|
||||
|
||||
class AvatarOptimize implements ShouldQueue
|
||||
{
|
||||
|
@ -63,6 +64,13 @@ class AvatarOptimize implements ShouldQueue
|
|||
$avatar->save();
|
||||
Cache::forget('avatar:' . $avatar->profile_id);
|
||||
$this->deleteOldAvatar($avatar->media_path, $this->current);
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
|
||||
$this->uploadToCloud($avatar);
|
||||
} else {
|
||||
$avatar->cdn_url = null;
|
||||
$avatar->save();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
@ -79,4 +87,18 @@ class AvatarOptimize implements ShouldQueue
|
|||
@unlink($current);
|
||||
}
|
||||
}
|
||||
|
||||
protected function uploadToCloud($avatar)
|
||||
{
|
||||
$base = 'cache/avatars/' . $avatar->profile_id;
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$disk->deleteDirectory($base);
|
||||
$path = $base . '/' . 'avatar_' . strtolower(Str::random(random_int(3,6))) . $avatar->change_count . '.' . pathinfo($avatar->media_path, PATHINFO_EXTENSION);
|
||||
$url = $disk->put($path, Storage::get($avatar->media_path));
|
||||
$avatar->media_path = $path;
|
||||
$avatar->cdn_url = $disk->url($path);
|
||||
$avatar->save();
|
||||
Storage::delete($avatar->media_path);
|
||||
Cache::forget('avatar:' . $avatar->profile_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ class RemoteAvatarFetch implements ShouldQueue
|
|||
* @var int
|
||||
*/
|
||||
public $tries = 1;
|
||||
public $timeout = 300;
|
||||
public $maxExceptions = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
|
|
|
@ -60,13 +60,21 @@ class CommentPipeline implements ShouldQueue
|
|||
$actor = $comment->profile;
|
||||
|
||||
if(config('database.default') === 'mysql') {
|
||||
DB::transaction(function() use($status) {
|
||||
$count = DB::select( DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)"), [ 'kid' => $status->id]);
|
||||
$status->reply_count = count($count);
|
||||
$status->save();
|
||||
});
|
||||
$exp = DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)");
|
||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||
$count = DB::select($expQuery, [ 'kid' => $status->id ]);
|
||||
$status->reply_count = count($count);
|
||||
$status->save();
|
||||
} else {
|
||||
$status->reply_count = $status->reply_count + 1;
|
||||
$status->save();
|
||||
}
|
||||
|
||||
StatusService::del($comment->id);
|
||||
StatusService::del($status->id);
|
||||
Cache::forget('status:replies:all:' . $comment->id);
|
||||
Cache::forget('status:replies:all:' . $status->id);
|
||||
|
||||
if ($actor->id === $target->id || $status->comments_disabled == true) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use DB;
|
|||
use Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
|
@ -49,7 +50,12 @@ use App\{
|
|||
UserFilter,
|
||||
UserSetting,
|
||||
};
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollVote;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\UserPronoun;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
|
||||
class DeleteAccountPipeline implements ShouldQueue
|
||||
{
|
||||
|
@ -57,6 +63,11 @@ class DeleteAccountPipeline implements ShouldQueue
|
|||
|
||||
protected $user;
|
||||
|
||||
public $timeout = 900;
|
||||
public $tries = 3;
|
||||
public $maxExceptions = 1;
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
@ -65,144 +76,107 @@ class DeleteAccountPipeline implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
$user = $this->user;
|
||||
$this->deleteUserColumns($user);
|
||||
AccountService::del($user->profile_id);
|
||||
$profile = $user->profile;
|
||||
$id = $user->profile_id;
|
||||
Status::whereProfileId($id)->chunk(50, function($statuses) {
|
||||
foreach($statuses as $status) {
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
});
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
AccountLog::whereItemType('App\User')->whereItemId($user->id)->forceDelete();
|
||||
});
|
||||
AccountLog::whereItemType('App\User')->whereItemId($user->id)->forceDelete();
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
AccountInterstitial::whereUserId($user->id)->delete();
|
||||
});
|
||||
AccountInterstitial::whereUserId($user->id)->delete();
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
if($user->profile) {
|
||||
$avatar = $user->profile->avatar;
|
||||
$path = $avatar->media_path;
|
||||
if(!in_array($path, [
|
||||
'public/avatars/default.jpg',
|
||||
'public/avatars/default.png'
|
||||
])) {
|
||||
if(config('pixelfed.cloud_storage')) {
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$disk->delete($path);
|
||||
}
|
||||
$disk = Storage::disk(config('filesystems.local'));
|
||||
$disk->delete($path);
|
||||
}
|
||||
// Delete Avatar
|
||||
$profile->avatar->forceDelete();
|
||||
|
||||
$avatar->forceDelete();
|
||||
}
|
||||
// Delete Poll Votes
|
||||
PollVote::whereProfileId($id)->delete();
|
||||
|
||||
$id = $user->profile_id;
|
||||
// Delete Polls
|
||||
Poll::whereProfileId($id)->delete();
|
||||
|
||||
ImportData::whereProfileId($id)
|
||||
->cursor()
|
||||
->each(function($data) {
|
||||
$path = storage_path('app/'.$data->path);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
$data->delete();
|
||||
});
|
||||
ImportJob::whereProfileId($id)
|
||||
->cursor()
|
||||
->each(function($data) {
|
||||
$path = storage_path('app/'.$data->media_json);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
$data->delete();
|
||||
});
|
||||
MediaTag::whereProfileId($id)->delete();
|
||||
Bookmark::whereProfileId($id)->forceDelete();
|
||||
EmailVerification::whereUserId($user->id)->forceDelete();
|
||||
StatusHashtag::whereProfileId($id)->delete();
|
||||
DirectMessage::whereFromId($id)->orWhere('to_id', $id)->delete();
|
||||
StatusArchived::whereProfileId($id)->delete();
|
||||
UserPronoun::whereProfileId($id)->delete();
|
||||
FollowRequest::whereFollowingId($id)
|
||||
->orWhere('follower_id', $id)
|
||||
->forceDelete();
|
||||
Follower::whereProfileId($id)
|
||||
->orWhere('following_id', $id)
|
||||
->forceDelete();
|
||||
Like::whereProfileId($id)->forceDelete();
|
||||
});
|
||||
// Delete Portfolio
|
||||
Portfolio::whereProfileId($id)->delete();
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
$pid = $this->user->profile_id;
|
||||
|
||||
StoryView::whereProfileId($pid)->delete();
|
||||
$stories = Story::whereProfileId($pid)->get();
|
||||
foreach($stories as $story) {
|
||||
$path = storage_path('app/'.$story->path);
|
||||
ImportData::whereProfileId($id)
|
||||
->cursor()
|
||||
->each(function($data) {
|
||||
$path = storage_path('app/'.$data->path);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
$story->forceDelete();
|
||||
}
|
||||
$data->delete();
|
||||
});
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
$medias = Media::whereUserId($user->id)->get();
|
||||
foreach($medias as $media) {
|
||||
if(config('pixelfed.cloud_storage')) {
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$disk->delete($media->media_path);
|
||||
$disk->delete($media->thumbnail_path);
|
||||
ImportJob::whereProfileId($id)
|
||||
->cursor()
|
||||
->each(function($data) {
|
||||
$path = storage_path('app/'.$data->media_json);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
$disk = Storage::disk(config('filesystems.local'));
|
||||
$disk->delete($media->media_path);
|
||||
$disk->delete($media->thumbnail_path);
|
||||
$media->forceDelete();
|
||||
$data->delete();
|
||||
});
|
||||
|
||||
MediaTag::whereProfileId($id)->delete();
|
||||
Bookmark::whereProfileId($id)->forceDelete();
|
||||
EmailVerification::whereUserId($user->id)->forceDelete();
|
||||
StatusHashtag::whereProfileId($id)->delete();
|
||||
DirectMessage::whereFromId($id)->orWhere('to_id', $id)->delete();
|
||||
Conversation::whereFromId($id)->orWhere('to_id', $id)->delete();
|
||||
StatusArchived::whereProfileId($id)->delete();
|
||||
UserPronoun::whereProfileId($id)->delete();
|
||||
FollowRequest::whereFollowingId($id)
|
||||
->orWhere('follower_id', $id)
|
||||
->forceDelete();
|
||||
Follower::whereProfileId($id)
|
||||
->orWhere('following_id', $id)
|
||||
->each(function($follow) {
|
||||
FollowerService::remove($follow->profile_id, $follow->following_id);
|
||||
$follow->delete();
|
||||
});
|
||||
FollowerService::delCache($id);
|
||||
Like::whereProfileId($id)->forceDelete();
|
||||
Mention::whereProfileId($id)->forceDelete();
|
||||
|
||||
StoryView::whereProfileId($id)->delete();
|
||||
$stories = Story::whereProfileId($id)->get();
|
||||
foreach($stories as $story) {
|
||||
$path = storage_path('app/'.$story->path);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
});
|
||||
$story->forceDelete();
|
||||
}
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
Mention::whereProfileId($user->profile_id)->forceDelete();
|
||||
Notification::whereProfileId($user->profile_id)
|
||||
->orWhere('actor_id', $user->profile_id)
|
||||
->forceDelete();
|
||||
});
|
||||
UserDevice::whereUserId($user->id)->forceDelete();
|
||||
UserFilter::whereUserId($user->id)->forceDelete();
|
||||
UserSetting::whereUserId($user->id)->forceDelete();
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
$collections = Collection::whereProfileId($user->profile_id)->get();
|
||||
foreach ($collections as $collection) {
|
||||
$collection->items()->delete();
|
||||
$collection->delete();
|
||||
}
|
||||
Contact::whereUserId($user->id)->delete();
|
||||
HashtagFollow::whereUserId($user->id)->delete();
|
||||
OauthClient::whereUserId($user->id)->delete();
|
||||
DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
|
||||
DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
|
||||
ProfileSponsor::whereProfileId($user->profile_id)->delete();
|
||||
});
|
||||
Mention::whereProfileId($id)->forceDelete();
|
||||
Notification::whereProfileId($id)
|
||||
->orWhere('actor_id', $id)
|
||||
->forceDelete();
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
Status::whereProfileId($user->profile_id)->forceDelete();
|
||||
Report::whereUserId($user->id)->forceDelete();
|
||||
PublicTimelineService::warmCache(true, 400);
|
||||
$this->deleteProfile($user);
|
||||
});
|
||||
}
|
||||
$collections = Collection::whereProfileId($id)->get();
|
||||
foreach ($collections as $collection) {
|
||||
$collection->items()->delete();
|
||||
$collection->delete();
|
||||
}
|
||||
Contact::whereUserId($user->id)->delete();
|
||||
HashtagFollow::whereUserId($user->id)->delete();
|
||||
OauthClient::whereUserId($user->id)->delete();
|
||||
DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
|
||||
DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
|
||||
ProfileSponsor::whereProfileId($id)->delete();
|
||||
|
||||
protected function deleteProfile($user) {
|
||||
DB::transaction(function() use ($user) {
|
||||
Profile::whereUserId($user->id)->delete();
|
||||
$this->deleteUserSettings($user);
|
||||
});
|
||||
}
|
||||
|
||||
protected function deleteUserSettings($user) {
|
||||
|
||||
DB::transaction(function() use ($user) {
|
||||
UserDevice::whereUserId($user->id)->forceDelete();
|
||||
UserFilter::whereUserId($user->id)->forceDelete();
|
||||
UserSetting::whereUserId($user->id)->forceDelete();
|
||||
});
|
||||
Report::whereUserId($user->id)->forceDelete();
|
||||
PublicTimelineService::warmCache(true, 400);
|
||||
$this->deleteUserColumns($user);
|
||||
AccountService::del($user->profile_id);
|
||||
Profile::whereUserId($user->id)->delete();
|
||||
}
|
||||
|
||||
protected function deleteUserColumns($user)
|
||||
|
|
|
@ -39,6 +39,7 @@ use App\{
|
|||
ReportLog,
|
||||
StatusHashtag,
|
||||
Status,
|
||||
StatusView,
|
||||
Story,
|
||||
StoryView,
|
||||
User,
|
||||
|
@ -46,6 +47,10 @@ use App\{
|
|||
UserFilter,
|
||||
UserSetting,
|
||||
};
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollVote;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class DeleteRemoteProfilePipeline implements ShouldQueue
|
||||
{
|
||||
|
@ -53,6 +58,11 @@ class DeleteRemoteProfilePipeline implements ShouldQueue
|
|||
|
||||
protected $profile;
|
||||
|
||||
public $timeout = 900;
|
||||
public $tries = 3;
|
||||
public $maxExceptions = 1;
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public function __construct(Profile $profile)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
|
@ -61,80 +71,85 @@ class DeleteRemoteProfilePipeline implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
$profile = $this->profile;
|
||||
$pid = $profile->id;
|
||||
|
||||
if($profile->domain == null || $profile->private_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function() use ($profile) {
|
||||
$profile->avatar->forceDelete();
|
||||
$profile->status = 'delete';
|
||||
$profile->save();
|
||||
|
||||
$id = $profile->id;
|
||||
AccountService::del($pid);
|
||||
|
||||
MediaTag::whereProfileId($id)->delete();
|
||||
StatusHashtag::whereProfileId($id)->delete();
|
||||
DirectMessage::whereFromId($id)->delete();
|
||||
FollowRequest::whereFollowingId($id)
|
||||
->orWhere('follower_id', $id)
|
||||
->forceDelete();
|
||||
Follower::whereProfileId($id)
|
||||
->orWhere('following_id', $id)
|
||||
->forceDelete();
|
||||
Like::whereProfileId($id)->forceDelete();
|
||||
// Delete statuses
|
||||
Status::whereProfileId($pid)
|
||||
->chunk(50, function($statuses) {
|
||||
foreach($statuses as $status) {
|
||||
DeleteRemoteStatusPipeline::dispatch($status)->onQueue('delete');
|
||||
}
|
||||
});
|
||||
|
||||
DB::transaction(function() use ($profile) {
|
||||
$pid = $profile->id;
|
||||
StoryView::whereProfileId($pid)->delete();
|
||||
$stories = Story::whereProfileId($pid)->get();
|
||||
foreach($stories as $story) {
|
||||
$path = storage_path('app/'.$story->path);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
$story->forceDelete();
|
||||
// Delete Poll Votes
|
||||
PollVote::whereProfileId($pid)->delete();
|
||||
|
||||
// Delete Polls
|
||||
Poll::whereProfileId($pid)->delete();
|
||||
|
||||
// Delete Avatar
|
||||
$profile->avatar->forceDelete();
|
||||
|
||||
// Delete media tags
|
||||
MediaTag::whereProfileId($pid)->delete();
|
||||
|
||||
// Delete DMs
|
||||
DirectMessage::whereFromId($pid)->orWhere('to_id', $pid)->delete();
|
||||
Conversation::whereFromId($pid)->orWhere('to_id', $pid)->delete();
|
||||
|
||||
// Delete FollowRequests
|
||||
FollowRequest::whereFollowingId($pid)
|
||||
->orWhere('follower_id', $pid)
|
||||
->delete();
|
||||
|
||||
// Delete relationships
|
||||
Follower::whereProfileId($pid)
|
||||
->orWhere('following_id', $pid)
|
||||
->delete();
|
||||
|
||||
// Delete likes
|
||||
Like::whereProfileId($pid)->forceDelete();
|
||||
|
||||
// Delete Story Views + Stories
|
||||
StoryView::whereProfileId($pid)->delete();
|
||||
$stories = Story::whereProfileId($pid)->get();
|
||||
foreach($stories as $story) {
|
||||
$path = storage_path('app/'.$story->path);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
});
|
||||
$story->forceDelete();
|
||||
}
|
||||
|
||||
DB::transaction(function() use ($profile) {
|
||||
$medias = Media::whereProfileId($profile->id)->get();
|
||||
foreach($medias as $media) {
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
$thumb = storage_path('app/'.$media->thumbnail_path);
|
||||
if(is_file($path)) {
|
||||
unlink($path);
|
||||
// Delete mutes/blocks
|
||||
UserFilter::whereFilterableType('App\Profile')->whereFilterableId($pid)->delete();
|
||||
|
||||
// Delete mentions
|
||||
Mention::whereProfileId($pid)->forceDelete();
|
||||
|
||||
// Delete notifications
|
||||
Notification::whereProfileId($pid)
|
||||
->orWhere('actor_id', $pid)
|
||||
->chunk(50, function($notifications) {
|
||||
foreach($notifications as $n) {
|
||||
$n->forceDelete();
|
||||
}
|
||||
if(is_file($thumb)) {
|
||||
unlink($thumb);
|
||||
}
|
||||
$media->forceDelete();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
DB::transaction(function() use ($profile) {
|
||||
Mention::whereProfileId($profile->id)->forceDelete();
|
||||
Notification::whereProfileId($profile->id)
|
||||
->orWhere('actor_id', $profile->id)
|
||||
->forceDelete();
|
||||
});
|
||||
// Delete reports
|
||||
Report::whereProfileId($profile->id)->orWhere('reported_profile_id')->forceDelete();
|
||||
|
||||
DB::transaction(function() use ($profile) {
|
||||
Status::whereProfileId($profile->id)
|
||||
->cursor()
|
||||
->each(function($status) {
|
||||
AccountInterstitial::where('item_type', 'App\Status')
|
||||
->where('item_id', $status->id)
|
||||
->delete();
|
||||
$status->forceDelete();
|
||||
});
|
||||
Report::whereProfileId($profile->id)->forceDelete();
|
||||
$this->deleteProfile($profile);
|
||||
});
|
||||
}
|
||||
|
||||
protected function deleteProfile($profile) {
|
||||
DB::transaction(function() use ($profile) {
|
||||
Profile::findOrFail($profile->id)->delete();
|
||||
});
|
||||
// Delete profile
|
||||
Profile::findOrFail($profile->id)->delete();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,12 @@ use App\Status;
|
|||
use App\StatusHashtag;
|
||||
use App\StatusView;
|
||||
use App\Notification;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Services\StatusService;
|
||||
use App\Jobs\ProfilePipeline\DecrementPostCount;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
use Cache;
|
||||
|
||||
class DeleteRemoteStatusPipeline implements ShouldQueue
|
||||
{
|
||||
|
@ -30,6 +32,11 @@ class DeleteRemoteStatusPipeline implements ShouldQueue
|
|||
|
||||
protected $status;
|
||||
|
||||
public $timeout = 30;
|
||||
public $tries = 2;
|
||||
public $maxExceptions = 1;
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
|
@ -37,7 +44,7 @@ class DeleteRemoteStatusPipeline implements ShouldQueue
|
|||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status->withoutRelations();
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,9 +56,12 @@ class DeleteRemoteStatusPipeline implements ShouldQueue
|
|||
{
|
||||
$status = $this->status;
|
||||
|
||||
if(AccountService::get($status->profile_id, true)) {
|
||||
DecrementPostCount::dispatch($status->profile_id)->onQueue('feed');
|
||||
}
|
||||
|
||||
NetworkTimelineService::del($status->id);
|
||||
StatusService::del($status->id, true);
|
||||
DecrementPostCount::dispatchNow($status->profile_id);
|
||||
Bookmark::whereStatusId($status->id)->delete();
|
||||
Notification::whereItemType('App\Status')
|
||||
->whereItemId($status->id)
|
||||
|
@ -62,13 +72,14 @@ class DeleteRemoteStatusPipeline implements ShouldQueue
|
|||
Media::whereStatusId($status->id)
|
||||
->get()
|
||||
->each(function($media) {
|
||||
MediaDeletePipeline::dispatchNow($media);
|
||||
MediaDeletePipeline::dispatch($media)->onQueue('mmo');
|
||||
});
|
||||
Mention::whereStatusId($status->id)->forceDelete();
|
||||
Report::whereObjectType('App\Status')->whereObjectId($status->id)->delete();
|
||||
StatusHashtag::whereStatusId($status->id)->delete();
|
||||
StatusView::whereStatusId($status->id)->delete();
|
||||
Status::whereReblogOfId($status->id)->forceDelete();
|
||||
$status->delete();
|
||||
$status->forceDelete();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Jobs\FollowPipeline;
|
||||
|
||||
use App\Follower;
|
||||
use App\Notification;
|
||||
use Cache;
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
@ -11,6 +12,8 @@ use Illuminate\Queue\InteractsWithQueue;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
|
||||
class FollowPipeline implements ShouldQueue
|
||||
{
|
||||
|
@ -46,9 +49,45 @@ class FollowPipeline implements ShouldQueue
|
|||
$actor = $follower->actor;
|
||||
$target = $follower->target;
|
||||
|
||||
if(!$actor || !$target) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cache::forget('profile:following:' . $actor->id);
|
||||
Cache::forget('profile:following:' . $target->id);
|
||||
|
||||
FollowerService::add($actor->id, $target->id);
|
||||
|
||||
$actorProfileSync = Cache::get(FollowerService::FOLLOWING_SYNC_KEY . $actor->id);
|
||||
if(!$actorProfileSync) {
|
||||
FollowServiceWarmCache::dispatch($actor->id)->onQueue('low');
|
||||
} else {
|
||||
if($actor->following_count) {
|
||||
$actor->increment('following_count');
|
||||
} else {
|
||||
$count = Follower::whereProfileId($actor->id)->count();
|
||||
$actor->following_count = $count;
|
||||
$actor->save();
|
||||
}
|
||||
Cache::put(FollowerService::FOLLOWING_SYNC_KEY . $actor->id, 1, 604800);
|
||||
AccountService::del($actor->id);
|
||||
}
|
||||
|
||||
$targetProfileSync = Cache::get(FollowerService::FOLLOWERS_SYNC_KEY . $target->id);
|
||||
if(!$targetProfileSync) {
|
||||
FollowServiceWarmCache::dispatch($target->id)->onQueue('low');
|
||||
} else {
|
||||
if($target->followers_count) {
|
||||
$target->increment('followers_count');
|
||||
} else {
|
||||
$count = Follower::whereFollowingId($target->id)->count();
|
||||
$target->followers_count = $count;
|
||||
$target->save();
|
||||
}
|
||||
Cache::put(FollowerService::FOLLOWERS_SYNC_KEY . $target->id, 1, 604800);
|
||||
AccountService::del($target->id);
|
||||
}
|
||||
|
||||
if($target->domain || !$target->private_key) {
|
||||
return;
|
||||
}
|
||||
|
|
87
app/Jobs/FollowPipeline/FollowServiceWarmCache.php
Normal file
87
app/Jobs/FollowPipeline/FollowServiceWarmCache.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\FollowPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use Cache;
|
||||
use DB;
|
||||
use App\Profile;
|
||||
|
||||
class FollowServiceWarmCache implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $profileId;
|
||||
public $tries = 5;
|
||||
public $timeout = 5000;
|
||||
public $failOnTimeout = false;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($profileId)
|
||||
{
|
||||
$this->profileId = $profileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = $this->profileId;
|
||||
|
||||
$account = AccountService::get($id, true);
|
||||
|
||||
if(!$account) {
|
||||
Cache::put(FollowerService::FOLLOWERS_SYNC_KEY . $id, 1, 604800);
|
||||
Cache::put(FollowerService::FOLLOWING_SYNC_KEY . $id, 1, 604800);
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('followers')
|
||||
->select('id', 'following_id', 'profile_id')
|
||||
->whereFollowingId($id)
|
||||
->orderBy('id')
|
||||
->chunk(200, function($followers) use($id) {
|
||||
foreach($followers as $follow) {
|
||||
FollowerService::add($follow->profile_id, $id);
|
||||
}
|
||||
});
|
||||
|
||||
DB::table('followers')
|
||||
->select('id', 'following_id', 'profile_id')
|
||||
->whereProfileId($id)
|
||||
->orderBy('id')
|
||||
->chunk(200, function($followers) use($id) {
|
||||
foreach($followers as $follow) {
|
||||
FollowerService::add($id, $follow->following_id);
|
||||
}
|
||||
});
|
||||
|
||||
Cache::put(FollowerService::FOLLOWERS_SYNC_KEY . $id, 1, 604800);
|
||||
Cache::put(FollowerService::FOLLOWING_SYNC_KEY . $id, 1, 604800);
|
||||
|
||||
$profile = Profile::find($id);
|
||||
if($profile) {
|
||||
$profile->following_count = DB::table('followers')->whereProfileId($id)->count();
|
||||
$profile->followers_count = DB::table('followers')->whereFollowingId($id)->count();
|
||||
$profile->save();
|
||||
}
|
||||
|
||||
AccountService::del($id);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
114
app/Jobs/FollowPipeline/UnfollowPipeline.php
Normal file
114
app/Jobs/FollowPipeline/UnfollowPipeline.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\FollowPipeline;
|
||||
|
||||
use App\Follower;
|
||||
use App\FollowRequest;
|
||||
use App\Notification;
|
||||
use App\Profile;
|
||||
use Cache;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\NotificationService;
|
||||
|
||||
class UnfollowPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $actor;
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($actor, $target)
|
||||
{
|
||||
$this->actor = $actor;
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$actor = $this->actor;
|
||||
$target = $this->target;
|
||||
|
||||
$actorProfile = Profile::find($actor);
|
||||
if(!$actorProfile) {
|
||||
return;
|
||||
}
|
||||
$targetProfile = Profile::find($target);
|
||||
if(!$targetProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
FollowerService::remove($actor, $target);
|
||||
|
||||
$actorProfileSync = Cache::get(FollowerService::FOLLOWING_SYNC_KEY . $actor);
|
||||
if(!$actorProfileSync) {
|
||||
FollowServiceWarmCache::dispatch($actor)->onQueue('low');
|
||||
} else {
|
||||
if($actorProfile->following_count) {
|
||||
$actorProfile->decrement('following_count');
|
||||
} else {
|
||||
$count = Follower::whereProfileId($actor)->count();
|
||||
$actorProfile->following_count = $count;
|
||||
$actorProfile->save();
|
||||
}
|
||||
Cache::put(FollowerService::FOLLOWING_SYNC_KEY . $actor, 1, 604800);
|
||||
AccountService::del($actor);
|
||||
}
|
||||
|
||||
$targetProfileSync = Cache::get(FollowerService::FOLLOWERS_SYNC_KEY . $target);
|
||||
if(!$targetProfileSync) {
|
||||
FollowServiceWarmCache::dispatch($target)->onQueue('low');
|
||||
} else {
|
||||
if($targetProfile->followers_count) {
|
||||
$targetProfile->decrement('followers_count');
|
||||
} else {
|
||||
$count = Follower::whereFollowingId($target)->count();
|
||||
$targetProfile->followers_count = $count;
|
||||
$targetProfile->save();
|
||||
}
|
||||
Cache::put(FollowerService::FOLLOWERS_SYNC_KEY . $target, 1, 604800);
|
||||
AccountService::del($target);
|
||||
}
|
||||
|
||||
if($targetProfile->domain == null) {
|
||||
Notification::withTrashed()
|
||||
->whereProfileId($target)
|
||||
->whereAction('follow')
|
||||
->whereActorId($actor)
|
||||
->whereItemId($target)
|
||||
->whereItemType('App\Profile')
|
||||
->get()
|
||||
->each(function($n) {
|
||||
NotificationService::del($n->profile_id, $n->id);
|
||||
$n->forceDelete();
|
||||
});
|
||||
}
|
||||
|
||||
if($actorProfile->domain == null && config('instance.timeline.home.cached')) {
|
||||
Cache::forget('pf:timelines:home:' . $actor);
|
||||
}
|
||||
|
||||
FollowRequest::whereFollowingId($target)
|
||||
->whereFollowerId($actor)
|
||||
->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -39,16 +39,18 @@ class ImageOptimize implements ShouldQueue
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(config('pixelfed.optimize_image') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$media = $this->media;
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
if (!is_file($path) || $media->skip_optimize) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageResize::dispatch($media);
|
||||
if(config('pixelfed.optimize_image') == false) {
|
||||
ImageThumbnail::dispatch($media)->onQueue('mmo');
|
||||
return;
|
||||
} else {
|
||||
ImageResize::dispatch($media)->onQueue('mmo');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,12 +49,16 @@ class ImageResize implements ShouldQueue
|
|||
return;
|
||||
}
|
||||
|
||||
if(!config('pixelfed.optimize_image')) {
|
||||
ImageThumbnail::dispatch($media)->onQueue('mmo');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$img = new Image();
|
||||
$img->resizeImage($media);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
ImageThumbnail::dispatch($media);
|
||||
ImageThumbnail::dispatch($media)->onQueue('mmo');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,6 @@ class ImageThumbnail implements ShouldQueue
|
|||
$media->processed_at = Carbon::now();
|
||||
$media->save();
|
||||
|
||||
ImageUpdate::dispatch($media);
|
||||
ImageUpdate::dispatch($media)->onQueue('mmo');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,10 +61,12 @@ class ImageUpdate implements ShouldQueue
|
|||
return;
|
||||
}
|
||||
|
||||
if (in_array($media->mime, $this->protectedMimes) == true) {
|
||||
ImageOptimizer::optimize($thumb);
|
||||
if(!$media->skip_optimize) {
|
||||
ImageOptimizer::optimize($path);
|
||||
if(config('pixelfed.optimize_image')) {
|
||||
if (in_array($media->mime, $this->protectedMimes) == true) {
|
||||
ImageOptimizer::optimize($thumb);
|
||||
if(!$media->skip_optimize) {
|
||||
ImageOptimizer::optimize($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,32 +2,35 @@
|
|||
|
||||
namespace App\Jobs\InboxPipeline;
|
||||
|
||||
use App\Util\ActivityPub\Inbox;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Inbox;
|
||||
|
||||
class SharedInboxWorker implements ShouldQueue
|
||||
class ActivityHandler implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $request;
|
||||
protected $profile;
|
||||
protected $username;
|
||||
protected $headers;
|
||||
protected $payload;
|
||||
|
||||
public $timeout = 60;
|
||||
public $timeout = 300;
|
||||
public $tries = 1;
|
||||
public $maxExceptions = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($request, $payload)
|
||||
public function __construct($headers, $username, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->username = $username;
|
||||
$this->headers = $headers;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
|
@ -38,6 +41,7 @@ class SharedInboxWorker implements ShouldQueue
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
(new Inbox($this->request, null, $this->payload))->handleSharedInbox();
|
||||
(new Inbox($this->headers, $this->username, $this->payload))->handle();
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -14,8 +14,9 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Zttp\Zttp;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
|
||||
class DeleteWorker implements ShouldQueue
|
||||
{
|
||||
|
@ -24,8 +25,8 @@ class DeleteWorker implements ShouldQueue
|
|||
protected $headers;
|
||||
protected $payload;
|
||||
|
||||
public $timeout = 120;
|
||||
public $tries = 3;
|
||||
public $timeout = 300;
|
||||
public $tries = 1;
|
||||
public $maxExceptions = 1;
|
||||
|
||||
/**
|
||||
|
@ -68,50 +69,43 @@ class DeleteWorker implements ShouldQueue
|
|||
))
|
||||
) {
|
||||
$actor = $payload['actor'];
|
||||
$hash = strlen($actor) <= 48 ?
|
||||
'b:' . base64_encode($actor) :
|
||||
'h:' . hash('sha256', $actor);
|
||||
|
||||
$key = 'ap:inbox:actor-delete-exists:' . $hash;
|
||||
$actorDelete = Cache::remember($key, now()->addMinutes(15), function() use($actor) {
|
||||
return Profile::whereRemoteUrl($actor)
|
||||
->whereNotNull('domain')
|
||||
->exists();
|
||||
});
|
||||
if($actorDelete) {
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
Cache::set($key, false);
|
||||
$profile = Profile::whereNotNull('domain')
|
||||
->whereNull('status')
|
||||
->whereRemoteUrl($actor)
|
||||
->first();
|
||||
if($profile) {
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('delete');
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
$actorDelete = Profile::whereRemoteUrl($actor)->exists();
|
||||
if($actorDelete) {
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
Cache::set($key, false);
|
||||
$profile = Profile::whereNotNull('domain')
|
||||
->whereNull('status')
|
||||
->whereRemoteUrl($actor)
|
||||
->first();
|
||||
if($profile) {
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox');
|
||||
}
|
||||
return 1;
|
||||
} else {
|
||||
// Signature verification failed, exit.
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
} else {
|
||||
// Signature verification failed, exit.
|
||||
// Remote user doesn't exist, exit early.
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
} else {
|
||||
// Remote user doesn't exist, exit early.
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$profile = null;
|
||||
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return 1;
|
||||
} else if($this->blindKeyRotation($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return 1;
|
||||
} else {
|
||||
return 1;
|
||||
$profile = null;
|
||||
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
ActivityHandler::dispatch($headers, $profile, $payload)->onQueue('delete');
|
||||
return 1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function verifySignature($headers, $payload)
|
||||
|
@ -121,17 +115,22 @@ class DeleteWorker implements ShouldQueue
|
|||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
|
||||
if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||
|
@ -140,20 +139,20 @@ class DeleteWorker implements ShouldQueue
|
|||
&& is_array($bodyDecoded['object'])
|
||||
&& isset($bodyDecoded['object']['attributedTo'])
|
||||
) {
|
||||
$attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']);
|
||||
if(is_array($attr)) {
|
||||
if(isset($attr['id'])) {
|
||||
$attr = $attr['id'];
|
||||
} else {
|
||||
$attr = "";
|
||||
}
|
||||
}
|
||||
if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
|
||||
return;
|
||||
}
|
||||
$attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']);
|
||||
if(is_array($attr)) {
|
||||
if(isset($attr['id'])) {
|
||||
$attr = $attr['id'];
|
||||
} else {
|
||||
$attr = "";
|
||||
}
|
||||
}
|
||||
if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
|
@ -161,11 +160,11 @@ class DeleteWorker implements ShouldQueue
|
|||
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
if(!$actor) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||
if(!$pkey) {
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
$inboxPath = "/f/inbox";
|
||||
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||
|
@ -192,6 +191,11 @@ class DeleteWorker implements ShouldQueue
|
|||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
|
||||
if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||
if(!$actor) {
|
||||
|
@ -200,10 +204,20 @@ class DeleteWorker implements ShouldQueue
|
|||
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||
return;
|
||||
}
|
||||
$res = Zttp::timeout(60)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
|
||||
try {
|
||||
$res = Http::timeout(20)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
} catch (ConnectionException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!$res->ok()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(!isset($res['publicKey'], $res['publicKey']['id'])) {
|
||||
return;
|
||||
|
|
|
@ -5,190 +5,215 @@ namespace App\Jobs\InboxPipeline;
|
|||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\{
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Inbox
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Inbox
|
||||
};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Zttp\Zttp;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Lottery;
|
||||
|
||||
class InboxValidator implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $username;
|
||||
protected $headers;
|
||||
protected $payload;
|
||||
protected $username;
|
||||
protected $headers;
|
||||
protected $payload;
|
||||
|
||||
public $timeout = 60;
|
||||
public $tries = 1;
|
||||
public $timeout = 300;
|
||||
public $tries = 1;
|
||||
public $maxExceptions = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($username, $headers, $payload)
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->headers = $headers;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($username, $headers, $payload)
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->headers = $headers;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$username = $this->username;
|
||||
$headers = $this->headers;
|
||||
$payload = json_decode($this->payload, true, 8);
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$username = $this->username;
|
||||
$headers = $this->headers;
|
||||
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
|
||||
if(empty($headers) || empty($this->payload) || !isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 3600);
|
||||
}
|
||||
$payload = json_decode($this->payload, true, 8);
|
||||
|
||||
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 3600);
|
||||
}
|
||||
|
||||
if(empty($profile) || empty($headers) || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
|
||||
|
||||
if($profile->status != null) {
|
||||
return;
|
||||
}
|
||||
if(empty($profile) || empty($headers) || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($this->verifySignature($headers, $profile, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else if($this->blindKeyRotation($headers, $profile, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if($profile->status != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
if($this->verifySignature($headers, $profile, $payload) == true) {
|
||||
if(isset($payload['type']) && in_array($payload['type'], ['Follow', 'Accept']) ) {
|
||||
ActivityHandler::dispatch($headers, $profile, $payload)->onQueue('follow');
|
||||
} else {
|
||||
$onQueue = Lottery::odds(1, 12)->winner(fn () => 'high')->loser(fn () => 'inbox')->choose();
|
||||
ActivityHandler::dispatch($headers, $profile, $payload)->onQueue($onQueue);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
protected function verifySignature($headers, $profile, $payload)
|
||||
{
|
||||
$body = $this->payload;
|
||||
$bodyDecoded = $payload;
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if(!isset($bodyDecoded['id'])) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
if(isset($bodyDecoded['object'])
|
||||
&& is_array($bodyDecoded['object'])
|
||||
&& isset($bodyDecoded['object']['attributedTo'])
|
||||
) {
|
||||
$attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']);
|
||||
if(is_array($attr)) {
|
||||
if(isset($attr['id'])) {
|
||||
$attr = $attr['id'];
|
||||
} else {
|
||||
$attr = "";
|
||||
}
|
||||
}
|
||||
if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return;
|
||||
abort(400, 'Invalid request');
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
$actorUrl = Helpers::pluckval($bodyDecoded['actor']);
|
||||
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||
if(!$pkey) {
|
||||
return 0;
|
||||
}
|
||||
$inboxPath = "/users/{$profile->username}/inbox";
|
||||
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||
if($verified == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function blindKeyRotation($headers, $profile, $payload)
|
||||
{
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||
return;
|
||||
}
|
||||
$res = Zttp::timeout(60)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) {
|
||||
return;
|
||||
}
|
||||
if($res['publicKey']['id'] !== $actor->key_id) {
|
||||
return;
|
||||
}
|
||||
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$actor->save();
|
||||
return $this->verifySignature($headers, $profile, $payload);
|
||||
}
|
||||
protected function verifySignature($headers, $profile, $payload)
|
||||
{
|
||||
$body = $this->payload;
|
||||
$bodyDecoded = $payload;
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return false;
|
||||
}
|
||||
if(!$date) {
|
||||
return false;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if(!isset($bodyDecoded['id'])) {
|
||||
return false;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
|
||||
if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
if(isset($bodyDecoded['object'])
|
||||
&& is_array($bodyDecoded['object'])
|
||||
&& isset($bodyDecoded['object']['attributedTo'])
|
||||
) {
|
||||
$attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']);
|
||||
if(is_array($attr)) {
|
||||
if(isset($attr['id'])) {
|
||||
$attr = $attr['id'];
|
||||
} else {
|
||||
$attr = "";
|
||||
}
|
||||
}
|
||||
if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return false;
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
$actorUrl = Helpers::pluckval($bodyDecoded['actor']);
|
||||
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
if(!$actor) {
|
||||
return false;
|
||||
}
|
||||
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||
if(!$pkey) {
|
||||
return false;
|
||||
}
|
||||
$inboxPath = "/users/{$profile->username}/inbox";
|
||||
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||
if($verified == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function blindKeyRotation($headers, $profile, $payload)
|
||||
{
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
|
||||
if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Http::timeout(20)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
} catch (ConnectionException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!$res->ok()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) {
|
||||
return;
|
||||
}
|
||||
if($res['publicKey']['id'] !== $actor->key_id) {
|
||||
return;
|
||||
}
|
||||
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$actor->save();
|
||||
return $this->verifySignature($headers, $profile, $payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,180 +5,195 @@ namespace App\Jobs\InboxPipeline;
|
|||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\{
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Inbox
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Inbox
|
||||
};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Zttp\Zttp;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
|
||||
class InboxWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $headers;
|
||||
protected $payload;
|
||||
protected $headers;
|
||||
protected $payload;
|
||||
|
||||
public $timeout = 60;
|
||||
public $tries = 1;
|
||||
public $timeout = 300;
|
||||
public $tries = 1;
|
||||
public $maxExceptions = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($headers, $payload)
|
||||
{
|
||||
$this->headers = $headers;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($headers, $payload)
|
||||
{
|
||||
$this->headers = $headers;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = null;
|
||||
$headers = $this->headers;
|
||||
$payload = json_decode($this->payload, true, 8);
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = null;
|
||||
$headers = $this->headers;
|
||||
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 3600);
|
||||
}
|
||||
if(empty($headers) || empty($this->payload) || !isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
$payload = json_decode($this->payload, true, 8);
|
||||
|
||||
if(empty($headers) || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 3600);
|
||||
}
|
||||
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else if($this->blindKeyRotation($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
ActivityHandler::dispatch($headers, $profile, $payload)->onQueue('shared');
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function verifySignature($headers, $payload)
|
||||
{
|
||||
$body = $this->payload;
|
||||
$bodyDecoded = $payload;
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if(!isset($bodyDecoded['id'])) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
if(isset($bodyDecoded['object'])
|
||||
&& is_array($bodyDecoded['object'])
|
||||
&& isset($bodyDecoded['object']['attributedTo'])
|
||||
) {
|
||||
$attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']);
|
||||
if(is_array($attr)) {
|
||||
if(isset($attr['id'])) {
|
||||
$attr = $attr['id'];
|
||||
} else {
|
||||
$attr = "";
|
||||
}
|
||||
}
|
||||
if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return;
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
$actorUrl = Helpers::pluckval($bodyDecoded['actor']);
|
||||
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||
if(!$pkey) {
|
||||
return 0;
|
||||
}
|
||||
$inboxPath = "/f/inbox";
|
||||
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||
if($verified == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
protected function verifySignature($headers, $payload)
|
||||
{
|
||||
$body = $this->payload;
|
||||
$bodyDecoded = $payload;
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return false;
|
||||
}
|
||||
if(!$date) {
|
||||
return false;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if(!isset($bodyDecoded['id'])) {
|
||||
return false;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
|
||||
protected function blindKeyRotation($headers, $payload)
|
||||
{
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||
return;
|
||||
}
|
||||
$res = Zttp::timeout(60)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) {
|
||||
return;
|
||||
}
|
||||
if($res['publicKey']['id'] !== $actor->key_id) {
|
||||
return;
|
||||
}
|
||||
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$actor->save();
|
||||
return $this->verifySignature($headers, $payload);
|
||||
}
|
||||
if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
if(isset($bodyDecoded['object'])
|
||||
&& is_array($bodyDecoded['object'])
|
||||
&& isset($bodyDecoded['object']['attributedTo'])
|
||||
) {
|
||||
$attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']);
|
||||
if(is_array($attr)) {
|
||||
if(isset($attr['id'])) {
|
||||
$attr = $attr['id'];
|
||||
} else {
|
||||
$attr = "";
|
||||
}
|
||||
}
|
||||
if(parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return false;
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
$actorUrl = Helpers::pluckval($bodyDecoded['actor']);
|
||||
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
if(!$actor) {
|
||||
return false;
|
||||
}
|
||||
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||
if(!$pkey) {
|
||||
return false;
|
||||
}
|
||||
$inboxPath = "/f/inbox";
|
||||
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||
if($verified == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function blindKeyRotation($headers, $payload)
|
||||
{
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
if(!isset($signatureData['keyId'], $signatureData['signature'], $signatureData['headers']) || isset($signatureData['error'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Http::timeout(20)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
} catch (ConnectionException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!$res->ok()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(!$res || empty($res) || !isset($res['publicKey']) || !isset($res['publicKey']['id'])) {
|
||||
return;
|
||||
}
|
||||
if($res['publicKey']['id'] !== $actor->key_id) {
|
||||
return;
|
||||
}
|
||||
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$actor->save();
|
||||
return $this->verifySignature($headers, $payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,11 @@ class MediaDeletePipeline implements ShouldQueue
|
|||
|
||||
protected $media;
|
||||
|
||||
public $timeout = 300;
|
||||
public $tries = 3;
|
||||
public $maxExceptions = 1;
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public function __construct(Media $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
|
@ -39,33 +44,28 @@ class MediaDeletePipeline implements ShouldQueue
|
|||
if(config_cache('pixelfed.cloud_storage') == true) {
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
|
||||
if($path) {
|
||||
if($path && $disk->exists($path)) {
|
||||
$disk->delete($path);
|
||||
}
|
||||
|
||||
if($thumb) {
|
||||
if($thumb && $disk->exists($thumb)) {
|
||||
$disk->delete($thumb);
|
||||
}
|
||||
|
||||
if(count($e) > 4 && count($disk->files($i)) == 0) {
|
||||
$disk->deleteDirectory($i);
|
||||
}
|
||||
}
|
||||
|
||||
$disk = Storage::disk(config('filesystems.local'));
|
||||
|
||||
if($path && $disk->exists($path)) {
|
||||
$disk->delete($path);
|
||||
}
|
||||
|
||||
if($thumb && $disk->exists($thumb)) {
|
||||
$disk->delete($thumb);
|
||||
}
|
||||
if(count($e) > 4 && count($disk->files($i)) == 0) {
|
||||
$disk->deleteDirectory($i);
|
||||
}
|
||||
|
||||
$media->forceDelete();
|
||||
$media->delete();
|
||||
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\MediaPipeline;
|
||||
|
||||
use App\Media;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class MediaFixLocalFilesystemCleanupPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 1800;
|
||||
public $tries = 5;
|
||||
public $maxExceptions = 1;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if(config_cache('pixelfed.cloud_storage') == false) {
|
||||
// Only run if cloud storage is enabled
|
||||
return;
|
||||
}
|
||||
|
||||
$disk = Storage::disk('local');
|
||||
$cloud = Storage::disk(config('filesystems.cloud'));
|
||||
|
||||
Media::whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
|
||||
->chunk(20, function ($medias) use($disk, $cloud) {
|
||||
foreach($medias as $media) {
|
||||
if(!str_starts_with($media->media_path, 'public')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if($disk->exists($media->media_path) && $cloud->exists($media->media_path)) {
|
||||
$disk->delete($media->media_path);
|
||||
}
|
||||
|
||||
if($media->thumbnail_path) {
|
||||
if($disk->exists($media->thumbnail_path)) {
|
||||
$disk->delete($media->thumbnail_path);
|
||||
}
|
||||
}
|
||||
|
||||
$paths = explode('/', $media->media_path);
|
||||
if(count($paths) === 7) {
|
||||
array_pop($paths);
|
||||
$baseDir = implode('/', $paths);
|
||||
|
||||
if(count($disk->allFiles($baseDir)) === 0) {
|
||||
$disk->deleteDirectory($baseDir);
|
||||
|
||||
array_pop($paths);
|
||||
$baseDir = implode('/', $paths);
|
||||
|
||||
if(count($disk->allFiles($baseDir)) === 0) {
|
||||
$disk->deleteDirectory($baseDir);
|
||||
|
||||
array_pop($paths);
|
||||
$baseDir = implode('/', $paths);
|
||||
|
||||
if(count($disk->allFiles($baseDir)) === 0) {
|
||||
$disk->deleteDirectory($baseDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue