Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
![]() |
649692dcc1 |
680 changed files with 44720 additions and 142937 deletions
|
@ -7,7 +7,7 @@ jobs:
|
|||
build:
|
||||
docker:
|
||||
# Specify the version you desire here
|
||||
- image: cimg/php:7.4.26
|
||||
- image: circleci/php:7.3-cli-stretch-node
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
|
@ -22,6 +22,7 @@ jobs:
|
|||
- checkout
|
||||
|
||||
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
|
||||
- run: sudo -E docker-php-ext-install bcmath pcntl zip
|
||||
|
||||
# Download and cache dependencies
|
||||
|
||||
|
|
15
.env.docker
15
.env.docker
|
@ -56,16 +56,11 @@ MAIL_ENCRYPTION=null
|
|||
|
||||
## Databases (MySQL)
|
||||
DB_CONNECTION=mysql
|
||||
DB_DATABASE=pixelfed_prod
|
||||
DB_HOST=db
|
||||
DB_PASSWORD=pixelfed_db_pass
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=pixelfed
|
||||
DB_USERNAME=pixelfed
|
||||
# pass the same values to the db itself
|
||||
MYSQL_DATABASE=pixelfed_prod
|
||||
MYSQL_PASSWORD=pixelfed_db_pass
|
||||
MYSQL_RANDOM_ROOT_PASSWORD=true
|
||||
MYSQL_USER=pixelfed
|
||||
DB_PASSWORD=pixelfed
|
||||
|
||||
## Databases (Postgres)
|
||||
#DB_CONNECTION=pgsql
|
||||
|
@ -79,7 +74,7 @@ MYSQL_USER=pixelfed
|
|||
REDIS_CLIENT=phpredis
|
||||
REDIS_SCHEME=tcp
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=redis_password
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
REDIS_DATABASE=0
|
||||
|
||||
|
@ -120,7 +115,7 @@ PF_COSTAR_ENABLED=false
|
|||
MEDIA_EXIF_DATABASE=false
|
||||
|
||||
## Logging
|
||||
LOG_CHANNEL=stderr
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
## Image
|
||||
IMAGE_DRIVER=imagick
|
||||
|
|
401
CHANGELOG.md
401
CHANGELOG.md
|
@ -1,365 +1,16 @@
|
|||
# Release Notes
|
||||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.2...dev)
|
||||
|
||||
### Added
|
||||
- Custom Emoji ([#3166](https://github.com/pixelfed/pixelfed/pull/3166))
|
||||
|
||||
### Metro 2.0 UI
|
||||
- Added Hovercards ([16ced7b4](https://github.com/pixelfed/pixelfed/commit/16ced7b4))
|
||||
- Fix word-break on statuses ([16ced7b4](https://github.com/pixelfed/pixelfed/commit/16ced7b4))
|
||||
- Add pronouns to hovercards ([33f863e8](https://github.com/pixelfed/pixelfed/commit/33f863e8))
|
||||
- Improved onboarding ([042c5b6c](https://github.com/pixelfed/pixelfed/commit/042c5b6c))
|
||||
|
||||
### Updated
|
||||
- Updated MediaStorageService, fix remote avatar bug. ([1c20d696](https://github.com/pixelfed/pixelfed/commit/1c20d696))
|
||||
- Updated WebfingerService. Fixes #3167. ([aff74566](https://github.com/pixelfed/pixelfed/commit/aff74566))
|
||||
- Updated ComposeModal, add max file size and allowed mime types. Fixes #3162. ([879281cc](https://github.com/pixelfed/pixelfed/commit/879281cc))
|
||||
- Updated profile embeds, fix NaN bug and improve performance. ([3bd211d7](https://github.com/pixelfed/pixelfed/commit/3bd211d7))
|
||||
- Updated ApiV1Controller, improve follow count cache invalidation. ([4b6effb9](https://github.com/pixelfed/pixelfed/commit/4b6effb9))
|
||||
- Updated web routes, fix atom feeds for account usernames containing a dot. ([8c54ab57](https://github.com/pixelfed/pixelfed/commit/8c54ab57))
|
||||
- Updated atom feeds, include media alt text. Fixes #3184. ([5d9b6863](https://github.com/pixelfed/pixelfed/commit/5d9b6863))
|
||||
- Updated ApiV1Controller, add custom_emoji endpoint. ([16e72518](https://github.com/pixelfed/pixelfed/commit/16e72518))
|
||||
- Updated InternalApiController, redirect remote post and profiles to Metro 2.0. ([3c35158e](https://github.com/pixelfed/pixelfed/commit/3c35158e))
|
||||
- Updated BaseApiController, improve favourites endpoint. ([f063cb01](https://github.com/pixelfed/pixelfed/commit/f063cb01))
|
||||
- Updated ApiV1Controller, invalidate status reply cache on new reply. ([3c261bbf](https://github.com/pixelfed/pixelfed/commit/3c261bbf))
|
||||
- Updated PublicApiController, add bookmark state to timeline endpoints. ([c0b1e042](https://github.com/pixelfed/pixelfed/commit/c0b1e042))
|
||||
- Updated ApiV1Controller, fix private status replies returning 404. ([73226360](https://github.com/pixelfed/pixelfed/commit/73226360))
|
||||
- Updated StatusService, use BookmarkService for bookmarked state. ([a7d71551](https://github.com/pixelfed/pixelfed/commit/a7d71551))
|
||||
- Updated Apis, added ReblogService to improve reblogged state for api entities ([6cfd6be5](https://github.com/pixelfed/pixelfed/commit/6cfd6be5))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.2 (2022-01-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.1...v0.11.2)
|
||||
|
||||
### Breaking
|
||||
- Dropped support for PHP 7.3 [#3041](https://github.com/pixelfed/pixelfed/pull/3041)
|
||||
|
||||
### Metro 2.0 UI
|
||||
- Added UI Settings modal and fixed height media previews setting ([f2467e71](https://github.com/pixelfed/pixelfed/commit/f2467e71))
|
||||
- Set max-width of 1440px for larger screens ([af68872a](https://github.com/pixelfed/pixelfed/commit/af68872a))
|
||||
- Add link to sidebar profile card ([85964510](https://github.com/pixelfed/pixelfed/commit/85964510))
|
||||
- Improved search bar, now resolves (and imports) remote accounts and posts, including webfinger addresses ([c8a667f2](https://github.com/pixelfed/pixelfed/commit/c8a667f2))
|
||||
- Added user facing changelog at `/i/web/whats-new` ([e61dc66a](https://github.com/pixelfed/pixelfed/commit/e61dc66a))
|
||||
|
||||
### Configuration
|
||||
- Enable network timeline by default ([b95aec12](https://github.com/pixelfed/pixelfed/commit/b95aec12))
|
||||
|
||||
### Postgres Compatibility
|
||||
- Fix Story recent endpoint on postgres instances ([ddf41dc3](https://github.com/pixelfed/pixelfed/commit/ddf41dc3))
|
||||
- Fix Direct Message conversations endpoint on postgres instances ([fcabc9be](https://github.com/pixelfed/pixelfed/commit/fcabc9be))
|
||||
|
||||
### Added
|
||||
- Manual email verification requests. ([bc659387](https://github.com/pixelfed/pixelfed/commit/bc659387))
|
||||
- Added StatusMentionService, fixes #3026. ([e5387d67](https://github.com/pixelfed/pixelfed/commit/e5387d67))
|
||||
- Cloud Backups, a command to store backups on S3 or compatible filesystems. [#3037](https://github.com/pixelfed/pixelfed/pull/3037) ([3515a98e](https://github.com/pixelfed/pixelfed/commit/3515a98e))
|
||||
- Web UI Localizations + Crowdin integration. ([f7d9b40b](https://github.com/pixelfed/pixelfed/commit/f7d9b40b)) ([7ff120c9](https://github.com/pixelfed/pixelfed/commit/7ff120c9))
|
||||
- Store remote avatars locally if S3 not enabled. ([b4bd0400](https://github.com/pixelfed/pixelfed/commit/b4bd0400))
|
||||
|
||||
### Updated
|
||||
- Updated NotificationService, fix 500 bug. ([4a609dc3](https://github.com/pixelfed/pixelfed/commit/4a609dc3))
|
||||
- Updated HttpSignatures, update instance actor headers. Fixes #2935. ([a900de21](https://github.com/pixelfed/pixelfed/commit/a900de21))
|
||||
- Updated NoteTransformer, fix tag array. ([7b3e672d](https://github.com/pixelfed/pixelfed/commit/7b3e672d))
|
||||
- Updated video presenters, add playsinline attribute to video tags. ([0299aa5b](https://github.com/pixelfed/pixelfed/commit/0299aa5b))
|
||||
- Updated RemotePost, RemoteProfile components, add fallback avatars. ([754151dc](https://github.com/pixelfed/pixelfed/commit/754151dc))
|
||||
- Updated FederationController, move well-known to api middleware and cache webfinger lookups. ([4505d1f0](https://github.com/pixelfed/pixelfed/commit/4505d1f0))
|
||||
- Updated InstanceActorController, improve json seralization by not escaping slashes. ([0a8eb81b](https://github.com/pixelfed/pixelfed/commit/0a8eb81b))
|
||||
- Refactor following & relationship logic. Replace FollowerObserver with FollowerService and added RelationshipService to cache results. Removed NotificationTransformer includes and replaced with cached services to improve performance and reduce database queries. ([80d9b939](https://github.com/pixelfed/pixelfed/commit/80d9b939))
|
||||
- Updated PublicApiController, use AccountService in accountStatuses method. ([bef959f4](https://github.com/pixelfed/pixelfed/commit/bef959f4))
|
||||
- Updated auth config, add throttle limit to password resets. ([2609c86a](https://github.com/pixelfed/pixelfed/commit/2609c86a))
|
||||
- Updated StatusCard component, add relationship state button. ([0436b124](https://github.com/pixelfed/pixelfed/commit/0436b124))
|
||||
- Updated Timeline component, cascade relationship state change. ([f4bd5672](https://github.com/pixelfed/pixelfed/commit/f4bd5672))
|
||||
- Updated Activity component, only show context button for actionable activities. ([7886fd59](https://github.com/pixelfed/pixelfed/commit/7886fd59))
|
||||
- Updated Autospam service, use silent classification for better user experience. ([f0d4c172](https://github.com/pixelfed/pixelfed/commit/f0d4c172))
|
||||
- Updated Profile component, improve error messages when block/mute limit reached. ([02237845](https://github.com/pixelfed/pixelfed/commit/02237845))
|
||||
- Updated Activity component, fix missing types. ([5167c68d](https://github.com/pixelfed/pixelfed/commit/5167c68d))
|
||||
- Updated Timeline component, apply block/mute filters client side for local and network timelines. ([be194b8a](https://github.com/pixelfed/pixelfed/commit/be194b8a))
|
||||
- Updated public timeline api, use cached sorted set and client side block/mute filtering. ([37abcf38](https://github.com/pixelfed/pixelfed/commit/37abcf38))
|
||||
- Updated public timeline api, add experimental cache. ([192553ff](https://github.com/pixelfed/pixelfed/commit/192553ff))
|
||||
- Updated dark mode styles, fix black box on stories. Closes #2982. ([3169f68e](https://github.com/pixelfed/pixelfed/commit/3169f68e))
|
||||
- Updated verify_credentials api endpoint to improve performance. ([7df3540b](https://github.com/pixelfed/pixelfed/commit/7df3540b))
|
||||
- Updated Localization util, filter out .DS_Store. ([0107e8fd](https://github.com/pixelfed/pixelfed/commit/0107e8fd))
|
||||
- Updated PublicApiController, fix private account statuses api. Closes #2995. ([aa2dd26c](https://github.com/pixelfed/pixelfed/commit/aa2dd26c))
|
||||
- Updated Status model, use AccountService to generate urls instead of loading profile relation. ([2ae527c0](https://github.com/pixelfed/pixelfed/commit/2ae527c0))
|
||||
- Updated Autospam service, add mark all as read and mark all as not spam options and filter active, spam and not spam reports. ([ae8c7517](https://github.com/pixelfed/pixelfed/commit/ae8c7517))
|
||||
- Updated UserInviteController, fixes #3017. ([b8e9056e](https://github.com/pixelfed/pixelfed/commit/b8e9056e))
|
||||
- Updated AccountService, add dynamic user settings methods. ([2aa73c1f](https://github.com/pixelfed/pixelfed/commit/2aa73c1f))
|
||||
- Updated MediaStorageService, improve header parsing. ([9d9e9ce7](https://github.com/pixelfed/pixelfed/commit/9d9e9ce7))
|
||||
- Updated SearchApiV2Service, improve performance and include hashtag post counts when applicable ([fbaed93e](https://github.com/pixelfed/pixelfed/commit/fbaed93e))
|
||||
- Updated AccountTransformer, add note_text and location fields. ([98f76abb](https://github.com/pixelfed/pixelfed/commit/98f76abb))
|
||||
- Updated UserSetting model, cast compose_settings and other as json. ([03420278](https://github.com/pixelfed/pixelfed/commit/03420278))
|
||||
- Updated ApiV1Controller, improve settings and add discoverPosts endpoint. ([079804e6](https://github.com/pixelfed/pixelfed/commit/079804e6))
|
||||
- Updated LikePipeline jobs, fix likes_count calculation. ([fe64e187](https://github.com/pixelfed/pixelfed/commit/fe64e187))
|
||||
- Updated InternalApiController, prevent moderation actions against admin accounts. ([945a7e49](https://github.com/pixelfed/pixelfed/commit/945a7e49))
|
||||
- Updated CommentPipeline, move reply_count calculation to comment pipeline job and improve count calculation. ([b6b0837f](https://github.com/pixelfed/pixelfed/commit/b6b0837f))
|
||||
- Updated ApiV1Controller, improve statusesById perf and dispatch CommentPipeline job when applicable. ([466286af](https://github.com/pixelfed/pixelfed/commit/466286af))
|
||||
- Updated MediaService, return empty array if cant find status. ([c2910e5d](https://github.com/pixelfed/pixelfed/commit/c2910e5d))
|
||||
- Updated StatusService, improve cache invalidation. ([83b48b56](https://github.com/pixelfed/pixelfed/commit/83b48b56))
|
||||
- Updated Hashtag component, fix spinner. ([fefbc44a](https://github.com/pixelfed/pixelfed/commit/fefbc44a))
|
||||
- Updated NotificationCard, update api endpoint and add group notification types. ([e09a14d8](https://github.com/pixelfed/pixelfed/commit/e09a14d8))
|
||||
- Updated ContextMenu component, fix account url paths. ([01ca1edd](https://github.com/pixelfed/pixelfed/commit/01ca1edd))
|
||||
- Updated PollCard component, add showBorder prop. ([0c8fffbd](https://github.com/pixelfed/pixelfed/commit/0c8fffbd))
|
||||
- Updated PhotoPresenter component, add lightbox toggle. ([0cc1365f](https://github.com/pixelfed/pixelfed/commit/0cc1365f))
|
||||
- Updated console kernel, add db session garbage collector that runs twice daily. ([03b0a62a](https://github.com/pixelfed/pixelfed/commit/03b0a62a))
|
||||
- Updated ComposeController, refactor compose_settings. ([edc2958b](https://github.com/pixelfed/pixelfed/commit/edc2958b))
|
||||
- Updated StatusEntityLexer, prevent boosts and replies from being added to PublicTimelineService. ([32707372](https://github.com/pixelfed/pixelfed/commit/32707372))
|
||||
- Updated SpaController, persist web language changes. ([7bc684e5](https://github.com/pixelfed/pixelfed/commit/7bc684e5))
|
||||
- Updated LoginController, bump decayMinutes from 1 to 60. ([6bf92bed](https://github.com/pixelfed/pixelfed/commit/6bf92bed))
|
||||
- Updated SPA, rewrite autolink urls to SPA when applicable. ([0837b410](https://github.com/pixelfed/pixelfed/commit/0837b410))
|
||||
- Updated site config, increase ttl and enable SPA by default. ([469d49d8](https://github.com/pixelfed/pixelfed/commit/469d49d8))
|
||||
- Updated Webfinger, fixes #3050. ([ff7ee3bd](https://github.com/pixelfed/pixelfed/commit/ff7ee3bd))
|
||||
- Updated status api, autolink caption before returning response. ([b00a453b](https://github.com/pixelfed/pixelfed/commit/b00a453b))
|
||||
- Updated Timeline, add new ui promo in timelines that can be hidden using localstorage. ([e13959ae](https://github.com/pixelfed/pixelfed/commit/e13959ae))
|
||||
- Updated FederationController, increase webfinger cache ttl from 12 hours to 14 days. ([745c3580](https://github.com/pixelfed/pixelfed/commit/745c3580))
|
||||
- Updated DiscoverController, add yearly option and increase limit from 15 to 30 posts. ([10b6058c](https://github.com/pixelfed/pixelfed/commit/10b6058c))
|
||||
- Updated RemoteAvatarFetch job, fixed bug preventing new avatars from being stored. ([92bc2845](https://github.com/pixelfed/pixelfed/commit/92bc2845))
|
||||
- Updated AccountService, fix json casting. ([e5f8f344](https://github.com/pixelfed/pixelfed/commit/e5f8f344))
|
||||
- Updated ApiV1Controller, fix illegal operator bug by setting default min_id. ([415826f2](https://github.com/pixelfed/pixelfed/commit/415826f2))
|
||||
- Updated StatusService, add getMastodon method for mastoapi compatibility. ([36a129fe](https://github.com/pixelfed/pixelfed/commit/36a129fe))
|
||||
- Updated PublicApiController, fix accountStatuses pagination operator. ([85fc9dd0](https://github.com/pixelfed/pixelfed/commit/85fc9dd0))
|
||||
- Updated PublicApiController, enforce only_media on accountStatuses method. Fixes #3105. ([861a2d36](https://github.com/pixelfed/pixelfed/commit/861a2d36))
|
||||
- Updated ApiV1Controller, add mastoapi strict mode. ([46485426](https://github.com/pixelfed/pixelfed/commit/46485426))
|
||||
- Updated AccountController, refresh RelationshipService on mute/block. ([6f1b0245](https://github.com/pixelfed/pixelfed/commit/6f1b0245))
|
||||
- Updated ApiV1Controller, fix version on instance endpoint. ([a6261221](https://github.com/pixelfed/pixelfed/commit/a6261221))
|
||||
- Updated components, fix api endpoints. Fixes #3138. ([e724633e](https://github.com/pixelfed/pixelfed/commit/e724633e))
|
||||
- Updated ApiV1Controller, fix public timeline endpoint. ([80c7def3](https://github.com/pixelfed/pixelfed/commit/80c7def3))
|
||||
- Updated PublicApiController, fix public timeline endpoint. ([dcb7ba9c](https://github.com/pixelfed/pixelfed/commit/dcb7ba9c))
|
||||
- Updated ApiV1Controller, fix home timeline entities. ([6fc0dcb3](https://github.com/pixelfed/pixelfed/commit/6fc0dcb3))
|
||||
- Updated ApiV1Controller, fix favourites endpoints ([d6d99385](https://github.com/pixelfed/pixelfed/commit/d6d99385))
|
||||
- Updated ApiV1Controller, fix reblogs endpoints ([de42d84c](https://github.com/pixelfed/pixelfed/commit/de42d84c))
|
||||
- Updated SearchApiV2Service, resolve remote queries. ([c8a667f2](https://github.com/pixelfed/pixelfed/commit/c8a667f2))
|
||||
|
||||
## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1)
|
||||
### Added
|
||||
- WebP Support ([069a0e4a](https://github.com/pixelfed/pixelfed/commit/069a0e4a))
|
||||
- Auto Following support for admins ([68aa2540](https://github.com/pixelfed/pixelfed/commit/68aa2540))
|
||||
- Mark as spammer mod tool, unlists and applies content warning to existing and future post ([6d956a86](https://github.com/pixelfed/pixelfed/commit/6d956a86))
|
||||
- Diagnostics for error page and admin dashboard ([64725ecc](https://github.com/pixelfed/pixelfed/commit/64725ecc))
|
||||
- Default media licenses and media license sync ([ea0fc90c](https://github.com/pixelfed/pixelfed/commit/ea0fc90c))
|
||||
- Customize media description/alt-text length limit ([072d55d1](https://github.com/pixelfed/pixelfed/commit/072d55d1))
|
||||
- Federate Media Licenses ([14a1367a](https://github.com/pixelfed/pixelfed/commit/14a1367a))
|
||||
- Archive Posts ([e9ef0c88](https://github.com/pixelfed/pixelfed/commit/e9ef0c88))
|
||||
- Polls ([77092200](https://github.com/pixelfed/pixelfed/commit/77092200))
|
||||
- Federated Stories (#2895)
|
||||
|
||||
### Updated
|
||||
- Updated PrettyNumber, fix deprecated warning. ([20ec870b](https://github.com/pixelfed/pixelfed/commit/20ec870b))
|
||||
- Updated landing page, use config_cache. ([54920294](https://github.com/pixelfed/pixelfed/commit/54920294))
|
||||
- Updated Timeline, implement suggested post opt out. ([66750d34](https://github.com/pixelfed/pixelfed/commit/66750d34))
|
||||
- Updated Notification component, add at (@) symbol for remote profiles and local urls for remote posts and profile. ([aafd6a21](https://github.com/pixelfed/pixelfed/commit/aafd6a21))
|
||||
- Updated Activity component, add at (@) symbol for remote profiles and local urls for remote posts and profile. ([a2211815](https://github.com/pixelfed/pixelfed/commit/a2211815))
|
||||
- Updated Profile, add linkified bio, joined date, follows you label and improved website handling. ([8ee10436](https://github.com/pixelfed/pixelfed/commit/8ee10436))
|
||||
- Updated routes, add legacy webfinger profile redirect. ([93c7af74](https://github.com/pixelfed/pixelfed/commit/93c7af74))
|
||||
- Updated StoryController, fix expiration time bug. ([39e57f95](https://github.com/pixelfed/pixelfed/commit/39e57f95))
|
||||
- Updated Profile component, fix remote urls. ([6e56dbed](https://github.com/pixelfed/pixelfed/commit/6e56dbed))
|
||||
- Updated verify email screen, add contact admin link. ([f37952d6](https://github.com/pixelfed/pixelfed/commit/f37952d6))
|
||||
- Updated RemoteProfile component, implement pagination. ([02b04a4b](https://github.com/pixelfed/pixelfed/commit/02b04a4b))
|
||||
- Updated AP Helpers, generate notification for remote replies. ([8edd8294](https://github.com/pixelfed/pixelfed/commit/8edd8294))
|
||||
- Updated like api, store status_profile_id and is_comment. ([c8c6b983](https://github.com/pixelfed/pixelfed/commit/c8c6b983))
|
||||
- Updated Remote Post + Profile hashtag to redirect to local urls. ([1fa08644](https://github.com/pixelfed/pixelfed/commit/1fa08644))
|
||||
- Updated Inbox, delete notifications on tombstone. ([ef63124d](https://github.com/pixelfed/pixelfed/commit/ef63124d))
|
||||
- Updated NotificationCard, fix missing status bug. ([a3a86d46](https://github.com/pixelfed/pixelfed/commit/a3a86d46))
|
||||
- Updated Activity component, fix comment bug. ([9a2db8eb](https://github.com/pixelfed/pixelfed/commit/9a2db8eb))
|
||||
- Updated Inbox, fix tombstone bug. ([929ff5eb](https://github.com/pixelfed/pixelfed/commit/929ff5eb))
|
||||
- Updated LikeService, skip self likes. ([3741c76d](https://github.com/pixelfed/pixelfed/commit/3741c76d))
|
||||
- Updated StatusController, improve share api perf (11s to 72ms). ([d48ebb82](https://github.com/pixelfed/pixelfed/commit/d48ebb82))
|
||||
- Updated ApiController, fix nulls in hashtag endpoint. ([f1208de0](https://github.com/pixelfed/pixelfed/commit/f1208de0))
|
||||
- Updated SharePipeline, add Undo->Announce support. ([c8e40e0f](https://github.com/pixelfed/pixelfed/commit/c8e40e0f))
|
||||
- Updated NetworkTimeline, fix remote comment urls. ([308acc91](https://github.com/pixelfed/pixelfed/commit/308acc91))
|
||||
- Updated Timeline component, abstracted reusable partials. ([858f3f9e](https://github.com/pixelfed/pixelfed/commit/858f3f9e))
|
||||
- Updated Timeline, fix suggested posts. ([3ba5c88c](https://github.com/pixelfed/pixelfed/commit/3ba5c88c))
|
||||
- Updated Timeline, disable new post update checker and hide reaction bar on network timeline. ([1e3d3a69](https://github.com/pixelfed/pixelfed/commit/1e3d3a69))
|
||||
- Updated PublicApiController, improve network timeline perf. ([e5f683fd](https://github.com/pixelfed/pixelfed/commit/e5f683fd))
|
||||
- Updated Network Timeline, use existing Timeline component. ([0deaafc0](https://github.com/pixelfed/pixelfed/commit/0deaafc0))
|
||||
- Updated PostComponent, show like count to owner using MomentUI. ([e9c46bab](https://github.com/pixelfed/pixelfed/commit/e9c46bab))
|
||||
- Updated ContextMenu, add missing statusUrl method. ([3cffdb11](https://github.com/pixelfed/pixelfed/commit/3cffdb11))
|
||||
- Updated PublicApiController, add LikeService to Network timeline. ([82895591](https://github.com/pixelfed/pixelfed/commit/82895591))
|
||||
- Updated moderator api, expire cached status in StatusService. ([f215ee26](https://github.com/pixelfed/pixelfed/commit/f215ee26))
|
||||
- Updated StatusHashtagService, fix null status bug. ([51a277e1](https://github.com/pixelfed/pixelfed/commit/51a277e1))
|
||||
- Updated NotificationService, use zrevrangebyscore for api. ([d43e6d8d](https://github.com/pixelfed/pixelfed/commit/d43e6d8d))
|
||||
- Updated ApiV1Controller, use PublicTimelineService. ([f67c67bc](https://github.com/pixelfed/pixelfed/commit/f67c67bc))
|
||||
- Updated ApiV1Controller, use ProfileService for verify_credentials. ([352aa573](https://github.com/pixelfed/pixelfed/commit/352aa573))
|
||||
- Updated RemotePost.vue, fix content warning button. ([7647e724](https://github.com/pixelfed/pixelfed/commit/7647e724))
|
||||
- Updated AdminMediaController, improve perf and use simple pagination. ([f2686cac](https://github.com/pixelfed/pixelfed/commit/f2686cac))
|
||||
- Updated PostComponent, fix MomentUI like counter. ([42c6121a](https://github.com/pixelfed/pixelfed/commit/42c6121a))
|
||||
- Updated status views, remove like counts from status embed. ([1a2e41b1](https://github.com/pixelfed/pixelfed/commit/1a2e41b1))
|
||||
- Updated Profile, fix unauthenticated private profiles. ([9017f7c4](https://github.com/pixelfed/pixelfed/commit/9017f7c4))
|
||||
- Updated PublicApiController, impr home timeline perf. ([4fe42e5b](https://github.com/pixelfed/pixelfed/commit/4fe42e5b))
|
||||
- Updated Timeline.vue, fix comment button. ([b6b5ce7c](https://github.com/pixelfed/pixelfed/commit/b6b5ce7c))
|
||||
- Updated StatusEntityLexer, only add specific status types to PublicTimelineService. ([1fdcbe5b](https://github.com/pixelfed/pixelfed/commit/1fdcbe5b))
|
||||
- Updated ActivityPub helpers, fix comment threading in statusFetch() method ([26b9c140](https://github.com/pixelfed/pixelfed/commit/26b9c140))
|
||||
- Updated NotificationCard, fix typo in mention, share and comments. Fixes #2848. ([b37bb426](https://github.com/pixelfed/pixelfed/commit/b37bb426))
|
||||
- Updated StatusCard.vue, add togglecw events to other presenters. ([9607243f](https://github.com/pixelfed/pixelfed/commit/9607243f))
|
||||
- Updated presenters, fix content warning layout. ([fc56acb8](https://github.com/pixelfed/pixelfed/commit/fc56acb8))
|
||||
- Updated reply blade view, fix missing avatar and media images. ([5fb33772](https://github.com/pixelfed/pixelfed/commit/5fb33772))
|
||||
- Updated components, add fallback default avatar. ([726553f5](https://github.com/pixelfed/pixelfed/commit/726553f5))
|
||||
- Updated job queue, separate deletes into their own queue. ([7f421392](https://github.com/pixelfed/pixelfed/commit/7f421392))
|
||||
- Updated DiscoverController, use UserFilterService on trendingApi. ([135474ae](https://github.com/pixelfed/pixelfed/commit/135474ae))
|
||||
- Updated PublicApiController, use UserFilterService in public timeline endpoint. ([ca6e491c](https://github.com/pixelfed/pixelfed/commit/ca6e491c))
|
||||
- Updated ContextMenu, add View Profile link. ([8544bcbd](https://github.com/pixelfed/pixelfed/commit/8544bcbd))
|
||||
- Updated presenters, improve content warnings. ([86422c81](https://github.com/pixelfed/pixelfed/commit/86422c81))
|
||||
- Updated Timeline.vue, increase pagination limit from 3 to 12 and add empty feed placeholder. ([916e8f71](https://github.com/pixelfed/pixelfed/commit/916e8f71))
|
||||
- Updated Timeline.vue, improve followed hashtags. ([728f10d7](https://github.com/pixelfed/pixelfed/commit/728f10d7))
|
||||
- Updated PostComponent, use profileUrl method for comments. ([7ed65fc9](https://github.com/pixelfed/pixelfed/commit/7ed65fc9))
|
||||
- Updated Timeline, fix empty timeline card. ([11eb6acd](https://github.com/pixelfed/pixelfed/commit/11eb6acd))
|
||||
- Updated ap helpers, set text type when appropriate. ([9f4f983f](https://github.com/pixelfed/pixelfed/commit/9f4f983f))
|
||||
- Updated StatusCard, add text support. ([ed14ee48](https://github.com/pixelfed/pixelfed/commit/ed14ee48))
|
||||
- Updated PublicApiController, filter out text replies on home timeline. ([86219b57](https://github.com/pixelfed/pixelfed/commit/86219b57))
|
||||
- Updated RemotePost.vue, improve text only post UI. ([b0257be2](https://github.com/pixelfed/pixelfed/commit/b0257be2))
|
||||
- Updated Timeline, make text-only posts opt-in by default. ([0153ed6d](https://github.com/pixelfed/pixelfed/commit/0153ed6d))
|
||||
- Updated LikeController, add UndoLikePipeline and federate Undo Like activities. ([8ac8fcad](https://github.com/pixelfed/pixelfed/commit/8ac8fcad))
|
||||
- Updated Settings, add default license and enforced media descriptions. ([67e3f604](https://github.com/pixelfed/pixelfed/commit/67e3f604))
|
||||
- Updated Compose Apis, make media descriptions/alt text length limit configurable. Default length: 1000. ([072d55d1](https://github.com/pixelfed/pixelfed/commit/072d55d1))
|
||||
- Updated ApiV1Controller, add default license support. ([2a791f19](https://github.com/pixelfed/pixelfed/commit/2a791f19))
|
||||
- Updated StatusTransformers, remove includes and use cached services. ([09d5198c](https://github.com/pixelfed/pixelfed/commit/09d5198c))
|
||||
- Updated RemotePost component, update likes reaction bar. ([1060dd23](https://github.com/pixelfed/pixelfed/commit/1060dd23))
|
||||
- Updated FollowPipeline, fix cache invalidation bug. ([c1f14f89](https://github.com/pixelfed/pixelfed/commit/c1f14f89))
|
||||
- Updated PublicApiController, improve accountStatuses api perf. ([bce8edd9](https://github.com/pixelfed/pixelfed/commit/bce8edd9))
|
||||
- Updated ApiControllers, use NotificationService. ([f9516ac3](https://github.com/pixelfed/pixelfed/commit/f9516ac3))
|
||||
- Updated Notification components, fix old notifications with missing attributes. ([b6e226ae](https://github.com/pixelfed/pixelfed/commit/b6e226ae))
|
||||
- Updated LikeController, improve query perf. ([f3d6023e](https://github.com/pixelfed/pixelfed/commit/f3d6023e))
|
||||
- Updated License util, add nameToId method. ([f6131ed7](https://github.com/pixelfed/pixelfed/commit/f6131ed7))
|
||||
- Updated RemoteProfile, add warning about potentially out of date information. ([7274574c](https://github.com/pixelfed/pixelfed/commit/7274574c))
|
||||
- Updated NotifcationCard.vue component, add refresh button for cold notification cache. ([0e178a33](https://github.com/pixelfed/pixelfed/commit/0e178a33))
|
||||
- Updated RemoteProfile component, add follower modals. ([c4146a30](https://github.com/pixelfed/pixelfed/commit/c4146a30))
|
||||
- Updated FollowerService, cache audience. ([22257cc2](https://github.com/pixelfed/pixelfed/commit/22257cc2))
|
||||
- Updated StatusService, add non-public option and improve cache invalidation. ([15c4fdd9](https://github.com/pixelfed/pixelfed/commit/15c4fdd9))
|
||||
- Updated ContactAdmin mail, set New Support Message subject. ([bc3add05](https://github.com/pixelfed/pixelfed/commit/bc3add05))
|
||||
- Updated StatusTransformer, prioritize scope over deprecated visibility attribute. ([6e45021f](https://github.com/pixelfed/pixelfed/commit/6e45021f))
|
||||
- Updated StatusService, invalidate profile embed cache on deletion. ([acaf630d](https://github.com/pixelfed/pixelfed/commit/acaf630d))
|
||||
- Updated status.reply view, fix archived post leakage. ([4fb3d1fa](https://github.com/pixelfed/pixelfed/commit/4fb3d1fa))
|
||||
- Updated PostComponents, re-add time to timestamp. ([c5281dcd](https://github.com/pixelfed/pixelfed/commit/c5281dcd))
|
||||
- Updated follow intent, fix follower count leak. ([03199e2f](https://github.com/pixelfed/pixelfed/commit/03199e2f))
|
||||
- Updated Status model, add poll relation and allow up to 2 urls to autolink. ([2593cdee](https://github.com/pixelfed/pixelfed/commit/2593cdee))
|
||||
- Updated snowflake id generation to improve randomness. ([e5aea490](https://github.com/pixelfed/pixelfed/commit/e5aea490))
|
||||
- Updated Timeline, remove recent posts. ([7641b731](https://github.com/pixelfed/pixelfed/commit/7641b731))
|
||||
- Updated InstanceCrawlPipeline, remove unused variable. ([e73cf531](https://github.com/pixelfed/pixelfed/commit/e73cf531))
|
||||
- Updated StoryComposeController, fix expiry bug. ([7dee8f58](https://github.com/pixelfed/pixelfed/commit/7dee8f58))
|
||||
- Updated Profile, fix following count bug. ([ee9f0795](https://github.com/pixelfed/pixelfed/commit/ee9f0795))
|
||||
- Updated DirectMessageController, fix autocomplete bug. ([0f00be4d](https://github.com/pixelfed/pixelfed/commit/0f00be4d))
|
||||
- Updated StoryService, fix division by zero bug. ([6ae1ba0a](https://github.com/pixelfed/pixelfed/commit/6ae1ba0a))
|
||||
- Updated ApiV1Controller, fix empty public timeline bug. ([0584f9ee](https://github.com/pixelfed/pixelfed/commit/0584f9ee))
|
||||
|
||||
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
|
||||
### Added
|
||||
- Autocomplete Support (hashtags + mentions) ([de514f7d](https://github.com/pixelfed/pixelfed/commit/de514f7d))
|
||||
- Creative Commons Licenses ([552e950](https://github.com/pixelfed/pixelfed/commit/552e950))
|
||||
- Network Timeline ([af7face4](https://github.com/pixelfed/pixelfed/commit/af7face4))
|
||||
- Admin config settings ([f2066b74](https://github.com/pixelfed/pixelfed/commit/f2066b74))
|
||||
- Profile pronouns ([fabb57a9](https://github.com/pixelfed/pixelfed/commit/fabb57a9))
|
||||
- Hashtag timeline api support ([241ae036](https://github.com/pixelfed/pixelfed/commit/241ae036))
|
||||
- New admin dashboard layout ([eb7d5a4e](https://github.com/pixelfed/pixelfed/commit/eb7d5a4e))
|
||||
- Fresh about page layout ([92dc7af6](https://github.com/pixelfed/pixelfed/commit/92dc7af6))
|
||||
- Instance Rules ([a4efbb75](https://github.com/pixelfed/pixelfed/commit/a4efbb75))
|
||||
- New Home Timeline ([56215be7](https://github.com/pixelfed/pixelfed/commit/56215be7))
|
||||
|
||||
### Updated
|
||||
- Updated AdminController, fix variable name in updateSpam method. ([6edaf940](https://github.com/pixelfed/pixelfed/commit/6edaf940))
|
||||
- Updated RemoteAvatarFetch, only dispatch jobs if cloud storage is enabled. ([4f40f6f5](https://github.com/pixelfed/pixelfed/commit/4f40f6f5))
|
||||
- Updated StatusService, add ttl of 7 days. ([6e44ae0b](https://github.com/pixelfed/pixelfed/commit/6e44ae0b))
|
||||
- Updated StatusHashtagService, use StatusService for statuses. ([0355b567](https://github.com/pixelfed/pixelfed/commit/0355b567))
|
||||
- Updated StatusHashtagService, remove deprecated methods. ([aa4c718d](https://github.com/pixelfed/pixelfed/commit/aa4c718d))
|
||||
- Updated ApiV1Controller, add StatusService del calls to update likes_count, reblogs_count and reply_count. ([05b9445c](https://github.com/pixelfed/pixelfed/commit/05b9445c))
|
||||
- Updated Like, Status and Comment controllers to add StatusService del() method to update counts. ([eab4370c](https://github.com/pixelfed/pixelfed/commit/eab4370c))
|
||||
- Updated ComposeController, use placeholder image for video media. Fixes #2595. ([789ed4b4](https://github.com/pixelfed/pixelfed/commit/789ed4b4))
|
||||
- Updated DiscoverController, change api schema. ([2eea0409](https://github.com/pixelfed/pixelfed/commit/2eea0409))
|
||||
- Updated StatusDelete pipeline, call StatusService::del() to remove status from cache. ([3f772ff8](https://github.com/pixelfed/pixelfed/commit/3f772ff8))
|
||||
- Updated StatusHashtagTransformer, add blurhash attribute. ([899bbeba](https://github.com/pixelfed/pixelfed/commit/899bbeba))
|
||||
- Updated status square previews, add blurhash and improved content warnings. ([39e389dd](https://github.com/pixelfed/pixelfed/commit/39e389dd))
|
||||
- Updated Blurhash util, add default hash for invalid media. ([38a37c15](https://github.com/pixelfed/pixelfed/commit/38a37c15))
|
||||
- Updated VideoThumbnail job, generate blurhash for videos. ([896452c7](https://github.com/pixelfed/pixelfed/commit/896452c7))
|
||||
- Updated MediaTransformers, add default blurhash attribute. ([3f14a4c4](https://github.com/pixelfed/pixelfed/commit/3f14a4c4))
|
||||
- Updated Timeline.vue, fix hashtag status previews. ([7768e844](https://github.com/pixelfed/pixelfed/commit/7768e844))
|
||||
- Updated AP helpers, fix statusFetch 404s. ([3419379a](https://github.com/pixelfed/pixelfed/commit/3419379a))
|
||||
- Updated InternalApiController, update discoverPosts method to improve performance. ([9862a855](https://github.com/pixelfed/pixelfed/commit/9862a855))
|
||||
- Updated DiscoverComponent, add blurhash and like/comment counts. ([a8ebdd2e](https://github.com/pixelfed/pixelfed/commit/a8ebdd2e))
|
||||
- Updated DiscoverComponent, add spinner loaders and remove deprecated sections. ([34869247](https://github.com/pixelfed/pixelfed/commit/34869247))
|
||||
- Updated AccountController, add mutes and blocks endpoint to pixelfed api. ([1fb7e2b2](https://github.com/pixelfed/pixelfed/commit/1fb7e2b2))
|
||||
- Updated AccountService, cache object and observe changes. ([b299da93](https://github.com/pixelfed/pixelfed/commit/b299da93))
|
||||
- Updated webfinger util, fail on invalid webfinger url. Fixes ([#2613](https://github.com/pixelfed/pixelfed/issues/2613)) ([2d11317c](https://github.com/pixelfed/pixelfed/commit/2d11317c))
|
||||
- Updated MediaStorageService, dispatch deletes to MediaDeletePipeline. ([37dbb3de](https://github.com/pixelfed/pixelfed/commit/37dbb3de))
|
||||
- Updated ComposeController, use MediaStorageService for media deletes. ([ab5469ff](https://github.com/pixelfed/pixelfed/commit/ab5469ff))
|
||||
- Updated StatusDeletePipeline, use MediaStorageService for media deletes. ([9fd90e17](https://github.com/pixelfed/pixelfed/commit/9fd90e17))
|
||||
- Updated Discover, allow public discover access. ([1404ac6e](https://github.com/pixelfed/pixelfed/commit/1404ac6e))
|
||||
- Updated pixelfed config, add media_fast_process setting. ([6bee5072](https://github.com/pixelfed/pixelfed/commit/6bee5072))
|
||||
- Updated ComposeController, add mediaProcessingCheck method. ([33b625f5](https://github.com/pixelfed/pixelfed/commit/33b625f5))
|
||||
- Updated ComposeModal, add processing step disabled by default. ([e6e76e80](https://github.com/pixelfed/pixelfed/commit/e6e76e80))
|
||||
- Updated DiscoverComponent, allow unauthenticated if enabled. ([a1059a6e](https://github.com/pixelfed/pixelfed/commit/a1059a6e))
|
||||
- Updated components, improve content warnings. ([a9e98965](https://github.com/pixelfed/pixelfed/commit/a9e98965))
|
||||
- Updated ComposeModal, prevent tagging empty users. Fixes #2633. ([ceae664c](https://github.com/pixelfed/pixelfed/commit/ceae664c))
|
||||
- Updated ComposeModal, show filter warning for unsupported browsers. ([12ce7602](https://github.com/pixelfed/pixelfed/commit/12ce7602))
|
||||
- Updated Hashtag component, fix null infinite loading bug. Fixes #2637. ([55136518](https://github.com/pixelfed/pixelfed/commit/55136518))
|
||||
- Updated filesystems config, add backup driver to store backups on other filesystems. ([ae90eef9](https://github.com/pixelfed/pixelfed/commit/ae90eef9))
|
||||
- Updated Embeds. Fix Profile + Status embeds, remove following count and improve cache invalidation and hidden follower counts. ([5ac9d0e8](https://github.com/pixelfed/pixelfed/commit/5ac9d0e8))
|
||||
- Updated FederationController, return 404 for invalid webfinger addresses. Fixes ([#2647](https://github.com/pixelfed/pixelfed/issues/2647)). ([deb6f115](https://github.com/pixelfed/pixelfed/commit/deb6f115))
|
||||
- Updated InboxPipeline, fail earlier for invalid public keys. Fixes ([#2648](https://github.com/pixelfed/pixelfed/issues/2648)). ([d1c5e9b8](https://github.com/pixelfed/pixelfed/commit/d1c5e9b8))
|
||||
- Updated Status model, refactor liked and shared methods to fix cache invalidation bug. ([f05c3b66](https://github.com/pixelfed/pixelfed/commit/f05c3b66))
|
||||
- Updated Timeline component, add inline reports modal. ([e64b4bd3](https://github.com/pixelfed/pixelfed/commit/e64b4bd3))
|
||||
- Updated federation pipeline, add locks. ([ddc76887](https://github.com/pixelfed/pixelfed/commit/ddc76887))
|
||||
- Updated MediaStorageService, improve head checks to fix failed jobs. ([1769cdfd](https://github.com/pixelfed/pixelfed/commit/1769cdfd))
|
||||
- Updated user admin, remove expensive db query and add search. ([8feeadbf](https://github.com/pixelfed/pixelfed/commit/8feeadbf))
|
||||
- Updated Compose apis, prevent private accounts from posting public or unlisted scopes. ([f53bfa6f](https://github.com/pixelfed/pixelfed/commit/f53bfa6f))
|
||||
- Updated font icons, use font-display:swap. ([77d4353a](https://github.com/pixelfed/pixelfed/commit/77d4353a))
|
||||
- Updated ComposeModal, limit visibility scope for private accounts. ([001d4105](https://github.com/pixelfed/pixelfed/commit/001d4105))
|
||||
- Updated ComposeController, add autocomplete apis for hashtags and mentions. ([f0e48a09](https://github.com/pixelfed/pixelfed/commit/f0e48a09))
|
||||
- Updated StatusController, invalidate profile embed cache on status delete. ([9c8a87c3](https://github.com/pixelfed/pixelfed/commit/9c8a87c3))
|
||||
- Updated moderation api, invalidate profile embed. ([b2501bfc](https://github.com/pixelfed/pixelfed/commit/b2501bfc))
|
||||
- Updated Nodeinfo util, use last_active_at for monthly active user count. ([d200c12c](https://github.com/pixelfed/pixelfed/commit/d200c12c))
|
||||
- Updated PhotoPresenter, add width and height to images. ([3f8202e2](https://github.com/pixelfed/pixelfed/commit/3f8202e2))
|
||||
- Updated Compose Apis, refactor rate limits. ([42375b3d](https://github.com/pixelfed/pixelfed/commit/42375b3d))
|
||||
- Updated PublicApiController, show unlisted comments. ([e1c6297e](https://github.com/pixelfed/pixelfed/commit/e1c6297e))
|
||||
- Updated ApiV1Controller, add missing variable. ([886ea617](https://github.com/pixelfed/pixelfed/commit/886ea617))
|
||||
- Updated PublicApiController, limit network pagination to 3 months. ([10119bbb](https://github.com/pixelfed/pixelfed/commit/10119bbb))
|
||||
- Updated admin instance page, add search and improve performance. ([f5829373](https://github.com/pixelfed/pixelfed/commit/f5829373))
|
||||
- Updated AdminInstanceController, invalidate banned domain cache when updated. ([35393edf](https://github.com/pixelfed/pixelfed/commit/35393edf))
|
||||
- Updated AP Helpers, use instance filtering. ([66b4f8c7](https://github.com/pixelfed/pixelfed/commit/66b4f8c7))
|
||||
- Updated ApiV1Controller, add missing instance api attributes. ([64b86546](https://github.com/pixelfed/pixelfed/commit/64b86546))
|
||||
- Updated story garbage collection, handle non active stories and new ephemeral story media directory. ([c43f8bcc](https://github.com/pixelfed/pixelfed/commit/c43f8bcc))
|
||||
- Updated Stories, add crop and duration settings to composer. ([c8edca69](https://github.com/pixelfed/pixelfed/commit/c8edca69))
|
||||
- Updated instance endpoint, add custom description. ([668e936e](https://github.com/pixelfed/pixelfed/commit/668e936e))
|
||||
- Updated StoryCompose component, improve full screen preview. ([39a76103](https://github.com/pixelfed/pixelfed/commit/39a76103))
|
||||
- Updated Helpers, fix broken tests. ([22dddaa0](https://github.com/pixelfed/pixelfed/commit/22dddaa0))
|
||||
- Updated StoryController, fix cache crop bug. ([c2f8faae](https://github.com/pixelfed/pixelfed/commit/c2f8faae))
|
||||
- Updated StoryController, optimize photo size by resizing to 9:16 aspect. ([e66ed9a2](https://github.com/pixelfed/pixelfed/commit/e66ed9a2))
|
||||
- Updated StoryCompose crop logic. ([2ead622c](https://github.com/pixelfed/pixelfed/commit/2ead622c))
|
||||
- Updated StatusController, allow license edits without 24 hour limit. ([c799a01a](https://github.com/pixelfed/pixelfed/commit/c799a01a))
|
||||
- Updated Settings, remove reports page. ([9cf962ff](https://github.com/pixelfed/pixelfed/commit/9cf962ff))
|
||||
- Updated ProfileService, use account transformer. ([391b1287](https://github.com/pixelfed/pixelfed/commit/391b1287))
|
||||
- Updated LikeController, hide like counts. ([ea687240](https://github.com/pixelfed/pixelfed/commit/ea687240))
|
||||
- Updated StatusTransformers, add liked_by attribute. ([372bacb0](https://github.com/pixelfed/pixelfed/commit/372bacb0))
|
||||
- Updated PostComponent, change like logic. ([0a35f5d6](https://github.com/pixelfed/pixelfed/commit/0a35f5d6))
|
||||
- Updated Timeline component, change like logic. ([7bcbf96b](https://github.com/pixelfed/pixelfed/commit/7bcbf96b))
|
||||
- Updated LikeService, fix likedBy method. ([a5e64da6](https://github.com/pixelfed/pixelfed/commit/a5e64da6))
|
||||
- Updated PublicApiController, increase public timeline to 6 months from 3. ([8a736432](https://github.com/pixelfed/pixelfed/commit/8a736432))
|
||||
- Updated LikeService, show like count to status owner. ([4408e2ef](https://github.com/pixelfed/pixelfed/commit/4408e2ef))
|
||||
- Updated admin settings, add rules. ([a4efbb75](https://github.com/pixelfed/pixelfed/commit/a4efbb75))
|
||||
- Updated LikeService, fix authentication bug. ([c9abd70e](https://github.com/pixelfed/pixelfed/commit/c9abd70e))
|
||||
- Updated StatusTransformer, fix missing tags attribute. ([dac326e9](https://github.com/pixelfed/pixelfed/commit/dac326e9))
|
||||
- Updated ComposeController, bail on empty attachments. ([061b145b](https://github.com/pixelfed/pixelfed/commit/061b145b))
|
||||
- Updated landing and about page. ([92dc7af6](https://github.com/pixelfed/pixelfed/commit/92dc7af6))
|
||||
- Updated AdminStatsService, fix postgres bug. ([af719135](https://github.com/pixelfed/pixelfed/commit/af719135))
|
||||
- Updated api, remove auth requirement for hashtag timeline. ([c8e43c60](https://github.com/pixelfed/pixelfed/commit/c8e43c60))
|
||||
- Updated NotificationCard component, fix default value. ([78ad4e77](https://github.com/pixelfed/pixelfed/commit/78ad4e77))
|
||||
- Updated Timeline component, show counts and make sidebar footer lighter. ([0788bffa](https://github.com/pixelfed/pixelfed/commit/0788bffa))
|
||||
- Updated AuthServiceProvider, increase default token + refresh token lifetime. ([178ed63d](https://github.com/pixelfed/pixelfed/commit/178ed63d))
|
||||
- Updated liked by, fix remote username urls. ([f767d99a](https://github.com/pixelfed/pixelfed/commit/f767d99a))
|
||||
- Updated StatusController, add cache invalidation for timeline cursor. ([f3bf2fd4](https://github.com/pixelfed/pixelfed/commit/f3bf2fd4))
|
||||
- Updated PublicApiController, add recent feed support to home timeline. ([1e230e80](https://github.com/pixelfed/pixelfed/commit/1e230e80))
|
||||
- Updated Inbox, fix reply/comment bug by moving attachment validation to Note with attachments. ([28df9f7e](https://github.com/pixelfed/pixelfed/commit/28df9f7e))
|
||||
- Updated PrettyNumber, add decimal option. ([84520fe1](https://github.com/pixelfed/pixelfed/commit/84520fe1))
|
||||
- Updated app config, change default descriptions. ([7d24560d](https://github.com/pixelfed/pixelfed/commit/7d24560d))
|
||||
- Updated NotificationCard, fix loading bug. ([69567e19](https://github.com/pixelfed/pixelfed/commit/69567e19))
|
||||
- Updated DirectMessageController, disable exception logging for invalid urls. Fixes ([#2752](https://github.com/pixelfed/pixelfed/issues/2752)). ([2d0a253e](https://github.com/pixelfed/pixelfed/commit/2d0a253e))
|
||||
|
||||
## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10)
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.9...dev)
|
||||
### Added
|
||||
- Direct Messages ([d63569c](https://github.com/pixelfed/pixelfed/commit/d63569c))
|
||||
- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5)) ([3ee1215a](https://github.com/pixelfed/pixelfed/commit/3ee1215a))
|
||||
- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5))
|
||||
- Custom content warnings for remote posts ([6afc61a4](https://github.com/pixelfed/pixelfed/commit/6afc61a4))
|
||||
- Thai translations ([74cd536](https://github.com/pixelfed/pixelfed/commit/74cd536))
|
||||
- Added Bookmarks to v1 api ([99cb48c5](https://github.com/pixelfed/pixelfed/commit/99cb48c5))
|
||||
- Added New Post notification to Timeline ([a0e7c4d5](https://github.com/pixelfed/pixelfed/commit/a0e7c4d5))
|
||||
- Add Instagram Import ([e2a6bdd0](https://github.com/pixelfed/pixelfed/commit/e2a6bdd0))
|
||||
- Add notification preview to NotificationCard ([28445e27](https://github.com/pixelfed/pixelfed/commit/28445e27))
|
||||
- Add Grid Mode to Timelines ([c1853ca8](https://github.com/pixelfed/pixelfed/commit/c1853ca8))
|
||||
- Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5))
|
||||
- Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020))
|
||||
- Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
|
||||
|
@ -373,7 +24,6 @@
|
|||
- Add autospam feature ([b892bcf0](https://github.com/pixelfed/pixelfed/commit/b892bcf0))
|
||||
- Add hCaptcha ([082c1ccb](https://github.com/pixelfed/pixelfed/commit/082c1ccb))
|
||||
- Add StatusView model to store views for discover algorithm ([7a68ee94](https://github.com/pixelfed/pixelfed/commit/7a68ee94))
|
||||
- Add Year in Review feature (mysql only) ([f32072a3](https://github.com/pixelfed/pixelfed/commit/f32072a3))
|
||||
|
||||
### Updated
|
||||
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
|
||||
|
@ -495,8 +145,8 @@
|
|||
- Updated avatars, use jpeg default. ([f6528c84](https://github.com/pixelfed/pixelfed/commit/f6528c84))
|
||||
- Updated antispam bouncer, change recent from 1 week to 3 months. ([7d818197](https://github.com/pixelfed/pixelfed/commit/7d818197))
|
||||
- Updated Post components, fix remote post and profile urls. ([cfcf17f3](https://github.com/pixelfed/pixelfed/commit/cfcf17f3))
|
||||
- Updated migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
|
||||
- Updated LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
|
||||
- Update migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
|
||||
- Update LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
|
||||
- Updated Profile, fix status count. ([6dcd472b](https://github.com/pixelfed/pixelfed/commit/6dcd472b))
|
||||
- Updated StatusService, cast response to array. ([0fbde91e](https://github.com/pixelfed/pixelfed/commit/0fbde91e))
|
||||
- Updated status model, use scope over deprecated visibility attribute. ([f70826e1](https://github.com/pixelfed/pixelfed/commit/f70826e1))
|
||||
|
@ -505,44 +155,7 @@
|
|||
- Updated AP helpers, fixed federation bug. ([a52564f3](https://github.com/pixelfed/pixelfed/commit/a52564f3))
|
||||
- Updated Helpers, cache profiles. ([1f672ecf](https://github.com/pixelfed/pixelfed/commit/1f672ecf))
|
||||
- Updated DiscoverController, improve trending api performance. ([d8d3331f](https://github.com/pixelfed/pixelfed/commit/d8d3331f))
|
||||
- Updated InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
|
||||
- Updated StoryItemTransformer, increase story duration from 5 seconds to 10 seconds. ([5b0b14fc](https://github.com/pixelfed/pixelfed/commit/5b0b14fc))
|
||||
- Updated StatusController, add view method. ([0cfc12c5](https://github.com/pixelfed/pixelfed/commit/0cfc12c5))
|
||||
- Updated MediaPathService, add story method. ([aac44309](https://github.com/pixelfed/pixelfed/commit/aac44309))
|
||||
- Updated StatusDelete job, handle cloud storage media deletes. ([4b1a0fd7](https://github.com/pixelfed/pixelfed/commit/4b1a0fd7))
|
||||
- Updated ImageOptimizePipeline, add skip_optimize and MediaStorageService support. ([234f72f3](https://github.com/pixelfed/pixelfed/commit/234f72f3))
|
||||
- Updated Media model, add cdn support to url and thumbnailUrl methods. ([57fa889d](https://github.com/pixelfed/pixelfed/commit/57fa889d))
|
||||
- Updated MediaController, remove deprecated endpoint. ([8132db74](https://github.com/pixelfed/pixelfed/commit/8132db74))
|
||||
- Updated api controllers, deprecate old endpoints. ([4415af1b](https://github.com/pixelfed/pixelfed/commit/4415af1b))
|
||||
- Updated mobile apis, add blurhash. ([cf40526e](https://github.com/pixelfed/pixelfed/commit/cf40526e))
|
||||
- Updated Image media util, store dimensions of media not thumbnail. ([40bd64aa](https://github.com/pixelfed/pixelfed/commit/40bd64aa))
|
||||
- Updated MediaTransformers, include meta attribute with focus and dimensions. ([f8cbe1e4](https://github.com/pixelfed/pixelfed/commit/f8cbe1e4))
|
||||
- Updated storage, add remote media cache directory. ([0eabbfdd](https://github.com/pixelfed/pixelfed/commit/0eabbfdd))
|
||||
- Updated backup config, prevents gateway timeouts for large databases using mysql. ([9cd4bd74](https://github.com/pixelfed/pixelfed/commit/9cd4bd74))
|
||||
- Updated MediaPipeline, handle cloud object storage. ([be6d12fc](https://github.com/pixelfed/pixelfed/commit/be6d12fc))
|
||||
- Updated AP Helpers, use MediaStoragePipeline. ([01a1ffd6](https://github.com/pixelfed/pixelfed/commit/01a1ffd6))
|
||||
- Updated RemoteProfile component, change thumbnail url. ([c1118956](https://github.com/pixelfed/pixelfed/commit/c1118956))
|
||||
- Updated blade views. ([9683e846](https://github.com/pixelfed/pixelfed/commit/9683e846))
|
||||
- Updated cache config, use phpredis by default. ([ed6877df](https://github.com/pixelfed/pixelfed/commit/ed6877df))
|
||||
- Updated components, fix url rewriter. Closes #2538. ([e8cc66dc](https://github.com/pixelfed/pixelfed/commit/e8cc66dc))
|
||||
- Updated UserCreate command, closes #2581. ([b2b8c9f9](https://github.com/pixelfed/pixelfed/commit/b2b8c9f9))
|
||||
- Updated AvatarController, remove deprecated thumb_path. ([889c3d87](https://github.com/pixelfed/pixelfed/commit/889c3d87))
|
||||
- Updated VideoThumbnail, add MediaStoragePipeline. ([98c44f7b](https://github.com/pixelfed/pixelfed/commit/98c44f7b))
|
||||
- Updated StatusDelete pipeline, fix object storage thumbnail deletion. ([f930c4bd](https://github.com/pixelfed/pixelfed/commit/f930c4bd))
|
||||
- Updated MediaStorageService, clear transformer cache after storing media. ([ce6ab80d](https://github.com/pixelfed/pixelfed/commit/ce6ab80d))
|
||||
- Updated MediaTransformer, remove cache busting. ([258b2729](https://github.com/pixelfed/pixelfed/commit/258b2729))
|
||||
- Updated AP helpers, only run MediaStoragePipeline if using cloud storage. ([77f21b4b](https://github.com/pixelfed/pixelfed/commit/77f21b4b))
|
||||
- Updated AvatarObserver, add logic to delete avatars stored in S3. ([9eafc31e](https://github.com/pixelfed/pixelfed/commit/9eafc31e))
|
||||
- Updated Profile model, use cdn_url for avatars. ([ea8e4261](https://github.com/pixelfed/pixelfed/commit/ea8e4261))
|
||||
- Updated ActivityPubFetchService, add url validation. ([654b08d3](https://github.com/pixelfed/pixelfed/commit/654b08d3))
|
||||
- Updated MediaStorageService, add avatar method. ([94a9f685](https://github.com/pixelfed/pixelfed/commit/94a9f685))
|
||||
- Updated AvatarPipeline, add remote avatar fetch. ([4c148055](https://github.com/pixelfed/pixelfed/commit/4c148055))
|
||||
- Updated ComposeController, update media version. ([cc2d4bf8](https://github.com/pixelfed/pixelfed/commit/cc2d4bf8))
|
||||
- Updated AP Helpers, add blurhash and RemoteAvatarFetch. ([de8828e8](https://github.com/pixelfed/pixelfed/commit/de8828e8))
|
||||
- Updated Timeline, prevent nextTick() when reloading same comment modal. Fixes #2584. ([cc84125b](https://github.com/pixelfed/pixelfed/commit/cc84125b))
|
||||
- Updated site config, add labels to config. ([abe9cb3d](https://github.com/pixelfed/pixelfed/commit/abe9cb3d))
|
||||
- Update StatusLabelService, change config key. ([4abfe76a](https://github.com/pixelfed/pixelfed/commit/4abfe76a))
|
||||
|
||||
- Update InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
|
||||
|
||||
## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
|
||||
### Added
|
||||
|
@ -600,7 +213,7 @@
|
|||
- Updated StatusTransformer, fixes #[2113](https://github.com/pixelfed/pixelfed/issues/2113) ([eefa6e0d](https://github.com/pixelfed/pixelfed/commit/eefa6e0d))
|
||||
- Updated InternalApiController, limit remote profile ui to remote profiles ([d918a68e](https://github.com/pixelfed/pixelfed/commit/d918a68e))
|
||||
- Updated NotificationCard, fix pagination bug #[2019](https://github.com/pixelfed/pixelfed/issues/2019) ([32beaad5](https://github.com/pixelfed/pixelfed/commit/32beaad5))
|
||||
|
||||
-
|
||||
|
||||
## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
|
||||
### Added
|
||||
|
|
|
@ -11,19 +11,13 @@
|
|||
A free and ethical photo sharing platform, powered by ActivityPub federation.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/pixelfed-screenshot.jpg">
|
||||
<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/Screen%20Shot%202019-09-08%20at%2010.40.54%20PM.png">
|
||||
</p>
|
||||
|
||||
## Official Documentation
|
||||
|
||||
Documentation for Pixelfed can be found on the [Pixelfed documentation website](https://docs.pixelfed.org/).
|
||||
|
||||
## Run on YunoHost
|
||||
|
||||
[](https://install-app.yunohost.org/?app=pixelfed)
|
||||
|
||||
Pixelfed app for [YunoHost](https://yunohost.org 'YunoHost'). See [the package source code](https://github.com/YunoHost-Apps/pixelfed_ynh 'pixelfed_ynh repository on GitHub')
|
||||
|
||||
## License
|
||||
|
||||
Pixelfed is open-sourced software licensed under the AGPL license.
|
||||
|
@ -33,6 +27,7 @@ Pixelfed is open-sourced software licensed under the AGPL license.
|
|||
The ways you can communicate on the project are below. Before interacting, please
|
||||
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
* IRC: [#pixelfed](irc://chat.freenode.net/pixelfed) on irc.freenode.net
|
||||
* Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
|
||||
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
## Reporting a Vulnerability
|
||||
|
||||
If you discover any security related issues, please email hello@pixelfed.org instead of using the issue tracker.
|
|
@ -6,9 +6,6 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class AccountLog extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['*'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
|
|
@ -14,21 +14,9 @@ class Avatar extends Model
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = [
|
||||
'deleted_at',
|
||||
'last_fetched_at',
|
||||
'last_processed_at'
|
||||
];
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
protected $fillable = ['profile_id'];
|
||||
|
||||
protected $visible = [
|
||||
'id',
|
||||
'profile_id',
|
||||
'media_path',
|
||||
'size',
|
||||
];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace App;
|
|||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\HasSnowflakePrimary;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
|
||||
class Collection extends Model
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\HasSnowflakePrimary;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
|
||||
class CollectionItem extends Model
|
||||
{
|
||||
|
|
|
@ -1,219 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Avatar;
|
||||
use App\Profile;
|
||||
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AvatarSync extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'avatars:sync';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Perform actions on avatars';
|
||||
|
||||
public $found = 0;
|
||||
public $notFetched = 0;
|
||||
public $fixed = 0;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Welcome to the avatar sync manager');
|
||||
|
||||
$actions = [
|
||||
'Analyze',
|
||||
'Full Analyze',
|
||||
'Fetch - Fetch missing remote avatars',
|
||||
'Fix - Fix remote accounts without avatar record',
|
||||
'Sync - Store latest remote avatars',
|
||||
];
|
||||
|
||||
$name = $this->choice(
|
||||
'Select an action',
|
||||
$actions,
|
||||
0,
|
||||
1,
|
||||
false
|
||||
);
|
||||
|
||||
$this->info('Selected: ' . $name);
|
||||
|
||||
switch($name) {
|
||||
case $actions[0]:
|
||||
$this->analyze();
|
||||
break;
|
||||
|
||||
case $actions[1]:
|
||||
$this->fullAnalyze();
|
||||
break;
|
||||
|
||||
case $actions[2]:
|
||||
$this->fetch();
|
||||
break;
|
||||
|
||||
case $actions[3]:
|
||||
$this->fix();
|
||||
break;
|
||||
|
||||
case $actions[4]:
|
||||
$this->sync();
|
||||
break;
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function incr($name)
|
||||
{
|
||||
switch($name) {
|
||||
case 'found':
|
||||
$this->found = $this->found + 1;
|
||||
break;
|
||||
|
||||
case 'notFetched':
|
||||
$this->notFetched = $this->notFetched + 1;
|
||||
break;
|
||||
|
||||
case 'fixed':
|
||||
$this->fixed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function analyze()
|
||||
{
|
||||
$count = Avatar::whereIsRemote(true)->whereNull('cdn_url')->count();
|
||||
$this->info('Found ' . $count . ' profiles with blank avatars.');
|
||||
$this->line(' ');
|
||||
$this->comment('We suggest running php artisan avatars:sync again and selecting the sync option');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
||||
protected function fullAnalyze()
|
||||
{
|
||||
$count = Profile::count();
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
Profile::chunk(5000, function($profiles) use ($bar) {
|
||||
foreach($profiles as $profile) {
|
||||
if($profile->domain == null) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
$avatar = Avatar::whereProfileId($profile->id)->first();
|
||||
if(!$avatar || $avatar->cdn_url == null) {
|
||||
$this->incr('notFetched');
|
||||
}
|
||||
$this->incr('found');
|
||||
$bar->advance();
|
||||
}
|
||||
});
|
||||
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info('Found ' . $this->found . ' remote accounts');
|
||||
$this->info('Found ' . $this->notFetched . ' remote avatars to fetch');
|
||||
}
|
||||
|
||||
protected function fetch()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fix()
|
||||
{
|
||||
Profile::chunk(5000, function($profiles) {
|
||||
foreach($profiles as $profile) {
|
||||
if($profile->domain == null || $profile->private_key) {
|
||||
continue;
|
||||
}
|
||||
$avatar = Avatar::whereProfileId($profile->id)->first();
|
||||
if($avatar) {
|
||||
continue;
|
||||
}
|
||||
$avatar = new Avatar;
|
||||
$avatar->is_remote = true;
|
||||
$avatar->profile_id = $profile->id;
|
||||
$avatar->save();
|
||||
$this->incr('fixed');
|
||||
}
|
||||
});
|
||||
$this->line(' ');
|
||||
$this->line(' ');
|
||||
$this->info('Fixed ' . $this->fixed . ' accounts with a blank avatar');
|
||||
}
|
||||
|
||||
protected function sync()
|
||||
{
|
||||
Avatar::whereIsRemote(true)
|
||||
->with('profile')
|
||||
->chunk(10, function($avatars) {
|
||||
foreach($avatars as $avatar) {
|
||||
RemoteAvatarFetch::dispatch($avatar->profile);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Backup\BackupDestination\BackupDestination;
|
||||
|
||||
class BackupToCloud extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'backup:cloud';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send backups to cloud storage';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$localDisk = Storage::disk('local');
|
||||
$cloudDisk = Storage::disk('backup');
|
||||
$backupDestination = new BackupDestination($localDisk, '', 'local');
|
||||
|
||||
if(
|
||||
empty(config('filesystems.disks.backup.key')) ||
|
||||
empty(config('filesystems.disks.backup.secret')) ||
|
||||
empty(config('filesystems.disks.backup.endpoint')) ||
|
||||
empty(config('filesystems.disks.backup.region')) ||
|
||||
empty(config('filesystems.disks.backup.bucket'))
|
||||
) {
|
||||
$this->error('Backup disk not configured.');
|
||||
$this->error('See https://docs.pixelfed.org/technical-documentation/env.html#filesystem for more information.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$newest = $backupDestination->newestBackup();
|
||||
$name = $newest->path();
|
||||
$parts = explode('/', $name);
|
||||
$fileName = array_pop($parts);
|
||||
$storagePath = 'backups';
|
||||
$path = storage_path('app/'. $name);
|
||||
$file = $cloudDisk->putFileAs($storagePath, new File($path), $fileName, 'private');
|
||||
$this->info("Backup file successfully saved!");
|
||||
$url = $cloudDisk->url($file);
|
||||
$this->table(
|
||||
['Name', 'URL'],
|
||||
[
|
||||
[$fileName, $url]
|
||||
],
|
||||
);
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DatabaseSessionGarbageCollector extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'gc:sessions';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Database sessions garbage collector';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(config('session.driver') !== 'database') {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
DB::transaction(function() {
|
||||
DB::table('sessions')->whereNull('user_id')->delete();
|
||||
});
|
||||
|
||||
DB::transaction(function() {
|
||||
$ts = now()->subMonths(3)->timestamp;
|
||||
DB::table('sessions')->where('last_activity', '<', $ts)->delete();
|
||||
});
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ExportLanguages extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'i18n:export';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Build and export js localization files.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(config('app.env') !== 'local') {
|
||||
$this->error('This command is meant for development purposes and should only be run in a local environment');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$path = base_path('resources/lang');
|
||||
$langs = [];
|
||||
|
||||
foreach (new \DirectoryIterator($path) as $io) {
|
||||
$name = $io->getFilename();
|
||||
$skip = ['vendor'];
|
||||
if($io->isDot() || in_array($name, $skip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if($io->isDir()) {
|
||||
array_push($langs, $name);
|
||||
}
|
||||
}
|
||||
|
||||
$exportDir = resource_path('assets/js/i18n/');
|
||||
$exportDirAlt = public_path('_lang/');
|
||||
|
||||
foreach($langs as $lang) {
|
||||
$strings = \Lang::get('web', [], $lang);
|
||||
$json = json_encode($strings, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
$path = "{$exportDir}{$lang}.json";
|
||||
file_put_contents($path, $json);
|
||||
$pathAlt = "{$exportDirAlt}{$lang}.json";
|
||||
file_put_contents($pathAlt, $json);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ class FailedJobGC extends Command
|
|||
{
|
||||
FailedJob::chunk(50, function($jobs) {
|
||||
foreach($jobs as $job) {
|
||||
if($job->failed_at->lt(now()->subHours(48))) {
|
||||
if($job->failed_at->lt(now()->subMonth())) {
|
||||
$job->delete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ class ImportCities extends Command
|
|||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
ini_set('memory_limit', '256M');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,8 +75,6 @@ class ImportCities extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$old_memory_limit = ini_get('memory_limit');
|
||||
ini_set('memory_limit', '256M');
|
||||
$path = storage_path('app/cities.json');
|
||||
|
||||
if(hash_file('sha512', $path) !== self::CHECKSUM) {
|
||||
|
@ -137,7 +136,6 @@ class ImportCities extends Command
|
|||
$this->line('');
|
||||
$this->info('Successfully imported ' . $cityCount . ' entries!');
|
||||
$this->line('');
|
||||
ini_set('memory_limit', $old_memory_limit);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
use App\Jobs\StoryPipeline\StoryExpire;
|
||||
use App\Jobs\StoryPipeline\StoryRotateMedia;
|
||||
use App\Services\StoryService;
|
||||
use Illuminate\Support\Facades\{
|
||||
DB,
|
||||
Storage
|
||||
};
|
||||
use App\{
|
||||
Story,
|
||||
StoryView
|
||||
};
|
||||
|
||||
class StoryGC extends Command
|
||||
{
|
||||
|
@ -44,41 +45,61 @@ class StoryGC extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->archiveExpiredStories();
|
||||
$this->rotateMedia();
|
||||
$this->directoryScan();
|
||||
$this->deleteViews();
|
||||
$this->deleteStories();
|
||||
}
|
||||
|
||||
protected function archiveExpiredStories()
|
||||
protected function directoryScan()
|
||||
{
|
||||
$stories = Story::whereActive(true)
|
||||
->where('created_at', '<', now()->subHours(24))
|
||||
->get();
|
||||
$day = now()->day;
|
||||
|
||||
if($day != 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
|
||||
|
||||
$t1 = Storage::directories('public/_esm.t1');
|
||||
$t2 = Storage::directories('public/_esm.t2');
|
||||
|
||||
$dirs = array_merge($t1, $t2);
|
||||
|
||||
foreach($dirs as $dir) {
|
||||
$hash = last(explode('/', $dir));
|
||||
if($hash != $monthHash) {
|
||||
$this->info('Found directory to delete: ' . $dir);
|
||||
$this->deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function deleteDirectory($path)
|
||||
{
|
||||
Storage::deleteDirectory($path);
|
||||
}
|
||||
|
||||
protected function deleteViews()
|
||||
{
|
||||
StoryView::where('created_at', '<', now()->subMinutes(1441))->delete();
|
||||
}
|
||||
|
||||
protected function deleteStories()
|
||||
{
|
||||
$stories = Story::where('created_at', '<', now()->subMinutes(1441))->take(50)->get();
|
||||
|
||||
if($stories->count() == 0) {
|
||||
exit;
|
||||
}
|
||||
|
||||
foreach($stories as $story) {
|
||||
StoryExpire::dispatch($story)->onQueue('story');
|
||||
if(Storage::exists($story->path) == true) {
|
||||
Storage::delete($story->path);
|
||||
}
|
||||
}
|
||||
|
||||
protected function rotateMedia()
|
||||
{
|
||||
$queue = StoryService::rotateQueue();
|
||||
|
||||
if(!$queue || count($queue) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
collect($queue)
|
||||
->each(function($id) {
|
||||
$story = StoryService::getById($id);
|
||||
if(!$story) {
|
||||
StoryService::removeRotateQueue($id);
|
||||
return;
|
||||
}
|
||||
if($story->created_at->gt(now()->subMinutes(20))) {
|
||||
return;
|
||||
}
|
||||
StoryRotateMedia::dispatch($story)->onQueue('story');
|
||||
StoryService::removeRotateQueue($id);
|
||||
DB::transaction(function() use($story) {
|
||||
StoryView::whereStoryId($story->id)->delete();
|
||||
$story->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class UserCreate extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:create {--name=} {--username=} {--email=} {--password=} {--is_admin=0} {--confirm_email=0}';
|
||||
protected $signature = 'user:create';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -40,26 +40,6 @@ class UserCreate extends Command
|
|||
{
|
||||
$this->info('Creating a new user...');
|
||||
|
||||
$o = $this->options();
|
||||
|
||||
if( $o['name'] &&
|
||||
$o['username'] &&
|
||||
$o['email'] &&
|
||||
$o['password']
|
||||
) {
|
||||
$user = new User;
|
||||
$user->username = $o['username'];
|
||||
$user->name = $o['name'];
|
||||
$user->email = $o['email'];
|
||||
$user->password = bcrypt($o['password']);
|
||||
$user->is_admin = (bool) $o['is_admin'];
|
||||
$user->email_verified_at = (bool) $o['confirm_email'] ? now() : null;
|
||||
$user->save();
|
||||
|
||||
$this->info('Successfully created user!');
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $this->ask('Name');
|
||||
|
||||
$username = $this->ask('Username');
|
||||
|
|
|
@ -31,7 +31,6 @@ class Kernel extends ConsoleKernel
|
|||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace App\Exceptions;
|
|||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
|
@ -14,10 +13,7 @@ class Handler extends ExceptionHandler
|
|||
* @var array
|
||||
*/
|
||||
protected $dontReport = [
|
||||
OAuthServerException::class,
|
||||
\Zttp\ConnectionException::class,
|
||||
\GuzzleHttp\Exception\ConnectException::class,
|
||||
\Illuminate\Http\Client\ConnectionException::class
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -42,22 +38,6 @@ class Handler extends ExceptionHandler
|
|||
parent::report($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->reportable(function (\BadMethodCallException $e) {
|
||||
return app()->environment() !== 'production';
|
||||
});
|
||||
|
||||
$this->reportable(function (\Illuminate\Http\Client\ConnectionException $e) {
|
||||
return app()->environment() !== 'production';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
|
@ -68,11 +48,6 @@ class Handler extends ExceptionHandler
|
|||
*/
|
||||
public function render($request, Throwable $exception)
|
||||
{
|
||||
if ($request->wantsJson())
|
||||
return response()->json(
|
||||
['error' => $exception->getMessage()],
|
||||
method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500
|
||||
);
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Services\SnowflakeService;
|
||||
|
||||
trait HasSnowflakePrimary
|
||||
{
|
||||
public static function bootHasSnowflakePrimary()
|
||||
{
|
||||
static::saving(function ($model) {
|
||||
if (is_null($model->getKey())) {
|
||||
$keyName = $model->getKeyName();
|
||||
$id = SnowflakeService::next();
|
||||
$model->setAttribute($keyName, $id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -22,13 +22,6 @@ use App\{
|
|||
User,
|
||||
UserFilter
|
||||
};
|
||||
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\UserFilterService;
|
||||
use App\Services\RelationshipService;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
@ -37,8 +30,6 @@ class AccountController extends Controller
|
|||
'user.block',
|
||||
];
|
||||
|
||||
const FILTER_LIMIT = 'You cannot block or mute more than 100 accounts';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
|
@ -76,10 +67,7 @@ class AccountController extends Controller
|
|||
|
||||
public function verifyEmail(Request $request)
|
||||
{
|
||||
$recentSent = EmailVerification::whereUserId(Auth::id())
|
||||
->whereDate('created_at', '>', now()->subHours(12))->count();
|
||||
|
||||
return view('account.verify_email', compact('recentSent'));
|
||||
return view('account.verify_email');
|
||||
}
|
||||
|
||||
public function sendVerifyEmail(Request $request)
|
||||
|
@ -148,12 +136,6 @@ class AccountController extends Controller
|
|||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$count = UserFilterService::muteCount($user->id);
|
||||
abort_if($count >= 100, 422, self::FILTER_LIMIT);
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($user->id)->count();
|
||||
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
|
||||
}
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.mute';
|
||||
|
@ -185,7 +167,6 @@ class AccountController extends Controller
|
|||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
@ -236,7 +217,6 @@ class AccountController extends Controller
|
|||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json([200]);
|
||||
|
@ -253,12 +233,6 @@ class AccountController extends Controller
|
|||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$count = UserFilterService::blockCount($user->id);
|
||||
abort_if($count >= 100, 422, self::FILTER_LIMIT);
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($user->id)->count();
|
||||
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
|
||||
}
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type.'.block';
|
||||
|
@ -269,7 +243,7 @@ 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 == $user->id || $profile->user->is_admin == true) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
|
@ -291,7 +265,6 @@ class AccountController extends Controller
|
|||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
@ -342,7 +315,6 @@ class AccountController extends Controller
|
|||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
Cache::forget("api:local:exp:rec:$pid");
|
||||
RelationshipService::refresh($pid, $profile->id);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
@ -515,82 +487,4 @@ class AccountController extends Controller
|
|||
public function accountRestored(Request $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function accountMutes(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|integer|min:1|max:40'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$limit = $request->input('limit') ?? 40;
|
||||
|
||||
$mutes = UserFilter::whereUserId($user->profile_id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('mute')
|
||||
->simplePaginate($limit)
|
||||
->pluck('filterable_id');
|
||||
|
||||
$accounts = Profile::find($mutes);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
$url = $request->url();
|
||||
$page = $request->input('page', 1);
|
||||
$next = $page < 40 ? $page + 1 : 40;
|
||||
$prev = $page > 1 ? $page - 1 : 1;
|
||||
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
|
||||
return response()->json($res, 200, ['Link' => $links]);
|
||||
}
|
||||
|
||||
public function accountBlocks(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|integer|min:1|max:40',
|
||||
'page' => 'nullable|integer|min:1|max:10'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$limit = $request->input('limit') ?? 40;
|
||||
|
||||
$blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
|
||||
->whereUserId($user->profile_id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->simplePaginate($limit)
|
||||
->pluck('filterable_id');
|
||||
|
||||
$profiles = Profile::findOrFail($blocked);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
$url = $request->url();
|
||||
$page = $request->input('page', 1);
|
||||
$next = $page < 40 ? $page + 1 : 40;
|
||||
$prev = $page > 1 ? $page - 1 : 1;
|
||||
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
|
||||
return response()->json($res, 200, ['Link' => $links]);
|
||||
|
||||
}
|
||||
|
||||
public function accountBlocksV2(Request $request)
|
||||
{
|
||||
return response()->json(UserFilterService::blocks($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function accountMutesV2(Request $request)
|
||||
{
|
||||
return response()->json(UserFilterService::mutes($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function accountFiltersV2(Request $request)
|
||||
{
|
||||
return response()->json(UserFilterService::filters($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,58 +14,29 @@ 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'
|
||||
])
|
||||
Rule::in(['autocw', 'unlisted', 'banned'])
|
||||
],
|
||||
]);
|
||||
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')) {
|
||||
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);
|
||||
case 'autocw':
|
||||
$instances = Instance::whereAutoCw(true)->orderByDesc('id')->paginate(5);
|
||||
break;
|
||||
case 'unlisted':
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereUnlisted(true)->orderByDesc('id')->simplePaginate(10);
|
||||
$instances = Instance::whereUnlisted(true)->orderByDesc('id')->paginate(5);
|
||||
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);
|
||||
$instances = Instance::whereBanned(true)->orderByDesc('id')->paginate(5);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10);
|
||||
$instances = Instance::orderByDesc('id')->paginate(5);
|
||||
}
|
||||
|
||||
return view('admin.instances.home', compact('instances'));
|
||||
}
|
||||
|
||||
|
@ -126,10 +97,6 @@ trait AdminInstanceController
|
|||
break;
|
||||
}
|
||||
|
||||
Cache::forget('instances:banned:domains');
|
||||
Cache::forget('instances:unlisted:domains');
|
||||
Cache::forget('instances:auto_cw:domains');
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@ trait AdminMediaController
|
|||
],
|
||||
'search' => 'nullable|string|min:1|max:20'
|
||||
]);
|
||||
|
||||
if($request->filled('search')) {
|
||||
$profiles = Profile::where('username', 'like', '%'.$request->input('search').'%')->pluck('id')->toArray();
|
||||
$media = Media::whereHas('status')
|
||||
|
@ -43,8 +42,7 @@ trait AdminMediaController
|
|||
$media = MediaBlocklist::latest()->paginate(12);
|
||||
return view('admin.media.home', compact('media'));
|
||||
}
|
||||
|
||||
$media = Media::whereNull('remote_url')->orderby('id', 'desc')->simplePaginate(12);
|
||||
$media = Media::whereHas('status')->with('status')->orderby('id', 'desc')->paginate(12);
|
||||
return view('admin.media.home', compact('media'));
|
||||
}
|
||||
|
||||
|
|
|
@ -3,372 +3,12 @@
|
|||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Cache;
|
||||
use App\Report;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Contact,
|
||||
Hashtag,
|
||||
Newsroom,
|
||||
OauthClient,
|
||||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
Story,
|
||||
User
|
||||
};
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Services\StoryService;
|
||||
|
||||
trait AdminReportController
|
||||
{
|
||||
public function reports(Request $request)
|
||||
{
|
||||
$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
|
||||
$page = $request->input('page') ?? 1;
|
||||
|
||||
$ai = Cache::remember('admin-dash:reports:ai-count', 3600, function() {
|
||||
return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count();
|
||||
});
|
||||
|
||||
$spam = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
|
||||
});
|
||||
|
||||
$mailVerifications = Redis::scard('email:manual');
|
||||
|
||||
if($filter == 'open' && $page == 1) {
|
||||
$reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) {
|
||||
return Report::whereHas('status')
|
||||
->whereHas('reportedUser')
|
||||
->whereHas('reporter')
|
||||
->orderBy('created_at','desc')
|
||||
->when($filter, function($q, $filter) {
|
||||
return $filter == 'open' ?
|
||||
$q->whereNull('admin_seen') :
|
||||
$q->whereNotNull('admin_seen');
|
||||
})
|
||||
->paginate(6);
|
||||
});
|
||||
} else {
|
||||
$reports = Report::whereHas('status')
|
||||
->whereHas('reportedUser')
|
||||
->whereHas('reporter')
|
||||
->orderBy('created_at','desc')
|
||||
->when($filter, function($q, $filter) {
|
||||
return $filter == 'open' ?
|
||||
$q->whereNull('admin_seen') :
|
||||
$q->whereNotNull('admin_seen');
|
||||
})
|
||||
->paginate(6);
|
||||
}
|
||||
|
||||
return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications'));
|
||||
}
|
||||
|
||||
public function showReport(Request $request, $id)
|
||||
{
|
||||
$report = Report::findOrFail($id);
|
||||
return view('admin.reports.show', compact('report'));
|
||||
}
|
||||
|
||||
public function appeals(Request $request)
|
||||
{
|
||||
$appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
|
||||
->whereNull('appeal_handled_at')
|
||||
->latest()
|
||||
->paginate(6);
|
||||
return view('admin.reports.appeals', compact('appeals'));
|
||||
}
|
||||
|
||||
public function showAppeal(Request $request, $id)
|
||||
{
|
||||
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
$meta = json_decode($appeal->meta);
|
||||
return view('admin.reports.show_appeal', compact('appeal', 'meta'));
|
||||
}
|
||||
|
||||
public function spam(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions'
|
||||
]);
|
||||
|
||||
$tab = $request->input('tab', 'home');
|
||||
|
||||
$openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->count();
|
||||
});
|
||||
|
||||
$monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')
|
||||
->where('created_at', '>', now()->subMonth())
|
||||
->count();
|
||||
});
|
||||
|
||||
$totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')->count();
|
||||
});
|
||||
|
||||
$uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')
|
||||
->whereIsSpam(null)
|
||||
->whereNotNull('appeal_handled_at')
|
||||
->exists();
|
||||
});
|
||||
|
||||
$avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function() {
|
||||
if(config('database.default') != 'mysql') {
|
||||
return 0;
|
||||
}
|
||||
return AccountInterstitial::selectRaw('*, count(id) as counter')
|
||||
->whereType('post.autospam')
|
||||
->groupBy('user_id')
|
||||
->get()
|
||||
->avg('counter');
|
||||
});
|
||||
|
||||
$avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function() {
|
||||
if(config('database.default') != 'mysql') {
|
||||
return "0";
|
||||
}
|
||||
$seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get();
|
||||
if(!$seconds) {
|
||||
return "0";
|
||||
}
|
||||
$mins = floor($seconds->avg('timediff') / 60);
|
||||
|
||||
if($mins < 60) {
|
||||
return $mins . ' min(s)';
|
||||
}
|
||||
|
||||
if($mins < 2880) {
|
||||
return floor($mins / 60) . ' hour(s)';
|
||||
}
|
||||
|
||||
return floor($mins / 60 / 24) . ' day(s)';
|
||||
});
|
||||
$avgCount = $totalCount && $avg ? floor($totalCount / $avg) : "0";
|
||||
|
||||
if(in_array($tab, ['home', 'spam', 'not-spam'])) {
|
||||
$appeals = AccountInterstitial::whereType('post.autospam')
|
||||
->when($tab, function($q, $tab) {
|
||||
switch($tab) {
|
||||
case 'home':
|
||||
return $q->whereNull('appeal_handled_at');
|
||||
break;
|
||||
case 'spam':
|
||||
return $q->whereIsSpam(true);
|
||||
break;
|
||||
case 'not-spam':
|
||||
return $q->whereIsSpam(false);
|
||||
break;
|
||||
}
|
||||
})
|
||||
->latest()
|
||||
->paginate(6);
|
||||
|
||||
if($tab !== 'home') {
|
||||
$appeals = $appeals->appends(['tab' => $tab]);
|
||||
}
|
||||
} else {
|
||||
$appeals = new class {
|
||||
public function count() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function render() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized'));
|
||||
}
|
||||
|
||||
public function showSpam(Request $request, $id)
|
||||
{
|
||||
$appeal = AccountInterstitial::whereType('post.autospam')
|
||||
->findOrFail($id);
|
||||
$meta = json_decode($appeal->meta);
|
||||
return view('admin.reports.show_spam', compact('appeal', 'meta'));
|
||||
}
|
||||
|
||||
public function fixUncategorizedSpam(Request $request)
|
||||
{
|
||||
if(Cache::get('admin-dash:reports:spam-sync-active')) {
|
||||
return redirect('/i/admin/reports/autospam');
|
||||
}
|
||||
|
||||
Cache::put('admin-dash:reports:spam-sync-active', 1, 900);
|
||||
|
||||
AccountInterstitial::chunk(500, function($reports) {
|
||||
foreach($reports as $report) {
|
||||
if($report->item_type != 'App\Status') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if($report->type != 'post.autospam') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if($report->is_spam != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = StatusService::get($report->item_id, false);
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
$scope = $status['visibility'];
|
||||
$report->is_spam = $scope == 'unlisted';
|
||||
$report->in_violation = $report->is_spam;
|
||||
$report->severity_index = 1;
|
||||
$report->save();
|
||||
}
|
||||
});
|
||||
|
||||
Cache::forget('admin-dash:reports:spam-sync');
|
||||
return redirect('/i/admin/reports/autospam');
|
||||
}
|
||||
|
||||
public function updateSpam(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$appeal = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
|
||||
$meta = json_decode($appeal->meta);
|
||||
$res = ['status' => 'success'];
|
||||
$now = now();
|
||||
Cache::forget('admin-dash:reports:spam-count:total');
|
||||
Cache::forget('admin-dash:reports:spam-count:30d');
|
||||
|
||||
if($action == 'dismiss') {
|
||||
$appeal->is_spam = true;
|
||||
$appeal->appeal_handled_at = $now;
|
||||
$appeal->save();
|
||||
|
||||
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-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;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
public function updateAppeal(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:dismiss,approve'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
|
||||
if($action == 'dismiss') {
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
Cache::forget('admin-dash:reports:ai-count');
|
||||
return redirect('/i/admin/reports/appeals');
|
||||
}
|
||||
|
||||
switch ($appeal->type) {
|
||||
case 'post.cw':
|
||||
$status = $appeal->status;
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
break;
|
||||
|
||||
case 'post.unlist':
|
||||
$status = $appeal->status;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
break;
|
||||
|
||||
default:
|
||||
# code...
|
||||
break;
|
||||
}
|
||||
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
StatusService::del($status->id, true);
|
||||
Cache::forget('admin-dash:reports:ai-count');
|
||||
|
||||
return redirect('/i/admin/reports/appeals');
|
||||
}
|
||||
|
||||
public function updateReport(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
@ -393,7 +33,6 @@ trait AdminReportController
|
|||
$report = Report::findOrFail($id);
|
||||
|
||||
$this->handleReportAction($report, $action);
|
||||
Cache::forget('admin-dash:reports:list-cache');
|
||||
|
||||
return response()->json(['msg'=> 'Success']);
|
||||
}
|
||||
|
@ -413,20 +52,17 @@ trait AdminReportController
|
|||
$item->is_nsfw = true;
|
||||
$item->save();
|
||||
$report->nsfw = true;
|
||||
StatusService::del($item->id, true);
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
$item->visibility = 'unlisted';
|
||||
$item->save();
|
||||
Cache::forget('profiles:private');
|
||||
StatusService::del($item->id, true);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
// Todo: fire delete job
|
||||
$report->admin_seen = null;
|
||||
StatusService::del($item->id, true);
|
||||
break;
|
||||
|
||||
case 'shadowban':
|
||||
|
@ -479,55 +115,4 @@ trait AdminReportController
|
|||
];
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function reportMailVerifications(Request $request)
|
||||
{
|
||||
$ids = Redis::smembers('email:manual');
|
||||
$ignored = Redis::smembers('email:manual-ignored');
|
||||
$reports = [];
|
||||
if($ids) {
|
||||
$reports = collect($ids)
|
||||
->filter(function($id) use($ignored) {
|
||||
return !in_array($id, $ignored);
|
||||
})
|
||||
->map(function($id) {
|
||||
$account = AccountService::get($id);
|
||||
$user = User::whereProfileId($id)->first();
|
||||
if(!$user) {
|
||||
return [];
|
||||
}
|
||||
$account['email'] = $user->email;
|
||||
return $account;
|
||||
})
|
||||
->filter(function($res) {
|
||||
return $res && isset($res['id']);
|
||||
})
|
||||
->values();
|
||||
}
|
||||
return view('admin.reports.mail_verification', compact('reports', 'ignored'));
|
||||
}
|
||||
|
||||
public function reportMailVerifyIgnore(Request $request)
|
||||
{
|
||||
$id = $request->input('id');
|
||||
Redis::sadd('email:manual-ignored', $id);
|
||||
return redirect('/i/admin/reports');
|
||||
}
|
||||
|
||||
public function reportMailVerifyApprove(Request $request)
|
||||
{
|
||||
$id = $request->input('id');
|
||||
$user = User::whereProfileId($id)->firstOrFail();
|
||||
Redis::srem('email:manual', $id);
|
||||
Redis::srem('email:manual-ignored', $id);
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
return redirect('/i/admin/reports');
|
||||
}
|
||||
|
||||
public function reportMailVerifyClearIgnored(Request $request)
|
||||
{
|
||||
Redis::del('email:manual-ignored');
|
||||
return [200];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,192 +6,14 @@ use Artisan, Cache, DB;
|
|||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
|
||||
use App\Models\InstanceActor;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use App\Models\ConfigCache;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Util\Site\Config;
|
||||
|
||||
trait AdminSettingsController
|
||||
{
|
||||
public function settings(Request $request)
|
||||
{
|
||||
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
|
||||
$cloud_disk = config('filesystems.cloud');
|
||||
$cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret'));
|
||||
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
|
||||
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null;
|
||||
$jpeg = in_array('image/jpg', $types) ? true : in_array('image/jpeg', $types);
|
||||
$png = in_array('image/png', $types);
|
||||
$gif = in_array('image/gif', $types);
|
||||
$mp4 = in_array('video/mp4', $types);
|
||||
$webp = in_array('image/webp', $types);
|
||||
|
||||
// $system = [
|
||||
// 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')),
|
||||
// 'max_upload_size' => ini_get('post_max_size'),
|
||||
// 'image_driver' => config('image.driver'),
|
||||
// 'image_driver_loaded' => extension_loaded(config('image.driver'))
|
||||
// ];
|
||||
|
||||
return view('admin.settings.home', compact(
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'mp4',
|
||||
'webp',
|
||||
'rules',
|
||||
'cloud_storage',
|
||||
'cloud_disk',
|
||||
'cloud_ready',
|
||||
// 'system'
|
||||
));
|
||||
}
|
||||
|
||||
public function settingsHomeStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'nullable|string',
|
||||
'short_description' => 'nullable',
|
||||
'long_description' => 'nullable',
|
||||
'max_photo_size' => 'nullable|integer|min:1',
|
||||
'max_album_length' => 'nullable|integer|min:1|max:100',
|
||||
'image_quality' => 'nullable|integer|min:1|max:100',
|
||||
'type_jpeg' => 'nullable',
|
||||
'type_png' => 'nullable',
|
||||
'type_gif' => 'nullable',
|
||||
'type_mp4' => 'nullable',
|
||||
'type_webp' => 'nullable',
|
||||
]);
|
||||
|
||||
if($request->filled('rule_delete')) {
|
||||
$index = (int) $request->input('rule_delete');
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$json = json_decode($rules, true);
|
||||
if(!$rules || empty($json)) {
|
||||
return;
|
||||
}
|
||||
unset($json[$index]);
|
||||
$json = json_encode(array_values($json));
|
||||
ConfigCacheService::put('app.rules', $json);
|
||||
return 200;
|
||||
}
|
||||
|
||||
$media_types = explode(',', config_cache('pixelfed.media_types'));
|
||||
$media_types_original = $media_types;
|
||||
|
||||
$mimes = [
|
||||
'type_jpeg' => 'image/jpeg',
|
||||
'type_png' => 'image/png',
|
||||
'type_gif' => 'image/gif',
|
||||
'type_mp4' => 'video/mp4',
|
||||
'type_webp' => 'image/webp',
|
||||
];
|
||||
|
||||
foreach ($mimes as $key => $value) {
|
||||
if($request->input($key) == 'on') {
|
||||
if(!in_array($value, $media_types)) {
|
||||
array_push($media_types, $value);
|
||||
}
|
||||
} else {
|
||||
$media_types = array_diff($media_types, [$value]);
|
||||
}
|
||||
}
|
||||
|
||||
if($media_types !== $media_types_original) {
|
||||
ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
|
||||
}
|
||||
|
||||
$keys = [
|
||||
'name' => 'app.name',
|
||||
'short_description' => 'app.short_description',
|
||||
'long_description' => 'app.description',
|
||||
'max_photo_size' => 'pixelfed.max_photo_size',
|
||||
'max_album_length' => 'pixelfed.max_album_length',
|
||||
'image_quality' => 'pixelfed.image_quality',
|
||||
'account_limit' => 'pixelfed.max_account_size',
|
||||
'custom_css' => 'uikit.custom.css',
|
||||
'custom_js' => 'uikit.custom.js',
|
||||
'about_title' => 'about.title'
|
||||
];
|
||||
|
||||
foreach ($keys as $key => $value) {
|
||||
$cc = ConfigCache::whereK($value)->first();
|
||||
$val = $request->input($key);
|
||||
if($cc && $cc->v != $val) {
|
||||
ConfigCacheService::put($value, $val);
|
||||
}
|
||||
}
|
||||
|
||||
$bools = [
|
||||
'activitypub' => 'federation.activitypub.enabled',
|
||||
'open_registration' => 'pixelfed.open_registration',
|
||||
'mobile_apis' => 'pixelfed.oauth_enabled',
|
||||
'stories' => 'instance.stories.enabled',
|
||||
'ig_import' => 'pixelfed.import.instagram.enabled',
|
||||
'spam_detection' => 'pixelfed.bouncer.enabled',
|
||||
'require_email_verification' => 'pixelfed.enforce_email_verification',
|
||||
'enforce_account_limit' => 'pixelfed.enforce_account_limit',
|
||||
'show_custom_css' => 'uikit.show_custom.css',
|
||||
'show_custom_js' => 'uikit.show_custom.js',
|
||||
'cloud_storage' => 'pixelfed.cloud_storage',
|
||||
'account_autofollow' => 'account.autofollow'
|
||||
];
|
||||
|
||||
foreach ($bools as $key => $value) {
|
||||
$active = $request->input($key) == 'on';
|
||||
|
||||
if($key == 'activitypub' && $active && !InstanceActor::exists()) {
|
||||
Artisan::call('instance:actor');
|
||||
}
|
||||
|
||||
if( $key == 'mobile_apis' &&
|
||||
$active &&
|
||||
!file_exists(storage_path('oauth-public.key')) &&
|
||||
!file_exists(storage_path('oauth-private.key'))
|
||||
) {
|
||||
Artisan::call('passport:keys');
|
||||
Artisan::call('route:cache');
|
||||
}
|
||||
|
||||
if(config_cache($value) !== $active) {
|
||||
ConfigCacheService::put($value, (bool) $active);
|
||||
}
|
||||
}
|
||||
|
||||
if($request->filled('new_rule')) {
|
||||
$rules = ConfigCacheService::get('app.rules');
|
||||
$val = $request->input('new_rule');
|
||||
if(!$rules) {
|
||||
ConfigCacheService::put('app.rules', json_encode([$val]));
|
||||
} else {
|
||||
$json = json_decode($rules, true);
|
||||
$json[] = $val;
|
||||
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
|
||||
}
|
||||
Cache::forget('api:v1:instance-data:rules');
|
||||
Cache::forget('api:v1:instance-data-response');
|
||||
}
|
||||
|
||||
if($request->filled('account_autofollow_usernames')) {
|
||||
$usernames = explode(',', $request->input('account_autofollow_usernames'));
|
||||
$names = [];
|
||||
|
||||
foreach($usernames as $n) {
|
||||
$p = Profile::whereUsername($n)->first();
|
||||
if(!$p) {
|
||||
continue;
|
||||
}
|
||||
array_push($names, $p->username);
|
||||
}
|
||||
|
||||
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
|
||||
}
|
||||
|
||||
Cache::forget(Config::CACHE_KEY);
|
||||
|
||||
return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!');
|
||||
return view('admin.settings.home');
|
||||
}
|
||||
|
||||
public function settingsBackups(Request $request)
|
||||
|
@ -201,6 +23,51 @@ trait AdminSettingsController
|
|||
return view('admin.settings.backups', compact('files'));
|
||||
}
|
||||
|
||||
public function settingsConfig(Request $request)
|
||||
{
|
||||
$editor = config('pixelfed.admin.env_editor');
|
||||
$config = !$editor ? false : file_get_contents(base_path('.env'));
|
||||
$backup = !$editor ? false : (is_file(base_path('.env.backup')) ? file_get_contents(base_path('.env.backup')) : false);
|
||||
return view('admin.settings.config', compact('editor', 'config', 'backup'));
|
||||
}
|
||||
|
||||
public function settingsConfigStore(Request $request)
|
||||
{
|
||||
if(config('pixelfed.admin.env_editor') !== true) {
|
||||
abort(400);
|
||||
}
|
||||
$res = $request->input('res');
|
||||
|
||||
$old = file_get_contents(app()->environmentFilePath());
|
||||
if(empty($old) || $old != $res) {
|
||||
$oldFile = fopen(app()->environmentFilePath().'.backup', 'w');
|
||||
fwrite($oldFile, $old);
|
||||
fclose($oldFile);
|
||||
}
|
||||
|
||||
$file = fopen(app()->environmentFilePath(), 'w');
|
||||
fwrite($file, $res);
|
||||
fclose($file);
|
||||
Artisan::call('config:cache');
|
||||
return ['msg' => 200];
|
||||
}
|
||||
|
||||
public function settingsConfigRestore(Request $request)
|
||||
{
|
||||
if(config('pixelfed.admin.env_editor') !== true) {
|
||||
abort(400);
|
||||
}
|
||||
$res = file_get_contents(app()->environmentFilePath().'.backup');
|
||||
if(empty($res)) {
|
||||
abort(400, 'No backup exists.');
|
||||
}
|
||||
$file = fopen(app()->environmentFilePath(), 'w');
|
||||
fwrite($file, $res);
|
||||
fclose($file);
|
||||
Artisan::call('config:cache');
|
||||
return ['msg' => 200];
|
||||
}
|
||||
|
||||
public function settingsMaintenance(Request $request)
|
||||
{
|
||||
return view('admin.settings.maintenance');
|
||||
|
@ -217,6 +84,15 @@ trait AdminSettingsController
|
|||
return view('admin.settings.features');
|
||||
}
|
||||
|
||||
public function settingsHomeStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'APP_NAME' => 'required|string',
|
||||
]);
|
||||
// Artisan::call('config:clear');
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function settingsPages(Request $request)
|
||||
{
|
||||
$pages = Page::orderByDesc('updated_at')->paginate(10);
|
||||
|
|
|
@ -16,27 +16,14 @@ trait AdminUserController
|
|||
{
|
||||
public function users(Request $request)
|
||||
{
|
||||
$search = $request->has('a') && $request->query('a') == 'search' ? $request->query('q') : null;
|
||||
$col = $request->query('col') ?? 'id';
|
||||
$dir = $request->query('dir') ?? 'desc';
|
||||
$offset = $request->has('page') ? $request->input('page') : 0;
|
||||
$pagination = [
|
||||
'prev' => $offset > 0 ? $offset - 1 : null,
|
||||
'next' => $offset + 1,
|
||||
'query' => $search ? '&a=search&q=' . $search : null
|
||||
];
|
||||
$users = User::select('id', 'username', 'status', 'profile_id')
|
||||
$users = User::select('id', 'username', 'status')
|
||||
->withCount('statuses')
|
||||
->orderBy($col, $dir)
|
||||
->when($search, function($q, $search) {
|
||||
return $q->where('username', 'like', "%{$search}%");
|
||||
})
|
||||
->when($offset, function($q, $offset) {
|
||||
return $q->offset(($offset * 10));
|
||||
})
|
||||
->limit(10)
|
||||
->get();
|
||||
->simplePaginate(10);
|
||||
|
||||
return view('admin.users.home', compact('users', 'pagination'));
|
||||
return view('admin.users.home', compact('users'));
|
||||
}
|
||||
|
||||
public function userShow(Request $request, $id)
|
||||
|
|
|
@ -11,39 +11,30 @@ use App\{
|
|||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
Story,
|
||||
User
|
||||
};
|
||||
use DB, Cache, Storage;
|
||||
use DB, Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Http\Controllers\Admin\{
|
||||
AdminDiscoverController,
|
||||
AdminInstanceController,
|
||||
AdminReportController,
|
||||
// AdminGroupsController,
|
||||
AdminMediaController,
|
||||
AdminSettingsController,
|
||||
// AdminStorageController,
|
||||
AdminSupportController,
|
||||
AdminUserController
|
||||
};
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Services\AdminStatsService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\StoryService;
|
||||
use App\Models\CustomEmoji;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
use AdminReportController,
|
||||
AdminDiscoverController,
|
||||
// AdminGroupsController,
|
||||
AdminMediaController,
|
||||
AdminSettingsController,
|
||||
AdminInstanceController,
|
||||
// AdminStorageController,
|
||||
AdminUserController;
|
||||
|
||||
public function __construct()
|
||||
|
@ -61,15 +52,9 @@ class AdminController extends Controller
|
|||
|
||||
public function statuses(Request $request)
|
||||
{
|
||||
$statuses = Status::orderBy('id', 'desc')->cursorPaginate(10);
|
||||
$data = $statuses->map(function($status) {
|
||||
return StatusService::get($status->id, false);
|
||||
})
|
||||
->filter(function($s) {
|
||||
return $s;
|
||||
})
|
||||
->toArray();
|
||||
return view('admin.statuses.home', compact('statuses', 'data'));
|
||||
$statuses = Status::orderBy('id', 'desc')->simplePaginate(10);
|
||||
|
||||
return view('admin.statuses.home', compact('statuses'));
|
||||
}
|
||||
|
||||
public function showStatus(Request $request, $id)
|
||||
|
@ -79,6 +64,139 @@ class AdminController extends Controller
|
|||
return view('admin.statuses.show', compact('status'));
|
||||
}
|
||||
|
||||
public function reports(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'filter' => 'nullable|string|in:all,open,closed'
|
||||
]);
|
||||
$filter = $request->input('filter');
|
||||
$reports = Report::orderBy('created_at','desc')
|
||||
->when($filter, function($q, $filter) {
|
||||
return $filter == 'open' ?
|
||||
$q->whereNull('admin_seen') :
|
||||
$q->whereNotNull('admin_seen');
|
||||
})
|
||||
->paginate(4);
|
||||
return view('admin.reports.home', compact('reports'));
|
||||
}
|
||||
|
||||
public function showReport(Request $request, $id)
|
||||
{
|
||||
$report = Report::findOrFail($id);
|
||||
return view('admin.reports.show', compact('report'));
|
||||
}
|
||||
|
||||
public function appeals(Request $request)
|
||||
{
|
||||
$appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
|
||||
->whereNull('appeal_handled_at')
|
||||
->latest()
|
||||
->paginate(6);
|
||||
return view('admin.reports.appeals', compact('appeals'));
|
||||
}
|
||||
|
||||
public function showAppeal(Request $request, $id)
|
||||
{
|
||||
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
$meta = json_decode($appeal->meta);
|
||||
return view('admin.reports.show_appeal', compact('appeal', 'meta'));
|
||||
}
|
||||
|
||||
public function spam(Request $request)
|
||||
{
|
||||
$appeals = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->latest()
|
||||
->paginate(6);
|
||||
return view('admin.reports.spam', compact('appeals'));
|
||||
}
|
||||
|
||||
public function showSpam(Request $request, $id)
|
||||
{
|
||||
$appeal = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
$meta = json_decode($appeal->meta);
|
||||
return view('admin.reports.show_spam', compact('appeal', 'meta'));
|
||||
}
|
||||
|
||||
public function updateSpam(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:dismiss,approve'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$appeal = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
|
||||
$meta = json_decode($appeal->meta);
|
||||
|
||||
if($action == 'dismiss') {
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
return redirect('/i/admin/reports/autospam');
|
||||
}
|
||||
|
||||
$status = $appeal->status;
|
||||
$status->is_nsfw = $meta->is_nsfw;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
return redirect('/i/admin/reports/autospam');
|
||||
}
|
||||
|
||||
public function updateAppeal(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:dismiss,approve'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
|
||||
->whereNull('appeal_handled_at')
|
||||
->findOrFail($id);
|
||||
|
||||
if($action == 'dismiss') {
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
return redirect('/i/admin/reports/appeals');
|
||||
}
|
||||
|
||||
switch ($appeal->type) {
|
||||
case 'post.cw':
|
||||
$status = $appeal->status;
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
break;
|
||||
|
||||
case 'post.unlist':
|
||||
$status = $appeal->status;
|
||||
$status->scope = 'public';
|
||||
$status->visibility = 'public';
|
||||
$status->save();
|
||||
break;
|
||||
|
||||
default:
|
||||
# code...
|
||||
break;
|
||||
}
|
||||
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->save();
|
||||
|
||||
return redirect('/i/admin/reports/appeals');
|
||||
}
|
||||
|
||||
public function profiles(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
@ -316,173 +434,4 @@ class AdminController extends Controller
|
|||
$redirect = $news->published_at ? $news->permalink() : $news->editUrl();
|
||||
return redirect($redirect);
|
||||
}
|
||||
|
||||
public function diagnosticsHome(Request $request)
|
||||
{
|
||||
return view('admin.diagnostics.home');
|
||||
}
|
||||
|
||||
public function diagnosticsDecrypt(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'payload' => 'required'
|
||||
]);
|
||||
|
||||
$key = 'exception_report:';
|
||||
$decrypted = decrypt($request->input('payload'));
|
||||
|
||||
if(!starts_with($decrypted, $key)) {
|
||||
abort(403, 'Can only decrypt error diagnostics');
|
||||
}
|
||||
|
||||
$res = [
|
||||
'decrypted' => substr($decrypted, strlen($key))
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function stories(Request $request)
|
||||
{
|
||||
$stories = Story::with('profile')->latest()->paginate(10);
|
||||
$stats = StoryService::adminStats();
|
||||
return view('admin.stories.home', compact('stories', 'stats'));
|
||||
}
|
||||
|
||||
public function customEmojiHome(Request $request)
|
||||
{
|
||||
if(!config('federation.custom_emoji.enabled')) {
|
||||
return view('admin.custom-emoji.not-enabled');
|
||||
}
|
||||
$this->validate($request, [
|
||||
'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search'
|
||||
]);
|
||||
|
||||
if($request->has('cc')) {
|
||||
Cache::forget('pf:admin:custom_emoji:stats');
|
||||
Cache::forget('pf:custom_emoji');
|
||||
return redirect(route('admin.custom-emoji'));
|
||||
}
|
||||
|
||||
$sort = $request->input('sort') ?? 'all';
|
||||
|
||||
if($sort == 'search' && empty($request->input('q'))) {
|
||||
return redirect(route('admin.custom-emoji'));
|
||||
}
|
||||
|
||||
$pg = config('database.default') == 'pgsql';
|
||||
|
||||
$emojis = CustomEmoji::when($sort, function($query, $sort) use($request, $pg) {
|
||||
if($sort == 'all') {
|
||||
if($pg) {
|
||||
return $query->latest();
|
||||
} else {
|
||||
return $query->groupBy('shortcode')->latest();
|
||||
}
|
||||
} else if($sort == 'local') {
|
||||
return $query->latest()->where('domain', '=', config('pixelfed.domain.app'));
|
||||
} else if($sort == 'remote') {
|
||||
return $query->latest()->where('domain', '!=', config('pixelfed.domain.app'));
|
||||
} else if($sort == 'duplicates') {
|
||||
return $query->latest()->groupBy('shortcode')->havingRaw('count(*) > 1');
|
||||
} else if($sort == 'disabled') {
|
||||
return $query->latest()->whereDisabled(true);
|
||||
} else if($sort == 'search') {
|
||||
$q = $query
|
||||
->latest()
|
||||
->where('shortcode', 'like', '%' . $request->input('q') . '%')
|
||||
->orWhere('domain', 'like', '%' . $request->input('q') . '%');
|
||||
if(!$request->has('dups')) {
|
||||
$q = $q->groupBy('shortcode');
|
||||
}
|
||||
return $q;
|
||||
}
|
||||
})
|
||||
->simplePaginate(10)
|
||||
->withQueryString();
|
||||
|
||||
$stats = Cache::remember('pf:admin:custom_emoji:stats', 43200, function() use($pg) {
|
||||
$res = [
|
||||
'total' => CustomEmoji::count(),
|
||||
'active' => CustomEmoji::whereDisabled(false)->count(),
|
||||
'remote' => CustomEmoji::where('domain', '!=', config('pixelfed.domain.app'))->count(),
|
||||
];
|
||||
|
||||
if($pg) {
|
||||
$res['duplicate'] = CustomEmoji::select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1')->count();
|
||||
} else {
|
||||
$res['duplicate'] = CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count();
|
||||
}
|
||||
|
||||
return $res;
|
||||
});
|
||||
|
||||
return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats'));
|
||||
}
|
||||
|
||||
public function customEmojiToggleActive(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::findOrFail($id);
|
||||
$emoji->disabled = !$emoji->disabled;
|
||||
$emoji->save();
|
||||
$key = CustomEmoji::CACHE_KEY . str_replace(':', '', $emoji->shortcode);
|
||||
Cache::forget($key);
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function customEmojiAdd(Request $request)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
return view('admin.custom-emoji.add');
|
||||
}
|
||||
|
||||
public function customEmojiStore(Request $request)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
$this->validate($request, [
|
||||
'shortcode' => [
|
||||
'required',
|
||||
'min:3',
|
||||
'max:80',
|
||||
'starts_with::',
|
||||
'ends_with::',
|
||||
Rule::unique('custom_emoji')->where(function ($query) use($request) {
|
||||
return $query->whereDomain(config('pixelfed.domain.app'))
|
||||
->whereShortcode($request->input('shortcode'));
|
||||
})
|
||||
],
|
||||
'emoji' => 'required|file|mimes:jpg,png|max:' . (config('federation.custom_emoji.max_size') / 1000)
|
||||
]);
|
||||
|
||||
$emoji = new CustomEmoji;
|
||||
$emoji->shortcode = $request->input('shortcode');
|
||||
$emoji->domain = config('pixelfed.domain.app');
|
||||
$emoji->save();
|
||||
|
||||
$fileName = $emoji->id . '.' . $request->emoji->extension();
|
||||
$request->emoji->storeAs('public/emoji', $fileName);
|
||||
$emoji->media_path = 'emoji/' . $fileName;
|
||||
$emoji->save();
|
||||
Cache::forget('pf:custom_emoji');
|
||||
return redirect(route('admin.custom-emoji'));
|
||||
}
|
||||
|
||||
public function customEmojiDelete(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::findOrFail($id);
|
||||
Storage::delete("public/{$emoji->media_path}");
|
||||
Cache::forget('pf:custom_emoji');
|
||||
$emoji->delete();
|
||||
return redirect(route('admin.custom-emoji'));
|
||||
}
|
||||
|
||||
public function customEmojiShowDuplicates(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
|
||||
$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
|
||||
return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,8 +15,7 @@ use App\{
|
|||
Media,
|
||||
Notification,
|
||||
Profile,
|
||||
Status,
|
||||
StatusArchived
|
||||
Status
|
||||
};
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
|
@ -37,11 +36,9 @@ use App\Jobs\VideoPipeline\{
|
|||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Services\AccountService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class BaseApiController extends Controller
|
||||
{
|
||||
|
@ -57,40 +54,26 @@ class BaseApiController extends Controller
|
|||
public function notifications(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$limit = $request->input('limit', 20);
|
||||
|
||||
$since = $request->input('since_id');
|
||||
$min = $request->input('min_id');
|
||||
$max = $request->input('max_id');
|
||||
|
||||
if(!$since && !$min && !$max) {
|
||||
$min = 1;
|
||||
}
|
||||
|
||||
$maxId = null;
|
||||
$minId = null;
|
||||
|
||||
if($max) {
|
||||
$res = NotificationService::getMax($pid, $max, $limit);
|
||||
$ids = NotificationService::getRankedMaxId($pid, $max, $limit);
|
||||
if(!empty($ids)) {
|
||||
$maxId = max($ids);
|
||||
$minId = min($ids);
|
||||
}
|
||||
$pg = $request->input('pg');
|
||||
if($pg == true) {
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($pid)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->latest()
|
||||
->simplePaginate(10);
|
||||
$resource = new Fractal\Resource\Collection($notifications, new NotificationTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
} else {
|
||||
$res = NotificationService::getMin($pid, $min ?? $since, $limit);
|
||||
$ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
|
||||
if(!empty($ids)) {
|
||||
$maxId = max($ids);
|
||||
$minId = min($ids);
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
|
||||
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
|
||||
NotificationService::warmCache($pid, 400, true);
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|integer|min:1|max:10',
|
||||
'limit' => 'nullable|integer|min:1|max:40'
|
||||
]);
|
||||
$limit = $request->input('limit') ?? 10;
|
||||
$page = $request->input('page') ?? 1;
|
||||
$end = (int) $page * $limit;
|
||||
$start = (int) $end - $limit;
|
||||
$res = NotificationService::get($pid, $start, $end);
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
|
@ -200,6 +183,7 @@ class BaseApiController extends Controller
|
|||
$avatar = Avatar::whereProfileId($profile->id)->firstOrFail();
|
||||
$opath = $avatar->media_path;
|
||||
$avatar->media_path = "$public/$name";
|
||||
$avatar->thumb_path = null;
|
||||
$avatar->change_count = ++$avatar->change_count;
|
||||
$avatar->last_processed_at = null;
|
||||
$avatar->save();
|
||||
|
@ -217,17 +201,117 @@ class BaseApiController extends Controller
|
|||
|
||||
public function showTempMedia(Request $request, $profileId, $mediaId, $timestamp)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
abort_if(!$request->user(), 403);
|
||||
abort_if(!$request->hasValidSignature(), 404);
|
||||
abort_if(Auth::user()->profile_id != $profileId, 404);
|
||||
$media = Media::whereProfileId(Auth::user()->profile_id)->findOrFail($mediaId);
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
return response()->file($path);
|
||||
}
|
||||
|
||||
public function uploadMedia(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
abort_if(!$request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'file.*' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:' . config('pixelfed.media_types'),
|
||||
'max:' . config('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24'
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
if(config('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$limit = (int) config('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), $mimes) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->store($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = null;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $photo->getMimeType();
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
$media->save();
|
||||
|
||||
$url = URL::temporarySignedRoute(
|
||||
'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id, 'timestamp' => time()]
|
||||
);
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
ImageOptimize::dispatch($media);
|
||||
break;
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media);
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $url;
|
||||
$res['url'] = $url;
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function deleteMedia(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
abort_if(!$request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1|exists:media,id'
|
||||
]);
|
||||
|
||||
$media = Media::whereNull('status_id')
|
||||
->whereUserId(Auth::id())
|
||||
->findOrFail($request->input('id'));
|
||||
|
||||
Storage::delete($media->media_path);
|
||||
Storage::delete($media->thumbnail_path);
|
||||
|
||||
$media->forceDelete();
|
||||
|
||||
return response()->json([
|
||||
'msg' => 'Successfully deleted',
|
||||
'code' => 200
|
||||
]);
|
||||
}
|
||||
|
||||
public function verifyCredentials(Request $request)
|
||||
|
@ -236,9 +320,17 @@ class BaseApiController extends Controller
|
|||
abort_if(!$user, 403);
|
||||
if($user->status != null) {
|
||||
Auth::logout();
|
||||
abort(403);
|
||||
return redirect('/login');
|
||||
}
|
||||
$res = AccountService::get($user->profile_id);
|
||||
$key = 'user:last_active_at:id:'.$user->id;
|
||||
$ttl = now()->addMinutes(5);
|
||||
Cache::remember($key, $ttl, function() use($user) {
|
||||
$user->last_active_at = now();
|
||||
$user->save();
|
||||
return;
|
||||
});
|
||||
$resource = new Fractal\Resource\Item($user->profile, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
|
@ -259,98 +351,26 @@ class BaseApiController extends Controller
|
|||
|
||||
public function accountLikes(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$this->validate($request, [
|
||||
'page' => 'sometimes|int|min:1|max:20',
|
||||
'limit' => 'sometimes|int|min:1|max:10'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$limit = $request->input('limit', 10);
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$res = \DB::table('likes')
|
||||
->whereProfileId($user->profile_id)
|
||||
$limit = 10;
|
||||
$page = (int) $request->input('page', 1);
|
||||
|
||||
if($page > 20) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$favourites = $user->profile->likes()
|
||||
->latest()
|
||||
->simplePaginate($limit)
|
||||
->map(function($id) {
|
||||
$status = StatusService::get($id->status_id, false);
|
||||
$status['favourited'] = true;
|
||||
return $status;
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
return response()->json($res);
|
||||
}
|
||||
->pluck('status_id');
|
||||
|
||||
public function archive(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$statuses = Status::find($favourites)->reverse();
|
||||
|
||||
$status = Status::whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereProfileId($request->user()->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
if($status->scope === 'archived') {
|
||||
return [200];
|
||||
}
|
||||
|
||||
$archive = new StatusArchived;
|
||||
$archive->status_id = $status->id;
|
||||
$archive->profile_id = $status->profile_id;
|
||||
$archive->original_scope = $status->scope;
|
||||
$archive->save();
|
||||
|
||||
$status->scope = 'archived';
|
||||
$status->visibility = 'draft';
|
||||
$status->save();
|
||||
StatusService::del($status->id, true);
|
||||
AccountService::syncPostCount($status->profile_id);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function unarchive(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$status = Status::whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereProfileId($request->user()->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
if($status->scope !== 'archived') {
|
||||
return [200];
|
||||
}
|
||||
|
||||
$archive = StatusArchived::whereStatusId($status->id)
|
||||
->whereProfileId($status->profile_id)
|
||||
->firstOrFail();
|
||||
|
||||
$status->scope = $archive->original_scope;
|
||||
$status->visibility = $archive->original_scope;
|
||||
$status->save();
|
||||
$archive->delete();
|
||||
StatusService::del($status->id, true);
|
||||
AccountService::syncPostCount($status->profile_id);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function archivedPosts(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$statuses = Status::whereProfileId($request->user()->profile_id)
|
||||
->whereScope('archived')
|
||||
->orderByDesc('id')
|
||||
->simplePaginate(10);
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class InstanceApiController extends Controller {
|
|||
|
||||
$res = [
|
||||
'uri' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'title' => config('app.name'),
|
||||
'description' => '',
|
||||
'version' => config('pixelfed.version'),
|
||||
'urls' => [],
|
||||
|
|
|
@ -27,10 +27,7 @@ class LoginController extends Controller
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/i/web';
|
||||
|
||||
protected $maxAttempts = 5;
|
||||
protected $decayMinutes = 60;
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
|
|
@ -32,7 +32,7 @@ class RegisterController extends Controller
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/i/web';
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
@ -44,13 +44,6 @@ class RegisterController extends Controller
|
|||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
public function getRegisterToken()
|
||||
{
|
||||
return \Cache::remember('pf:register:rt', 900, function() {
|
||||
return str_random(40);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
|
@ -83,7 +76,7 @@ class RegisterController extends Controller
|
|||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[0])) {
|
||||
if (!ctype_alpha($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
|
@ -117,18 +110,8 @@ class RegisterController extends Controller
|
|||
},
|
||||
];
|
||||
|
||||
$rt = [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if($value !== $this->getRegisterToken()) {
|
||||
return $fail('Something went wrong');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
$rules = [
|
||||
'agecheck' => 'required|accepted',
|
||||
'rt' => $rt,
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'username' => $usernameRules,
|
||||
'email' => $emailRules,
|
||||
|
@ -171,7 +154,7 @@ class RegisterController extends Controller
|
|||
*/
|
||||
public function showRegistrationForm()
|
||||
{
|
||||
if(config_cache('pixelfed.open_registration')) {
|
||||
if(config('pixelfed.open_registration')) {
|
||||
$limit = config('pixelfed.max_users');
|
||||
if($limit) {
|
||||
abort_if($limit <= User::count(), 404);
|
||||
|
@ -192,12 +175,12 @@ class RegisterController extends Controller
|
|||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
abort_if(config_cache('pixelfed.open_registration') == false, 400);
|
||||
abort_if(config('pixelfed.open_registration') == false, 400);
|
||||
|
||||
$count = User::count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
|
||||
if(false == config_cache('pixelfed.open_registration') || $limit && $limit <= $count) {
|
||||
if(false == config('pixelfed.open_registration') || $limit && $limit <= $count) {
|
||||
return abort(403);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ class AvatarController extends Controller
|
|||
$avatar = Avatar::firstOrNew(['profile_id' => $profile->id]);
|
||||
$currentAvatar = $avatar->recentlyCreated ? null : storage_path('app/'.$profile->avatar->media_path);
|
||||
$avatar->media_path = "$public/$name";
|
||||
$avatar->thumb_path = null;
|
||||
$avatar->change_count = ++$avatar->change_count;
|
||||
$avatar->last_processed_at = null;
|
||||
$avatar->save();
|
||||
|
@ -120,7 +121,10 @@ class AvatarController extends Controller
|
|||
$avatar = $profile->avatar;
|
||||
|
||||
if( $avatar->media_path == 'public/avatars/default.png' ||
|
||||
$avatar->media_path == 'public/avatars/default.jpg'
|
||||
$avatar->thumb_path == 'public/avatars/default.png' ||
|
||||
$avatar->media_path == 'public/avatars/default.jpg' ||
|
||||
$avatar->thumb_path == 'public/avatars/default.jpg'
|
||||
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -129,7 +133,12 @@ class AvatarController extends Controller
|
|||
@unlink(storage_path('app/' . $avatar->media_path));
|
||||
}
|
||||
|
||||
if(is_file(storage_path('app/' . $avatar->thumb_path))) {
|
||||
@unlink(storage_path('app/' . $avatar->thumb_path));
|
||||
}
|
||||
|
||||
$avatar->media_path = 'public/avatars/default.jpg';
|
||||
$avatar->thumb_path = 'public/avatars/default.jpg';
|
||||
$avatar->change_count = $avatar->change_count + 1;
|
||||
$avatar->save();
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ use App\Bookmark;
|
|||
use App\Status;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\BookmarkService;
|
||||
|
||||
class BookmarkController extends Controller
|
||||
{
|
||||
|
@ -29,10 +28,7 @@ class BookmarkController extends Controller
|
|||
);
|
||||
|
||||
if (!$bookmark->wasRecentlyCreated) {
|
||||
BookmarkService::del($profile->id, $status->id);
|
||||
$bookmark->delete();
|
||||
} else {
|
||||
BookmarkService::add($profile->id, $status->id);
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
|
|
|
@ -18,7 +18,6 @@ use League\Fractal;
|
|||
use App\Transformer\Api\StatusTransformer;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
|
@ -73,11 +72,13 @@ class CommentController extends Controller
|
|||
$reply->visibility = $scope;
|
||||
$reply->save();
|
||||
|
||||
$status->reply_count++;
|
||||
$status->save();
|
||||
|
||||
return $reply;
|
||||
});
|
||||
|
||||
StatusService::del($status->id);
|
||||
NewStatusPipeline::dispatch($reply);
|
||||
NewStatusPipeline::dispatch($reply, false);
|
||||
CommentPipeline::dispatch($status, $reply);
|
||||
|
||||
if ($request->ajax()) {
|
||||
|
|
|
@ -1,735 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Cache, Storage, URL;
|
||||
use Carbon\Carbon;
|
||||
use App\{
|
||||
Avatar,
|
||||
Hashtag,
|
||||
Like,
|
||||
Media,
|
||||
MediaTag,
|
||||
Notification,
|
||||
Profile,
|
||||
Place,
|
||||
Status,
|
||||
UserFilter,
|
||||
UserSetting
|
||||
};
|
||||
use App\Models\Poll;
|
||||
use App\Transformer\Api\{
|
||||
MediaTransformer,
|
||||
MediaDraftTransformer,
|
||||
StatusTransformer,
|
||||
StatusStatelessTransformer
|
||||
};
|
||||
use League\Fractal;
|
||||
use App\Util\Media\Filter;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Jobs\AvatarPipeline\AvatarOptimize;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Jobs\VideoPipeline\{
|
||||
VideoOptimize,
|
||||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Services\AccountService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\StatusService;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use App\Util\Media\License;
|
||||
|
||||
class ComposeController extends Controller
|
||||
{
|
||||
protected $fractal;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
{
|
||||
return view('status.compose');
|
||||
}
|
||||
|
||||
public function mediaUpload(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'file.*' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:' . 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'
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
|
||||
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
|
||||
|
||||
return $dailyLimit >= 250;
|
||||
});
|
||||
|
||||
abort_if($limitReached == true, 429);
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
|
||||
abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->store($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$mime = $photo->getMimeType();
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = null;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $mime;
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
$media->version = 3;
|
||||
$media->save();
|
||||
|
||||
$preview_url = $media->url() . '?v=' . time();
|
||||
$url = $media->url() . '?v=' . time();
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
case 'image/webp':
|
||||
ImageOptimize::dispatch($media);
|
||||
break;
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media);
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $preview_url;
|
||||
$res['url'] = $url;
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function mediaUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-updates:' . $user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
|
||||
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
|
||||
|
||||
return $dailyLimit >= 500;
|
||||
});
|
||||
|
||||
abort_if($limitReached == true, 429);
|
||||
|
||||
$photo = $request->file('file');
|
||||
$id = $request->input('id');
|
||||
|
||||
$media = Media::whereUserId($user->id)
|
||||
->whereProfileId($user->profile_id)
|
||||
->whereNull('status_id')
|
||||
->findOrFail($id);
|
||||
|
||||
$media->save();
|
||||
|
||||
$fragments = explode('/', $media->media_path);
|
||||
$name = last($fragments);
|
||||
array_pop($fragments);
|
||||
$dir = implode('/', $fragments);
|
||||
$path = $photo->storeAs($dir, $name);
|
||||
$res = [
|
||||
'url' => $media->url() . '?v=' . time()
|
||||
];
|
||||
ImageOptimize::dispatch($media);
|
||||
Cache::forget($limitKey);
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function mediaDelete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1|exists:media,id'
|
||||
]);
|
||||
|
||||
$media = Media::whereNull('status_id')
|
||||
->whereUserId(Auth::id())
|
||||
->findOrFail($request->input('id'));
|
||||
|
||||
MediaStorageService::delete($media, true);
|
||||
|
||||
$media->forceDelete();
|
||||
|
||||
return response()->json([
|
||||
'msg' => 'Successfully deleted',
|
||||
'code' => 200
|
||||
]);
|
||||
}
|
||||
|
||||
public function searchTag(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:50'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
if(Str::of($q)->startsWith('@')) {
|
||||
if(strlen($q) < 3) {
|
||||
return [];
|
||||
}
|
||||
$q = mb_substr($q, 1);
|
||||
}
|
||||
|
||||
$blocked = UserFilter::whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->whereFilterableId($request->user()->profile_id)
|
||||
->pluck('user_id');
|
||||
|
||||
$blocked->push($request->user()->profile_id);
|
||||
|
||||
$results = Profile::select('id','domain','username')
|
||||
->whereNotIn('id', $blocked)
|
||||
->whereNull('domain')
|
||||
->where('username','like','%'.$q.'%')
|
||||
->limit(15)
|
||||
->get()
|
||||
->map(function($r) {
|
||||
return [
|
||||
'id' => (string) $r->id,
|
||||
'name' => $r->username,
|
||||
'privacy' => true,
|
||||
'avatar' => $r->avatarUrl()
|
||||
];
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function searchUntag(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'status_id' => 'required',
|
||||
'profile_id' => 'required'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$status_id = $request->input('status_id');
|
||||
$profile_id = (int) $request->input('profile_id');
|
||||
|
||||
abort_if((int) $user->profile_id !== $profile_id, 400);
|
||||
|
||||
$tag = MediaTag::whereStatusId($status_id)
|
||||
->whereProfileId($profile_id)
|
||||
->first();
|
||||
|
||||
if(!$tag) {
|
||||
return [];
|
||||
}
|
||||
Notification::whereItemType('App\MediaTag')
|
||||
->whereItemId($tag->id)
|
||||
->whereProfileId($profile_id)
|
||||
->whereAction('tagged')
|
||||
->delete();
|
||||
|
||||
MediaTagService::untag($status_id, $profile_id);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function searchLocation(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;
|
||||
}
|
||||
|
||||
public function searchMentionAutocomplete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:2|max:50'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
if(Str::of($q)->startsWith('@')) {
|
||||
if(strlen($q) < 3) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$blocked = UserFilter::whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->whereFilterableId($request->user()->profile_id)
|
||||
->pluck('user_id');
|
||||
|
||||
$blocked->push($request->user()->profile_id);
|
||||
|
||||
$results = Profile::select('id','domain','username')
|
||||
->whereNotIn('id', $blocked)
|
||||
->where('username','like','%'.$q.'%')
|
||||
->groupBy('domain')
|
||||
->limit(15)
|
||||
->get()
|
||||
->map(function($profile) {
|
||||
$username = $profile->domain ? substr($profile->username, 1) : $profile->username;
|
||||
return [
|
||||
'key' => '@' . str_limit($username, 30),
|
||||
'value' => $username,
|
||||
];
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function searchHashtagAutocomplete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:2|max:50'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
$results = Hashtag::select('slug')
|
||||
->where('slug', 'like', '%'.$q.'%')
|
||||
->whereIsNsfw(false)
|
||||
->whereIsBanned(false)
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(function($tag) {
|
||||
return [
|
||||
'key' => '#' . $tag->slug,
|
||||
'value' => $tag->slug
|
||||
];
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'media.*' => 'required',
|
||||
'media.*.id' => 'required|integer|min:1',
|
||||
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
|
||||
'media.*.license' => 'nullable|string|max:140',
|
||||
'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
|
||||
'place' => 'nullable',
|
||||
'comments_disabled' => 'nullable',
|
||||
'tagged' => 'nullable',
|
||||
'license' => 'nullable|integer|min:1|max:16'
|
||||
// 'optimize_media' => 'nullable'
|
||||
]);
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
$blockedKeywords = config('costar.keyword.block');
|
||||
if($blockedKeywords !== null && $request->caption) {
|
||||
$keywords = config('costar.keyword.block');
|
||||
foreach($keywords as $kw) {
|
||||
if(Str::contains($request->caption, $kw) == true) {
|
||||
abort(400, 'Invalid object');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
$limitKey = 'compose:rate-limit:store:' . $user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
|
||||
$dailyLimit = Status::whereProfileId($user->profile_id)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->where('created_at', '>', now()->subDays(1))
|
||||
->count();
|
||||
|
||||
return $dailyLimit >= 100;
|
||||
});
|
||||
|
||||
abort_if($limitReached == true, 429);
|
||||
|
||||
$license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null;
|
||||
|
||||
$visibility = $request->input('visibility');
|
||||
$medias = $request->input('media');
|
||||
$attachments = [];
|
||||
$status = new Status;
|
||||
$mimes = [];
|
||||
$place = $request->input('place');
|
||||
$cw = $request->input('cw');
|
||||
$tagged = $request->input('tagged');
|
||||
$optimize_media = (bool) $request->input('optimize_media');
|
||||
|
||||
foreach($medias as $k => $media) {
|
||||
if($k + 1 > config_cache('pixelfed.max_album_length')) {
|
||||
continue;
|
||||
}
|
||||
$m = Media::findOrFail($media['id']);
|
||||
if($m->profile_id !== $profile->id || $m->status_id) {
|
||||
abort(403, 'Invalid media id');
|
||||
}
|
||||
$m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
|
||||
$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;
|
||||
}
|
||||
$m->save();
|
||||
$attachments[] = $m;
|
||||
array_push($mimes, $m->mime);
|
||||
}
|
||||
|
||||
abort_if(empty($attachments), 422);
|
||||
|
||||
$mediaType = StatusController::mimeTypeCheck($mimes);
|
||||
|
||||
if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
|
||||
abort(400, __('exception.compose.invalid.album'));
|
||||
}
|
||||
|
||||
if($place && is_array($place)) {
|
||||
$status->place_id = $place['id'];
|
||||
}
|
||||
|
||||
if($request->filled('comments_disabled')) {
|
||||
$status->comments_disabled = (bool) $request->input('comments_disabled');
|
||||
}
|
||||
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$status->scope = 'draft';
|
||||
$status->profile_id = $profile->id;
|
||||
$status->save();
|
||||
|
||||
foreach($attachments as $media) {
|
||||
$media->status_id = $status->id;
|
||||
$media->save();
|
||||
}
|
||||
|
||||
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
|
||||
$visibility = $profile->is_private ? 'private' : $visibility;
|
||||
$cw = $profile->cw == true ? true : $cw;
|
||||
$status->is_nsfw = $cw;
|
||||
$status->visibility = $visibility;
|
||||
$status->scope = $visibility;
|
||||
$status->type = $mediaType;
|
||||
$status->save();
|
||||
|
||||
foreach($tagged as $tg) {
|
||||
$mt = new MediaTag;
|
||||
$mt->status_id = $status->id;
|
||||
$mt->media_id = $status->media->first()->id;
|
||||
$mt->profile_id = $tg['id'];
|
||||
$mt->tagged_username = $tg['name'];
|
||||
$mt->is_public = true;
|
||||
$mt->metadata = json_encode([
|
||||
'_v' => 1,
|
||||
]);
|
||||
$mt->save();
|
||||
MediaTagService::set($mt->status_id, $mt->profile_id);
|
||||
MediaTagService::sendNotification($mt);
|
||||
}
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
Cache::forget('user:account:id:'.$profile->user_id);
|
||||
Cache::forget('_api:statuses:recent_9:'.$profile->id);
|
||||
Cache::forget('profile:status_count:'.$profile->id);
|
||||
Cache::forget('status:transformer:media:attachments:'.$status->id);
|
||||
Cache::forget($user->storageUsedKey());
|
||||
Cache::forget('profile:embed:' . $status->profile_id);
|
||||
Cache::forget($limitKey);
|
||||
|
||||
return $status->url();
|
||||
}
|
||||
|
||||
public function storeText(Request $request)
|
||||
{
|
||||
abort_unless(config('exp.top'), 404);
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
|
||||
'place' => 'nullable',
|
||||
'comments_disabled' => 'nullable',
|
||||
'tagged' => 'nullable',
|
||||
]);
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
$blockedKeywords = config('costar.keyword.block');
|
||||
if($blockedKeywords !== null && $request->caption) {
|
||||
$keywords = config('costar.keyword.block');
|
||||
foreach($keywords as $kw) {
|
||||
if(Str::contains($request->caption, $kw) == true) {
|
||||
abort(400, 'Invalid object');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$visibility = $request->input('visibility');
|
||||
$status = new Status;
|
||||
$place = $request->input('place');
|
||||
$cw = $request->input('cw');
|
||||
$tagged = $request->input('tagged');
|
||||
|
||||
if($place && is_array($place)) {
|
||||
$status->place_id = $place['id'];
|
||||
}
|
||||
|
||||
if($request->filled('comments_disabled')) {
|
||||
$status->comments_disabled = (bool) $request->input('comments_disabled');
|
||||
}
|
||||
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->profile_id = $profile->id;
|
||||
$entities = Extractor::create()->extract($status->caption);
|
||||
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
|
||||
$cw = $profile->cw == true ? true : $cw;
|
||||
$status->is_nsfw = $cw;
|
||||
$status->visibility = $visibility;
|
||||
$status->scope = $visibility;
|
||||
$status->type = 'text';
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$status->entities = json_encode(array_merge([
|
||||
'timg' => [
|
||||
'version' => 0,
|
||||
'bg_id' => 1,
|
||||
'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
|
||||
'length' => strlen($status->caption),
|
||||
]
|
||||
], $entities), JSON_UNESCAPED_SLASHES);
|
||||
$status->save();
|
||||
|
||||
foreach($tagged as $tg) {
|
||||
$mt = new MediaTag;
|
||||
$mt->status_id = $status->id;
|
||||
$mt->media_id = $status->media->first()->id;
|
||||
$mt->profile_id = $tg['id'];
|
||||
$mt->tagged_username = $tg['name'];
|
||||
$mt->is_public = true;
|
||||
$mt->metadata = json_encode([
|
||||
'_v' => 1,
|
||||
]);
|
||||
$mt->save();
|
||||
MediaTagService::set($mt->status_id, $mt->profile_id);
|
||||
MediaTagService::sendNotification($mt);
|
||||
}
|
||||
|
||||
|
||||
Cache::forget('user:account:id:'.$profile->user_id);
|
||||
Cache::forget('_api:statuses:recent_9:'.$profile->id);
|
||||
Cache::forget('profile:status_count:'.$profile->id);
|
||||
|
||||
return $status->url();
|
||||
}
|
||||
|
||||
public function mediaProcessingCheck(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$media = Media::whereUserId($request->user()->id)
|
||||
->whereNull('status_id')
|
||||
->findOrFail($request->input('id'));
|
||||
|
||||
if(config('pixelfed.media_fast_process')) {
|
||||
return [
|
||||
'finished' => true
|
||||
];
|
||||
}
|
||||
|
||||
$finished = false;
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
case 'video/mp4':
|
||||
$finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
|
||||
break;
|
||||
|
||||
default:
|
||||
# code...
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
'finished' => $finished
|
||||
];
|
||||
}
|
||||
|
||||
public function composeSettings(Request $request)
|
||||
{
|
||||
$uid = $request->user()->id;
|
||||
$default = [
|
||||
'default_license' => 1,
|
||||
'media_descriptions' => false,
|
||||
'max_altext_length' => config_cache('pixelfed.max_altext_length')
|
||||
];
|
||||
$settings = AccountService::settings($uid);
|
||||
if(isset($settings['other']) && isset($settings['other']['scope'])) {
|
||||
$s = $settings['compose_settings'];
|
||||
$s['default_scope'] = $settings['other']['scope'];
|
||||
$settings['compose_settings'] = $s;
|
||||
}
|
||||
|
||||
return array_merge($default, $settings['compose_settings']);
|
||||
}
|
||||
|
||||
public function createPoll(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private',
|
||||
'comments_disabled' => 'nullable',
|
||||
'expiry' => 'required|in:60,360,1440,10080',
|
||||
'pollOptions' => 'required|array|min:1|max:4'
|
||||
]);
|
||||
|
||||
abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
|
||||
|
||||
abort_if(Status::whereType('poll')
|
||||
->whereProfileId($request->user()->profile_id)
|
||||
->whereCaption($request->input('caption'))
|
||||
->where('created_at', '>', now()->subDays(2))
|
||||
->exists()
|
||||
, 422, 'Duplicate detected.');
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $request->user()->profile_id;
|
||||
$status->caption = $request->input('caption');
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$status->visibility = 'draft';
|
||||
$status->scope = 'draft';
|
||||
$status->type = 'poll';
|
||||
$status->local = true;
|
||||
$status->save();
|
||||
|
||||
$poll = new Poll;
|
||||
$poll->status_id = $status->id;
|
||||
$poll->profile_id = $status->profile_id;
|
||||
$poll->poll_options = $request->input('pollOptions');
|
||||
$poll->expires_at = now()->addMinutes($request->input('expiry'));
|
||||
$poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
|
||||
return 0;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
$status->visibility = $request->input('visibility');
|
||||
$status->scope = $request->input('visibility');
|
||||
$status->save();
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
|
||||
return ['url' => $status->url()];
|
||||
}
|
||||
}
|
|
@ -390,6 +390,7 @@ class DirectMessageController extends Controller
|
|||
$min_id = $request->input('min_id');
|
||||
|
||||
$r = Profile::findOrFail($pid);
|
||||
// $r = Profile::whereNull('domain')->findOrFail($pid);
|
||||
|
||||
if($min_id) {
|
||||
$res = DirectMessage::select('*')
|
||||
|
@ -499,8 +500,8 @@ class DirectMessageController extends Controller
|
|||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
'mimes:' . config('pixelfed.media_types'),
|
||||
'max:' . config('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
'to_id' => 'required'
|
||||
|
@ -521,18 +522,18 @@ class DirectMessageController extends Controller
|
|||
$hidden = false;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
if(config('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
$limit = (int) config('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
$mimes = explode(',', config('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
@ -589,15 +590,11 @@ class DirectMessageController extends Controller
|
|||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:2|max:50',
|
||||
'remote' => 'nullable',
|
||||
'remote' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
$r = $request->input('remote', false);
|
||||
|
||||
if($r && !Str::of($q)->contains('.')) {
|
||||
return [];
|
||||
}
|
||||
$r = $request->input('remote');
|
||||
|
||||
if($r && Helpers::validateUrl($q)) {
|
||||
Helpers::profileFetch($q);
|
||||
|
|
|
@ -24,7 +24,6 @@ use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
|||
use App\Services\StatusHashtagService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\UserFilterService;
|
||||
|
||||
class DiscoverController extends Controller
|
||||
{
|
||||
|
@ -38,7 +37,7 @@ class DiscoverController extends Controller
|
|||
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check() && config('instance.discover.public') == false, 403);
|
||||
abort_if(!Auth::check(), 403);
|
||||
return view('discover.home');
|
||||
}
|
||||
|
||||
|
@ -55,21 +54,63 @@ class DiscoverController extends Controller
|
|||
|
||||
public function showCategory(Request $request, $slug)
|
||||
{
|
||||
abort(404);
|
||||
abort_if(!Auth::check(), 403);
|
||||
|
||||
$tag = DiscoverCategory::whereActive(true)
|
||||
->whereSlug($slug)
|
||||
->firstOrFail();
|
||||
|
||||
$posts = Cache::remember('discover:category-'.$tag->id.':posts', now()->addMinutes(15), function() use ($tag) {
|
||||
$tagids = $tag->hashtags->pluck('id')->toArray();
|
||||
$sids = StatusHashtag::whereIn('hashtag_id', $tagids)->orderByDesc('status_id')->take(500)->pluck('status_id')->toArray();
|
||||
$posts = Status::whereScope('public')->whereIn('id', $sids)->whereNull('uri')->whereType('photo')->whereNull('in_reply_to_id')->whereNull('reblog_of_id')->orderByDesc('created_at')->take(39)->get();
|
||||
return $posts;
|
||||
});
|
||||
$tag->posts_count = Cache::remember('discover:category-'.$tag->id.':posts_count', now()->addMinutes(30), function() use ($tag) {
|
||||
return $tag->posts()->whereScope('public')->count();
|
||||
});
|
||||
return view('discover.tags.category', compact('tag', 'posts'));
|
||||
}
|
||||
|
||||
public function showLoops(Request $request)
|
||||
{
|
||||
abort(404);
|
||||
if(config('exp.loops') != true) {
|
||||
return redirect('/');
|
||||
}
|
||||
return view('discover.loops.home');
|
||||
}
|
||||
|
||||
public function loopsApi(Request $request)
|
||||
{
|
||||
abort(404);
|
||||
abort_if(!config('exp.loops'), 403);
|
||||
|
||||
// todo proper pagination, maybe LoopService
|
||||
$res = Cache::remember('discover:loops:recent', now()->addHours(6), function() {
|
||||
$loops = Status::whereType('video')
|
||||
->whereNull('uri')
|
||||
->whereScope('public')
|
||||
->latest()
|
||||
->take(18)
|
||||
->get();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($loops, new StatusStatelessTransformer());
|
||||
return $this->fractal->createData($resource)->toArray();
|
||||
});
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function loopWatch(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
abort_if(!config('exp.loops'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'integer|min:1'
|
||||
]);
|
||||
$id = $request->input('id');
|
||||
|
||||
// todo log loops
|
||||
|
||||
return response()->json(200);
|
||||
}
|
||||
|
||||
|
@ -88,51 +129,61 @@ class DiscoverController extends Controller
|
|||
$tag = $request->input('hashtag');
|
||||
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
if($page == 1) {
|
||||
$res['follows'] = HashtagFollow::whereUserId(Auth::id())
|
||||
->whereHashtagId($hashtag->id)
|
||||
->exists();
|
||||
}
|
||||
$res['hashtag'] = [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url()
|
||||
];
|
||||
$res['tags'] = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||
if($page == 1) {
|
||||
$res['follows'] = HashtagFollow::whereUserId(Auth::id())->whereHashtagId($hashtag->id)->exists();
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function profilesDirectory(Request $request)
|
||||
{
|
||||
return redirect('/')
|
||||
->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
|
||||
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
|
||||
return view('discover.profiles.home');
|
||||
}
|
||||
|
||||
public function profilesDirectoryApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'integer|max:10'
|
||||
]);
|
||||
|
||||
return ['error' => 'Temporarily unavailable.'];
|
||||
|
||||
$page = $request->input('page') ?? 1;
|
||||
$key = 'discover:profiles:page:' . $page;
|
||||
$ttl = now()->addHours(12);
|
||||
|
||||
$res = Cache::remember($key, $ttl, function() {
|
||||
$profiles = Profile::whereNull('domain')
|
||||
->whereNull('status')
|
||||
->whereIsPrivate(false)
|
||||
->has('statuses')
|
||||
->whereIsSuggestable(true)
|
||||
// ->inRandomOrder()
|
||||
->simplePaginate(8);
|
||||
$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
|
||||
return $this->fractal->createData($resource)->toArray();
|
||||
});
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function trendingApi(Request $request)
|
||||
{
|
||||
abort_if(config('instance.discover.public') == false && !Auth::check(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'range' => 'nullable|string|in:daily,monthly,yearly',
|
||||
'range' => 'nullable|string|in:daily,monthly'
|
||||
]);
|
||||
|
||||
$range = $request->input('range');
|
||||
$days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365);
|
||||
$ttls = [
|
||||
1 => 1500,
|
||||
31 => 14400,
|
||||
365 => 86400
|
||||
];
|
||||
$key = ':api:discover:trending:v2.12:range:' . $days;
|
||||
$range = $request->input('range') == 'monthly' ? 31 : 1;
|
||||
|
||||
$ids = Cache::remember($key, $ttls[$days], function() use($days) {
|
||||
$key = ':api:discover:trending:v2.8:range:' . $range;
|
||||
$ttl = now()->addMinutes(15);
|
||||
|
||||
$ids = Cache::remember($key, $ttl, function() use($range) {
|
||||
$days = $range == 1 ? 2 : 31;
|
||||
$min_id = SnowflakeService::byDate(now()->subDays($days));
|
||||
return DB::table('statuses')
|
||||
->select(
|
||||
return Status::select(
|
||||
'id',
|
||||
'scope',
|
||||
'type',
|
||||
|
@ -150,20 +201,13 @@ class DiscoverController extends Controller
|
|||
])
|
||||
->whereIsNsfw(false)
|
||||
->orderBy('likes_count','desc')
|
||||
->take(30)
|
||||
->take(15)
|
||||
->pluck('id');
|
||||
});
|
||||
|
||||
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
|
||||
|
||||
$res = $ids->map(function($s) {
|
||||
return StatusService::get($s);
|
||||
})->filter(function($s) use($filtered) {
|
||||
return
|
||||
$s &&
|
||||
!in_array($s['account']['id'], $filtered) &&
|
||||
isset($s['account']);
|
||||
})->values();
|
||||
});
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
@ -173,7 +217,7 @@ class DiscoverController extends Controller
|
|||
$res = StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
|
||||
->groupBy('hashtag_id')
|
||||
->orderBy('total','desc')
|
||||
->where('created_at', '>', now()->subDays(90))
|
||||
->where('created_at', '>', now()->subDays(4))
|
||||
->take(9)
|
||||
->get()
|
||||
->map(function($h) {
|
||||
|
@ -190,6 +234,22 @@ class DiscoverController extends Controller
|
|||
|
||||
public function trendingPlaces(Request $request)
|
||||
{
|
||||
return [];
|
||||
$res = Status::select('place_id',DB::raw('count(place_id) as total'))
|
||||
->whereNotNull('place_id')
|
||||
->where('created_at','>',now()->subDays(14))
|
||||
->groupBy('place_id')
|
||||
->orderBy('total')
|
||||
->limit(4)
|
||||
->get()
|
||||
->map(function($s){
|
||||
$p = $s->place;
|
||||
return [
|
||||
'name' => $p->name,
|
||||
'country' => $p->country,
|
||||
'url' => $p->url()
|
||||
];
|
||||
});
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\InboxPipeline\{
|
||||
DeleteWorker,
|
||||
InboxWorker,
|
||||
InboxValidator
|
||||
};
|
||||
|
@ -35,14 +34,14 @@ class FederationController extends Controller
|
|||
public function nodeinfoWellKnown()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
return response()->json(Nodeinfo::wellKnown())
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
|
||||
public function nodeinfo()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
return response()->json(Nodeinfo::get())
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
|
||||
|
@ -50,27 +49,21 @@ class FederationController extends Controller
|
|||
{
|
||||
abort_if(!config('federation.webfinger.enabled'), 400);
|
||||
|
||||
abort_if(!$request->has('resource') || !$request->filled('resource'), 400);
|
||||
abort_if(!$request->filled('resource'), 400);
|
||||
|
||||
$resource = $request->input('resource');
|
||||
$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');
|
||||
abort_if(strpos($resource, $domain) == false, 400);
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if(empty($parsed) || $parsed['domain'] !== $domain) {
|
||||
if($parsed['domain'] !== config('pixelfed.domain.app')) {
|
||||
abort(400);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
abort_if($profile->status != null, 400);
|
||||
if($profile->status != null) {
|
||||
return ProfileController::accountCheck($profile);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
Cache::put($key, $webfinger, 1209600);
|
||||
|
||||
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
|
||||
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
|
||||
|
@ -86,7 +79,7 @@ class FederationController extends Controller
|
|||
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.outbox'), 404);
|
||||
|
||||
$profile = Profile::whereNull('domain')
|
||||
|
@ -106,41 +99,29 @@ class FederationController extends Controller
|
|||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.inbox'), 404);
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
$obj = json_decode($payload, true, 8);
|
||||
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
} else {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
$obj = json_decode($payload, true, 8);
|
||||
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
} else {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
|
@ -163,7 +144,7 @@ class FederationController extends Controller
|
|||
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
|
|
|
@ -12,7 +12,6 @@ use Auth, Cache;
|
|||
use Illuminate\Http\Request;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\FollowerService;
|
||||
|
||||
class FollowerController extends Controller
|
||||
{
|
||||
|
@ -72,8 +71,6 @@ class FollowerController extends Controller
|
|||
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');
|
||||
|
@ -90,7 +87,6 @@ class FollowerController extends Controller
|
|||
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) {
|
||||
|
@ -105,7 +101,6 @@ class FollowerController extends Controller
|
|||
Follower::whereProfileId($user->id)
|
||||
->whereFollowingId($target->id)
|
||||
->delete();
|
||||
FollowerService::remove($user->id, $target->id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,6 @@ trait Instagram
|
|||
{
|
||||
public function instagram()
|
||||
{
|
||||
if(config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
return view('settings.import.instagram.home');
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,6 @@ trait Mastodon
|
|||
{
|
||||
public function mastodon()
|
||||
{
|
||||
if(config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
return view('settings.import.mastodon.home');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ class ImportController extends Controller
|
|||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
|
||||
if(config('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class InstanceActorController extends Controller
|
|||
{
|
||||
$res = Cache::rememberForever(InstanceActor::PROFILE_KEY, function() {
|
||||
$res = (new InstanceActor())->first()->getActor();
|
||||
return json_encode($res, JSON_UNESCAPED_SLASHES);
|
||||
return json_encode($res);
|
||||
});
|
||||
return response($res)->header('Content-Type', 'application/json');
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ namespace App\Http\Controllers;
|
|||
use Illuminate\Http\Request;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Bookmark,
|
||||
DirectMessage,
|
||||
DiscoverCategory,
|
||||
Hashtag,
|
||||
|
@ -17,11 +16,9 @@ use App\{
|
|||
Profile,
|
||||
StatusHashtag,
|
||||
Status,
|
||||
User,
|
||||
UserFilter,
|
||||
};
|
||||
use Auth,Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Carbon\Carbon;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
|
@ -31,7 +28,6 @@ use App\Transformer\Api\{
|
|||
};
|
||||
use App\Util\Media\Filter;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Jobs\ModPipeline\HandleSpammerPipeline;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
@ -39,11 +35,6 @@ use Illuminate\Support\Str;
|
|||
use App\Services\MediaTagService;
|
||||
use App\Services\ModLogService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\DiscoverService;
|
||||
use App\Services\BookmarkService;
|
||||
|
||||
class InternalApiController extends Controller
|
||||
{
|
||||
|
@ -70,21 +61,61 @@ class InternalApiController extends Controller
|
|||
|
||||
public function discoverPosts(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$filters = UserFilterService::filters($pid);
|
||||
$forYou = DiscoverService::getForYou();
|
||||
$posts = $forYou->take(50)->map(function($post) {
|
||||
return StatusService::get($post);
|
||||
$profile = Auth::user()->profile;
|
||||
$pid = $profile->id;
|
||||
$following = Cache::remember('feature:discover:following:'.$pid, now()->addMinutes(15), function() use ($pid) {
|
||||
return Follower::whereProfileId($pid)->pluck('following_id')->toArray();
|
||||
});
|
||||
$filters = Cache::remember("user:filter:list:$pid", now()->addMinutes(15), function() use($pid) {
|
||||
$private = Profile::whereIsPrivate(true)
|
||||
->orWhere('unlisted', true)
|
||||
->orWhere('status', '!=', null)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
$filters = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')
|
||||
->toArray();
|
||||
return array_merge($private, $filters);
|
||||
});
|
||||
$following = array_merge($following, $filters);
|
||||
|
||||
$sql = config('database.default') !== 'pgsql';
|
||||
|
||||
$posts = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'is_nsfw',
|
||||
'profile_id',
|
||||
'type',
|
||||
'uri',
|
||||
'created_at'
|
||||
)
|
||||
->whereNull('uri')
|
||||
->whereIn('type', ['photo','photo:album', 'video'])
|
||||
->whereIsNsfw(false)
|
||||
->whereVisibility('public')
|
||||
->whereNotIn('profile_id', $following)
|
||||
->when($sql, function($q, $s) {
|
||||
return $q->where('created_at', '>', now()->subMonths(3));
|
||||
})
|
||||
->filter(function($post) use($filters) {
|
||||
return $post &&
|
||||
isset($post['account']) &&
|
||||
isset($post['account']['id']) &&
|
||||
!in_array($post['account']['id'], $filters);
|
||||
->with('media')
|
||||
->inRandomOrder()
|
||||
->latest()
|
||||
->take(39)
|
||||
->get();
|
||||
|
||||
$res = [
|
||||
'posts' => $posts->map(function($post) {
|
||||
return [
|
||||
'type' => $post->type,
|
||||
'url' => $post->url(),
|
||||
'thumb' => $post->thumb(),
|
||||
];
|
||||
})
|
||||
->take(12)
|
||||
->values();
|
||||
return response()->json(compact('posts'));
|
||||
];
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function directMessage(Request $request, $profileId, $threadId)
|
||||
|
@ -109,15 +140,13 @@ class InternalApiController extends Controller
|
|||
|
||||
public function statusReplies(Request $request, int $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|int|min:1|max:6'
|
||||
]);
|
||||
$parent = Status::whereScope('public')->findOrFail($id);
|
||||
$limit = $request->input('limit') ?? 3;
|
||||
|
||||
$children = Status::whereInReplyToId($parent->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->take($limit)
|
||||
->take(3)
|
||||
->get();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($children, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
|
@ -152,8 +181,8 @@ class InternalApiController extends Controller
|
|||
Rule::in([
|
||||
'addcw',
|
||||
'remcw',
|
||||
'unlist',
|
||||
'spammer'
|
||||
'unlist'
|
||||
|
||||
])
|
||||
],
|
||||
'item_id' => 'required|integer|min:1',
|
||||
|
@ -168,12 +197,9 @@ class InternalApiController extends Controller
|
|||
$item_id = $request->input('item_id');
|
||||
$item_type = $request->input('item_type');
|
||||
|
||||
$status = Status::findOrFail($item_id);
|
||||
$author = User::whereProfileId($status->profile_id)->first();
|
||||
abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
|
||||
|
||||
switch($action) {
|
||||
case 'addcw':
|
||||
$status = Status::findOrFail($item_id);
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
ModLogService::boot()
|
||||
|
@ -189,6 +215,7 @@ class InternalApiController extends Controller
|
|||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
|
||||
if($status->uri == null) {
|
||||
$media = $status->media;
|
||||
$ai = new AccountInterstitial;
|
||||
|
@ -219,6 +246,7 @@ class InternalApiController extends Controller
|
|||
break;
|
||||
|
||||
case 'remcw':
|
||||
$status = Status::findOrFail($item_id);
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
ModLogService::boot()
|
||||
|
@ -244,6 +272,7 @@ class InternalApiController extends Controller
|
|||
break;
|
||||
|
||||
case 'unlist':
|
||||
$status = Status::whereScope('public')->findOrFail($item_id);
|
||||
$status->scope = $status->visibility = 'unlisted';
|
||||
$status->save();
|
||||
PublicTimelineService::del($status->id);
|
||||
|
@ -288,55 +317,135 @@ class InternalApiController extends Controller
|
|||
$u->save();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'spammer':
|
||||
HandleSpammerPipeline::dispatch($status->profile);
|
||||
ModLogService::boot()
|
||||
->user(Auth::user())
|
||||
->objectUid($status->profile->user_id)
|
||||
->objectId($status->id)
|
||||
->objectType('App\User::class')
|
||||
->action('admin.status.moderate')
|
||||
->metadata([
|
||||
'action' => 'spammer',
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
break;
|
||||
}
|
||||
|
||||
StatusService::del($status->id, true);
|
||||
return ['msg' => 200];
|
||||
}
|
||||
|
||||
public function composePost(Request $request)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'media.*' => 'required',
|
||||
'media.*.id' => 'required|integer|min:1',
|
||||
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
|
||||
'media.*.license' => 'nullable|string|max:140',
|
||||
'media.*.alt' => 'nullable|string|max:140',
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
|
||||
'place' => 'nullable',
|
||||
'comments_disabled' => 'nullable',
|
||||
'tagged' => 'nullable'
|
||||
]);
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
$blockedKeywords = config('costar.keyword.block');
|
||||
if($blockedKeywords !== null && $request->caption) {
|
||||
$keywords = config('costar.keyword.block');
|
||||
foreach($keywords as $kw) {
|
||||
if(Str::contains($request->caption, $kw) == true) {
|
||||
abort(400, 'Invalid object');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$visibility = $request->input('visibility');
|
||||
$medias = $request->input('media');
|
||||
$attachments = [];
|
||||
$status = new Status;
|
||||
$mimes = [];
|
||||
$place = $request->input('place');
|
||||
$cw = $request->input('cw');
|
||||
$tagged = $request->input('tagged');
|
||||
|
||||
foreach($medias as $k => $media) {
|
||||
if($k + 1 > config('pixelfed.max_album_length')) {
|
||||
continue;
|
||||
}
|
||||
$m = Media::findOrFail($media['id']);
|
||||
if($m->profile_id !== $profile->id || $m->status_id) {
|
||||
abort(403, 'Invalid media id');
|
||||
}
|
||||
$m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
|
||||
$m->license = $media['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($cw == true || $profile->cw == true) {
|
||||
$m->is_nsfw = $cw;
|
||||
$status->is_nsfw = $cw;
|
||||
}
|
||||
$m->save();
|
||||
$attachments[] = $m;
|
||||
array_push($mimes, $m->mime);
|
||||
}
|
||||
|
||||
$mediaType = StatusController::mimeTypeCheck($mimes);
|
||||
|
||||
if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
|
||||
abort(400, __('exception.compose.invalid.album'));
|
||||
}
|
||||
|
||||
if($place && is_array($place)) {
|
||||
$status->place_id = $place['id'];
|
||||
}
|
||||
|
||||
if($request->filled('comments_disabled')) {
|
||||
$status->comments_disabled = (bool) $request->input('comments_disabled');
|
||||
}
|
||||
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->scope = 'draft';
|
||||
$status->profile_id = $profile->id;
|
||||
$status->save();
|
||||
|
||||
foreach($attachments as $media) {
|
||||
$media->status_id = $status->id;
|
||||
$media->save();
|
||||
}
|
||||
|
||||
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
|
||||
$cw = $profile->cw == true ? true : $cw;
|
||||
$status->is_nsfw = $cw;
|
||||
$status->visibility = $visibility;
|
||||
$status->scope = $visibility;
|
||||
$status->type = $mediaType;
|
||||
$status->save();
|
||||
|
||||
foreach($tagged as $tg) {
|
||||
$mt = new MediaTag;
|
||||
$mt->status_id = $status->id;
|
||||
$mt->media_id = $status->media->first()->id;
|
||||
$mt->profile_id = $tg['id'];
|
||||
$mt->tagged_username = $tg['name'];
|
||||
$mt->is_public = true; // (bool) $tg['privacy'] ?? 1;
|
||||
$mt->metadata = json_encode([
|
||||
'_v' => 1,
|
||||
]);
|
||||
$mt->save();
|
||||
MediaTagService::set($mt->status_id, $mt->profile_id);
|
||||
MediaTagService::sendNotification($mt);
|
||||
}
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
Cache::forget('user:account:id:'.$profile->user_id);
|
||||
Cache::forget('_api:statuses:recent_9:'.$profile->id);
|
||||
Cache::forget('profile:status_count:'.$profile->id);
|
||||
Cache::forget($user->storageUsedKey());
|
||||
return $status->url();
|
||||
}
|
||||
|
||||
public function bookmarks(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$res = Bookmark::whereProfileId($pid)
|
||||
->orderByDesc('created_at')
|
||||
->simplePaginate(10)
|
||||
->map(function($bookmark) use($pid) {
|
||||
$status = StatusService::get($bookmark->status_id, false);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['bookmarked_at'] = $bookmark->created_at->format('c');
|
||||
$statuses = Auth::user()->profile
|
||||
->bookmarks()
|
||||
->withCount(['likes','comments'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->simplePaginate(10);
|
||||
|
||||
if($status) {
|
||||
BookmarkService::add($pid, $status['id']);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($bookmark) {
|
||||
return $bookmark && isset($bookmark['id']);
|
||||
})
|
||||
->values();
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
@ -419,25 +528,25 @@ class InternalApiController extends Controller
|
|||
|
||||
public function remoteProfile(Request $request, $id)
|
||||
{
|
||||
return redirect('/i/web/profile/' . $id);
|
||||
$profile = Profile::whereNull('status')
|
||||
->whereNotNull('domain')
|
||||
->findOrFail($id);
|
||||
$user = Auth::user();
|
||||
|
||||
return view('profile.remote', compact('profile', 'user'));
|
||||
}
|
||||
|
||||
public function remoteStatus(Request $request, $profileId, $statusId)
|
||||
{
|
||||
return redirect('/i/web/post/' . $statusId);
|
||||
}
|
||||
$user = Profile::whereNull('status')
|
||||
->whereNotNull('domain')
|
||||
->findOrFail($profileId);
|
||||
|
||||
public function requestEmailVerification(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$exists = Redis::sismember('email:manual', $pid);
|
||||
return view('account.email.request_verification', compact('exists'));
|
||||
}
|
||||
|
||||
public function requestEmailVerificationStore(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
Redis::sadd('email:manual', $pid);
|
||||
return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']);
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('visibility', ['public', 'unlisted'])
|
||||
->findOrFail($statusId);
|
||||
$template = $status->in_reply_to_id ? 'status.reply' : 'status.remote';
|
||||
return view($template, compact('user', 'status'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,12 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
use App\Jobs\LikePipeline\UnlikePipeline;
|
||||
use App\Like;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class LikeController extends Controller
|
||||
{
|
||||
|
@ -29,11 +27,15 @@ class LikeController extends Controller
|
|||
$profile = $user->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
if (Like::whereStatusId($status->id)->whereProfileId($profile->id)->exists()) {
|
||||
$count = $status->likes()->count();
|
||||
|
||||
if ($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||
UnlikePipeline::dispatch($like);
|
||||
$like->forceDelete();
|
||||
$count--;
|
||||
$status->likes_count = $count;
|
||||
$status->save();
|
||||
} else {
|
||||
$count = $status->likes_count > 4 ? $status->likes_count : $status->likes()->count();
|
||||
$like = Like::firstOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'status_id' => $status->id
|
||||
|
@ -56,10 +58,9 @@ class LikeController extends Controller
|
|||
}
|
||||
|
||||
Cache::forget('status:'.$status->id.':likedby:userid:'.$user->id);
|
||||
StatusService::refresh($status->id);
|
||||
|
||||
if ($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => 0];
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
} else {
|
||||
$response = redirect($status->url());
|
||||
}
|
||||
|
|
|
@ -22,6 +22,39 @@ class MediaController extends Controller
|
|||
|
||||
public function composeUpdate(Request $request, $id)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
$this->validate($request, [
|
||||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:' . config('pixelfed.media_types'),
|
||||
'max:' . config('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$media = Media::whereUserId($user->id)
|
||||
->whereProfileId($user->profile_id)
|
||||
->whereNull('status_id')
|
||||
->findOrFail($id);
|
||||
|
||||
$media->version = 2;
|
||||
$media->save();
|
||||
|
||||
$fragments = explode('/', $media->media_path);
|
||||
$name = last($fragments);
|
||||
array_pop($fragments);
|
||||
$dir = implode('/', $fragments);
|
||||
$path = $photo->storeAs($dir, $name);
|
||||
$res = [];
|
||||
$res['url'] = URL::temporarySignedRoute(
|
||||
'temp-media', now()->addHours(1), ['profileId' => $media->profile_id, 'mediaId' => $media->id, 'timestamp' => time()]
|
||||
);
|
||||
ImageOptimize::dispatch($media);
|
||||
return $res;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,44 @@ class MediaTagController extends Controller
|
|||
|
||||
public function usernameLookup(Request $request)
|
||||
{
|
||||
abort(404);
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:50'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
if(Str::of($q)->startsWith('@')) {
|
||||
if(strlen($q) < 3) {
|
||||
return [];
|
||||
}
|
||||
$q = mb_substr($q, 1);
|
||||
}
|
||||
|
||||
$blocked = UserFilter::whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->whereFilterableId($request->user()->profile_id)
|
||||
->pluck('user_id');
|
||||
|
||||
$blocked->push($request->user()->profile_id);
|
||||
|
||||
$results = Profile::select('id','domain','username')
|
||||
->whereNotIn('id', $blocked)
|
||||
->whereNull('domain')
|
||||
->where('username','like','%'.$q.'%')
|
||||
->limit(15)
|
||||
->get()
|
||||
->map(function($r) {
|
||||
return [
|
||||
'id' => (string) $r->id,
|
||||
'name' => $r->username,
|
||||
'privacy' => true,
|
||||
'avatar' => $r->avatarUrl()
|
||||
];
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function untagProfile(Request $request)
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Status;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollVote;
|
||||
use App\Services\PollService;
|
||||
use App\Services\FollowerService;
|
||||
|
||||
class PollController extends Controller
|
||||
{
|
||||
public function getPoll(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.polls.enabled'), 404);
|
||||
|
||||
$poll = Poll::findOrFail($id);
|
||||
$status = Status::findOrFail($poll->status_id);
|
||||
if($status->scope != 'public') {
|
||||
abort_if(!$request->user(), 403);
|
||||
if($request->user()->profile_id != $status->profile_id) {
|
||||
abort_if(!FollowerService::follows($request->user()->profile_id, $status->profile_id), 404);
|
||||
}
|
||||
}
|
||||
$pid = $request->user() ? $request->user()->profile_id : false;
|
||||
$poll = PollService::getById($id, $pid);
|
||||
return $poll;
|
||||
}
|
||||
|
||||
public function vote(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.polls.enabled'), 404);
|
||||
|
||||
abort_unless($request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'choices' => 'required|array'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$poll_id = $id;
|
||||
$choices = $request->input('choices');
|
||||
|
||||
// todo: implement multiple choice
|
||||
$choice = $choices[0];
|
||||
|
||||
$poll = Poll::findOrFail($poll_id);
|
||||
|
||||
abort_if(now()->gt($poll->expires_at), 422, 'Poll expired.');
|
||||
|
||||
abort_if(PollVote::wherePollId($poll_id)->whereProfileId($pid)->exists(), 400, 'Already voted.');
|
||||
|
||||
$vote = new PollVote;
|
||||
$vote->status_id = $poll->status_id;
|
||||
$vote->profile_id = $pid;
|
||||
$vote->poll_id = $poll->id;
|
||||
$vote->choice = $choice;
|
||||
$vote->save();
|
||||
|
||||
$poll->votes_count = $poll->votes_count + 1;
|
||||
$poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($choice) {
|
||||
return $choice == $key ? $tally + 1 : $tally;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
PollService::del($poll->status_id);
|
||||
$res = PollService::get($poll->status_id, $pid);
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ namespace App\Http\Controllers;
|
|||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use DB;
|
||||
use View;
|
||||
use App\Follower;
|
||||
use App\FollowRequest;
|
||||
|
@ -14,9 +13,6 @@ use App\Story;
|
|||
use App\User;
|
||||
use App\UserFilter;
|
||||
use League\Fractal;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use App\Transformer\ActivityPub\ProfileOutbox;
|
||||
|
@ -31,7 +27,7 @@ class ProfileController extends Controller
|
|||
->whereUsername($username)
|
||||
->firstOrFail();
|
||||
|
||||
if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
||||
if($request->wantsJson() && config('federation.activitypub.enabled')) {
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
return $this->buildProfile($request, $user);
|
||||
|
@ -51,13 +47,13 @@ class ProfileController extends Controller
|
|||
});
|
||||
|
||||
if ($user->is_private == true) {
|
||||
$profile = null;
|
||||
return view('profile.private', compact('user'));
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$owner = false;
|
||||
$is_following = false;
|
||||
|
||||
$is_admin = $user->user->is_admin;
|
||||
$profile = $user;
|
||||
$settings = [
|
||||
'crawlable' => $settings->crawlable,
|
||||
|
@ -118,7 +114,7 @@ class ProfileController extends Controller
|
|||
{
|
||||
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
|
||||
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
||||
if ($request->wantsJson() && config('federation.activitypub.enabled')) {
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
|
||||
|
@ -176,7 +172,7 @@ class ProfileController extends Controller
|
|||
|
||||
public function showActivityPub(Request $request, $user)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.enabled'), 404);
|
||||
abort_if($user->domain, 404);
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
|
@ -189,35 +185,19 @@ class ProfileController extends Controller
|
|||
{
|
||||
abort_if(!config('federation.atom.enabled'), 404);
|
||||
|
||||
$pid = AccountService::usernameToId($user);
|
||||
|
||||
abort_if(!$pid, 404);
|
||||
|
||||
$profile = AccountService::get($pid);
|
||||
|
||||
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
|
||||
|
||||
$items = DB::table('statuses')
|
||||
->whereProfileId($pid)
|
||||
->whereVisibility('public')
|
||||
->whereType('photo')
|
||||
->latest()
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function($status) {
|
||||
return StatusService::get($status->id);
|
||||
})
|
||||
->filter(function($status) {
|
||||
return $status &&
|
||||
isset($status['account']) &&
|
||||
isset($status['media_attachments']) &&
|
||||
count($status['media_attachments']);
|
||||
})
|
||||
->values();
|
||||
$permalink = config('app.url') . "/users/{$profile['username']}.atom";
|
||||
|
||||
return response()
|
||||
->view('atom.user', compact('profile', 'items', 'permalink'))
|
||||
$profile = $user = Profile::whereNull('status')->whereNull('domain')->whereUsername($user)->whereIsPrivate(false)->firstOrFail();
|
||||
if($profile->status != null) {
|
||||
return $this->accountCheck($profile);
|
||||
}
|
||||
if($profile->is_private || Auth::check()) {
|
||||
$blocked = $this->blockedProfileCheck($profile);
|
||||
$check = $this->privateProfileCheck($profile, null);
|
||||
if($check || $blocked) {
|
||||
return redirect($profile->url());
|
||||
}
|
||||
}
|
||||
$items = $profile->statuses()->whereHas('media')->whereIn('visibility',['public', 'unlisted'])->orderBy('created_at', 'desc')->take(10)->get();
|
||||
return response()->view('atom.user', compact('profile', 'items'))
|
||||
->header('Content-Type', 'application/atom+xml');
|
||||
}
|
||||
|
||||
|
@ -245,26 +225,24 @@ class ProfileController extends Controller
|
|||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
if(AccountService::canEmbed($profile->user_id) == false) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
$content = Cache::remember('profile:embed:'.$profile->id, now()->addHours(12), function() use($profile) {
|
||||
return View::make('profile.embed')->with(compact('profile'))->render();
|
||||
});
|
||||
|
||||
$profile = AccountService::get($profile->id);
|
||||
$res = view('profile.embed', compact('profile'));
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
public function stories(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
$pid = $profile->id;
|
||||
$authed = Auth::user()->profile_id;
|
||||
abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404);
|
||||
$authed = Auth::user()->profile;
|
||||
abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404);
|
||||
$exists = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->exists();
|
||||
abort_unless($exists, 404);
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
abort_unless($exists > 0, 404);
|
||||
return view('profile.story', compact('pid', 'profile'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,11 +12,9 @@ use App\{
|
|||
Profile,
|
||||
StatusHashtag,
|
||||
Status,
|
||||
StatusView,
|
||||
UserFilter
|
||||
};
|
||||
use Auth, Cache, DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Auth,Cache;
|
||||
use Carbon\Carbon;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\{
|
||||
|
@ -27,21 +25,14 @@ use App\Transformer\Api\{
|
|||
};
|
||||
use App\Services\{
|
||||
AccountService,
|
||||
BookmarkService,
|
||||
FollowerService,
|
||||
LikeService,
|
||||
PublicTimelineService,
|
||||
ProfileService,
|
||||
ReblogService,
|
||||
RelationshipService,
|
||||
StatusService,
|
||||
SnowflakeService,
|
||||
UserFilterService
|
||||
};
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
||||
|
||||
class PublicApiController extends Controller
|
||||
{
|
||||
protected $fractal;
|
||||
|
@ -91,44 +82,29 @@ class PublicApiController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
public function getStatus(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$status = StatusService::get($id, false);
|
||||
abort_if(!$status, 404);
|
||||
if(in_array($status['visibility'], ['public', 'unlisted'])) {
|
||||
return $status;
|
||||
}
|
||||
$pid = $request->user()->profile_id;
|
||||
if($status['account']['id'] == $pid) {
|
||||
return $status;
|
||||
}
|
||||
if($status['visibility'] == 'private') {
|
||||
if(FollowerService::follows($pid, $status['account']['id'])) {
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function status(Request $request, $username, int $postid)
|
||||
{
|
||||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->findOrFail($postid);
|
||||
$this->scopeCheck($profile, $status);
|
||||
if(!$request->user()) {
|
||||
$res = ['status' => StatusService::get($status->id)];
|
||||
} else {
|
||||
if(!Auth::check()) {
|
||||
$res = Cache::remember('wapi:v1:status:stateless_byid:' . $status->id, now()->addMinutes(30), function() use($status) {
|
||||
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
|
||||
$res = [
|
||||
'status' => $this->fractal->createData($item)->toArray(),
|
||||
];
|
||||
return $res;
|
||||
});
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
|
||||
$res = [
|
||||
'status' => $this->fractal->createData($item)->toArray(),
|
||||
];
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function statusState(Request $request, $username, $postid)
|
||||
public function statusState(Request $request, $username, int $postid)
|
||||
{
|
||||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->findOrFail($postid);
|
||||
|
@ -174,9 +150,14 @@ class PublicApiController extends Controller
|
|||
|
||||
if(Auth::check()) {
|
||||
$p = Auth::user()->profile;
|
||||
$scope = $p->id == $status->profile_id || FollowerService::follows($p->id, $profile->id) ? ['public', 'private', 'unlisted'] : ['public','unlisted'];
|
||||
$filtered = UserFilter::whereUserId($p->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')->toArray();
|
||||
$scope = $p->id == $status->profile_id ? ['public', 'private'] : ['public'];
|
||||
} else {
|
||||
$scope = ['public', 'unlisted'];
|
||||
$filtered = [];
|
||||
$scope = ['public'];
|
||||
}
|
||||
|
||||
if($request->filled('min_id') || $request->filled('max_id')) {
|
||||
|
@ -184,7 +165,8 @@ class PublicApiController extends Controller
|
|||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', $scope)
|
||||
->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->select('id', 'caption', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->where('id', '>=', $request->min_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
|
@ -193,16 +175,18 @@ class PublicApiController extends Controller
|
|||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', $scope)
|
||||
->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->select('id', 'caption', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->where('id', '<=', $request->max_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
} else {
|
||||
$replies = Status::whereInReplyToId($status->id)
|
||||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', $scope)
|
||||
->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->select('id', 'caption', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
|
@ -215,15 +199,9 @@ class PublicApiController extends Controller
|
|||
|
||||
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' => []
|
||||
]);
|
||||
}
|
||||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->findOrFail($id);
|
||||
$this->scopeCheck($profile, $status);
|
||||
$likes = $this->getLikes($status);
|
||||
return response()->json([
|
||||
'data' => $likes
|
||||
|
@ -232,16 +210,9 @@ class PublicApiController extends Controller
|
|||
|
||||
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
|
||||
|
@ -302,42 +273,51 @@ class PublicApiController extends Controller
|
|||
$max = $request->input('max_id');
|
||||
$limit = $request->input('limit') ?? 3;
|
||||
$user = $request->user();
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
if(config('exp.cached_public_timeline') == false) {
|
||||
$key = 'user:last_active_at:id:'.$user->id;
|
||||
$ttl = now()->addMinutes(5);
|
||||
Cache::remember($key, $ttl, function() use($user) {
|
||||
$user->last_active_at = now();
|
||||
$user->save();
|
||||
return;
|
||||
});
|
||||
|
||||
$filtered = UserFilter::whereUserId($user->profile_id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereIn('filter_type', ['mute', 'block'])
|
||||
->pluck('filterable_id')->toArray();
|
||||
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
$id = $min ?? $max;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'scope',
|
||||
'local'
|
||||
)
|
||||
->where('id', $dir, $id)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
'local',
|
||||
'reply_count',
|
||||
'comments_disabled',
|
||||
'place_id',
|
||||
'likes_count',
|
||||
'reblogs_count',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->where('id', $dir, $id)
|
||||
->whereIn('type', ['text', 'photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereLocal(true)
|
||||
->whereScope('public')
|
||||
->orderBy('id', 'desc')
|
||||
->where('created_at', '>', now()->subMonths(3))
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::getFull($s->id, $user->profile_id);
|
||||
if(!$status) {
|
||||
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);
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values();
|
||||
$res = $timeline->toArray();
|
||||
->get();
|
||||
} else {
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
|
@ -358,66 +338,20 @@ class PublicApiController extends Controller
|
|||
'likes_count',
|
||||
'reblogs_count',
|
||||
'updated_at'
|
||||
)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
)->whereIn('type', ['text', 'photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
->whereLocal(true)
|
||||
->whereScope('public')
|
||||
->orderBy('id', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::getFull($s->id, $user->profile_id);
|
||||
if(!$status) {
|
||||
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);
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values();
|
||||
|
||||
$res = $timeline->toArray();
|
||||
}
|
||||
} else {
|
||||
Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() {
|
||||
if(PublicTimelineService::count() == 0) {
|
||||
PublicTimelineService::warmCache(true, 400);
|
||||
}
|
||||
});
|
||||
|
||||
if ($max) {
|
||||
$feed = PublicTimelineService::getRankedMaxId($max, $limit);
|
||||
} else if ($min) {
|
||||
$feed = PublicTimelineService::getRankedMinId($min, $limit);
|
||||
} else {
|
||||
$feed = PublicTimelineService::get(0, $limit);
|
||||
}
|
||||
|
||||
$res = collect($feed)
|
||||
->map(function($k) use($user) {
|
||||
$status = StatusService::get($k);
|
||||
if($user) {
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
|
||||
$status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k);
|
||||
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k);
|
||||
$status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
->where('created_at', '>', now()->subMonths(3))
|
||||
->orderBy('created_at', 'desc')
|
||||
->simplePaginate($limit);
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
|
||||
$res = $this->fractal->createData($fractal)->toArray();
|
||||
return response()->json($res);
|
||||
|
||||
}
|
||||
|
||||
public function homeTimelineApi(Request $request)
|
||||
|
@ -430,13 +364,9 @@ class PublicApiController extends Controller
|
|||
'page' => 'nullable|integer|max:40',
|
||||
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|max:40',
|
||||
'recent_feed' => 'nullable',
|
||||
'recent_min' => 'nullable|integer'
|
||||
'limit' => 'nullable|integer|max:40'
|
||||
]);
|
||||
|
||||
$recentFeed = $request->input('recent_feed') == 'true';
|
||||
$recentFeedMin = $request->input('recent_min');
|
||||
$page = $request->input('page');
|
||||
$min = $request->input('min_id');
|
||||
$max = $request->input('max_id');
|
||||
|
@ -444,52 +374,43 @@ class PublicApiController extends Controller
|
|||
$user = $request->user();
|
||||
|
||||
$key = 'user:last_active_at:id:'.$user->id;
|
||||
$ttl = now()->addMinutes(20);
|
||||
$ttl = now()->addMinutes(5);
|
||||
Cache::remember($key, $ttl, function() use($user) {
|
||||
$user->last_active_at = now();
|
||||
$user->save();
|
||||
return;
|
||||
});
|
||||
|
||||
$pid = $user->profile_id;
|
||||
// TODO: Use redis for timelines
|
||||
// $timeline = Timeline::build()->local();
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
// $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
|
||||
// return Profile::whereIsPrivate(true)
|
||||
// ->orWhere('unlisted', true)
|
||||
// ->orWhere('status', '!=', null)
|
||||
// ->pluck('id');
|
||||
// });
|
||||
|
||||
$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'];
|
||||
// $private = $private->diff($following)->flatten();
|
||||
|
||||
$textOnlyReplies = false;
|
||||
// $filters = UserFilter::whereUserId($pid)
|
||||
// ->whereFilterableType('App\Profile')
|
||||
// ->whereIn('filter_type', ['mute', 'block'])
|
||||
// ->pluck('filterable_id')->toArray();
|
||||
// $filtered = array_merge($private->toArray(), $filters);
|
||||
|
||||
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');
|
||||
}
|
||||
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
|
||||
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
$id = $min ?? $max;
|
||||
return Status::select(
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
|
@ -508,35 +429,17 @@ class PublicApiController extends Controller
|
|||
'reblogs_count',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)
|
||||
->whereIn('type', $types)
|
||||
->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
|
||||
return $q->whereNull('in_reply_to_id');
|
||||
})
|
||||
)->whereIn('type', ['text','photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('visibility',['public', 'unlisted', 'private'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
if(!$status) {
|
||||
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);
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
->get();
|
||||
} else {
|
||||
return Status::select(
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
|
@ -555,112 +458,24 @@ class PublicApiController extends Controller
|
|||
'reblogs_count',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)
|
||||
->whereIn('type', $types)
|
||||
->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
|
||||
return $q->whereNull('in_reply_to_id');
|
||||
})
|
||||
)->whereIn('type', ['text','photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
->whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('visibility',['public', 'unlisted', 'private'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
if(!$status) {
|
||||
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);
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
->simplePaginate($limit);
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
|
||||
$res = $this->fractal->createData($fractal)->toArray();
|
||||
return response()->json($res);
|
||||
|
||||
}
|
||||
|
||||
public function networkTimelineApi(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
abort_if(config('federation.network_timeline') == false, 404);
|
||||
|
||||
$this->validate($request,[
|
||||
'page' => 'nullable|integer|max:40',
|
||||
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|max:30'
|
||||
]);
|
||||
|
||||
$page = $request->input('page');
|
||||
$min = $request->input('min_id');
|
||||
$max = $request->input('max_id');
|
||||
$limit = $request->input('limit') ?? 3;
|
||||
$user = $request->user();
|
||||
$amin = SnowflakeService::byDate(now()->subDays(490));
|
||||
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
$id = $min ?? $max;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'type',
|
||||
'scope',
|
||||
'created_at',
|
||||
)
|
||||
->where('id', $dir, $id)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
->where('id', '>', $amin)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$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);
|
||||
return $status;
|
||||
});
|
||||
$res = $timeline->toArray();
|
||||
} else {
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'type',
|
||||
'scope',
|
||||
'created_at',
|
||||
)
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNotNull('uri')
|
||||
->whereScope('public')
|
||||
->where('id', '>', $amin)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function($s) use ($user) {
|
||||
$status = StatusService::get($s->id);
|
||||
$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);
|
||||
return $status;
|
||||
});
|
||||
$res = $timeline->toArray();
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
public function relationships(Request $request)
|
||||
|
@ -669,20 +484,17 @@ class PublicApiController extends Controller
|
|||
return response()->json([]);
|
||||
}
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|array|min:1|max:20',
|
||||
'id.*' => 'required|integer'
|
||||
]);
|
||||
$ids = collect($request->input('id'));
|
||||
$res = $ids->filter(function($v) use($pid) {
|
||||
return $v != $pid;
|
||||
})
|
||||
->map(function($id) use($pid) {
|
||||
return RelationshipService::get($pid, $id);
|
||||
$filtered = $ids->filter(function($v) {
|
||||
return $v != Auth::user()->profile->id;
|
||||
});
|
||||
|
||||
$relations = Profile::whereNull('status')->findOrFail($filtered->all());
|
||||
$fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
|
||||
$res = $this->fractal->createData($fractal)->toArray();
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
|
@ -694,80 +506,48 @@ class PublicApiController extends Controller
|
|||
|
||||
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 [];
|
||||
abort_unless(Auth::check(), 403);
|
||||
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
|
||||
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
|
||||
$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);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(!$account, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_unless(Auth::check(), 403);
|
||||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
$profile = Profile::with('user')
|
||||
->whereNull('status')
|
||||
->whereNull('domain')
|
||||
->findOrFail($id);
|
||||
|
||||
if(AccountService::hiddenFollowing($id)) {
|
||||
return [];
|
||||
}
|
||||
// filter by username
|
||||
$search = $request->input('fbu');
|
||||
$owner = Auth::id() == $profile->user_id;
|
||||
$filter = ($owner == true) && ($search != null);
|
||||
|
||||
if($request->has('page') && $request->page >= 5) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
|
||||
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
|
||||
|
||||
$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();
|
||||
if($search) {
|
||||
abort_if(!$owner, 404);
|
||||
$following = $profile->following()
|
||||
->where('profiles.username', 'like', '%'.$search.'%')
|
||||
->orderByDesc('followers.created_at')
|
||||
->paginate(10);
|
||||
} else {
|
||||
$following = $profile->following()
|
||||
->orderByDesc('followers.created_at')
|
||||
->paginate(10);
|
||||
}
|
||||
$resource = new Fractal\Resource\Collection($following, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
@ -784,78 +564,116 @@ class PublicApiController extends Controller
|
|||
'limit' => 'nullable|integer|min:1|max:24'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$profile = AccountService::get($id);
|
||||
abort_if(!$profile, 404);
|
||||
$profile = Profile::whereNull('status')->findOrFail($id);
|
||||
|
||||
$limit = $request->limit ?? 9;
|
||||
$max_id = $request->max_id;
|
||||
$min_id = $request->min_id;
|
||||
$scope = ['photo', 'photo:album', 'video', 'video:album'];
|
||||
$onlyMedia = $request->input('only_media', true);
|
||||
$scope = $request->only_media == true ?
|
||||
['photo', 'photo:album', 'video', 'video:album'] :
|
||||
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
|
||||
|
||||
if(!$min_id && !$max_id) {
|
||||
$min_id = 1;
|
||||
}
|
||||
|
||||
if($profile['locked']) {
|
||||
if(!$user) {
|
||||
if($profile->is_private) {
|
||||
if(!Auth::check()) {
|
||||
return response()->json([]);
|
||||
}
|
||||
$pid = $user->profile_id;
|
||||
$pid = Auth::user()->profile->id;
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
$visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : [];
|
||||
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
|
||||
} else {
|
||||
if($user) {
|
||||
$pid = $user->profile_id;
|
||||
if(Auth::check()) {
|
||||
$pid = Auth::user()->profile->id;
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
$visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
|
||||
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
|
||||
} else {
|
||||
$visibility = ['public', 'unlisted'];
|
||||
}
|
||||
}
|
||||
$dir = $min_id ? '>' : '<';
|
||||
$id = $min_id ?? $max_id;
|
||||
$res = Status::whereProfileId($profile['id'])
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
|
||||
$tag = in_array('private', $visibility) ? 'private' : 'public';
|
||||
if($min_id == 1 && $limit == 9 && $tag == 'public') {
|
||||
$limit = 9;
|
||||
$scope = ['photo', 'photo:album', 'video', 'video:album'];
|
||||
$key = '_api:statuses:recent_9:'.$profile->id;
|
||||
$res = Cache::remember($key, now()->addHours(24), function() use($profile, $scope, $visibility, $limit) {
|
||||
$dir = '>';
|
||||
$id = 1;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'reblogs_count',
|
||||
'scope',
|
||||
'visibility',
|
||||
'local',
|
||||
'place_id',
|
||||
'comments_disabled',
|
||||
'cw_summary',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereProfileId($profile->id)
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('scope', $visibility)
|
||||
->whereIn('visibility', $visibility)
|
||||
->limit($limit)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->map(function($s) use($user) {
|
||||
try {
|
||||
$status = StatusService::get($s->id, false);
|
||||
} catch (\Exception $e) {
|
||||
$status = false;
|
||||
}
|
||||
if($user && $status) {
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($onlyMedia) {
|
||||
if($onlyMedia) {
|
||||
if(
|
||||
!isset($s['media_attachments']) ||
|
||||
!is_array($s['media_attachments']) ||
|
||||
empty($s['media_attachments'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $s;
|
||||
})
|
||||
->values();
|
||||
->get();
|
||||
|
||||
return response()->json($res);
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusStatelessTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
});
|
||||
return $res;
|
||||
}
|
||||
|
||||
$dir = $min_id ? '>' : '<';
|
||||
$id = $min_id ?? $max_id;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'reblogs_count',
|
||||
'scope',
|
||||
'visibility',
|
||||
'local',
|
||||
'place_id',
|
||||
'comments_disabled',
|
||||
'cw_summary',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereProfileId($profile->id)
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('visibility', $visibility)
|
||||
->limit($limit)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusStatelessTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -98,27 +98,11 @@ class ReportController extends Controller
|
|||
$object_type = $request->input('type');
|
||||
$msg = $request->input('msg');
|
||||
$object = null;
|
||||
$types = [
|
||||
// original 3
|
||||
'spam',
|
||||
'sensitive',
|
||||
'abusive',
|
||||
|
||||
// new
|
||||
'underage',
|
||||
'copyright',
|
||||
'impersonation',
|
||||
'scam',
|
||||
'terrorism'
|
||||
];
|
||||
$types = ['spam', 'sensitive', 'abusive'];
|
||||
|
||||
if (!in_array($reportType, $types)) {
|
||||
if($request->wantsJson()) {
|
||||
return abort(400, 'Invalid report type');
|
||||
} else {
|
||||
return redirect('/timeline')->with('error', 'Invalid report type');
|
||||
}
|
||||
}
|
||||
|
||||
switch ($object_type) {
|
||||
case 'post':
|
||||
|
@ -131,29 +115,17 @@ class ReportController extends Controller
|
|||
break;
|
||||
|
||||
default:
|
||||
if($request->wantsJson()) {
|
||||
return abort(400, 'Invalid report type');
|
||||
} else {
|
||||
return redirect('/timeline')->with('error', 'Invalid report type');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($exists !== 0) {
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(200);
|
||||
} else {
|
||||
return redirect('/timeline')->with('error', 'You have already reported this!');
|
||||
}
|
||||
}
|
||||
|
||||
if ($object->profile_id == $profile->id) {
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(200);
|
||||
} else {
|
||||
return redirect('/timeline')->with('error', 'You cannot report your own content!');
|
||||
}
|
||||
}
|
||||
|
||||
$report = new Report();
|
||||
$report->profile_id = $profile->id;
|
||||
|
@ -162,13 +134,9 @@ class ReportController extends Controller
|
|||
$report->object_type = $object_type;
|
||||
$report->reported_profile_id = $object->profile_id;
|
||||
$report->type = $request->input('report');
|
||||
$report->message = e($request->input('msg'));
|
||||
$report->message = $request->input('msg');
|
||||
$report->save();
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(200);
|
||||
} else {
|
||||
return redirect('/timeline')->with('status', 'Report successfully sent!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ class SearchController extends Controller
|
|||
$hash = hash('sha256', $tag);
|
||||
if( Helpers::validateUrl($tag) != false &&
|
||||
Helpers::validateLocalUrl($tag) != true &&
|
||||
config_cache('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.remoteFollow') == true
|
||||
) {
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
|
@ -203,7 +203,7 @@ class SearchController extends Controller
|
|||
$ttl = now()->addHours(2);
|
||||
if( Helpers::validateUrl($tag) != false &&
|
||||
Helpers::validateLocalUrl($tag) != true &&
|
||||
config_cache('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.remoteFollow') == true
|
||||
) {
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
|
@ -321,18 +321,13 @@ class SearchController extends Controller
|
|||
|
||||
if(Status::whereUri($tag)->whereLocal(false)->exists()) {
|
||||
$item = Status::whereUri($tag)->first();
|
||||
$media = $item->firstMedia();
|
||||
$url = null;
|
||||
if($media) {
|
||||
$url = $media->remote_url;
|
||||
}
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => "/i/web/post/_/$item->profile_id/$item->id",
|
||||
'type' => 'status',
|
||||
'username' => $item->profile->username,
|
||||
'caption' => $item->rendered ?? $item->caption,
|
||||
'thumb' => $url,
|
||||
'thumb' => $item->firstMedia()->remote_url,
|
||||
'timestamp' => $item->created_at->diffForHumans()
|
||||
]];
|
||||
}
|
||||
|
@ -341,18 +336,13 @@ class SearchController extends Controller
|
|||
|
||||
if(isset($remote['type']) && $remote['type'] == 'Note') {
|
||||
$item = Helpers::statusFetch($tag);
|
||||
$media = $item->firstMedia();
|
||||
$url = null;
|
||||
if($media) {
|
||||
$url = $media->remote_url;
|
||||
}
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => "/i/web/post/_/$item->profile_id/$item->id",
|
||||
'type' => 'status',
|
||||
'username' => $item->profile->username,
|
||||
'caption' => $item->rendered ?? $item->caption,
|
||||
'thumb' => $url,
|
||||
'thumb' => $item->firstMedia()->remote_url,
|
||||
'timestamp' => $item->created_at->diffForHumans()
|
||||
]];
|
||||
}
|
||||
|
|
|
@ -4,12 +4,6 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use App\AccountLog;
|
||||
use App\Follower;
|
||||
use App\Like;
|
||||
use App\Status;
|
||||
use App\StatusHashtag;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SeasonalController extends Controller
|
||||
{
|
||||
|
@ -20,220 +14,7 @@ class SeasonalController extends Controller
|
|||
|
||||
public function yearInReview()
|
||||
{
|
||||
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
|
||||
abort_if(config('database.default') != 'mysql', 404);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
return view('account.yir', compact('profile'));
|
||||
}
|
||||
|
||||
public function getData(Request $request)
|
||||
{
|
||||
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
|
||||
abort_if(config('database.default') != 'mysql', 404);
|
||||
|
||||
$uid = $request->user()->id;
|
||||
$pid = $request->user()->profile_id;
|
||||
$epoch = '2020-01-01 00:00:00';
|
||||
$epochStart = '2020-01-01 00:00:00';
|
||||
$epochEnd = '2020-12-31 23:59:59';
|
||||
|
||||
$siteKey = 'seasonal:my2020:shared';
|
||||
$siteTtl = now()->addMonths(3);
|
||||
$userKey = 'seasonal:my2020:user:' . $uid;
|
||||
$userTtl = now()->addMonths(3);
|
||||
|
||||
$shared = Cache::remember($siteKey, $siteTtl, function() use($epochStart, $epochEnd) {
|
||||
return [
|
||||
'average' => [
|
||||
'posts' => round(Status::selectRaw('*, count(profile_id) as count')
|
||||
->whereNull('uri')
|
||||
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('profile_id')
|
||||
->pluck('count')
|
||||
->avg()),
|
||||
|
||||
'likes' => round(Like::selectRaw('*, count(profile_id) as count')
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('profile_id')
|
||||
->pluck('count')
|
||||
->avg()),
|
||||
],
|
||||
|
||||
'popular' => [
|
||||
|
||||
'hashtag' => StatusHashtag::selectRaw('*,count(hashtag_id) as count')
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->hashtag->name,
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
|
||||
'post' => Status::whereScope('public')
|
||||
->where('likes_count', '>', 1)
|
||||
->whereIsNsfw(false)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->orderByDesc('likes_count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($status) {
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'username' => (string) $status->profile->username,
|
||||
'created_at' => $status->created_at->format('M d, Y'),
|
||||
'type' => $status->type,
|
||||
'url' => $status->url(),
|
||||
'thumb' => $status->thumb(),
|
||||
'likes_count' => $status->likes_count,
|
||||
'reblogs_count' => $status->reblogs_count,
|
||||
'reply_count' => $status->reply_count ?? 0,
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
|
||||
'places' => Status::selectRaw('*, count(place_id) as count')
|
||||
->whereNotNull('place_id')
|
||||
->having('count', '>', 1)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('place_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->place->getName(),
|
||||
'url' => $sh->place->url(),
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first()
|
||||
],
|
||||
|
||||
];
|
||||
});
|
||||
|
||||
$res = Cache::remember($userKey, $userTtl, function() use($uid, $pid, $epochStart, $epochEnd, $request) {
|
||||
return [
|
||||
'account' => [
|
||||
'user_id' => $request->user()->id,
|
||||
'created_at' => $request->user()->created_at->format('M d, Y'),
|
||||
'created_this_year' => $request->user()->created_at->gt('2020-01-01 00:00:00'),
|
||||
'created_months_ago' => $request->user()->created_at->diffInMonths(now()),
|
||||
'followers_this_year' => Follower::whereFollowingId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'followed_this_year' => Follower::whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'most_popular' => Status::whereProfileId($pid)
|
||||
->where('likes_count', '>', 1)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->orderByDesc('likes_count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($status) {
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'username' => (string) $status->profile->username,
|
||||
'created_at' => $status->created_at->format('M d, Y'),
|
||||
'type' => $status->type,
|
||||
'url' => $status->url(),
|
||||
'thumb' => $status->thumb(),
|
||||
'likes_count' => $status->likes_count,
|
||||
'reblogs_count' => $status->reblogs_count,
|
||||
'reply_count' => $status->reply_count ?? 0,
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
'posts_count' => Status::whereProfileId($pid)
|
||||
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'likes_count' => Like::whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'hashtag' => StatusHashtag::selectRaw('*, count(hashtag_id) as count')
|
||||
->whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('profile_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->hashtag->name,
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
'places' => Status::selectRaw('*, count(place_id) as count')
|
||||
->whereNotNull('place_id')
|
||||
->having('count', '>', 1)
|
||||
->whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('place_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->place->getName(),
|
||||
'url' => $sh->place->url(),
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
'places_total' => Status::whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->whereNotNull('place_id')
|
||||
->count()
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json(array_merge($res, $shared));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
|
||||
abort_if(config('database.default') != 'mysql', 404);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$log = AccountLog::firstOrCreate([
|
||||
[
|
||||
'item_type' => 'App\User',
|
||||
'item_id' => $user->id,
|
||||
'user_id' => $user->id,
|
||||
'action' => 'seasonal.my2020.view'
|
||||
],
|
||||
[
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent()
|
||||
]
|
||||
]);
|
||||
return response()->json(200);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ use App\Media;
|
|||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Auth;
|
||||
use Cache;
|
||||
|
@ -17,7 +16,6 @@ use Mail;
|
|||
use Purify;
|
||||
use App\Mail\PasswordChange;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\PronounService;
|
||||
|
||||
trait HomeSettings
|
||||
{
|
||||
|
@ -27,14 +25,13 @@ trait HomeSettings
|
|||
$id = Auth::user()->profile->id;
|
||||
$storage = [];
|
||||
$used = Media::whereProfileId($id)->sum('size');
|
||||
$storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
|
||||
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
|
||||
$storage['used'] = $used;
|
||||
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
|
||||
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
|
||||
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
|
||||
$pronouns = PronounService::get($id);
|
||||
|
||||
return view('settings.home', compact('storage', 'pronouns'));
|
||||
return view('settings.home', compact('storage'));
|
||||
}
|
||||
|
||||
public function homeUpdate(Request $request)
|
||||
|
@ -43,8 +40,7 @@ trait HomeSettings
|
|||
'name' => 'required|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',
|
||||
'pronouns' => 'nullable|array|max:4'
|
||||
'language' => 'nullable|string|min:2|max:5'
|
||||
]);
|
||||
|
||||
$changes = false;
|
||||
|
@ -54,14 +50,12 @@ trait HomeSettings
|
|||
$language = $request->input('language');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$pronouns = $request->input('pronouns');
|
||||
$existingPronouns = PronounService::get($profile->id);
|
||||
$layout = $request->input('profile_layout');
|
||||
if($layout) {
|
||||
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
|
||||
}
|
||||
|
||||
$enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
|
||||
$enforceEmailVerification = config('pixelfed.enforce_email_verification');
|
||||
|
||||
// Only allow email to be updated if not yet verified
|
||||
if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
|
||||
|
@ -76,9 +70,9 @@ trait HomeSettings
|
|||
$profile->website = $website;
|
||||
}
|
||||
|
||||
if (strip_tags($profile->bio) != $bio) {
|
||||
if ($profile->bio != $bio) {
|
||||
$changes = true;
|
||||
$profile->bio = Autolink::create()->autolink($bio);
|
||||
$profile->bio = $bio;
|
||||
}
|
||||
|
||||
if($user->language != $language &&
|
||||
|
@ -88,14 +82,6 @@ trait HomeSettings
|
|||
$user->language = $language;
|
||||
session()->put('locale', $language);
|
||||
}
|
||||
|
||||
if($existingPronouns != $pronouns) {
|
||||
if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
|
||||
PronounService::clear($profile->id);
|
||||
} else {
|
||||
PronounService::put($profile->id, $pronouns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($changes === true) {
|
||||
|
@ -166,7 +152,7 @@ trait HomeSettings
|
|||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
$validate = config_cache('pixelfed.enforce_email_verification');
|
||||
$validate = config('pixelfed.enforce_email_verification');
|
||||
|
||||
if ($user->email != $email) {
|
||||
$changes = true;
|
||||
|
|
|
@ -74,11 +74,6 @@ trait PrivacySettings
|
|||
}
|
||||
Cache::forget('profile:settings:' . $profile->id);
|
||||
Cache::forget('user:account:id:' . $profile->user_id);
|
||||
Cache::forget('profile:follower_count:' . $profile->id);
|
||||
Cache::forget('profile:following_count:' . $profile->id);
|
||||
Cache::forget('profile:embed:' . $profile->id);
|
||||
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
|
||||
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
|
||||
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,7 @@ use App\Following;
|
|||
use App\ProfileSponsor;
|
||||
use App\Report;
|
||||
use App\UserFilter;
|
||||
use App\UserSetting;
|
||||
use Auth, Cookie, DB, Cache, Purify;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
@ -22,7 +20,6 @@ use App\Http\Controllers\Settings\{
|
|||
SecuritySettings
|
||||
};
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
|
@ -80,13 +77,13 @@ class SettingsController extends Controller
|
|||
|
||||
public function dataImport()
|
||||
{
|
||||
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
|
||||
abort_if(!config('pixelfed.import.instagram.enabled'), 404);
|
||||
return view('settings.import.home');
|
||||
}
|
||||
|
||||
public function dataImportInstagram()
|
||||
{
|
||||
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
|
||||
abort_if(!config('pixelfed.import.instagram.enabled'), 404);
|
||||
return view('settings.import.instagram.home');
|
||||
}
|
||||
|
||||
|
@ -154,6 +151,13 @@ class SettingsController extends Controller
|
|||
return view('settings.export.show');
|
||||
}
|
||||
|
||||
public function reportsHome(Request $request)
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$reports = Report::whereProfileId($profile->id)->orderByDesc('created_at')->paginate(10);
|
||||
return view('settings.reports', compact('reports'));
|
||||
}
|
||||
|
||||
public function metroDarkMode(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
@ -223,97 +227,7 @@ class SettingsController extends Controller
|
|||
$sponsors->sponsors = json_encode($res);
|
||||
$sponsors->save();
|
||||
$sponsors = $res;
|
||||
return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!');
|
||||
}
|
||||
|
||||
public function timelineSettings(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$top = Redis::zscore('pf:tl:top', $pid) != false;
|
||||
$replies = Redis::zscore('pf:tl:replies', $pid) != false;
|
||||
return view('settings.timeline', compact('top', 'replies'));
|
||||
}
|
||||
|
||||
public function updateTimelineSettings(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$top = $request->has('top') && $request->input('top') === 'on';
|
||||
$replies = $request->has('replies') && $request->input('replies') === 'on';
|
||||
|
||||
if($top) {
|
||||
Redis::zadd('pf:tl:top', $pid, $pid);
|
||||
} else {
|
||||
Redis::zrem('pf:tl:top', $pid);
|
||||
}
|
||||
|
||||
if($replies) {
|
||||
Redis::zadd('pf:tl:replies', $pid, $pid);
|
||||
} else {
|
||||
Redis::zrem('pf:tl:replies', $pid);
|
||||
}
|
||||
return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!');;
|
||||
}
|
||||
|
||||
public function mediaSettings(Request $request)
|
||||
{
|
||||
$setting = UserSetting::whereUserId($request->user()->id)->firstOrFail();
|
||||
$compose = $setting->compose_settings ? json_decode($setting->compose_settings, true) : [
|
||||
'default_license' => null,
|
||||
'media_descriptions' => false
|
||||
];
|
||||
return view('settings.media', compact('compose'));
|
||||
}
|
||||
|
||||
public function updateMediaSettings(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'default' => 'required|int|min:1|max:16',
|
||||
'sync' => 'nullable',
|
||||
'media_descriptions' => 'nullable'
|
||||
]);
|
||||
|
||||
$license = $request->input('default');
|
||||
$sync = $request->input('sync') == 'on';
|
||||
$media_descriptions = $request->input('media_descriptions') == 'on';
|
||||
$uid = $request->user()->id;
|
||||
|
||||
$setting = UserSetting::whereUserId($uid)->firstOrFail();
|
||||
$compose = json_decode($setting->compose_settings, true);
|
||||
$changed = false;
|
||||
|
||||
if($sync) {
|
||||
$key = 'pf:settings:mls_recently:'.$uid;
|
||||
if(Cache::get($key) == 2) {
|
||||
$msg = 'You can only sync licenses twice per 24 hours. Try again later.';
|
||||
return redirect(route('settings'))
|
||||
->with('error', $msg);
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($compose['default_license']) || $compose['default_license'] !== $license) {
|
||||
$compose['default_license'] = (int) $license;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if(!isset($compose['media_descriptions']) || $compose['media_descriptions'] !== $media_descriptions) {
|
||||
$compose['media_descriptions'] = $media_descriptions;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if($changed) {
|
||||
$setting->compose_settings = json_encode($compose);
|
||||
$setting->save();
|
||||
Cache::forget('profile:compose:settings:' . $request->user()->id);
|
||||
}
|
||||
|
||||
if($sync) {
|
||||
$val = Cache::has($key) ? 2 : 1;
|
||||
Cache::put($key, $val, 86400);
|
||||
MediaSyncLicensePipeline::dispatch($uid, $license);
|
||||
return redirect(route('settings'))->with('status', 'Media licenses successfully synced! It may take a few minutes to take effect for every post.');
|
||||
}
|
||||
|
||||
return redirect(route('settings'))->with('status', 'Media settings successfully updated!');
|
||||
return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!');;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ use App\Util\Lexer\PrettyNumber;
|
|||
use App\{Follower, Page, Profile, Status, User, UserFilter};
|
||||
use App\Util\Localization\Localization;
|
||||
use App\Services\FollowerService;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
|
@ -24,7 +23,16 @@ class SiteController extends Controller
|
|||
|
||||
public function homeGuest()
|
||||
{
|
||||
return view('site.index');
|
||||
$data = Cache::remember('site:landing:data', now()->addHours(3), function() {
|
||||
return [
|
||||
'stats' => [
|
||||
'posts' => App\Util\Lexer\PrettyNumber::convert(App\Status::count()),
|
||||
'likes' => App\Util\Lexer\PrettyNumber::convert(App\Like::count()),
|
||||
'hashtags' => App\Util\Lexer\PrettyNumber::convert(App\StatusHashtag::count())
|
||||
],
|
||||
];
|
||||
});
|
||||
return view('site.index', compact('data'));
|
||||
}
|
||||
|
||||
public function homeTimeline(Request $request)
|
||||
|
@ -54,12 +62,16 @@ class SiteController extends Controller
|
|||
|
||||
public function about()
|
||||
{
|
||||
return Cache::remember('site.about_v2', now()->addMinutes(15), function() {
|
||||
$user_count = number_format(User::count());
|
||||
$post_count = number_format(Status::count());
|
||||
$rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null;
|
||||
return view('site.about', compact('rules', 'user_count', 'post_count'))->render();
|
||||
$page = Page::whereSlug('/site/about')->whereActive(true)->first();
|
||||
$stats = Cache::remember('site:about:stats-v1', now()->addHours(12), function() {
|
||||
return [
|
||||
'posts' => Status::count(),
|
||||
'users' => User::whereNull('status')->count(),
|
||||
'admin' => User::whereIsAdmin(true)->first()
|
||||
];
|
||||
});
|
||||
$path = $page ? 'site.about-custom' : 'site.about';
|
||||
return view($path, compact('page', 'stats'));
|
||||
}
|
||||
|
||||
public function language()
|
||||
|
@ -96,12 +108,10 @@ class SiteController extends Controller
|
|||
|
||||
public function redirectUrl(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'url' => 'required|url'
|
||||
]);
|
||||
$url = request()->input('url');
|
||||
abort_if(Helpers::validateUrl($url) == false, 404);
|
||||
return view('site.redirect', compact('url'));
|
||||
}
|
||||
|
||||
|
@ -139,19 +149,4 @@ class SiteController extends Controller
|
|||
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
public function legacyWebfingerRedirect(Request $request, $username, $domain)
|
||||
{
|
||||
$un = '@'.$username.'@'.$domain;
|
||||
$profile = Profile::whereUsername($un)
|
||||
->firstOrFail();
|
||||
|
||||
if($profile->domain == null) {
|
||||
$url = "/$profile->username";
|
||||
} else {
|
||||
$url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url();
|
||||
}
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Cache;
|
||||
use DB;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Util\Localization\Localization;
|
||||
|
||||
class SpaController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
abort_unless(config('exp.spa'), 404);
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function webPost(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('exp.spa'), 404);
|
||||
if($request->user()) {
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
if(SnowflakeService::byDate(now()->subDays(30)) > $id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$post = StatusService::get($id);
|
||||
|
||||
if(
|
||||
$post &&
|
||||
isset($post['url']) &&
|
||||
isset($post['local']) &&
|
||||
$post['local'] === true
|
||||
) {
|
||||
return redirect($post['url']);
|
||||
}
|
||||
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function webProfile(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('exp.spa'), 404);
|
||||
if($request->user()) {
|
||||
if(substr($id, 0, 1) == '@') {
|
||||
$id = AccountService::usernameToId(substr($id, 1));
|
||||
return redirect("/i/web/profile/{$id}");
|
||||
}
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
$account = AccountService::get($id);
|
||||
|
||||
if($account && isset($account['url'])) {
|
||||
return redirect($account['url']);
|
||||
}
|
||||
|
||||
return redirect('404');
|
||||
}
|
||||
|
||||
public function updateLanguage(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'v' => 'required|in:0.1,0.2',
|
||||
'l' => 'required|alpha_dash|max:5'
|
||||
]);
|
||||
|
||||
$lang = $request->input('l');
|
||||
$user = $request->user();
|
||||
|
||||
abort_if(!in_array($lang, Localization::languages()), 400);
|
||||
|
||||
$user->language = $lang;
|
||||
$user->save();
|
||||
session()->put('locale', $lang);
|
||||
|
||||
return ['language' => $lang];
|
||||
}
|
||||
|
||||
public function getPrivacy()
|
||||
{
|
||||
$body = $this->markdownToHtml('views/page/privacy.md');
|
||||
return [
|
||||
'body' => $body
|
||||
];
|
||||
}
|
||||
|
||||
public function getTerms()
|
||||
{
|
||||
$body = $this->markdownToHtml('views/page/terms.md');
|
||||
return [
|
||||
'body' => $body
|
||||
];
|
||||
}
|
||||
|
||||
protected function markdownToHtml($src, $ttl = 600)
|
||||
{
|
||||
return Cache::remember(
|
||||
'pf:doc_cache:markdown:' . $src,
|
||||
$ttl,
|
||||
function() use($src) {
|
||||
$path = resource_path($src);
|
||||
$file = file_get_contents($path);
|
||||
$converter = new CommonMarkConverter();
|
||||
return (string) $converter->convertToHtml($file);
|
||||
});
|
||||
}
|
||||
|
||||
public function usernameRedirect(Request $request, $username)
|
||||
{
|
||||
$id = AccountService::usernameToId($username);
|
||||
if(!$id) {
|
||||
return redirect('/i/web/404');
|
||||
}
|
||||
return redirect('/i/web/profile/' . $id);
|
||||
}
|
||||
}
|
|
@ -6,30 +6,24 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
|||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\Jobs\SharePipeline\SharePipeline;
|
||||
use App\Jobs\SharePipeline\UndoSharePipeline;
|
||||
use App\AccountInterstitial;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\StatusArchived;
|
||||
use App\StatusView;
|
||||
use App\Transformer\ActivityPub\StatusTransformer;
|
||||
use App\Transformer\ActivityPub\Verb\Note;
|
||||
use App\Transformer\ActivityPub\Verb\Question;
|
||||
use App\User;
|
||||
use Auth, DB, Cache;
|
||||
use Auth, Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use App\Util\Media\Filter;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Media\License;
|
||||
use App\Services\ReblogService;
|
||||
|
||||
class StatusController extends Controller
|
||||
{
|
||||
public function show(Request $request, $username, $id)
|
||||
public function show(Request $request, $username, int $id)
|
||||
{
|
||||
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
|
||||
|
@ -74,7 +68,7 @@ class StatusController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
||||
if ($request->wantsJson() && config('federation.activitypub.enabled')) {
|
||||
return $this->showActivityPub($request, $status);
|
||||
}
|
||||
|
||||
|
@ -84,7 +78,16 @@ class StatusController extends Controller
|
|||
|
||||
public function shortcodeRedirect(Request $request, $id)
|
||||
{
|
||||
abort(404);
|
||||
abort_if(strlen($id) < 5, 404);
|
||||
if(!Auth::check()) {
|
||||
return redirect('/login?next='.urlencode('/' . $request->path()));
|
||||
}
|
||||
$id = HashidService::decode($id);
|
||||
$status = Status::find($id);
|
||||
if(!$status) {
|
||||
return redirect('/404');
|
||||
}
|
||||
return redirect($status->url());
|
||||
}
|
||||
|
||||
public function showId(int $id)
|
||||
|
@ -208,8 +211,6 @@ class StatusController extends Controller
|
|||
|
||||
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) {
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatch($status);
|
||||
|
@ -232,21 +233,21 @@ class StatusController extends Controller
|
|||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$status = Status::whereScope('public')
|
||||
$status = Status::withCount('shares')
|
||||
->whereIn('scope', ['public', 'unlisted'])
|
||||
->findOrFail($request->input('item'));
|
||||
|
||||
$count = $status->reblogs_count;
|
||||
$count = $status->shares()->count();
|
||||
|
||||
$exists = Status::whereProfileId(Auth::user()->profile->id)
|
||||
->whereReblogOfId($status->id)
|
||||
->exists();
|
||||
if ($exists == true) {
|
||||
->count();
|
||||
if ($exists !== 0) {
|
||||
$shares = Status::whereProfileId(Auth::user()->profile->id)
|
||||
->whereReblogOfId($status->id)
|
||||
->get();
|
||||
foreach ($shares as $share) {
|
||||
UndoSharePipeline::dispatch($share);
|
||||
ReblogService::del($profile->id, $status->id);
|
||||
$share->delete();
|
||||
$count--;
|
||||
}
|
||||
} else {
|
||||
|
@ -257,11 +258,14 @@ class StatusController extends Controller
|
|||
$share->save();
|
||||
$count++;
|
||||
SharePipeline::dispatch($share);
|
||||
ReblogService::add($profile->id, $status->id);
|
||||
}
|
||||
|
||||
if($count >= 0) {
|
||||
$status->reblogs_count = $count;
|
||||
$status->save();
|
||||
}
|
||||
|
||||
Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
|
||||
StatusService::del($status->id);
|
||||
|
||||
if ($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
|
||||
|
@ -274,12 +278,11 @@ class StatusController extends Controller
|
|||
|
||||
public function showActivityPub(Request $request, $status)
|
||||
{
|
||||
$object = $status->type == 'poll' ? new Question() : new Note();
|
||||
$fractal = new Fractal\Manager();
|
||||
$resource = new Fractal\Resource\Item($status, $object);
|
||||
$resource = new Fractal\Resource\Item($status, new Note());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function edit(Request $request, $username, $id)
|
||||
|
@ -288,9 +291,9 @@ class StatusController extends Controller
|
|||
$user = Auth::user()->profile;
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->with(['media'])
|
||||
->where('created_at', '>', now()->subHours(24))
|
||||
->findOrFail($id);
|
||||
$licenses = License::get();
|
||||
return view('status.edit', compact('user', 'status', 'licenses'));
|
||||
return view('status.edit', compact('user', 'status'));
|
||||
}
|
||||
|
||||
public function editStore(Request $request, $username, $id)
|
||||
|
@ -299,21 +302,41 @@ class StatusController extends Controller
|
|||
$user = Auth::user()->profile;
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->with(['media'])
|
||||
->where('created_at', '>', now()->subHours(24))
|
||||
->findOrFail($id);
|
||||
|
||||
$this->validate($request, [
|
||||
'license' => 'nullable|integer|min:1|max:16',
|
||||
'id' => 'required|integer|min:1',
|
||||
'caption' => 'nullable',
|
||||
'filter' => 'nullable|alpha_dash|max:30',
|
||||
]);
|
||||
|
||||
$licenseId = $request->input('license');
|
||||
$id = $request->input('id');
|
||||
$caption = $request->input('caption');
|
||||
$filter = $request->input('filter');
|
||||
|
||||
$status->media->each(function($media) use($licenseId) {
|
||||
$media->license = $licenseId;
|
||||
$media = Media::whereProfileId($user->id)
|
||||
->whereStatusId($status->id)
|
||||
->findOrFail($id);
|
||||
|
||||
$changed = false;
|
||||
|
||||
if ($media->caption != $caption) {
|
||||
$media->caption = $caption;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($media->filter_class != $filter && in_array($filter, Filter::classes())) {
|
||||
$media->filter_class = $filter;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($changed === true) {
|
||||
$media->save();
|
||||
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
|
||||
});
|
||||
}
|
||||
|
||||
return redirect($status->url());
|
||||
return response()->json([], 200);
|
||||
}
|
||||
|
||||
protected function authCheck()
|
||||
|
@ -331,7 +354,7 @@ class StatusController extends Controller
|
|||
|
||||
public static function mimeTypeCheck($mimes)
|
||||
{
|
||||
$allowed = explode(',', config_cache('pixelfed.media_types'));
|
||||
$allowed = explode(',', config('pixelfed.media_types'));
|
||||
$count = count($mimes);
|
||||
$photos = 0;
|
||||
$videos = 0;
|
||||
|
@ -361,8 +384,6 @@ class StatusController extends Controller
|
|||
if($photos >= 1 && $videos >= 1) {
|
||||
return 'photo:video:album';
|
||||
}
|
||||
|
||||
return 'text';
|
||||
}
|
||||
|
||||
public function toggleVisibility(Request $request) {
|
||||
|
@ -387,33 +408,4 @@ class StatusController extends Controller
|
|||
|
||||
return response()->json([200]);
|
||||
}
|
||||
|
||||
public function storeView(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$views = $request->input('_v');
|
||||
$uid = $request->user()->profile_id;
|
||||
|
||||
if(empty($views) || !is_array($views)) {
|
||||
return response()->json(0);
|
||||
}
|
||||
|
||||
Cache::forget('profile:home-timeline-cursor:' . $request->user()->id);
|
||||
|
||||
foreach($views as $view) {
|
||||
if(!isset($view['sid']) || !isset($view['pid'])) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use($view, $uid) {
|
||||
StatusView::firstOrCreate([
|
||||
'status_id' => $view['sid'],
|
||||
'status_profile_id' => $view['pid'],
|
||||
'profile_id' => $uid
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,501 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Report;
|
||||
use App\DirectMessage;
|
||||
use App\Notification;
|
||||
use App\Status;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollVote;
|
||||
use App\Services\ProfileService;
|
||||
use App\Services\StoryService;
|
||||
use Cache, Storage;
|
||||
use Image as Intervention;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\MediaPathService;
|
||||
use FFMpeg;
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Format\Video\X264;
|
||||
use App\Jobs\StoryPipeline\StoryReactionDeliver;
|
||||
use App\Jobs\StoryPipeline\StoryReplyDeliver;
|
||||
use App\Jobs\StoryPipeline\StoryFanout;
|
||||
use App\Jobs\StoryPipeline\StoryDelete;
|
||||
use ImageOptimizer;
|
||||
|
||||
class StoryComposeController extends Controller
|
||||
{
|
||||
public function apiV1Add(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:image/jpeg,image/png,video/mp4',
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
]);
|
||||
|
||||
$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->storePhoto($photo, $user);
|
||||
|
||||
$story = new Story();
|
||||
$story->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
|
||||
];
|
||||
|
||||
if($story->type === 'video') {
|
||||
$video = FFMpeg::open($path);
|
||||
$duration = $video->getDurationInSeconds();
|
||||
$res['media_duration'] = $duration;
|
||||
if($duration > 500) {
|
||||
Storage::delete($story->path);
|
||||
$story->delete();
|
||||
return response()->json([
|
||||
'message' => 'Video duration cannot exceed 60 seconds'
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function storePhoto($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->storeAs($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);
|
||||
$img->orientate();
|
||||
$img->save($fpath, config_cache('pixelfed.image_quality'));
|
||||
$img->destroy();
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function cropPhoto(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'media_id' => 'required|integer|min:1',
|
||||
'width' => 'required',
|
||||
'height' => 'required',
|
||||
'x' => 'required',
|
||||
'y' => 'required'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$id = $request->input('media_id');
|
||||
$width = round($request->input('width'));
|
||||
$height = round($request->input('height'));
|
||||
$x = round($request->input('x'));
|
||||
$y = round($request->input('y'));
|
||||
|
||||
$story = Story::whereProfileId($user->profile_id)->findOrFail($id);
|
||||
|
||||
$path = storage_path('app/' . $story->path);
|
||||
|
||||
if(!is_file($path)) {
|
||||
abort(400, 'Invalid or missing media.');
|
||||
}
|
||||
|
||||
if($story->type === 'photo') {
|
||||
$img = Intervention::make($path);
|
||||
$img->crop($width, $height, $x, $y);
|
||||
$img->resize(1080, 1920, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
$img->save($path, config_cache('pixelfed.image_quality'));
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully cropped',
|
||||
];
|
||||
}
|
||||
|
||||
public function publishStory(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'media_id' => 'required',
|
||||
'duration' => 'required|integer|min:3|max:120',
|
||||
'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 apiV1Delete(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 compose(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
return view('stories.compose');
|
||||
}
|
||||
|
||||
public function createPoll(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
abort_if(!config_cache('instance.polls.enabled'), 404);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function publishStoryPoll(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'question' => 'required|string|min:6|max:140',
|
||||
'options' => 'required|array|min:2|max:4',
|
||||
'can_reply' => 'required|boolean',
|
||||
'can_react' => 'required|boolean'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$count = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
|
||||
if($count >= Story::MAX_PER_DAY) {
|
||||
abort(418, 'You have reached your limit for new Stories today.');
|
||||
}
|
||||
|
||||
$story = new Story;
|
||||
$story->type = 'poll';
|
||||
$story->story = json_encode([
|
||||
'question' => $request->input('question'),
|
||||
'options' => $request->input('options')
|
||||
]);
|
||||
$story->public = false;
|
||||
$story->local = true;
|
||||
$story->profile_id = $pid;
|
||||
$story->expires_at = now()->addMinutes(1440);
|
||||
$story->duration = 30;
|
||||
$story->can_reply = false;
|
||||
$story->can_react = false;
|
||||
$story->save();
|
||||
|
||||
$poll = new Poll;
|
||||
$poll->story_id = $story->id;
|
||||
$poll->profile_id = $pid;
|
||||
$poll->poll_options = $request->input('options');
|
||||
$poll->expires_at = $story->expires_at;
|
||||
$poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
|
||||
return 0;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
$story->active = true;
|
||||
$story->save();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully published',
|
||||
];
|
||||
}
|
||||
|
||||
public function storyPollVote(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sid' => 'required',
|
||||
'ci' => 'required|integer|min:0|max:3'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$ci = $request->input('ci');
|
||||
$story = Story::findOrFail($request->input('sid'));
|
||||
abort_if(!FollowerService::follows($pid, $story->profile_id), 403);
|
||||
$poll = Poll::whereStoryId($story->id)->firstOrFail();
|
||||
|
||||
$vote = new PollVote;
|
||||
$vote->profile_id = $pid;
|
||||
$vote->poll_id = $poll->id;
|
||||
$vote->story_id = $story->id;
|
||||
$vote->status_id = null;
|
||||
$vote->choice = $ci;
|
||||
$vote->save();
|
||||
|
||||
$poll->votes_count = $poll->votes_count + 1;
|
||||
$poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) {
|
||||
return $ci == $key ? $tally + 1 : $tally;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function storeReport(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'id' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$sid = $request->input('id');
|
||||
$type = $request->input('type');
|
||||
|
||||
$types = [
|
||||
// original 3
|
||||
'spam',
|
||||
'sensitive',
|
||||
'abusive',
|
||||
|
||||
// new
|
||||
'underage',
|
||||
'copyright',
|
||||
'impersonation',
|
||||
'scam',
|
||||
'terrorism'
|
||||
];
|
||||
|
||||
abort_if(!in_array($type, $types), 422, 'Invalid story report type');
|
||||
|
||||
$story = Story::findOrFail($sid);
|
||||
|
||||
abort_if($story->profile_id == $pid, 422, 'Cannot report your own story');
|
||||
abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow');
|
||||
|
||||
if( Report::whereProfileId($pid)
|
||||
->whereObjectType('App\Story')
|
||||
->whereObjectId($story->id)
|
||||
->exists()
|
||||
) {
|
||||
return response()->json(['error' => [
|
||||
'code' => 409,
|
||||
'message' => 'Cannot report the same story again'
|
||||
]], 409);
|
||||
}
|
||||
|
||||
$report = new Report;
|
||||
$report->profile_id = $pid;
|
||||
$report->user_id = $request->user()->id;
|
||||
$report->object_id = $story->id;
|
||||
$report->object_type = 'App\Story';
|
||||
$report->reported_profile_id = $story->profile_id;
|
||||
$report->type = $type;
|
||||
$report->message = null;
|
||||
$report->save();
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function react(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'sid' => 'required',
|
||||
'reaction' => 'required|string'
|
||||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$text = $request->input('reaction');
|
||||
|
||||
$story = Story::findOrFail($request->input('sid'));
|
||||
|
||||
abort_if(!$story->can_react, 422);
|
||||
abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story');
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $pid;
|
||||
$status->type = 'story:reaction';
|
||||
$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,
|
||||
'reaction' => $text
|
||||
]);
|
||||
$status->save();
|
||||
|
||||
$dm = new DirectMessage;
|
||||
$dm->to_id = $story->profile_id;
|
||||
$dm->from_id = $pid;
|
||||
$dm->type = 'story:react';
|
||||
$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)),
|
||||
'reaction' => $text
|
||||
]);
|
||||
$dm->save();
|
||||
|
||||
if($story->local) {
|
||||
// generate notification
|
||||
$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:react';
|
||||
$n->message = "{$request->user()->username} reacted to your story";
|
||||
$n->rendered = "{$request->user()->username} reacted to your story";
|
||||
$n->save();
|
||||
} else {
|
||||
StoryReactionDeliver::dispatch($story, $status)->onQueue('story');
|
||||
}
|
||||
|
||||
StoryService::reactIncrement($story->id, $pid);
|
||||
|
||||
return 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();
|
||||
|
||||
if($story->local) {
|
||||
// generate notification
|
||||
$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 200;
|
||||
}
|
||||
}
|
|
@ -4,128 +4,248 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\DirectMessage;
|
||||
use App\Follower;
|
||||
use App\Notification;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
use App\Services\PollService;
|
||||
use App\Services\ProfileService;
|
||||
use App\Services\StoryService;
|
||||
use Cache, Storage;
|
||||
use Image as Intervention;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\MediaPathService;
|
||||
use FFMpeg;
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Format\Video\X264;
|
||||
use League\Fractal\Manager;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Resource\Item;
|
||||
use App\Transformer\ActivityPub\Verb\StoryVerb;
|
||||
use App\Jobs\StoryPipeline\StoryViewDeliver;
|
||||
|
||||
class StoryController extends StoryComposeController
|
||||
|
||||
class StoryController extends Controller
|
||||
{
|
||||
public function recent(Request $request)
|
||||
public function apiV1Add(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:image/jpeg,image/png,video/mp4',
|
||||
'max:' . config('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if(Story::whereProfileId($user->profile_id)->where('expires_at', '>', now())->count() >= Story::MAX_PER_DAY) {
|
||||
abort(400, 'You have reached your limit for new Stories today.');
|
||||
}
|
||||
|
||||
$photo = $request->file('file');
|
||||
$path = $this->storePhoto($photo);
|
||||
|
||||
$story = new Story();
|
||||
$story->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->expires_at = now()->addHours(24);
|
||||
$story->save();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully added',
|
||||
'media_url' => url(Storage::url($story->path))
|
||||
];
|
||||
}
|
||||
|
||||
protected function storePhoto($photo)
|
||||
{
|
||||
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
|
||||
$sid = (string) Str::uuid();
|
||||
$rid = Str::random(9).'.'.Str::random(9);
|
||||
$mimes = explode(',', config('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'video/mp4'
|
||||
]) == false) {
|
||||
abort(400, 'Invalid media type');
|
||||
return;
|
||||
}
|
||||
|
||||
$storagePath = "public/_esm.t2/{$monthHash}/{$sid}/{$rid}";
|
||||
$path = $photo->store($storagePath);
|
||||
if(in_array($photo->getMimeType(), ['image/jpeg','image/png',])) {
|
||||
$fpath = storage_path('app/' . $path);
|
||||
$img = Intervention::make($fpath);
|
||||
$img->orientate();
|
||||
$img->save($fpath, config('pixelfed.image_quality'));
|
||||
$img->destroy();
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function apiV1Delete(Request $request, $id)
|
||||
{
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$story = Story::whereProfileId($user->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
if(Storage::exists($story->path) == true) {
|
||||
Storage::delete($story->path);
|
||||
}
|
||||
|
||||
$story->delete();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully deleted'
|
||||
];
|
||||
}
|
||||
|
||||
public function apiV1Recent(Request $request)
|
||||
{
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$profile = $request->user()->profile;
|
||||
$following = $profile->following->pluck('id')->toArray();
|
||||
|
||||
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()
|
||||
->map(function($s) {
|
||||
$r = new \StdClass;
|
||||
$r->id = $s->id;
|
||||
$r->profile_id = $s->profile_id;
|
||||
$r->type = $s->type;
|
||||
$r->path = $s->path;
|
||||
return $r;
|
||||
})
|
||||
->unique('profile_id');
|
||||
$db = Story::with('profile')
|
||||
->whereIn('profile_id', $following)
|
||||
->where('expires_at', '>', now())
|
||||
->distinct('profile_id')
|
||||
->take(9)
|
||||
->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)
|
||||
->groupBy('followers.following_id')
|
||||
->orderByDesc('id')
|
||||
$db = Story::with('profile')
|
||||
->whereIn('profile_id', $following)
|
||||
->where('expires_at', '>', now())
|
||||
->orderByDesc('expires_at')
|
||||
->groupBy('profile_id')
|
||||
->take(9)
|
||||
->get();
|
||||
}
|
||||
|
||||
$res = $s->map(function($s) use($pid) {
|
||||
$profile = AccountService::get($s->profile_id);
|
||||
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
|
||||
url("/i/rs/{$profile['id']}");
|
||||
$stories = $db->map(function($s, $k) {
|
||||
return [
|
||||
'pid' => $profile['id'],
|
||||
'avatar' => $profile['avatar'],
|
||||
'local' => $profile['local'],
|
||||
'username' => $profile['acct'],
|
||||
'latest' => [
|
||||
'id' => $s->id,
|
||||
'type' => $s->type,
|
||||
'preview_url' => url(Storage::url($s->path))
|
||||
],
|
||||
'url' => $url,
|
||||
'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)),
|
||||
'sid' => $s->id
|
||||
'id' => (string) $s->id,
|
||||
'photo' => $s->profile->avatarUrl(),
|
||||
'name' => $s->profile->username,
|
||||
'link' => $s->profile->url(),
|
||||
'lastUpdated' => (int) $s->created_at->format('U'),
|
||||
'seen' => $s->seen(),
|
||||
'items' => [],
|
||||
'pid' => (string) $s->profile->id
|
||||
];
|
||||
})
|
||||
->sortBy('seen')
|
||||
->values();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
});
|
||||
|
||||
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function profile(Request $request, $id)
|
||||
public function apiV1Fetch(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$authed = $request->user()->profile_id;
|
||||
$authed = $request->user()->profile;
|
||||
$profile = Profile::findOrFail($id);
|
||||
|
||||
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
|
||||
return [];
|
||||
if($id == $authed->id) {
|
||||
$publicOnly = true;
|
||||
} else {
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
}
|
||||
|
||||
$stories = Story::whereProfileId($profile->id)
|
||||
->whereActive(true)
|
||||
->orderBy('expires_at')
|
||||
->orderBy('expires_at', 'desc')
|
||||
->where('expires_at', '>', now())
|
||||
->when(!$publicOnly, function($query, $publicOnly) {
|
||||
return $query->wherePublic(true);
|
||||
})
|
||||
->get()
|
||||
->map(function($s, $k) use($authed) {
|
||||
$seen = StoryService::hasSeen($authed, $s->id);
|
||||
$res = [
|
||||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => (string) $s->id,
|
||||
'type' => $s->type,
|
||||
'duration' => $s->duration,
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'created_at' => $s->created_at->toAtomString(),
|
||||
'expires_at' => $s->expires_at->toAtomString(),
|
||||
'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null,
|
||||
'seen' => $seen,
|
||||
'progress' => $seen ? 100 : 0,
|
||||
'can_reply' => (bool) $s->can_reply,
|
||||
'can_react' => (bool) $s->can_react
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $s->created_at->format('U'),
|
||||
'expires_at' => (int) $s->expires_at->format('U'),
|
||||
'seen' => $s->seen()
|
||||
];
|
||||
|
||||
if($s->type == 'poll') {
|
||||
$res['question'] = json_decode($s->story, true)['question'];
|
||||
$res['options'] = json_decode($s->story, true)['options'];
|
||||
$res['voted'] = PollService::votedStory($s->id, $authed);
|
||||
if($res['voted']) {
|
||||
$res['voted_index'] = PollService::storyChoice($s->id, $authed);
|
||||
}
|
||||
})->toArray();
|
||||
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return $res;
|
||||
public function apiV1Item(Request $request, $id)
|
||||
{
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
$story = Story::with('profile')
|
||||
->where('expires_at', '>', now())
|
||||
->findOrFail($id);
|
||||
|
||||
$profile = $story->profile;
|
||||
if($story->profile_id == $authed->id) {
|
||||
$publicOnly = true;
|
||||
} else {
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
}
|
||||
|
||||
abort_if(!$publicOnly, 403);
|
||||
|
||||
$res = [
|
||||
'id' => (string) $story->id,
|
||||
'type' => Str::endsWith($story->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($story->path)),
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $story->created_at->format('U'),
|
||||
'expires_at' => (int) $story->expires_at->format('U'),
|
||||
'seen' => $story->seen()
|
||||
];
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function apiV1Profile(Request $request, $id)
|
||||
{
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
$profile = Profile::findOrFail($id);
|
||||
if($id == $authed->id) {
|
||||
$publicOnly = true;
|
||||
} else {
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
}
|
||||
|
||||
$stories = Story::whereProfileId($profile->id)
|
||||
->orderBy('expires_at')
|
||||
->where('expires_at', '>', now())
|
||||
->when(!$publicOnly, function($query, $publicOnly) {
|
||||
return $query->wherePublic(true);
|
||||
})
|
||||
->get()
|
||||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $s->created_at->format('U'),
|
||||
'expires_at' => (int) $s->expires_at->format('U'),
|
||||
'seen' => $s->seen()
|
||||
];
|
||||
})->toArray();
|
||||
if(count($stories) == 0) {
|
||||
return [];
|
||||
|
@ -133,159 +253,110 @@ class StoryController extends StoryComposeController
|
|||
$cursor = count($stories) - 1;
|
||||
$stories = [[
|
||||
'id' => (string) $stories[$cursor]['id'],
|
||||
'nodes' => $stories,
|
||||
'account' => AccountService::get($profile->id),
|
||||
'photo' => $profile->avatarUrl(),
|
||||
'name' => $profile->username,
|
||||
'link' => $profile->url(),
|
||||
'lastUpdated' => (int) now()->format('U'),
|
||||
'seen' => null,
|
||||
'items' => $stories,
|
||||
'pid' => (string) $profile->id
|
||||
]];
|
||||
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function viewed(Request $request)
|
||||
public function apiV1Viewed(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|min:1',
|
||||
'id' => 'required|integer|min:1|exists:stories',
|
||||
]);
|
||||
$id = $request->input('id');
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
|
||||
$story = Story::with('profile')
|
||||
->where('expires_at', '>', now())
|
||||
->orderByDesc('expires_at')
|
||||
->findOrFail($id);
|
||||
$exp = $story->expires_at;
|
||||
|
||||
$profile = $story->profile;
|
||||
|
||||
if($story->profile_id == $authed->id) {
|
||||
return [];
|
||||
$publicOnly = true;
|
||||
} else {
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
}
|
||||
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
abort_if(!$publicOnly, 403);
|
||||
|
||||
|
||||
$v = StoryView::firstOrCreate([
|
||||
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 exists(Request $request, $id)
|
||||
public function apiV1Exists(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
return response()->json(Story::whereProfileId($id)
|
||||
->whereActive(true)
|
||||
->exists());
|
||||
$res = (bool) Story::whereProfileId($id)
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function apiV1Me(Request $request)
|
||||
{
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$profile = $request->user()->profile;
|
||||
$stories = Story::whereProfileId($profile->id)
|
||||
->orderBy('expires_at')
|
||||
->where('expires_at', '>', now())
|
||||
->get()
|
||||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $s->created_at->format('U'),
|
||||
'expires_at' => (int) $s->expires_at->format('U'),
|
||||
'seen' => true
|
||||
];
|
||||
})->toArray();
|
||||
$ts = count($stories) ? last($stories)['time'] : null;
|
||||
$res = [
|
||||
'id' => (string) $profile->id,
|
||||
'photo' => $profile->avatarUrl(),
|
||||
'name' => $profile->username,
|
||||
'link' => $profile->url(),
|
||||
'lastUpdated' => $ts,
|
||||
'seen' => true,
|
||||
'items' => $stories
|
||||
];
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function compose(Request $request)
|
||||
{
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
return view('stories.compose');
|
||||
}
|
||||
|
||||
public function iRedirect(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 404);
|
||||
$username = $user->username;
|
||||
return redirect("/stories/{$username}");
|
||||
}
|
||||
|
||||
public function viewers(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sid' => 'required|string'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$story = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->findOrFail($sid);
|
||||
|
||||
$viewers = StoryView::whereStoryId($story->id)
|
||||
->latest()
|
||||
->simplePaginate(10)
|
||||
->map(function($view) {
|
||||
return AccountService::get($view->profile_id);
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function remoteStory(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$profile = Profile::findOrFail($id);
|
||||
if($profile->user_id != null || $profile->domain == null) {
|
||||
return redirect('/stories/' . $profile->username);
|
||||
}
|
||||
$pid = $profile->id;
|
||||
return view('stories.show_remote', compact('pid'));
|
||||
}
|
||||
|
||||
public function pollResults(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sid' => 'required|string'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$story = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->findOrFail($sid);
|
||||
|
||||
return PollService::storyResults($sid);
|
||||
}
|
||||
|
||||
public function getActivityObject(Request $request, $username, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled'), 404);
|
||||
|
||||
if(!$request->wantsJson()) {
|
||||
return redirect('/stories/' . $username);
|
||||
}
|
||||
|
||||
abort_if(!$request->hasHeader('Authorization'), 404);
|
||||
|
||||
$profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail();
|
||||
$story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id);
|
||||
|
||||
abort_if($story->bearcap_token == null, 404);
|
||||
abort_if(now()->gt($story->expires_at), 404);
|
||||
$token = substr($request->header('Authorization'), 7);
|
||||
abort_if(hash_equals($story->bearcap_token, $token) === false, 404);
|
||||
abort_if($story->created_at->lt(now()->subMinutes(20)), 404);
|
||||
|
||||
$fractal = new Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Item($story, new StoryVerb());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showSystemStory()
|
||||
{
|
||||
// return view('stories.system');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth, Cache;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TimelineController extends Controller
|
||||
|
@ -23,7 +29,6 @@ class TimelineController extends Controller
|
|||
|
||||
public function network(Request $request)
|
||||
{
|
||||
abort_if(config('federation.network_timeline') == false, 404);
|
||||
$this->validate($request, [
|
||||
'layout' => 'nullable|string|in:grid,feed'
|
||||
]);
|
||||
|
|
|
@ -9,16 +9,19 @@ use Illuminate\Support\Str;
|
|||
|
||||
class UserInviteController extends Controller
|
||||
{
|
||||
public function create(Request $request)
|
||||
public function __construct()
|
||||
{
|
||||
abort_if(!config('pixelfed.user_invites.enabled'), 404);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
abort_unless(Auth::check(), 403);
|
||||
return view('settings.invites.create');
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
{
|
||||
abort_if(!config('pixelfed.user_invites.enabled'), 404);
|
||||
abort_unless(Auth::check(), 403);
|
||||
$invites = UserInvite::whereUserId(Auth::id())->paginate(10);
|
||||
$limit = config('pixelfed.user_invites.limit.total');
|
||||
|
@ -28,7 +31,6 @@ class UserInviteController extends Controller
|
|||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_if(!config('pixelfed.user_invites.enabled'), 404);
|
||||
abort_unless(Auth::check(), 403);
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email|unique:users|unique:user_invites',
|
||||
|
|
|
@ -32,11 +32,10 @@ class Kernel extends HttpKernel
|
|||
\App\Http\Middleware\FrameGuard::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
|
||||
// 'restricted',
|
||||
],
|
||||
|
||||
|
|
|
@ -17,13 +17,12 @@ 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/*',
|
||||
'i/verify-email*',
|
||||
'i/verify-email',
|
||||
'log*',
|
||||
'site*',
|
||||
'i/confirm-email/*',
|
||||
'settings/home',
|
||||
'settings/email'
|
||||
|
|
|
@ -54,10 +54,11 @@ class AvatarOptimize implements ShouldQueue
|
|||
$img->fit(200, 200, function ($constraint) {
|
||||
$constraint->upsize();
|
||||
});
|
||||
$quality = config_cache('pixelfed.image_quality');
|
||||
$quality = config('pixelfed.image_quality');
|
||||
$img->save($file, $quality);
|
||||
|
||||
$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
|
||||
$avatar->thumb_path = $avatar->media_path;
|
||||
$avatar->change_count = ++$avatar->change_count;
|
||||
$avatar->last_processed_at = Carbon::now();
|
||||
$avatar->save();
|
||||
|
|
|
@ -45,6 +45,7 @@ class CreateAvatar implements ShouldQueue
|
|||
$avatar = new Avatar();
|
||||
$avatar->profile_id = $profile->id;
|
||||
$avatar->media_path = $path;
|
||||
$avatar->thumb_path = $path;
|
||||
$avatar->change_count = 0;
|
||||
$avatar->last_processed_at = \Carbon\Carbon::now();
|
||||
$avatar->save();
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\AvatarPipeline;
|
||||
|
||||
use App\Avatar;
|
||||
use App\Profile;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Support\Str;
|
||||
use Zttp\Zttp;
|
||||
use App\Http\Controllers\AvatarController;
|
||||
use Storage;
|
||||
use Log;
|
||||
use Illuminate\Http\File;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\ActivityPubFetchService;
|
||||
|
||||
class RemoteAvatarFetch implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $profile;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* The number of times the job may be attempted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Profile $profile)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->profile;
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if($profile->domain == null || $profile->private_key) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$avatar = Avatar::whereProfileId($profile->id)->first();
|
||||
|
||||
if(!$avatar) {
|
||||
$avatar = new Avatar;
|
||||
$avatar->profile_id = $profile->id;
|
||||
$avatar->save();
|
||||
}
|
||||
|
||||
if($avatar->media_path == null && $avatar->remote_url == null) {
|
||||
$avatar->media_path = 'public/avatars/default.jpg';
|
||||
$avatar->is_remote = true;
|
||||
$avatar->save();
|
||||
}
|
||||
|
||||
$person = Helpers::fetchFromUrl($profile->remote_url);
|
||||
|
||||
if(!$person || !isset($person['@context'])) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if( !isset($person['icon']) ||
|
||||
!isset($person['icon']['type']) ||
|
||||
!isset($person['icon']['url'])
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if($person['icon']['type'] !== 'Image') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!Helpers::validateUrl($person['icon']['url'])) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$icon = $person['icon'];
|
||||
|
||||
$avatar->remote_url = $icon['url'];
|
||||
$avatar->save();
|
||||
|
||||
MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ use App\{
|
|||
UserFilter
|
||||
};
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\StatusService;
|
||||
use DB, Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
|
@ -59,11 +58,6 @@ class CommentPipeline implements ShouldQueue
|
|||
$target = $status->profile;
|
||||
$actor = $comment->profile;
|
||||
|
||||
DB::transaction(function() use($status) {
|
||||
$status->reply_count = DB::table('statuses')->whereInReplyToId($status->id)->count();
|
||||
$status->save();
|
||||
});
|
||||
|
||||
if ($actor->id === $target->id || $status->comments_disabled == true) {
|
||||
return true;
|
||||
}
|
||||
|
@ -91,16 +85,6 @@ class CommentPipeline implements ShouldQueue
|
|||
|
||||
NotificationService::setNotification($notification);
|
||||
NotificationService::set($notification->profile_id, $notification->id);
|
||||
StatusService::del($comment->id);
|
||||
});
|
||||
|
||||
if($exists = Cache::get('status:replies:all:' . $status->id)) {
|
||||
if($exists && $exists->count() == 3) {
|
||||
} else {
|
||||
Cache::forget('status:replies:all:' . $status->id);
|
||||
}
|
||||
} else {
|
||||
Cache::forget('status:replies:all:' . $status->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\DeletePipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
|
||||
class FanoutDeletePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $profile;
|
||||
|
||||
public $timeout = 300;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($profile)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->profile;
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$audience = Cache::remember('pf:ap:known_instances', now()->addHours(6), function() {
|
||||
return Profile::whereNotNull('sharedInbox')->groupBy('sharedInbox')->pluck('sharedInbox')->toArray();
|
||||
});
|
||||
|
||||
$activity = [
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => $profile->permalink('#delete'),
|
||||
"type" => "Delete",
|
||||
"actor" => $profile->permalink(),
|
||||
"to" => [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
],
|
||||
"object" => $profile->permalink(),
|
||||
];
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
}
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -46,9 +46,6 @@ class FollowPipeline implements ShouldQueue
|
|||
$actor = $follower->actor;
|
||||
$target = $follower->target;
|
||||
|
||||
Cache::forget('profile:following:' . $actor->id);
|
||||
Cache::forget('profile:following:' . $target->id);
|
||||
|
||||
if($target->domain || !$target->private_key) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ class ImageOptimize implements ShouldQueue
|
|||
{
|
||||
$media = $this->media;
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
if (!is_file($path) || $media->skip_optimize) {
|
||||
if (!is_file($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class ImageResize implements ShouldQueue
|
|||
return;
|
||||
}
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
if (!is_file($path) || $media->skip_optimize) {
|
||||
if (!is_file($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,6 @@ use Illuminate\Queue\InteractsWithQueue;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use ImageOptimizer;
|
||||
use Illuminate\Http\File;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Jobs\MediaPipeline\MediaStoragePipeline;
|
||||
|
||||
class ImageUpdate implements ShouldQueue
|
||||
{
|
||||
|
@ -23,7 +21,6 @@ class ImageUpdate implements ShouldQueue
|
|||
protected $protectedMimes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -63,10 +60,8 @@ class ImageUpdate implements ShouldQueue
|
|||
|
||||
if (in_array($media->mime, $this->protectedMimes) == true) {
|
||||
ImageOptimizer::optimize($thumb);
|
||||
if(!$media->skip_optimize) {
|
||||
ImageOptimizer::optimize($path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_file($path) || !is_file($thumb)) {
|
||||
return;
|
||||
|
@ -78,6 +73,19 @@ class ImageUpdate implements ShouldQueue
|
|||
$media->size = $total;
|
||||
$media->save();
|
||||
|
||||
MediaStoragePipeline::dispatch($media);
|
||||
if(config('pixelfed.cloud_storage') == true) {
|
||||
$p = explode('/', $media->media_path);
|
||||
$monthHash = $p[2];
|
||||
$userHash = $p[3];
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$file = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($path), 'public');
|
||||
$url = Storage::disk(config('filesystems.cloud'))->url($file);
|
||||
$thumbFile = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($thumb), 'public');
|
||||
$thumbUrl = Storage::disk(config('filesystems.cloud'))->url($thumbFile);
|
||||
$media->thumbnail_url = $thumbUrl;
|
||||
$media->cdn_url = $url;
|
||||
$media->optimized_url = $url;
|
||||
$media->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ class ImportInstagram implements ShouldQueue
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
if(config('pixelfed.import.instagram.enabled') != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -101,11 +101,7 @@ class ImportInstagram implements ShouldQueue
|
|||
|
||||
$path = storage_path("app/$importData->path");
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$dir = "app/$storagePath";
|
||||
if(!is_dir(storage_path($dir))) {
|
||||
mkdir(storage_path($dir), 0755, true);
|
||||
}
|
||||
$newPath = "$dir/$filename";
|
||||
$newPath = "app/$storagePath/$filename";
|
||||
$fs->move($path,storage_path($newPath));
|
||||
$path = $newPath;
|
||||
$hash = \hash_file('sha256', storage_path($path));
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InboxPipeline;
|
||||
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\{
|
||||
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;
|
||||
|
||||
class DeleteWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $headers;
|
||||
protected $payload;
|
||||
|
||||
public $timeout = 60;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = 'pf:ap:del-lock:' . hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 300);
|
||||
}
|
||||
|
||||
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(empty($headers) || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if( $payload['type'] === 'Delete' &&
|
||||
( ( is_string($payload['object']) &&
|
||||
$payload['object'] === $payload['actor'] ) ||
|
||||
( is_array($payload['object']) &&
|
||||
isset($payload['object']['id'], $payload['object']['type']) &&
|
||||
$payload['object']['type'] === 'Person' &&
|
||||
$payload['actor'] === $payload['object']['id']
|
||||
))
|
||||
) {
|
||||
$actor = $payload['actor'];
|
||||
$hash = strlen($actor) <= 48 ?
|
||||
'b:' . base64_encode($actor) :
|
||||
'h:' . hash('sha256', $actor);
|
||||
|
||||
$lockKey = 'ap:inbox:actor-delete-exists:lock:' . $hash;
|
||||
Cache::lock($lockKey, 10)->block(5, function () use(
|
||||
$headers,
|
||||
$payload,
|
||||
$actor,
|
||||
$hash
|
||||
) {
|
||||
$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');
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// Signature verification failed, exit.
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Remote user doesn't exist, exit early.
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$profile = null;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
$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'])
|
||||
) {
|
||||
if(parse_url($bodyDecoded['object']['attributedTo'], PHP_URL_HOST) !== $keyDomain) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return;
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
$actorUrl = is_array($bodyDecoded['actor']) ? $bodyDecoded['actor'][0] : $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 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(5)->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['publicKey']['id'] !== $actor->key_id) {
|
||||
return;
|
||||
}
|
||||
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$actor->save();
|
||||
return $this->verifySignature($headers, $payload);
|
||||
}
|
||||
}
|
|
@ -53,15 +53,6 @@ class InboxValidator implements ShouldQueue
|
|||
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
|
||||
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 300);
|
||||
}
|
||||
|
||||
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
|
@ -182,9 +173,6 @@ class InboxValidator implements ShouldQueue
|
|||
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) {
|
||||
|
|
|
@ -49,15 +49,6 @@ class InboxWorker implements ShouldQueue
|
|||
$headers = $this->headers;
|
||||
$payload = json_decode($this->payload, true, 8);
|
||||
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 300);
|
||||
}
|
||||
|
||||
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
|
@ -170,9 +161,6 @@ class InboxWorker implements ShouldQueue
|
|||
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) {
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InstancePipeline;
|
||||
|
||||
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 Illuminate\Support\Facades\Http;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Services\NodeinfoService;
|
||||
|
||||
class FetchNodeinfoPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $instance;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Instance $instance)
|
||||
{
|
||||
$this->instance = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$instance = $this->instance;
|
||||
|
||||
$ni = NodeinfoService::get($instance->domain);
|
||||
if($ni) {
|
||||
if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) {
|
||||
$software = $ni['software']['name'];
|
||||
$instance->software = strtolower(strip_tags($software));
|
||||
$instance->last_crawled_at = now();
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->save();
|
||||
}
|
||||
} else {
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->last_crawled_at = now();
|
||||
$instance->save();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InstancePipeline;
|
||||
|
||||
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 Illuminate\Support\Facades\Http;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Services\NodeinfoService;
|
||||
|
||||
class InstanceCrawlPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Instance::whereNull('last_crawled_at')->whereNull('software')->chunk(50, function($instances) {
|
||||
foreach($instances as $instance) {
|
||||
FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Jobs\LikePipeline;
|
||||
|
||||
use Cache, DB, Log;
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
@ -14,7 +14,6 @@ use App\Util\ActivityPub\Helpers;
|
|||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\Like as LikeTransformer;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class LikePipeline implements ShouldQueue
|
||||
{
|
||||
|
@ -59,11 +58,6 @@ class LikePipeline implements ShouldQueue
|
|||
return;
|
||||
}
|
||||
|
||||
$status->likes_count = DB::table('likes')->whereStatusId($status->id)->count();
|
||||
$status->save();
|
||||
|
||||
StatusService::refresh($status->id);
|
||||
|
||||
if($status->url && $actor->domain == null) {
|
||||
return $this->remoteLikeDeliver();
|
||||
}
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\LikePipeline;
|
||||
|
||||
use Cache, DB, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\UndoLike as LikeTransformer;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class UnlikePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $timeout = 5;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Like $like)
|
||||
{
|
||||
$this->like = $like;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$like = $this->like;
|
||||
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if (!$status) {
|
||||
// Ignore notifications to deleted statuses
|
||||
return;
|
||||
}
|
||||
|
||||
$status->likes_count = DB::table('likes')->whereStatusId($status->id)->count();
|
||||
$status->save();
|
||||
|
||||
StatusService::refresh($status->id);
|
||||
|
||||
if($actor->id !== $status->profile_id && $status->url && $actor->domain == null) {
|
||||
$this->remoteLikeDeliver();
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->first();
|
||||
|
||||
if($exists) {
|
||||
$exists->delete();
|
||||
}
|
||||
|
||||
$like = Like::whereProfileId($actor->id)->whereStatusId($status->id)->first();
|
||||
|
||||
if(!$like) {
|
||||
return;
|
||||
}
|
||||
|
||||
$like->forceDelete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public function remoteLikeDeliver()
|
||||
{
|
||||
$like = $this->like;
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($like, new LikeTransformer());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
|
||||
|
||||
Helpers::sendSignedObject($actor, $url, $activity);
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
<?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 MediaDeletePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
|
||||
public function __construct(Media $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
$path = $media->media_path;
|
||||
$thumb = $media->thumbnail_path;
|
||||
|
||||
if(!$path) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$e = explode('/', $path);
|
||||
array_pop($e);
|
||||
$i = implode('/', $e);
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == true) {
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
if($disk->exists($path)) {
|
||||
$disk->delete($path);
|
||||
}
|
||||
if($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($disk->exists($path)) {
|
||||
$disk->delete($path);
|
||||
}
|
||||
if($disk->exists($thumb)) {
|
||||
$disk->delete($thumb);
|
||||
}
|
||||
if(count($e) > 4 && count($disk->files($i)) == 0) {
|
||||
$disk->deleteDirectory($i);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\MediaPipeline;
|
||||
|
||||
use App\Media;
|
||||
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 Illuminate\Support\Facades\Redis;
|
||||
use App\Services\MediaStorageService;
|
||||
|
||||
class MediaStoragePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
|
||||
public function __construct(Media $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
MediaStorageService::store($this->media);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\MediaPipeline;
|
||||
|
||||
use App\Media;
|
||||
use App\User;
|
||||
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 App\Services\StatusService;
|
||||
|
||||
class MediaSyncLicensePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $userId;
|
||||
protected $licenseId;
|
||||
|
||||
public function __construct($userId, $licenseId)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
$this->licenseId = $licenseId;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$licenseId = $this->licenseId;
|
||||
|
||||
if(!$licenseId || !$this->userId) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Media::whereUserId($this->userId)
|
||||
->chunk(100, function($medias) use($licenseId) {
|
||||
foreach($medias as $media) {
|
||||
$media->license = $licenseId;
|
||||
$media->save();
|
||||
Cache::forget('status:transformer:media:attachments:'. $media->status_id);
|
||||
StatusService::del($media->status_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\ModPipeline;
|
||||
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class HandleSpammerPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $profile;
|
||||
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public function __construct(Profile $profile)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->profile;
|
||||
|
||||
$profile->unlisted = true;
|
||||
$profile->cw = true;
|
||||
$profile->no_autolink = true;
|
||||
$profile->save();
|
||||
|
||||
Status::whereProfileId($profile->id)
|
||||
->chunk(50, function($statuses) {
|
||||
foreach($statuses as $status) {
|
||||
$status->is_nsfw = true;
|
||||
$status->scope = $status->scope === 'public' ? 'unlisted' : $status->scope;
|
||||
$status->visibility = $status->scope;
|
||||
$status->save();
|
||||
StatusService::del($status->id, true);
|
||||
}
|
||||
});
|
||||
|
||||
Cache::forget('_api:statuses:recent_9:'.$profile->id);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,6 @@ use League\Fractal\Serializer\ArraySerializer;
|
|||
use App\Transformer\ActivityPub\Verb\Announce;
|
||||
use GuzzleHttp\{Pool, Client, Promise};
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class SharePipeline implements ShouldQueue
|
||||
{
|
||||
|
@ -48,9 +47,8 @@ class SharePipeline implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$parent = $this->status->parent();
|
||||
$actor = $status->profile;
|
||||
$target = $parent->profile;
|
||||
$target = $status->parent()->profile;
|
||||
|
||||
if ($status->uri !== null) {
|
||||
// Ignore notifications to remote statuses
|
||||
|
@ -62,23 +60,19 @@ class SharePipeline implements ShouldQueue
|
|||
->whereAction('share')
|
||||
->whereItemId($status->reblog_of_id)
|
||||
->whereItemType('App\Status')
|
||||
->exists();
|
||||
->count();
|
||||
|
||||
if($target->id === $status->profile_id) {
|
||||
if ($target->id === $status->profile_id) {
|
||||
$this->remoteAnnounceDeliver();
|
||||
return true;
|
||||
}
|
||||
|
||||
if($exists === true) {
|
||||
if( $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->remoteAnnounceDeliver();
|
||||
|
||||
$parent->reblogs_count = $parent->shares()->count();
|
||||
$parent->save();
|
||||
StatusService::del($parent->id);
|
||||
|
||||
try {
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $target->id;
|
||||
|
@ -100,7 +94,7 @@ class SharePipeline implements ShouldQueue
|
|||
|
||||
public function remoteAnnounceDeliver()
|
||||
{
|
||||
if(config_cache('federation.activitypub.enabled') == false) {
|
||||
if(config('federation.activitypub.enabled') == false) {
|
||||
return true;
|
||||
}
|
||||
$status = $this->status;
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\SharePipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Status, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\UndoAnnounce;
|
||||
use GuzzleHttp\{Pool, Client, Promise};
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class UndoSharePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
protected $status;
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$actor = $status->profile;
|
||||
$parent = $status->parent();
|
||||
$target = $status->parent()->profile;
|
||||
|
||||
if ($status->uri !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($target->domain === null) {
|
||||
Notification::whereProfileId($target->id)
|
||||
->whereActorId($status->profile_id)
|
||||
->whereAction('share')
|
||||
->whereItemId($status->reblog_of_id)
|
||||
->whereItemType('App\Status')
|
||||
->delete();
|
||||
}
|
||||
|
||||
$this->remoteAnnounceDeliver();
|
||||
|
||||
if($parent->reblogs_count > 0) {
|
||||
$parent->reblogs_count = $parent->reblogs_count - 1;
|
||||
$parent->save();
|
||||
StatusService::del($parent->id);
|
||||
}
|
||||
|
||||
$status->forceDelete();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function remoteAnnounceDeliver()
|
||||
{
|
||||
if(config_cache('federation.activitypub.enabled') == false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$status = $this->status;
|
||||
$profile = $status->profile;
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($status, new UndoAnnounce());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$audience = $status->profile->getAudienceInbox();
|
||||
|
||||
if(empty($audience) || $status->scope != 'public') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
}
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ use Illuminate\Queue\SerializesModels;
|
|||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\CreateNote;
|
||||
use App\Transformer\ActivityPub\Verb\CreateQuestion;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
|
@ -63,20 +62,10 @@ class StatusActivityPubDeliver implements ShouldQueue
|
|||
return;
|
||||
}
|
||||
|
||||
switch($status->type) {
|
||||
case 'poll':
|
||||
$activitypubObject = new CreateQuestion();
|
||||
break;
|
||||
|
||||
default:
|
||||
$activitypubObject = new CreateNote();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($status, $activitypubObject);
|
||||
$resource = new Fractal\Resource\Item($status, new CreateNote());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
@ -93,9 +82,7 @@ class StatusActivityPubDeliver implements ShouldQueue
|
|||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use DB, Storage;
|
||||
use DB;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
MediaTag,
|
||||
|
@ -17,7 +17,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use League\Fractal;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteNote;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
@ -25,8 +24,6 @@ use GuzzleHttp\Pool;
|
|||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\MediaStorageService;
|
||||
|
||||
class StatusDelete implements ShouldQueue
|
||||
{
|
||||
|
@ -61,14 +58,17 @@ class StatusDelete implements ShouldQueue
|
|||
$status = $this->status;
|
||||
$profile = $this->status->profile;
|
||||
|
||||
StatusService::del($status->id, true);
|
||||
$count = $profile->statuses()
|
||||
->getQuery()
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->count();
|
||||
|
||||
if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
|
||||
$profile->status_count = $profile->status_count - 1;
|
||||
$profile->status_count = ($count - 1);
|
||||
$profile->save();
|
||||
}
|
||||
|
||||
if(config_cache('federation.activitypub.enabled') == true) {
|
||||
if(config('federation.activitypub.enabled') == true) {
|
||||
$this->fanoutDelete($status);
|
||||
} else {
|
||||
$this->unlinkRemoveMedia($status);
|
||||
|
@ -79,9 +79,20 @@ class StatusDelete implements ShouldQueue
|
|||
public function unlinkRemoveMedia($status)
|
||||
{
|
||||
foreach ($status->media as $media) {
|
||||
MediaStorageService::delete($media, true);
|
||||
}
|
||||
$thumbnail = storage_path("app/{$media->thumbnail_path}");
|
||||
$photo = storage_path("app/{$media->media_path}");
|
||||
|
||||
try {
|
||||
if (is_file($thumbnail)) {
|
||||
unlink($thumbnail);
|
||||
}
|
||||
if (is_file($photo)) {
|
||||
unlink($photo);
|
||||
}
|
||||
$media->delete();
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
if($status->in_reply_to_id) {
|
||||
DB::transaction(function() use($status) {
|
||||
$parent = Status::findOrFail($status->in_reply_to_id);
|
||||
|
@ -106,6 +117,7 @@ class StatusDelete implements ShouldQueue
|
|||
Report::whereObjectType('App\Status')
|
||||
->whereObjectId($status->id)
|
||||
->delete();
|
||||
|
||||
MediaTag::where('status_id', $status->id)
|
||||
->cursor()
|
||||
->each(function($tag) {
|
||||
|
@ -114,6 +126,7 @@ class StatusDelete implements ShouldQueue
|
|||
->forceDelete();
|
||||
$tag->delete();
|
||||
});
|
||||
|
||||
AccountInterstitial::where('item_type', 'App\Status')
|
||||
->where('item_id', $status->id)
|
||||
->delete();
|
||||
|
|
|
@ -52,12 +52,16 @@ class StatusEntityLexer implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
$profile = $this->status->profile;
|
||||
$status = $this->status;
|
||||
|
||||
if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
|
||||
$profile->status_count = $profile->status_count + 1;
|
||||
$count = $profile->statuses()
|
||||
->getQuery()
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->count();
|
||||
|
||||
$profile->status_count = $count;
|
||||
$profile->save();
|
||||
}
|
||||
|
||||
if($profile->no_autolink == false) {
|
||||
$this->parseEntities();
|
||||
|
@ -103,13 +107,9 @@ class StatusEntityLexer implements ShouldQueue
|
|||
}
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$slug = str_slug($tag, '-', false);
|
||||
$hashtag = Hashtag::where('slug', $slug)->first();
|
||||
if (!$hashtag) {
|
||||
$hashtag = Hashtag::create(
|
||||
$hashtag = Hashtag::firstOrCreate(
|
||||
['name' => $tag, 'slug' => $slug]
|
||||
);
|
||||
}
|
||||
|
||||
StatusHashtag::firstOrCreate(
|
||||
[
|
||||
'status_id' => $status->id,
|
||||
|
@ -150,29 +150,17 @@ class StatusEntityLexer implements ShouldQueue
|
|||
public function deliver()
|
||||
{
|
||||
$status = $this->status;
|
||||
$types = [
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video',
|
||||
'video:album',
|
||||
'photo:video:album'
|
||||
];
|
||||
|
||||
if(config_cache('pixelfed.bouncer.enabled')) {
|
||||
if(config('pixelfed.bouncer.enabled')) {
|
||||
Bouncer::get($status);
|
||||
}
|
||||
|
||||
if( $status->uri == null &&
|
||||
$status->scope == 'public' &&
|
||||
in_array($status->type, $types) &&
|
||||
$status->in_reply_to_id === null &&
|
||||
$status->reblog_of_id === null
|
||||
) {
|
||||
if($status->uri == null && $status->scope == 'public') {
|
||||
PublicTimelineService::add($status->id);
|
||||
}
|
||||
|
||||
if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
|
||||
StatusActivityPubDeliver::dispatch($status);
|
||||
if(config('federation.activitypub.enabled') == true && config('app.env') == 'production') {
|
||||
StatusActivityPubDeliver::dispatch($this->status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use App\Notification;
|
||||
use App\Status;
|
||||
use Cache;
|
||||
use DB;
|
||||
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 App\Services\NotificationService;
|
||||
|
||||
class StatusReplyPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $timeout = 5;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$actor = $status->profile;
|
||||
$reply = Status::find($status->in_reply_to_id);
|
||||
|
||||
if(!$actor || !$reply) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$target = $reply->profile;
|
||||
|
||||
$exists = Notification::whereProfileId($target->id)
|
||||
->whereActorId($actor->id)
|
||||
->whereIn('action', ['mention', 'comment'])
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if ($actor->id === $target || $exists !== 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
DB::transaction(function() use($target, $actor, $status) {
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'comment';
|
||||
$notification->message = $status->replyToText();
|
||||
$notification->rendered = $status->replyToHtml();
|
||||
$notification->item_id = $status->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
NotificationService::setNotification($notification);
|
||||
NotificationService::set($notification->profile_id, $notification->id);
|
||||
});
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
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\CustomEmojiService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class StatusTagsPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $activity;
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($activity, $status)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$res = $this->activity;
|
||||
|
||||
collect($res['tag'])
|
||||
->filter(function($tag) {
|
||||
// todo: finish hashtag + mention import
|
||||
// return in_array($tag['type'], ['Emoji', 'Hashtag', 'Mention']);
|
||||
return $tag && $tag['type'] == 'Emoji';
|
||||
})
|
||||
->map(function($tag) {
|
||||
CustomEmojiService::import($tag['id'], $this->status->id);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
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 Storage;
|
||||
use App\Story;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteStory;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryDelete implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story)
|
||||
{
|
||||
$this->story = $story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
|
||||
if($story->local == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
StoryService::removeRotateQueue($story->id);
|
||||
StoryService::delLatest($story->profile_id);
|
||||
StoryService::delById($story->id);
|
||||
|
||||
if(Storage::exists($story->path) == true) {
|
||||
Storage::delete($story->path);
|
||||
}
|
||||
|
||||
$story->views()->delete();
|
||||
|
||||
$profile = $story->profile;
|
||||
|
||||
$activity = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $story->url() . '#delete',
|
||||
'type' => 'Delete',
|
||||
'actor' => $profile->permalink(),
|
||||
'object' => [
|
||||
'id' => $story->url(),
|
||||
'type' => 'Story',
|
||||
],
|
||||
];
|
||||
|
||||
$this->fanoutExpiry($profile, $activity);
|
||||
|
||||
// delete notifications
|
||||
// delete polls
|
||||
// delete reports
|
||||
|
||||
$story->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
protected function fanoutExpiry($profile, $activity)
|
||||
{
|
||||
$audience = FollowerService::softwareAudience($profile->id, 'pixelfed');
|
||||
|
||||
if(empty($audience)) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
}
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
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 Storage;
|
||||
use App\Story;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteStory;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryExpire implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story)
|
||||
{
|
||||
$this->story = $story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
|
||||
if($story->local == false) {
|
||||
$this->handleRemoteExpiry();
|
||||
return;
|
||||
}
|
||||
|
||||
if($story->active == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($story->expires_at->gt(now())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$story->active = false;
|
||||
$story->save();
|
||||
|
||||
$this->rotateMediaPath();
|
||||
$this->fanoutExpiry();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
}
|
||||
|
||||
protected function rotateMediaPath()
|
||||
{
|
||||
$story = $this->story;
|
||||
$date = date('Y').date('m');
|
||||
$old = $story->path;
|
||||
$base = "story_archives/{$story->profile_id}/{$date}/";
|
||||
$paths = explode('/', $old);
|
||||
$path = array_pop($paths);
|
||||
$newPath = $base . $path;
|
||||
|
||||
if(Storage::exists($old) == true) {
|
||||
$dir = implode('/', $paths);
|
||||
Storage::move($old, $newPath);
|
||||
Storage::delete($old);
|
||||
$story->bearcap_token = null;
|
||||
$story->path = $newPath;
|
||||
$story->save();
|
||||
Storage::deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
|
||||
protected function fanoutExpiry()
|
||||
{
|
||||
$story = $this->story;
|
||||
$profile = $story->profile;
|
||||
|
||||
if($story->local == false || $story->remote_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
$audience = FollowerService::softwareAudience($story->profile_id, 'pixelfed');
|
||||
|
||||
if(empty($audience)) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($story, new DeleteStory());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
}
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
|
||||
protected function handleRemoteExpiry()
|
||||
{
|
||||
$story = $this->story;
|
||||
$story->active = false;
|
||||
$story->save();
|
||||
|
||||
$path = $story->path;
|
||||
|
||||
if(Storage::exists($path) == true) {
|
||||
Storage::delete($path);
|
||||
}
|
||||
|
||||
$story->views()->delete();
|
||||
$story->delete();
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use App\Story;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\CreateStory;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryFanout implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story)
|
||||
{
|
||||
$this->story = $story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
$profile = $story->profile;
|
||||
|
||||
if($story->local == false || $story->remote_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
|
||||
$audience = FollowerService::softwareAudience($story->profile_id, 'pixelfed');
|
||||
|
||||
if(empty($audience)) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($story, new CreateStory());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
}
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use App\Story;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\FollowerService;
|
||||
use App\Util\Lexer\Bearcap;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use App\Util\ActivityPub\Validator\StoryValidator;
|
||||
use App\Services\StoryService;
|
||||
use App\Services\MediaPathService;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class StoryFetch implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $activity;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$activity = $this->activity;
|
||||
$activityId = $activity['id'];
|
||||
$activityActor = $activity['actor'];
|
||||
|
||||
if(parse_url($activityId, PHP_URL_HOST) !== parse_url($activityActor, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bearcap = Bearcap::decode($activity['object']['object']);
|
||||
|
||||
if(!$bearcap) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = $bearcap['url'];
|
||||
$token = $bearcap['token'];
|
||||
|
||||
if(parse_url($activityId, PHP_URL_HOST) !== parse_url($url, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$headers = [
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})",
|
||||
];
|
||||
|
||||
try {
|
||||
$res = Http::withHeaders($headers)
|
||||
->timeout(30)
|
||||
->get($url);
|
||||
} catch (RequestException $e) {
|
||||
return false;
|
||||
} catch (ConnectionException $e) {
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = $res->json();
|
||||
|
||||
if(StoryValidator::validate($payload) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(Helpers::validateUrl($payload['attachment']['url']) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $payload['attachment']['type'] == 'Image' ? 'photo' : 'video';
|
||||
|
||||
$profile = Helpers::profileFetch($payload['attributedTo']);
|
||||
|
||||
$ext = pathinfo($payload['attachment']['url'], PATHINFO_EXTENSION);
|
||||
$storagePath = MediaPathService::story($profile);
|
||||
$fileName = Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $ext;
|
||||
$contextOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peername' => false
|
||||
]
|
||||
];
|
||||
$ctx = stream_context_create($contextOptions);
|
||||
$data = file_get_contents($payload['attachment']['url'], false, $ctx);
|
||||
$tmpBase = storage_path('app/remcache/');
|
||||
$tmpPath = $profile->id . '-' . $fileName;
|
||||
$tmpName = $tmpBase . $tmpPath;
|
||||
file_put_contents($tmpName, $data);
|
||||
$disk = Storage::disk(config('filesystems.default'));
|
||||
$path = $disk->putFileAs($storagePath, new File($tmpName), $fileName, 'public');
|
||||
$size = filesize($tmpName);
|
||||
unlink($tmpName);
|
||||
|
||||
$story = new Story;
|
||||
$story->profile_id = $profile->id;
|
||||
$story->object_id = $payload['id'];
|
||||
$story->size = $size;
|
||||
$story->mime = $payload['attachment']['mediaType'];
|
||||
$story->duration = $payload['duration'];
|
||||
$story->media_url = $payload['attachment']['url'];
|
||||
$story->type = $type;
|
||||
$story->public = false;
|
||||
$story->local = false;
|
||||
$story->active = true;
|
||||
$story->path = $path;
|
||||
$story->view_count = 0;
|
||||
$story->can_reply = $payload['can_reply'];
|
||||
$story->can_react = $payload['can_react'];
|
||||
$story->created_at = now()->parse($payload['published']);
|
||||
$story->expires_at = now()->parse($payload['expiresAt']);
|
||||
$story->save();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use App\Story;
|
||||
use App\Status;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
||||
class StoryReactionDeliver implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story, Status $status)
|
||||
{
|
||||
$this->story = $story;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
$status = $this->status;
|
||||
|
||||
if($story->local == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $story->profile;
|
||||
$actor = $status->profile;
|
||||
$to = $target->inbox_url;
|
||||
|
||||
$payload = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Story:Reaction',
|
||||
'to' => $target->permalink(),
|
||||
'actor' => $actor->permalink(),
|
||||
'content' => $status->caption,
|
||||
'inReplyTo' => $story->object_id,
|
||||
'published' => $status->created_at->toAtomString()
|
||||
];
|
||||
|
||||
Helpers::sendSignedObject($actor, $to, $payload);
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use App\Story;
|
||||
use App\Status;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
||||
class StoryReplyDeliver implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story, Status $status)
|
||||
{
|
||||
$this->story = $story;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
$status = $this->status;
|
||||
|
||||
if($story->local == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $story->profile;
|
||||
$actor = $status->profile;
|
||||
$to = $target->inbox_url;
|
||||
|
||||
$payload = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Story:Reply',
|
||||
'to' => $target->permalink(),
|
||||
'actor' => $actor->permalink(),
|
||||
'content' => $status->caption,
|
||||
'inReplyTo' => $story->object_id,
|
||||
'published' => $status->created_at->toAtomString()
|
||||
];
|
||||
|
||||
Helpers::sendSignedObject($actor, $to, $payload);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue