1
0
Fork 0

Compare commits

...

289 Commits

Author SHA1 Message Date
daniel 8aae92d75b
Merge pull request #5001 from pixelfed/staging
Staging
2024-03-10 05:44:24 -06:00
Daniel Supernault bf46f6f5f4
Update config_cache 2024-03-10 05:42:25 -06:00
Daniel Supernault b0cb4456a9
Update ApiV1Dot1Controller, use config_cache for in-app registration 2024-03-10 05:06:52 -06:00
Daniel Supernault 7785a2dae4
Update Config, use config_cache 2024-03-10 04:37:22 -06:00
daniel 57f4457637
Merge pull request #5000 from pixelfed/staging
Update config cache
2024-03-10 04:20:28 -06:00
Daniel Supernault 5e4d4eff9d
Update config cache 2024-03-10 04:19:44 -06:00
daniel 3132523798
Merge pull request #4999 from pixelfed/staging
Update web-api popular accounts route to its own method to remove the…
2024-03-09 23:27:14 -07:00
Daniel Supernault a4bc5ce3d0
Update web-api popular accounts route to its own method to remove the breaking oauth scope bug 2024-03-09 23:25:28 -07:00
daniel f4086d4381
Merge pull request #4997 from pixelfed/staging
Staging
2024-03-08 06:49:17 -07:00
Daniel Supernault 37a82cfb90
Update changelog 2024-03-08 06:44:29 -07:00
Daniel Supernault 4aa0e25f4c
Update commands, add user account delete cli command to federate account deletion 2024-03-08 06:44:02 -07:00
daniel 24c467c558
Merge pull request #4996 from pixelfed/staging
Staging
2024-03-08 06:04:27 -07:00
Daniel Supernault bcce1df6fc
Update AP transformers, add DeleteActor activity 2024-03-08 06:02:11 -07:00
Daniel Supernault a969ca502f
Add migrations 2024-03-08 06:01:35 -07:00
Daniel Supernault 36c518fe2c
Update web routes 2024-03-08 05:04:27 -07:00
Daniel Supernault 95199843e3
Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable 2024-03-08 05:03:29 -07:00
Daniel Supernault 853a729f76
Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup 2024-03-08 05:00:56 -07:00
Daniel Supernault e742d595a6
Update PrivacySettings controller, add cache invalidation 2024-03-08 04:43:57 -07:00
Daniel Supernault 2e5e68e447
Update AP Profile Transformer, fix suspended attributes 2024-03-08 04:24:13 -07:00
Daniel Supernault 63100fe950
Update AP Profile Transformer, fix movedTo attribute 2024-03-08 03:56:53 -07:00
Daniel Supernault 25f3fa06af
Update AP Profile Transformer, add `suspended` attribute 2024-03-08 03:49:47 -07:00
daniel f09313a512
Merge pull request #4993 from pixelfed/staging
Update Curated Onboarding view, fix concierge form
2024-03-08 02:36:33 -07:00
Daniel Supernault 15ad69f76e
Update Curated Onboarding view, fix concierge form 2024-03-08 02:35:44 -07:00
daniel 7c2ecd8706
Merge pull request #4992 from pixelfed/staging
Update SearchApiV2Service, use more efficient query
2024-03-07 03:34:10 -07:00
Daniel Supernault b89cd4c44f
Update changelog 2024-03-07 03:33:57 -07:00
Daniel Supernault cee618e844
Update SearchApiV2Service, use more efficient query 2024-03-07 03:33:18 -07:00
daniel 4516760ced
Merge pull request #4991 from pixelfed/staging
Update ApiV1Controller, use admin filter service
2024-03-07 03:13:13 -07:00
Daniel Supernault 94503a1cf9
Update ApiV1Controller, use admin filter service 2024-03-07 03:11:36 -07:00
daniel 5d7e091978
Merge pull request #4989 from pixelfed/staging
Staging
2024-03-07 02:40:25 -07:00
Daniel Supernault 18382e8a1f
Update DiscoverController, handle discover hashtag redirects 2024-03-07 02:38:50 -07:00
Daniel Supernault 592c84125c
Update StatusHashtagService, use more efficient cached count 2024-03-07 02:37:35 -07:00
daniel b6437380b4
Merge pull request #4988 from pixelfed/staging
Staging
2024-03-07 01:29:59 -07:00
Daniel Supernault e68fe64ffc
Update compiled assets 2024-03-07 01:29:31 -07:00
Daniel Supernault 3a27e637f8
Update Post.vue 2024-03-07 01:28:26 -07:00
daniel 0355830c5c
Merge pull request #4976 from pixelfed/staging
Update SoftwareUpdateService, add command to refresh latest versions
2024-03-05 07:05:37 -07:00
Daniel Supernault eccdbe1f57
Update changelog 2024-03-05 07:03:51 -07:00
Daniel Supernault 632f2cb619
Update SoftwareUpdateService, add command to refresh latest versions 2024-03-05 07:02:40 -07:00
daniel 23f7b74400
Merge pull request #4975 from pixelfed/staging
Staging
2024-03-05 06:46:16 -07:00
Daniel Supernault b1cdf4464f
Update docker workflow 2024-03-05 06:38:00 -07:00
Daniel Supernault b122c60de7
Update gitignore 2024-03-05 06:31:46 -07:00
Daniel Supernault 6036d96e3f
Update changelog 2024-03-05 06:28:14 -07:00
Daniel Supernault ff150ca6c9
Update compiled assets 2024-03-05 06:27:56 -07:00
daniel fc8462d565
Merge pull request #4886 from mbliznikova/4882_informative_err_message_for_mixed_media_album
Added an informative UI error message for attempt to create a mixed media album
2024-03-05 06:24:30 -07:00
Daniel Supernault 6231994253
Update compiled assets 2024-03-05 06:14:31 -07:00
daniel 5d21bba7b5
Merge pull request #4969 from shleeable/patch-15
Update navbar.vue
2024-03-05 06:10:43 -07:00
Daniel Supernault d18824e719
Update checkpoint view, improve input autocomplete. Fixes #4959 2024-03-05 06:07:37 -07:00
Daniel Supernault d3f6c71b8e
Update changelog 2024-03-05 06:05:26 -07:00
daniel 0bd3e0ab80
Merge pull request #4844 from jippi/jippi-fork
Refactor Docker/Compose
2024-03-05 06:03:14 -07:00
Daniel Supernault 5fb26a78bc
Bump version to 0.11.13 2024-03-05 06:00:37 -07:00
daniel 1251bf532c
Merge pull request #4974 from pixelfed/staging
API fixes
2024-03-05 05:43:08 -07:00
Daniel Supernault 03165ea46f
Update changelog 2024-03-05 05:40:52 -07:00
Daniel Supernault 3b5500b3a5
Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts 2024-03-05 05:39:47 -07:00
Daniel Supernault 1a811b1840
Update changelog 2024-03-05 04:44:02 -07:00
Daniel Supernault e3826c587d
Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 2024-03-05 04:43:21 -07:00
Daniel Supernault d6eac65555
Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters 2024-03-05 04:37:20 -07:00
daniel eb19c35343
Merge pull request #4973 from pixelfed/staging
Update ApiV1Controller, fix Notifications endpoint
2024-03-05 02:11:07 -07:00
Daniel Supernault 6cb1484b3e
cs fix 2024-03-05 01:58:15 -07:00
Daniel Supernault 01535a6cfe
Update ApiV1Controller, improve notification filtering 2024-03-05 01:56:16 -07:00
Daniel Supernault 31e6487dc9
Update changelog 2024-03-05 00:40:22 -07:00
Daniel Supernault a933615b8d
Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities 2024-03-05 00:39:50 -07:00
daniel a9b99d8f9d
Merge pull request #4972 from pixelfed/staging
Update ProfileMigration model, add target relation
2024-03-05 00:30:22 -07:00
Daniel Supernault 3f0539978e
Update ProfileMigration model, add target relation 2024-03-05 00:29:37 -07:00
daniel 712b6d27a9
Merge pull request #4968 from pixelfed/staging
Add Profile Migrations
2024-03-05 00:23:35 -07:00
Daniel Supernault 4a6be62128
Add account migration configurable, but enabled by default 2024-03-05 00:05:05 -07:00
Daniel Supernault 45bdfe1efd
Add Profile Migration federation 2024-03-04 23:16:32 -07:00
Shlee 7fd5599fc4
Update navbar.vue 2024-03-03 16:08:57 +10:30
Daniel Supernault 7613eec476
Update compiled assets 2024-03-02 04:24:56 -07:00
Daniel Supernault 9bc5338dbd
Update migration setting view 2024-03-02 04:23:48 -07:00
Daniel Supernault f8145a78cf
Add Profile Migrations 2024-03-02 04:21:04 -07:00
Christian Winther d92cf7f92f Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-02-29 21:34:36 +00:00
daniel 99611f90ea
Merge pull request #4964 from pixelfed/staging
Update AccountTransformer, fix follower/following count visibility bug
2024-02-29 05:00:55 -07:00
Daniel Supernault d5a6d9cc8d
Update changelog 2024-02-29 05:00:44 -07:00
Daniel Supernault 542d110673
Update AccountTransformer, fix follower/following count visibility bug 2024-02-29 04:59:13 -07:00
Daniel Supernault 402a4607c9
Update Inbox, fix flag validation condition, allow profile reports 2024-02-29 04:51:56 -07:00
Christian Winther 5d56460082 Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-02-29 10:46:45 +00:00
daniel 189e87f28a
Merge pull request #4962 from pixelfed/staging
Add Remote Reports to Admin Dashboard Reports page
2024-02-29 03:31:42 -07:00
Daniel Supernault c4190eec08
Update changelog 2024-02-29 03:27:46 -07:00
Daniel Supernault 0bb7d379c5
Update compiled assets 2024-02-29 03:27:28 -07:00
Daniel Supernault 372a116a2c
Add remote report components 2024-02-29 03:27:03 -07:00
Daniel Supernault ef0ff78e4a
Add Remote Reports to Admin Dashboard Reports page 2024-02-29 03:24:33 -07:00
Daniel Supernault ab9ecb6efd
Update AdminCuratedRegisterController, filter confirmation activities from activitylog 2024-02-29 02:04:43 -07:00
daniel e4f33e823d
Merge pull request #4961 from pixelfed/staging
Staging
2024-02-28 21:07:16 -07:00
Daniel Supernault 2f48df8ca8
Update kb, add email confirmation issues page 2024-02-28 21:06:21 -07:00
Christian Winther 6fa112162f Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-02-27 21:36:47 +00:00
Daniel Supernault a16309ac18
Update AdminReportController, add story report support 2024-02-26 21:39:09 -07:00
Daniel Supernault 767522a85c
Update AdminReports, add story reports and fix cs 2024-02-26 21:33:10 -07:00
daniel 8c1e136ce9
Merge pull request #4958 from pixelfed/staging
Add Curated Onboarding Templates
2024-02-26 21:05:24 -07:00
Daniel Supernault 071163b47b
Add Curated Onboarding Templates 2024-02-26 20:41:27 -07:00
Christian Winther acb699bf13 use the correct buildkit env for downloading binaries 2024-02-25 11:00:26 +00:00
Christian Winther 02369cce66 fix validation issues in the .env.docker file 2024-02-25 10:53:29 +00:00
Christian Winther c1c361ef9b tune for new dottie image 2024-02-24 23:29:45 +00:00
Christian Winther b08bb3669d bump dottie version 2024-02-24 23:00:38 +00:00
Christian Winther 1976af6dd1 ensure color in dottie output by passing through env 2024-02-24 22:50:48 +00:00
Christian Winther 020bda85db Merge remote-tracking branch 'pixelfed/staging' into jippi-fork 2024-02-24 21:42:13 +00:00
daniel 507f45f139
Merge pull request #4955 from pixelfed/staging
Update Curated Onboarding dashboard, improve application filtering an…
2024-02-24 03:50:37 -07:00
Daniel Supernault 795e91e3bc
Update changelog 2024-02-24 03:50:26 -07:00
Daniel Supernault 2b5d723582
Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state 2024-02-24 03:45:09 -07:00
daniel 7159d5cb3e
Merge pull request #4954 from pixelfed/staging
Update Inbox and StatusObserver, fix silently rejected direct message…
2024-02-23 19:40:29 -07:00
Daniel Supernault 84aeec3b4e
Update changelog 2024-02-23 19:37:55 -07:00
Daniel Supernault 089ba3c471
Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id 2024-02-23 19:37:02 -07:00
Christian Winther df1f62e734 update docs 2024-02-22 15:30:34 +00:00
Christian Winther c8c2e1c2eb update docs + paths 2024-02-22 15:24:51 +00:00
Christian Winther 2d8e81c83f ensure ownership of shared proxy conf 2024-02-22 15:14:32 +00:00
Christian Winther 515198b28c sync ignore files 2024-02-22 15:12:22 +00:00
Christian Winther f0e30c8ab6 expand docs for proxy nginx config 2024-02-22 15:02:53 +00:00
Christian Winther 7ffbd5d44a rename docker/bash to docker/shell 2024-02-22 14:58:03 +00:00
Christian Winther 5a43d7a65d improve error handling for [run-command-as] helper 2024-02-22 14:57:10 +00:00
Christian Winther 027f858d85 ensure correct ownership of ./storage/docker 2024-02-22 14:56:54 +00:00
Christian Winther e2821adcca fix spacing 2024-02-22 14:56:33 +00:00
Christian Winther af47d91e7d give nginx config default max upload size 2024-02-22 14:56:08 +00:00
Christian Winther 193d536ca1 update dottie 2024-02-22 14:54:20 +00:00
Christian Winther f264dd1cbb space redirects in shell scripts 2024-02-22 14:53:59 +00:00
Christian Winther d9d2a475d8 sort keys in compose 2024-02-22 14:53:39 +00:00
Christian Winther 8fd27c6f0c use remote build cache for faster local dev 2024-02-22 14:50:15 +00:00
Christian Winther 0addfe5605 allow .env control of a couple of PHP settings 2024-02-22 14:49:18 +00:00
Christian Winther 28b83b575f Bump dottie 2024-02-22 14:48:31 +00:00
Christian Winther 3bfd043792 update ignore files 2024-02-22 14:32:50 +00:00
Christian Winther 0ecebbb8bf push build cache to registry as well 2024-02-22 14:07:51 +00:00
Christian Winther 9c26bf26dd push build cache to registry as well 2024-02-22 13:40:32 +00:00
Christian Winther ae358e47cb push build cache to registry as well 2024-02-22 13:38:27 +00:00
Christian Winther 14f8478e6a push build cache to registry as well 2024-02-22 13:25:56 +00:00
Christian Winther 26d6f8f9fe push build cache to registry as well 2024-02-22 13:21:58 +00:00
daniel 36f84db03b
Merge pull request #4953 from pixelfed/staging
Staging
2024-02-22 03:51:39 -07:00
Daniel Supernault eadf2e9d1d
Update changelog 2024-02-22 03:51:10 -07:00
Daniel Supernault b0ecdc8162
Update compiled assets 2024-02-22 03:50:56 -07:00
Daniel Supernault 59c70239f8
Update Directory logic, add curated onboarding support 2024-02-22 03:39:13 -07:00
daniel b2f29a4590
Merge pull request #4952 from pixelfed/staging
Update AdminCuratedRegisterController, show oldest applications first
2024-02-22 01:55:20 -07:00
Daniel Supernault 4c5e8288b0
Update changelog 2024-02-22 01:55:08 -07:00
Daniel Supernault c4dde64119
Update AdminCuratedRegisterController, show oldest applications first 2024-02-22 01:54:18 -07:00
Christian Winther f486bfb73e add small dottie wrapper 2024-02-21 22:29:07 +00:00
Christian Winther adf1af3703 add small bash/artisan helper commands 2024-02-21 22:15:41 +00:00
Christian Winther abee7d4d62 add missing profiles 2024-02-21 21:52:59 +00:00
Christian Winther 5a9cfe1f2a Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-02-19 13:31:29 +00:00
Christian Winther 9117df186c more validation fixes 2024-02-19 00:52:12 +00:00
Christian Winther 4dc15bb37d fix validation 2024-02-19 00:48:36 +00:00
Christian Winther 9a1c4d42b5 drop php 8.1 support 2024-02-17 01:23:12 +00:00
Christian Winther 6edd712581 bump dottie 2024-02-17 01:19:59 +00:00
Christian Winther d4198b3262 Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-02-17 00:27:26 +00:00
Christian Winther d3bbfdb6e0 Merge branch 'staging' of github.com:jippi/pixelfed into jippi-fork 2024-02-13 00:52:18 +00:00
Christian Winther 49a778d128 add CODEOWNERS 2024-02-11 02:00:09 +00:00
Christian Winther fd62962d20 delete contrib 2024-02-11 01:57:11 +00:00
Christian Winther e18d6083a2 bump dottie 2024-02-11 01:24:26 +00:00
Christian Winther 143d5703dd update .env.docker 2024-02-10 23:08:22 +00:00
Christian Winther bc66b6da18 many small fixes and improvements 2024-02-10 20:03:04 +00:00
Christian Winther d8e1caec53 Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-02-10 10:44:02 +00:00
Christian Winther d9a9507cc8 sync 2024-02-10 00:30:06 +00:00
Christian Winther 5bd93b0f5e sync 2024-02-10 00:25:18 +00:00
Christian Winther 3d6efd098d sync 2024-02-10 00:23:53 +00:00
Christian Winther d374d73ba7
Merge branch 'staging' into jippi-fork 2024-02-08 01:17:25 +01:00
mbliznikova fd4f41a14e Added an informative UI error message for attempt to create a mixed media album 2024-01-30 19:19:25 +00:00
Christian Winther 2aeccf885f test: remove slash in tags 2024-01-27 00:01:06 +00:00
Christian Winther 043f914c8c test: change prefix for docker tags 2024-01-26 23:58:38 +00:00
Christian Winther 3723f36043 remove unneeded workflow triggers 2024-01-26 23:54:24 +00:00
Christian Winther ef37c8f234 name CI jobs 2024-01-26 23:48:04 +00:00
Christian Winther b73d452255 sort ARG in Dockerfile 2024-01-26 23:39:18 +00:00
Christian Winther 36850235a8 Merge remote-tracking branch 'origin/staging' into jippi-fork 2024-01-26 22:57:23 +00:00
Christian Winther 1a6e97c98b try to make 8.3 build working by building imagick from master branch 2024-01-26 22:51:15 +00:00
Christian Winther 8bdb0ca77b fix directory-is-empty and add tests to avoid regressions 2024-01-26 21:22:43 +00:00
Christian Winther c4f984b205 remove php extension FTP requirement 2024-01-26 20:46:09 +00:00
Christian Winther 1616c7cb11 make directory-is-empty more robust 2024-01-26 20:45:56 +00:00
Christian Winther ca5710b5ae fix 12-migrations.sh properly detecting new migrations and printing output 2024-01-26 20:45:38 +00:00
Christian Winther a665168031 change where overrides are placed 2024-01-26 20:45:08 +00:00
Christian Winther 335e6954d2 remove noisy log statements in as-boolean 2024-01-26 20:19:50 +00:00
Christian Winther 5c208d0519 allow easy overrides of any and all files in container via new override mount 2024-01-26 20:19:34 +00:00
Christian Winther aa2669c327 remove invalid/confusing statement in migrations about running migrations when it isn't enabled 2024-01-26 20:18:29 +00:00
Christian Winther 8189b01a26 improve naming of directory-is-empty 2024-01-26 20:17:54 +00:00
Christian Winther d372b9dee7 Set stop_signal for worker to stop Horizon more correct 2024-01-26 20:15:57 +00:00
Christian Winther d2ed117d3f improve Dockerfile for composer.json+composer.lock 2024-01-26 20:15:33 +00:00
Christian Winther 8d61b8d250 add Docker as recommended vscode plugin 2024-01-26 20:14:59 +00:00
Christian Winther c859367e10 fix 02-check-config.sh logic and bad .env.docker syntax 2024-01-26 20:14:40 +00:00
Christian Winther 6fee842b7a also build and push staging images 2024-01-26 18:24:15 +00:00
Christian Winther 627fffd1ce add .vscode with recommended plugins + settings
which will give a *great* out of the box experience for folks wanting to contribute and uses VS Code
2024-01-26 14:42:24 +00:00
Christian Winther f263dfc4e1 apply editorconfig + shellcheck + shellfmt to all files 2024-01-26 14:41:44 +00:00
Christian Winther c9a3e3aea7 automatically + by default turn off proxy acme if proxy is off 2024-01-23 19:34:48 +00:00
Christian Winther 8672453596 fix configuration loading before referencing config 2024-01-22 13:48:01 +00:00
Christian Winther 347ac6f82b fix Dockerfile indent 2024-01-18 17:33:24 +00:00
Christian Winther 70f4bc06a8 remove unsuded .gitmodules 2024-01-18 17:29:15 +00:00
Christian Winther a940bedf9e reference docs PR 2024-01-18 16:23:05 +00:00
Christian Winther 2d223d61ed remove docs
they now live in https://github.com/pixelfed/docs-next/pull/1

and https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/
2024-01-18 16:22:26 +00:00
Christian Winther f2b28ece6e longer entrypoint bars 2024-01-17 18:26:04 +00:00
Christian Winther 3598f9f8f4 move check-config to after fix-permissions 2024-01-17 18:24:35 +00:00
Christian Winther 29564a5809 docs tuning 2024-01-17 18:20:45 +00:00
Christian Winther 033db841f4 try github alerts 2024-01-17 18:19:04 +00:00
Christian Winther a383233710 try github alerts 2024-01-17 18:12:30 +00:00
Christian Winther 32ad4266d0 try github alerts 2024-01-17 18:10:12 +00:00
Christian Winther 1500791198 try github alerts 2024-01-17 18:07:45 +00:00
Christian Winther e858a453be try github alerts 2024-01-17 18:06:43 +00:00
Christian Winther 4729ffb7d5 try github alerts 2024-01-17 18:05:02 +00:00
Christian Winther 83d92c4819 try github alerts 2024-01-17 18:04:08 +00:00
Christian Winther eba2db76f2 try github alerts 2024-01-17 18:03:18 +00:00
Christian Winther a3fd373796 try github alerts 2024-01-17 18:02:38 +00:00
Christian Winther 98bae1316f cleanup .env.docker variable names and placement in the file 2024-01-17 17:51:37 +00:00
Christian Winther 068143639f fix gitignore 2024-01-17 16:45:05 +00:00
Christian Winther f135a240cd fix color 2024-01-17 16:41:01 +00:00
Christian Winther a094a0bd66 syntax fix 2024-01-17 16:30:40 +00:00
Christian Winther dc95d4d800 colors 2024-01-17 16:29:15 +00:00
Christian Winther ca0a25912a more color tuning 2024-01-17 16:25:34 +00:00
Christian Winther 62efe8b3d4 ensure default health check values 2024-01-17 16:18:29 +00:00
Christian Winther ead7c33275 more docker config tuning 2024-01-17 16:11:36 +00:00
Christian Winther cc9f673eea test proxy via direct url 2024-01-17 16:01:52 +00:00
Christian Winther 921f34d42e color tweaks 2024-01-17 15:59:58 +00:00
Christian Winther 82ab545f1a more clear separation between log entry points 2024-01-17 15:55:05 +00:00
Christian Winther eee17fe9f2 harden proxy health check to be https based 2024-01-17 15:53:17 +00:00
Christian Winther adbd66eb38 fix defaults 2024-01-17 15:52:09 +00:00
Christian Winther 3feb93b034 cleanup color output 2024-01-17 15:49:49 +00:00
Christian Winther a4646df8f2 add some health checks 2024-01-17 15:47:39 +00:00
Christian Winther 2d05eccb87 add bats testing 2024-01-17 15:37:12 +00:00
Christian Winther e70e13e265 possible fix is-false/true logic 2024-01-17 14:52:22 +00:00
Christian Winther 90c9d8b5a6 more toned down colors 2024-01-17 14:50:29 +00:00
Christian Winther 9ad04a285a fix missing output colors 2024-01-17 14:48:02 +00:00
Christian Winther 3a7fd8eac9 fix default variables 2024-01-17 14:46:07 +00:00
Christian Winther 45f1df78b0 update proxy-acme paths 2024-01-17 14:41:48 +00:00
Christian Winther 44266b950b conditionally initialize passport and instance actor 2024-01-17 14:29:24 +00:00
Christian Winther be2ba79dc2 bugfixes 2024-01-17 14:25:31 +00:00
Christian Winther d8b37e6870 debug redis 2024-01-17 14:13:38 +00:00
Christian Winther 6563d4d0b9 add goss (https://github.com/goss-org/goss) validation 2024-01-17 13:49:56 +00:00
Christian Winther afa335b7b5 add missing pecl back 2024-01-17 12:50:55 +00:00
Christian Winther a70f108616 fix shellcheck error 2024-01-16 20:53:54 +00:00
Christian Winther bb960fd485 Merge branch 'jippi-fork' of github.com:jippi/pixelfed into jippi-fork 2024-01-16 20:51:49 +00:00
Christian Winther 88ad5d6a4f ignore some shellchecks for .env files 2024-01-16 20:51:37 +00:00
Christian Winther 24220ef2a8
Merge branch 'pixelfed:dev' into jippi-fork 2024-01-16 21:49:51 +01:00
Christian Winther daba285ea7 tune-up 2024-01-15 23:54:41 +00:00
Christian Winther de96c5f06d migration docs 2024-01-15 23:50:16 +00:00
Christian Winther 72b454143b tweaking configs 2024-01-15 20:42:11 +00:00
Christian Winther af1df5edfd ooops 2024-01-15 20:24:02 +00:00
Christian Winther 2135199c97 tune the github workflow config 2024-01-15 20:19:04 +00:00
Christian Winther 9c426b48a1 more docs 2024-01-15 19:56:35 +00:00
Christian Winther 48e5d45b3f improve faq 2024-01-15 19:43:52 +00:00
Christian Winther 98660760c9 improve faq 2024-01-15 19:39:59 +00:00
Christian Winther 53eb9c11fc add faq 2024-01-15 19:20:22 +00:00
Christian Winther 903aeb7608 more cleanup 2024-01-15 18:53:54 +00:00
Christian Winther 685f62a5d0 allow skipping one-time setup tasks 2024-01-15 18:44:43 +00:00
Christian Winther 7f99bb1024 implement automatic shellcheck linting 2024-01-15 17:30:14 +00:00
Christian Winther fa10fe999e implement automatic shellcheck linting 2024-01-15 17:25:42 +00:00
Christian Winther b2d6d3dbe7 implement automatic shellcheck linting 2024-01-15 17:23:32 +00:00
Christian Winther f2f2517503 implement automatic shellcheck linting 2024-01-15 17:17:48 +00:00
Christian Winther ed0f9d64c8 implement automatic shellcheck linting 2024-01-15 17:16:00 +00:00
Christian Winther 901d11df60 more docs help 2024-01-15 16:16:58 +00:00
Christian Winther 20ef1c7b94 backfil docs 2024-01-15 16:13:59 +00:00
Christian Winther 20a15c2b65 split up docs into smaller docs 2024-01-15 16:09:07 +00:00
Christian Winther 01ecde1592 allow skipping one-time setup tasks 2024-01-15 15:32:29 +00:00
Christian Winther 9814a39fd8 more docs 2024-01-15 15:14:44 +00:00
Christian Winther 519704cbe8 more tuning 2024-01-15 14:57:40 +00:00
Christian Winther 543dac34f6 update path 2024-01-15 14:48:12 +00:00
Christian Winther edbc1e4d60 expand docs 2024-01-15 14:44:47 +00:00
Christian Winther c258a15761 cleanup a bit 2024-01-15 14:42:54 +00:00
Christian Winther 84c9aeb514 fixing postgresql and some more utility help 2024-01-15 14:16:54 +00:00
Christian Winther 73b6db168a
Merge branch 'pixelfed:dev' into jippi-fork 2024-01-15 13:17:02 +01:00
Christian Winther 6f0a6aeb3d fix hadolint path 2024-01-07 14:54:28 +00:00
Christian Winther 2e3c7e862c iterating on proxy + letsencrypt setup 2024-01-06 18:01:48 +00:00
Christian Winther 284bb26d92 sync 2024-01-06 16:43:48 +00:00
Christian Winther 9445980e04 expose both http and https ports 2024-01-06 15:57:20 +00:00
Christian Winther bd1cd9c4fc more docs 2024-01-06 15:39:30 +00:00
Christian Winther e228a1622d refactor layout 2024-01-06 14:19:36 +00:00
Christian Winther c9b11a4a29 remove testing key 2024-01-06 14:13:16 +00:00
Christian Winther 092f7f704c fix nginx? 2024-01-06 00:01:51 +00:00
Christian Winther 6edf266a14 quick take on applying migrations automatically 2024-01-05 23:54:17 +00:00
Christian Winther a8c5585e19 use upstream Docker images over self-built 2024-01-05 23:41:33 +00:00
Christian Winther a25b7910b2 first time setup and more refinements 2024-01-05 23:16:26 +00:00
Christian Winther 7db513b366 sync 2024-01-05 18:16:38 +00:00
Christian Winther 76e1199dc7 sync 2024-01-05 17:35:07 +00:00
Christian Winther 2e2ffc5519 comment build steps out to use remote image 2024-01-05 17:31:34 +00:00
Christian Winther d876533991 remove tmp token 2024-01-05 17:30:30 +00:00
Christian Winther c4404590f2 add first time setup logic 2024-01-05 17:29:45 +00:00
Christian Winther c1fbccb07c bootstrapping worked 2024-01-05 16:52:00 +00:00
Christian Winther 052c11882c tweak 10-storage.sh 2024-01-05 16:33:08 +00:00
Christian Winther 215b49ea3d rename2 2024-01-05 16:27:11 +00:00
Christian Winther 10674ac523 iterate on apache example with docker-compose 2024-01-05 16:18:48 +00:00
Christian Winther f2eb3df85f remove VOLUME and EXPOSE
see https://stackoverflow.com/a/52571354/1081818
2024-01-05 01:34:46 +00:00
Christian Winther 5cfd8e15a9 quotes 2024-01-05 00:16:36 +00:00
Christian Winther 99e2a045a6 more renaming for clarity 2024-01-05 00:11:20 +00:00
Christian Winther d13895a3e0 add 15-storage-permissions.sh to the docs 2024-01-04 23:15:46 +00:00
Christian Winther 895b51fd9f more tweaks 2024-01-04 23:04:25 +00:00
Christian Winther 890827d60e Merge branch 'dev' of github.com:pixelfed/pixelfed into jippi-fork 2024-01-04 22:34:57 +00:00
Christian Winther c12ef66c56 opt-in fixing of user/group ownership of files 2024-01-04 22:33:41 +00:00
Christian Winther c64571e46d more docs 2024-01-04 22:16:25 +00:00
Christian Winther f2c8497136 more clanup 2024-01-04 21:55:24 +00:00
Christian Winther ce34e4d046 more docs and rework 2024-01-04 21:21:00 +00:00
Christian Winther a08a5e7cde more docs and rework 2024-01-04 20:55:04 +00:00
Christian Winther e05575283a update docs 2024-01-04 16:12:18 +00:00
Christian Winther c369ef50a7 more refactoring for templating 2024-01-04 16:08:01 +00:00
Christian Winther 7dcca09c65 a bit of refactoring 2024-01-04 13:07:01 +00:00
Christian Winther 7b3e11012f merge dev 2024-01-04 11:33:54 +00:00
Christian Winther 0aee66810d fix editorconfig 2024-01-04 11:28:00 +00:00
Christian Winther 6244511cf8 don't hardcode UID/GID for runtime 2024-01-04 11:20:22 +00:00
Christian Winther f390c3c3e9 install all database extensions by default
lifted from https://github.com/pixelfed/pixelfed/pull/4172
2024-01-04 11:11:16 +00:00
Christian Winther cf080dda09 rename init files 2024-01-04 11:01:56 +00:00
Christian Winther b19d3a20dd only run kernel tasks on one server
lifted from https://github.com/pixelfed/pixelfed/pull/4634
2024-01-04 11:00:45 +00:00
Christian Winther 98211d3620 refactor Dockerfile and Docker workflow 2023-12-28 23:46:59 +00:00
149 changed files with 12323 additions and 6090 deletions

View File

@ -4,4 +4,4 @@
## Usage: redis-cli [flags] [args]
## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version"
redis-cli -p 6379 -h redis $@
exec redis-cli -p 6379 -h redis "$@"

View File

@ -1,8 +1,30 @@
data
Dockerfile
contrib/docker/Dockerfile.*
docker-compose*.yml
.dockerignore
.git
.gitignore
.env
.DS_Store
/.bash_history
/.bash_profile
/.bashrc
/.composer
/.env
/.env.dottie-backup
/.git
/.git-credentials
/.gitconfig
/.gitignore
/.idea
/.vagrant
/bootstrap/cache
/docker-compose-state/
/Homestead.json
/Homestead.yaml
/node_modules
/npm-debug.log
/public/hot
/public/storage
/public/vendor/horizon
/storage/*.key
/storage/docker
/vendor
/yarn-error.log
# Exceptions - these *MUST* be last
!/bootstrap/cache/.gitignore
!/public/vendor/horizon/.gitignore

View File

@ -7,3 +7,21 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.{sh,envsh,env,env*}]
indent_style = space
indent_size = 4
# ShellCheck config
shell_variant = bash # like -ln=bash
binary_next_line = true # like -bn
switch_case_indent = true # like -ci
space_redirects = false # like -sr
keep_padding = false # like -kp
function_next_line = true # like -fn
never_split = true # like -ns
simplify = true

File diff suppressed because it is too large Load Diff

View File

@ -1,78 +0,0 @@
APP_NAME="Pixelfed"
APP_ENV="production"
APP_KEY=
APP_DEBUG="false"
# Instance Configuration
OPEN_REGISTRATION="false"
ENFORCE_EMAIL_VERIFICATION="false"
PF_MAX_USERS="1000"
OAUTH_ENABLED="true"
# Media Configuration
PF_OPTIMIZE_IMAGES="true"
IMAGE_QUALITY="80"
MAX_PHOTO_SIZE="15000"
MAX_CAPTION_LENGTH="500"
MAX_ALBUM_LENGTH="4"
# Instance URL Configuration
APP_URL="http://localhost"
APP_DOMAIN="localhost"
ADMIN_DOMAIN="localhost"
SESSION_DOMAIN="localhost"
TRUST_PROXIES="*"
# Database Configuration
DB_CONNECTION="mysql"
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_DATABASE="pixelfed"
DB_USERNAME="pixelfed"
DB_PASSWORD="pixelfed"
# Redis Configuration
REDIS_CLIENT="predis"
REDIS_SCHEME="tcp"
REDIS_HOST="127.0.0.1"
REDIS_PASSWORD="null"
REDIS_PORT="6379"
# Laravel Configuration
SESSION_DRIVER="database"
CACHE_DRIVER="redis"
QUEUE_DRIVER="redis"
BROADCAST_DRIVER="log"
LOG_CHANNEL="stack"
HORIZON_PREFIX="horizon-"
# ActivityPub Configuration
ACTIVITY_PUB="false"
AP_REMOTE_FOLLOW="false"
AP_INBOX="false"
AP_OUTBOX="false"
AP_SHAREDINBOX="false"
# Experimental Configuration
EXP_EMC="true"
## Mail Configuration (Post-Installer)
MAIL_DRIVER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="pixelfed@example.com"
MAIL_FROM_NAME="Pixelfed"
## S3 Configuration (Post-Installer)
PF_ENABLE_CLOUD=false
FILESYSTEM_CLOUD=s3
#AWS_ACCESS_KEY_ID=
#AWS_SECRET_ACCESS_KEY=
#AWS_DEFAULT_REGION=
#AWS_BUCKET=<BucketName>
#AWS_URL=
#AWS_ENDPOINT=
#AWS_USE_PATH_STYLE_ENDPOINT=false

View File

@ -1,3 +1,5 @@
# shellcheck disable=SC2034,SC2148
APP_NAME="Pixelfed Test"
APP_ENV=local
APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
@ -62,8 +64,8 @@ CS_BLOCKED_DOMAINS='example.org,example.net,example.com'
CS_CW_DOMAINS='example.org,example.net,example.com'
CS_UNLISTED_DOMAINS='example.org,example.net,example.com'
## Optional
## Optional
#HORIZON_DARKMODE=false # Horizon theme darkmode
#HORIZON_EMBED=false # Single Docker Container mode
#HORIZON_EMBED=false # Single Docker Container mode
ENABLE_CONFIG_CACHE=false

View File

@ -1,125 +0,0 @@
---
name: Build Docker image
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- '*'
pull_request:
paths:
- .github/workflows/build-docker.yml
- contrib/docker/Dockerfile.apache
- contrib/docker/Dockerfile.fpm
permissions:
contents: read
jobs:
build-docker-apache:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Docker Lint
uses: hadolint/hadolint-action@v3.0.0
with:
dockerfile: contrib/docker/Dockerfile.apache
failure-threshold: error
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
secrets: inherit
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
if: github.event_name != 'pull_request'
- name: Fetch tags
uses: docker/metadata-action@v4
secrets: inherit
id: meta
with:
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
flavor: |
latest=auto
suffix=-apache
tags: |
type=edge,branch=dev
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=pr
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
file: contrib/docker/Dockerfile.apache
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-docker-fpm:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Docker Lint
uses: hadolint/hadolint-action@v3.0.0
with:
dockerfile: contrib/docker/Dockerfile.fpm
failure-threshold: error
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
secrets: inherit
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
if: github.event_name != 'pull_request'
- name: Fetch tags
uses: docker/metadata-action@v4
secrets: inherit
id: meta
with:
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
flavor: |
suffix=-fpm
tags: |
type=edge,branch=dev
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=pr
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
file: contrib/docker/Dockerfile.fpm
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

230
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,230 @@
---
name: Docker
on:
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
workflow_dispatch:
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
push:
branches:
- dev
- staging
tags:
- "*"
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
pull_request:
types:
- opened
- reopened
- synchronize
jobs:
lint:
name: hadolint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Docker Lint
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
failure-threshold: error
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
env:
SHELLCHECK_OPTS: --shell=bash --external-sources
with:
version: v0.9.0
additional_files: "*.envsh .env .env.docker .env.example .env.testing"
bats:
name: Bats Testing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run bats
run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats
build:
name: Build, Test, and Push
runs-on: ubuntu-latest
strategy:
fail-fast: false
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
matrix:
php_version:
- 8.2
- 8.3
target_runtime:
- apache
- fpm
- nginx
php_base:
- apache
- fpm
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude
exclude:
# targeting [apache] runtime with [fpm] base type doesn't make sense
- target_runtime: apache
php_base: fpm
# targeting [fpm] runtime with [apache] base type doesn't make sense
- target_runtime: fpm
php_base: apache
# targeting [nginx] runtime with [apache] base type doesn't make sense
- target_runtime: nginx
php_base: apache
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior
concurrency:
group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }}
cancel-in-progress: true
permissions:
contents: read
packages: write
env:
# Set the repo variable [DOCKER_HUB_USERNAME] to override the default
# at https://github.com/<user>/<project>/settings/variables/actions
DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }}
# Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default
# at https://github.com/<user>/<project>/settings/variables/actions
DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }}
# Set the repo variable [DOCKER_HUB_REPO] to override the default
# at https://github.com/<user>/<project>/settings/variables/actions
DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }}
# For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN]
# set to your Personal Access Token at https://github.com/<user>/<project>/settings/secrets/actions
#
# ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set!
HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
id: buildx
with:
version: v0.12.0 # *or* newer, needed for annotations to work
# See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry
- name: Log in to the GitHub Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub
- name: Login to Docker Hub registry (conditionally)
if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }}
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Docker meta
uses: docker/metadata-action@v5
id: meta
with:
images: |
name=ghcr.io/${{ github.repository }},enable=true
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
flavor: |
latest=auto
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
tags: |
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=branch,prefix=branch-
type=ref,event=pr,prefix=pr-
type=ref,event=tag
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Docker meta (Cache)
uses: docker/metadata-action@v5
id: cache
with:
images: |
name=ghcr.io/${{ github.repository }}-cache,enable=true
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
flavor: |
latest=auto
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
tags: |
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=branch,prefix=branch-
type=ref,event=pr,prefix=pr-
type=ref,event=tag
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
target: ${{ matrix.target_runtime }}-runtime
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
push: true
sbom: true
provenance: true
build-args: |
PHP_VERSION=${{ matrix.php_version }}
PHP_BASE_TYPE=${{ matrix.php_base }}
cache-from: |
type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
cache-to: |
type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
${{ steps.cache.outputs.tags }}
# goss validate the image
#
# See: https://github.com/goss-org/goss
- uses: e1himself/goss-installation-action@v1
with:
version: "v0.4.4"
- name: Execute Goss tests
run: |
dgoss run \
-v "./.env.testing:/var/www/.env" \
-e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \
-e "PHP_BASE_TYPE=${{ matrix.php_base }}" \
${{ steps.meta.outputs.tags }}

43
.gitignore vendored
View File

@ -1,22 +1,31 @@
.DS_Store
/.bash_history
/.bash_profile
/.bashrc
/.composer
/.env
/.env.dottie-backup
#/.git
/.git-credentials
/.gitconfig
#/.gitignore
/.idea
/.vagrant
/bootstrap/cache
/docker-compose-state/
/Homestead.json
/Homestead.yaml
/node_modules
/npm-debug.log
/public/hot
/public/storage
/public/vendor/horizon
/storage/*.key
/storage/docker
/vendor
/.idea
/.vscode
/.vagrant
/docker-volumes
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.env
.DS_Store
.bash_profile
.bash_history
.bashrc
.gitconfig
.git-credentials
/.composer/
/nginx.conf
/yarn-error.log
/public/build
# Exceptions - these *MUST* be last
!/bootstrap/cache/.gitignore
!/public/vendor/horizon/.gitignore

5
.hadolint.yaml Normal file
View File

@ -0,0 +1,5 @@
ignored:
- DL3002 # warning: Last USER should not be root
- DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
- SC2046 # warning: Quote this to prevent word splitting.
- SC2086 # info: Double quote to prevent globbing and word splitting.

4
.markdownlint.json Normal file
View File

@ -0,0 +1,4 @@
{
"MD013": false,
"MD014": false
}

12
.shellcheckrc Normal file
View File

@ -0,0 +1,12 @@
# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files
source-path=SCRIPTDIR
# Allow opening any 'source'd file, even if not specified as input
external-sources=true
# Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables
# Turn on warnings for unassigned uppercase variables
enable=check-unassigned-uppercase

14
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"recommendations": [
"foxundermoon.shell-format",
"timonwong.shellcheck",
"jetmartin.bats",
"aaron-bond.better-comments",
"streetsidesoftware.code-spell-checker",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"bmewburn.vscode-intelephense-client",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker"
]
}

21
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
"shellformat.useEditorConfig": true,
"[shellscript]": {
"files.eol": "\n",
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[yaml]": {
"editor.defaultFormatter": "redhat.vscode-yaml"
},
"[dockercompose]": {
"editor.defaultFormatter": "redhat.vscode-yaml",
"editor.autoIndent": "advanced",
},
"yaml.schemas": {
"https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json"
},
"files.associations": {
".env": "shellscript",
".env.*": "shellscript"
}
}

View File

@ -1,10 +1,35 @@
# Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.12...dev)
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.13...dev)
### Updates
- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6))
- Update Post.vue, fix cache bug ([3a27e637](https://github.com/pixelfed/pixelfed/commit/3a27e637))
- Update StatusHashtagService, use more efficient cached count ([592c8412](https://github.com/pixelfed/pixelfed/commit/592c8412))
- Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a))
- Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c))
- Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8))
- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7))
- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06))
- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9))
- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4))
- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595))
- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f))
- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843))
- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6))
- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)
### Features
- Curated Onboarding ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212))
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f))
### Updates
@ -14,7 +39,24 @@
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
- ([](https://github.com/pixelfed/pixelfed/commit/))
- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641))
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607))
- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106))
- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997))
- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b))
- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655))
- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58))
- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3))
- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7))
- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599))
- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a))
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)

18
CODEOWNERS Normal file
View File

@ -0,0 +1,18 @@
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
* @dansup
# Docker related files
.editorconfig @jippi @dansup
.env @jippi @dansup
.env.* @jippi @dansup
.hadolint.yaml @jippi @dansup
.shellcheckrc @jippi @dansup
/.github/ @jippi @dansup
/docker/ @jippi @dansup
/tests/ @jippi @dansup
docker-compose.migrate.yml @jippi @dansup
docker-compose.yml @jippi @dansup
goss.yaml @jippi @dansup

307
Dockerfile Normal file
View File

@ -0,0 +1,307 @@
# syntax=docker/dockerfile:1
# See https://hub.docker.com/r/docker/dockerfile
#######################################################
# Configuration
#######################################################
# See: https://github.com/mlocati/docker-php-extension-installer
ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80"
# See: https://github.com/composer/composer
ARG COMPOSER_VERSION="2.6"
# See: https://nginx.org/
ARG NGINX_VERSION="1.25.3"
# See: https://github.com/ddollar/forego
ARG FOREGO_VERSION="0.17.2"
# See: https://github.com/hairyhenderson/gomplate
ARG GOMPLATE_VERSION="v3.11.6"
# See: https://github.com/jippi/dottie
ARG DOTTIE_VERSION="v0.9.5"
###
# PHP base configuration
###
# See: https://hub.docker.com/_/php/tags
ARG PHP_VERSION="8.1"
# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants
ARG PHP_BASE_TYPE="apache"
ARG PHP_DEBIAN_RELEASE="bullseye"
ARG RUNTIME_UID=33 # often called 'www-data'
ARG RUNTIME_GID=33 # often called 'www-data'
# APT extra packages
ARG APT_PACKAGES_EXTRA=
# Extensions installed via [pecl install]
# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't
# ! been released yet (after +10 months)!
# ! See: https://github.com/Imagick/imagick/pull/641
ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58"
ARG PHP_PECL_EXTENSIONS_EXTRA=
# Extensions installed via [docker-php-ext-install]
ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd"
ARG PHP_EXTENSIONS_EXTRA=""
ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite"
# GPG key for nginx apt repository
ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62"
# GPP key path for nginx apt repository
ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg"
#######################################################
# Docker "copy from" images
#######################################################
# Composer docker image from Docker Hub
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
FROM composer:${COMPOSER_VERSION} AS composer-image
# php-extension-installer image from Docker Hub
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer
# nginx webserver from Docker Hub.
# Used to copy some docker-entrypoint files for [nginx-runtime]
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
FROM nginx:${NGINX_VERSION} AS nginx-image
# Forego is a Procfile "runner" that makes it trival to run multiple
# processes under a simple init / PID 1 process.
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
#
# See: https://github.com/nginx-proxy/forego
FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image
# Dottie makes working with .env files easier and safer
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
#
# See: https://github.com/jippi/dottie
FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image
# gomplate-image grabs the gomplate binary from GitHub releases
#
# It's in its own layer so it can be fetched in parallel with other build steps
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image
ARG TARGETARCH
ARG TARGETOS
ARG GOMPLATE_VERSION
RUN set -ex \
&& curl \
--silent \
--show-error \
--location \
--output /usr/local/bin/gomplate \
https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \
&& chmod +x /usr/local/bin/gomplate \
&& /usr/local/bin/gomplate --version
#######################################################
# Base image
#######################################################
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base
ARG BUILDKIT_SBOM_SCAN_STAGE="true"
ARG APT_PACKAGES_EXTRA
ARG PHP_DEBIAN_RELEASE
ARG PHP_VERSION
ARG RUNTIME_GID
ARG RUNTIME_UID
ARG TARGETPLATFORM
ENV DEBIAN_FRONTEND="noninteractive"
# Ensure we run all scripts through 'bash' rather than 'sh'
SHELL ["/bin/bash", "-c"]
RUN set -ex \
&& mkdir -pv /var/www/ \
&& chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www
WORKDIR /var/www/
ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA}
# Install and configure base layer
COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh
RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
/docker/install/base.sh
#######################################################
# PHP: extensions
#######################################################
FROM base AS php-extensions
ARG PHP_DEBIAN_RELEASE
ARG PHP_EXTENSIONS
ARG PHP_EXTENSIONS_DATABASE
ARG PHP_EXTENSIONS_EXTRA
ARG PHP_PECL_EXTENSIONS
ARG PHP_PECL_EXTENSIONS_EXTRA
ARG PHP_VERSION
ARG TARGETPLATFORM
COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh
RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \
--mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
PHP_EXTENSIONS=${PHP_EXTENSIONS} \
PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \
PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \
PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \
PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \
/docker/install/php-extensions.sh
#######################################################
# PHP: composer and source code
#######################################################
FROM php-extensions AS composer-and-src
ARG PHP_VERSION
ARG PHP_DEBIAN_RELEASE
ARG RUNTIME_UID
ARG RUNTIME_GID
ARG TARGETPLATFORM
# Make sure composer cache is targeting our cache mount later
ENV COMPOSER_CACHE_DIR="/cache/composer"
# Don't enforce any memory limits for composer
ENV COMPOSER_MEMORY_LIMIT=-1
# Disable interactvitity from composer
ENV COMPOSER_NO_INTERACTION=1
# Copy composer from https://hub.docker.com/_/composer
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
#! Changing user to runtime user
USER ${RUNTIME_UID}:${RUNTIME_GID}
# Install composer dependencies
# NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet)
RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION},sharing=locked,target=/cache/composer \
--mount=type=bind,source=composer.json,target=/var/www/composer.json \
--mount=type=bind,source=composer.lock,target=/var/www/composer.lock \
set -ex \
&& composer install --prefer-dist --no-autoloader --ignore-platform-reqs
# Copy all other files over
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/
#######################################################
# Runtime: base
#######################################################
FROM php-extensions AS shared-runtime
ARG RUNTIME_GID
ARG RUNTIME_UID
ENV RUNTIME_UID=${RUNTIME_UID}
ENV RUNTIME_GID=${RUNTIME_GID}
COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego
COPY --link --from=dottie-image /dottie /usr/local/bin/dottie
COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www
#! Changing user to runtime user
USER ${RUNTIME_UID}:${RUNTIME_GID}
# Generate optimized autoloader now that we have all files around
RUN set -ex \
&& composer dump-autoload --optimize
USER root
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
RUN set -ex \
&& cp --recursive --link --preserve=all storage storage.skel \
&& rm -rf html && ln -s public html
COPY docker/shared/root /
ENTRYPOINT ["/docker/entrypoint.sh"]
#######################################################
# Runtime: apache
#######################################################
FROM shared-runtime AS apache-runtime
COPY docker/apache/root /
RUN set -ex \
&& a2enmod rewrite remoteip proxy proxy_http \
&& a2enconf remoteip
CMD ["apache2-foreground"]
#######################################################
# Runtime: fpm
#######################################################
FROM shared-runtime AS fpm-runtime
COPY docker/fpm/root /
CMD ["php-fpm"]
#######################################################
# Runtime: nginx
#######################################################
FROM shared-runtime AS nginx-runtime
ARG NGINX_GPGKEY
ARG NGINX_GPGKEY_PATH
ARG NGINX_VERSION
ARG PHP_DEBIAN_RELEASE
ARG PHP_VERSION
ARG TARGETPLATFORM
# Install nginx dependencies
RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
set -ex \
&& gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \
&& gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \
&& echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}*
# copy docker entrypoints from the *real* nginx image directly
COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/
COPY docker/nginx/root /
COPY docker/nginx/Procfile .
STOPSIGNAL SIGQUIT
CMD ["forego", "start", "-r"]

View File

@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\Internal\SoftwareUpdateService;
use Cache;
class SoftwareUpdateRefresh extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:software-update-refresh';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Refresh latest software version data';
/**
* Execute the console command.
*/
public function handle()
{
$key = SoftwareUpdateService::cacheKey();
Cache::forget($key);
Cache::remember($key, 1209600, function() {
return SoftwareUpdateService::fetchLatest();
});
$this->info('Succesfully updated software versions!');
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Console\Commands;
use App\Instance;
use App\Profile;
use App\Transformer\ActivityPub\Verb\DeleteActor;
use App\User;
use App\Util\ActivityPub\HttpSignature;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use Illuminate\Console\Command;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\search;
use function Laravel\Prompts\table;
class UserAccountDelete extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:user-account-delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Federate Account Deletion';
/**
* Execute the console command.
*/
public function handle()
{
$id = search(
label: 'Search for the account to delete by username',
placeholder: 'john.appleseed',
options: fn (string $value) => strlen($value) > 0
? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all()
: [],
);
$user = User::withTrashed()->find($id);
table(
['Username', 'Name', 'Email', 'Created'],
[[$user->username, $user->name, $user->email, $user->created_at]]
);
$confirmed = confirm(
label: 'Do you want to federate this account deletion?',
default: false,
yes: 'Proceed',
no: 'Cancel',
hint: 'This action is irreversible'
);
if (! $confirmed) {
$this->error('Aborting...');
exit;
}
$profile = Profile::withTrashed()->find($user->profile_id);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($profile, new DeleteActor());
$activity = $fractal->createData($resource)->toArray();
$audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched'])
->where('nodeinfo_last_fetched', '>', now()->subHours(12))
->distinct()
->pluck('shared_inbox');
$payload = json_encode($activity);
$client = new Client([
'timeout' => 10,
]);
$version = config('pixelfed.version');
$appUrl = config('app.url');
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
foreach ($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity, [
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => $userAgent,
]);
yield function () use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
],
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => 50,
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
},
]);
$promise = $pool->promise();
$promise->wait();
}
}

View File

@ -25,31 +25,32 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('media:optimize')->hourlyAt(40);
$schedule->command('media:gc')->hourlyAt(5);
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('story:gc')->everyFiveMinutes();
$schedule->command('gc:failedjobs')->dailyAt(3);
$schedule->command('gc:passwordreset')->dailyAt('09:41');
$schedule->command('gc:sessions')->twiceDaily(13, 23);
$schedule->command('media:optimize')->hourlyAt(40)->onOneServer();
$schedule->command('media:gc')->hourlyAt(5)->onOneServer();
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
$schedule->command('story:gc')->everyFiveMinutes()->onOneServer();
$schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer();
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
$schedule->command('media:s3gc')->hourlyAt(15);
}
if(config('import.instagram.enabled')) {
$schedule->command('app:transform-imports')->everyTenMinutes();
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51);
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37);
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32);
if (config('import.instagram.enabled')) {
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
if(config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39);
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
}
}
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21');
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25);
$schedule->command('app:account-post-count-stat-update')->everySixHours(25);
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
}
/**
@ -59,7 +60,7 @@ class Kernel extends ConsoleKernel
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}

View File

@ -75,6 +75,7 @@ trait AdminDirectoryController
}
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
$res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled');
$res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
$res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
@ -124,7 +125,7 @@ trait AdminDirectoryController
$res['requirements_validator'] = $validator->errors();
$res['is_eligible'] = $res['open_registration'] &&
$res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) &&
$res['oauth_enabled'] &&
$res['activitypub_enabled'] &&
count($res['requirements_validator']) === 0 &&
@ -227,7 +228,7 @@ trait AdminDirectoryController
->each(function($name) {
Storage::delete($name);
});
$path = $request->file('banner_image')->store('public/headers');
$path = $request->file('banner_image')->storePublicly('public/headers');
$res['banner_image'] = $path;
ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
@ -249,7 +250,8 @@ trait AdminDirectoryController
{
$reqs = [];
$reqs['feature_config'] = [
'open_registration' => config_cache('pixelfed.open_registration'),
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
@ -265,7 +267,8 @@ trait AdminDirectoryController
];
$validator = Validator::make($reqs['feature_config'], [
'open_registration' => 'required|accepted',
'open_registration' => 'required_unless:curated_onboarding,true',
'curated_onboarding' => 'required_unless:open_registration,true',
'activitypub_enabled' => 'required|accepted',
'oauth_enabled' => 'required|accepted',
'media_types' => [

File diff suppressed because it is too large Load Diff

View File

@ -2,18 +2,18 @@
namespace App\Http\Controllers\Admin;
use Artisan, Cache, DB;
use Illuminate\Http\Request;
use Carbon\Carbon;
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
use App\Models\InstanceActor;
use App\Http\Controllers\Controller;
use App\Util\Lexer\PrettyNumber;
use App\Models\ConfigCache;
use App\Models\InstanceActor;
use App\Page;
use App\Profile;
use App\Services\AccountService;
use App\Services\ConfigCacheService;
use App\User;
use App\Util\Site\Config;
use Illuminate\Support\Str;
use Artisan;
use Cache;
use DB;
use Illuminate\Http\Request;
trait AdminSettingsController
{
@ -21,7 +21,7 @@ trait AdminSettingsController
{
$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'));
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null;
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
@ -35,6 +35,7 @@ trait AdminSettingsController
$openReg = (bool) config_cache('pixelfed.open_registration');
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
$accountMigration = (bool) config_cache('federation.migration');
return view('admin.settings.home', compact(
'jpeg',
@ -48,7 +49,8 @@ trait AdminSettingsController
'cloud_ready',
'availableAdmins',
'currentAdmin',
'regState'
'regState',
'accountMigration'
));
}
@ -67,41 +69,42 @@ trait AdminSettingsController
'type_mp4' => 'nullable',
'type_webp' => 'nullable',
'admin_account_id' => 'nullable',
'regs' => 'required|in:open,filtered,closed'
'regs' => 'required|in:open,filtered,closed',
'account_migration' => 'nullable',
]);
$orb = false;
$cob = false;
switch($request->input('regs')) {
switch ($request->input('regs')) {
case 'open':
$orb = true;
$cob = false;
break;
break;
case 'filtered':
$orb = false;
$cob = true;
break;
break;
case 'closed':
$orb = false;
$cob = false;
break;
break;
}
ConfigCacheService::put('pixelfed.open_registration', (bool) $orb);
ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob);
if($request->filled('admin_account_id')) {
if ($request->filled('admin_account_id')) {
ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
Cache::forget('api:v1:instance-data:contact');
Cache::forget('api:v1:instance-data-response-v1');
}
if($request->filled('rule_delete')) {
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)) {
if (! $rules || empty($json)) {
return;
}
unset($json[$index]);
@ -109,6 +112,7 @@ trait AdminSettingsController
ConfigCacheService::put('app.rules', $json);
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
return 200;
}
@ -124,8 +128,8 @@ trait AdminSettingsController
];
foreach ($mimes as $key => $value) {
if($request->input($key) == 'on') {
if(!in_array($value, $media_types)) {
if ($request->input($key) == 'on') {
if (! in_array($value, $media_types)) {
array_push($media_types, $value);
}
} else {
@ -133,7 +137,7 @@ trait AdminSettingsController
}
}
if($media_types !== $media_types_original) {
if ($media_types !== $media_types_original) {
ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
}
@ -147,15 +151,15 @@ trait AdminSettingsController
'account_limit' => 'pixelfed.max_account_size',
'custom_css' => 'uikit.custom.css',
'custom_js' => 'uikit.custom.js',
'about_title' => 'about.title'
'about_title' => 'about.title',
];
foreach ($keys as $key => $value) {
$cc = ConfigCache::whereK($value)->first();
$val = $request->input($key);
if($cc && $cc->v != $val) {
if ($cc && $cc->v != $val) {
ConfigCacheService::put($value, $val);
} else if(!empty($val)) {
} elseif (! empty($val)) {
ConfigCacheService::put($value, $val);
}
}
@ -175,33 +179,34 @@ trait AdminSettingsController
'account_autofollow' => 'account.autofollow',
'show_directory' => 'instance.landing.show_directory',
'show_explore_feed' => 'instance.landing.show_explore',
'account_migration' => 'federation.migration',
];
foreach ($bools as $key => $value) {
$active = $request->input($key) == 'on';
if($key == 'activitypub' && $active && !InstanceActor::exists()) {
if ($key == 'activitypub' && $active && ! InstanceActor::exists()) {
Artisan::call('instance:actor');
}
if( $key == 'mobile_apis' &&
if ($key == 'mobile_apis' &&
$active &&
!file_exists(storage_path('oauth-public.key')) &&
!file_exists(storage_path('oauth-private.key'))
! 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) {
if (config_cache($value) !== $active) {
ConfigCacheService::put($value, (bool) $active);
}
}
if($request->filled('new_rule')) {
if ($request->filled('new_rule')) {
$rules = ConfigCacheService::get('app.rules');
$val = $request->input('new_rule');
if(!$rules) {
if (! $rules) {
ConfigCacheService::put('app.rules', json_encode([$val]));
} else {
$json = json_decode($rules, true);
@ -212,13 +217,13 @@ trait AdminSettingsController
Cache::forget('api:v1:instance-data-response-v1');
}
if($request->filled('account_autofollow_usernames')) {
if ($request->filled('account_autofollow_usernames')) {
$usernames = explode(',', $request->input('account_autofollow_usernames'));
$names = [];
foreach($usernames as $n) {
foreach ($usernames as $n) {
$p = Profile::whereUsername($n)->first();
if(!$p) {
if (! $p) {
continue;
}
array_push($names, $p->username);
@ -236,6 +241,7 @@ trait AdminSettingsController
{
$path = storage_path('app/'.config('app.name'));
$files = is_dir($path) ? new \DirectoryIterator($path) : [];
return view('admin.settings.backups', compact('files'));
}
@ -247,6 +253,7 @@ trait AdminSettingsController
public function settingsStorage(Request $request)
{
$storage = [];
return view('admin.settings.storage', compact('storage'));
}
@ -258,6 +265,7 @@ trait AdminSettingsController
public function settingsPages(Request $request)
{
$pages = Page::orderByDesc('updated_at')->paginate(10);
return view('admin.pages.home', compact('pages'));
}
@ -275,30 +283,31 @@ trait AdminSettingsController
];
switch (config('database.default')) {
case 'pgsql':
$exp = DB::raw('select version();');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'Postgres',
'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
];
break;
$exp = DB::raw('select version();');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'Postgres',
'version' => explode(' ', DB::select($expQuery)[0]->version)[1],
];
break;
case 'mysql':
$exp = DB::raw('select version()');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'MySQL',
'version' => DB::select($expQuery)[0]->{'version()'}
];
break;
$exp = DB::raw('select version()');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'MySQL',
'version' => DB::select($expQuery)[0]->{'version()'},
];
break;
default:
$sys['database'] = [
'name' => 'Unknown',
'version' => '?'
];
break;
$sys['database'] = [
'name' => 'Unknown',
'version' => '?',
];
break;
}
return view('admin.settings.system', compact('sys'));
}
}

View File

@ -2,54 +2,72 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use App\Mail\CuratedRegisterRequestDetailsFromUser;
use App\Mail\CuratedRegisterAcceptUser;
use App\Mail\CuratedRegisterRejectUser;
use App\Mail\CuratedRegisterRequestDetailsFromUser;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use App\Models\CuratedRegisterTemplate;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class AdminCuratedRegisterController extends Controller
{
public function __construct()
{
$this->middleware(['auth','admin']);
$this->middleware(['auth', 'admin']);
}
public function index(Request $request)
{
$this->validate($request, [
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected'
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses',
'sort' => 'sometimes|in:asc,desc',
]);
$filter = $request->input('filter', 'open');
$records = CuratedRegister::when($filter, function($q, $filter) {
if($filter === 'open') {
return $q->where('is_rejected', false)
$sort = $request->input('sort', 'asc');
$records = CuratedRegister::when($filter, function ($q, $filter) {
if ($filter === 'open') {
return $q->where('is_rejected', false)
->where(function ($query) {
return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false);
})
->whereNotNull('email_verified_at')
->whereIsClosed(false);
} else if($filter === 'all') {
return $q;
} elseif ($filter === 'awaiting') {
return $q->whereIsClosed(false)
->whereNull('is_rejected')
->whereNull('is_approved');
} elseif ($filter === 'approved') {
return $q->whereIsClosed(true)->whereIsApproved(true);
} elseif ($filter === 'rejected') {
return $q->whereIsClosed(true)->whereIsRejected(true);
}
} elseif ($filter === 'all') {
return $q;
} elseif ($filter === 'responses') {
return $q->whereIsClosed(false)
->whereNotNull('email_verified_at')
->where('user_has_responded', true)
->where('is_awaiting_more_info', true);
} elseif ($filter === 'awaiting') {
return $q->whereIsClosed(false)
->where('is_rejected', false)
->where('is_approved', false)
->where('user_has_responded', false)
->where('is_awaiting_more_info', true);
} elseif ($filter === 'approved') {
return $q->whereIsClosed(true)->whereIsApproved(true);
} elseif ($filter === 'rejected') {
return $q->whereIsClosed(true)->whereIsRejected(true);
}
})
->when($sort, function ($query, $sort) {
return $query->orderBy('id', $sort);
})
->latest()
->paginate(10);
->paginate(10)
->withQueryString();
return view('admin.curated-register.index', compact('records', 'filter'));
}
public function show(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return view('admin.curated-register.show', compact('record'));
}
@ -65,10 +83,10 @@ class AdminCuratedRegisterController extends Controller
'message' => null,
'link' => null,
'timestamp' => $record->created_at,
]
],
]);
if($record->email_verified_at) {
if ($record->email_verified_at) {
$res->push([
'id' => 3,
'action' => 'email_verified_at',
@ -84,10 +102,15 @@ class AdminCuratedRegisterController extends Controller
$idx = 4;
$userResponses = collect([]);
foreach($activities as $activity) {
foreach ($activities as $activity) {
$idx++;
if($activity->from_user) {
if ($activity->type === 'user_resend_email_confirmation') {
continue;
}
if ($activity->from_user) {
$userResponses->push($activity);
continue;
}
$res->push([
@ -101,20 +124,22 @@ class AdminCuratedRegisterController extends Controller
]);
}
foreach($userResponses as $ur) {
$res = $res->map(function($r) use($ur) {
if(!isset($r['aid'])) {
foreach ($userResponses as $ur) {
$res = $res->map(function ($r) use ($ur) {
if (! isset($r['aid'])) {
return $r;
}
if($ur->reply_to_id === $r['aid']) {
if ($ur->reply_to_id === $r['aid']) {
$r['user_response'] = $ur;
return $r;
}
return $r;
});
}
if($record->is_approved) {
if ($record->is_approved) {
$idx++;
$res->push([
'id' => $idx,
@ -124,7 +149,7 @@ class AdminCuratedRegisterController extends Controller
'link' => null,
'timestamp' => $record->action_taken_at,
]);
} else if ($record->is_rejected) {
} elseif ($record->is_rejected) {
$idx++;
$res->push([
'id' => $idx,
@ -142,13 +167,14 @@ class AdminCuratedRegisterController extends Controller
public function apiMessagePreviewStore(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return $request->all();
}
public function apiMessageSendStore(Request $request, $id)
{
$this->validate($request, [
'message' => 'required|string|min:5|max:1000'
'message' => 'required|string|min:5|max:1000',
]);
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
@ -161,8 +187,10 @@ class AdminCuratedRegisterController extends Controller
$activity->message = $request->input('message');
$activity->save();
$record->is_awaiting_more_info = true;
$record->user_has_responded = false;
$record->save();
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
return $request->all();
}
@ -172,22 +200,23 @@ class AdminCuratedRegisterController extends Controller
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity;
$activity->message = $request->input('message');
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
}
public function previewMessageShow(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$record->message = $request->input('message');
return new \App\Mail\CuratedRegisterSendMessage($record);
}
public function apiHandleReject(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:reject-email,reject-silent'
'action' => 'required|in:reject-email,reject-silent',
]);
$action = $request->input('action');
$record = CuratedRegister::findOrFail($id);
@ -196,9 +225,10 @@ class AdminCuratedRegisterController extends Controller
$record->is_closed = true;
$record->action_taken_at = now();
$record->save();
if($action === 'reject-email') {
if ($action === 'reject-email') {
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
}
return [200];
}
@ -217,10 +247,89 @@ class AdminCuratedRegisterController extends Controller
'password' => $record->password,
'app_register_ip' => $record->ip_address,
'email_verified_at' => now(),
'register_source' => 'cur_onboarding'
'register_source' => 'cur_onboarding',
]);
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
return [200];
}
public function templates(Request $request)
{
$templates = CuratedRegisterTemplate::paginate(10);
return view('admin.curated-register.templates', compact('templates'));
}
public function templateCreate(Request $request)
{
return view('admin.curated-register.template-create');
}
public function templateEdit(Request $request, $id)
{
$template = CuratedRegisterTemplate::findOrFail($id);
return view('admin.curated-register.template-edit', compact('template'));
}
public function templateEditStore(Request $request, $id)
{
$this->validate($request, [
'name' => 'required|string|max:30',
'content' => 'required|string|min:5|max:3000',
'description' => 'nullable|sometimes|string|max:1000',
'active' => 'sometimes',
]);
$template = CuratedRegisterTemplate::findOrFail($id);
$template->name = $request->input('name');
$template->content = $request->input('content');
$template->description = $request->input('description');
$template->is_active = $request->boolean('active');
$template->save();
return redirect()->back()->with('status', 'Successfully updated template!');
}
public function templateDelete(Request $request, $id)
{
$template = CuratedRegisterTemplate::findOrFail($id);
$template->delete();
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!');
}
public function templateStore(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:30',
'content' => 'required|string|min:5|max:3000',
'description' => 'nullable|sometimes|string|max:1000',
'active' => 'sometimes',
]);
CuratedRegisterTemplate::create([
'name' => $request->input('name'),
'content' => $request->input('content'),
'description' => $request->input('description'),
'is_active' => $request->boolean('active'),
]);
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!');
}
public function getActiveTemplates(Request $request)
{
$templates = CuratedRegisterTemplate::whereIsActive(true)
->orderBy('order')
->get()
->map(function ($tmp) {
return [
'name' => $tmp->name,
'content' => $tmp->content,
];
});
return response()->json($templates);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -473,15 +473,15 @@ class ApiV1Dot1Controller extends Controller
{
return [
'open' => (bool) config_cache('pixelfed.open_registration'),
'iara' => config('pixelfed.allow_app_registration')
'iara' => (bool) config_cache('pixelfed.allow_app_registration'),
];
}
public function inAppRegistration(Request $request)
{
abort_if($request->user(), 404);
abort_unless(config_cache('pixelfed.open_registration'), 404);
abort_unless(config('pixelfed.allow_app_registration'), 404);
abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -609,8 +609,8 @@ class ApiV1Dot1Controller extends Controller
public function inAppRegistrationConfirm(Request $request)
{
abort_if($request->user(), 404);
abort_unless(config_cache('pixelfed.open_registration'), 404);
abort_unless(config('pixelfed.allow_app_registration'), 404);
abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);

View File

@ -104,7 +104,7 @@ class ApiV2Controller extends Controller
'max_featured_tags' => 0,
],
'statuses' => [
'max_characters' => (int) config('pixelfed.max_caption_length'),
'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
'characters_reserved_per_url' => 23
],

View File

@ -2,23 +2,18 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use DB;
use Cache;
use App\Comment;
use App\Jobs\CommentPipeline\CommentPipeline;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Util\Lexer\Autolink;
use App\Profile;
use App\Status;
use App\UserFilter;
use League\Fractal;
use App\Transformer\Api\StatusTransformer;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\StatusService;
use App\Status;
use App\Transformer\Api\StatusTransformer;
use App\UserFilter;
use App\Util\Lexer\Autolink;
use Auth;
use DB;
use Illuminate\Http\Request;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class CommentController extends Controller
{
@ -33,9 +28,9 @@ class CommentController extends Controller
abort(403);
}
$this->validate($request, [
'item' => 'required|integer|min:1',
'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'),
'sensitive' => 'nullable|boolean'
'item' => 'required|integer|min:1',
'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'),
'sensitive' => 'nullable|boolean',
]);
$comment = $request->input('comment');
$statusId = $request->input('item');
@ -45,7 +40,7 @@ class CommentController extends Controller
$profile = $user->profile;
$status = Status::findOrFail($statusId);
if($status->comments_disabled == true) {
if ($status->comments_disabled == true) {
return;
}
@ -55,11 +50,11 @@ class CommentController extends Controller
->whereFilterableId($profile->id)
->exists();
if($filtered == true) {
if ($filtered == true) {
return;
}
$reply = DB::transaction(function() use($comment, $status, $profile, $nsfw) {
$reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) {
$scope = $profile->is_private == true ? 'private' : 'public';
$autolink = Autolink::create()->autolink($comment);
$reply = new Status();

View File

@ -2,59 +2,38 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth, Cache, DB, Storage, URL;
use Carbon\Carbon;
use App\{
Avatar,
Collection,
CollectionItem,
Hashtag,
Like,
Media,
MediaTag,
Notification,
Profile,
Place,
Status,
UserFilter,
UserSetting
};
use App\Models\Poll;
use App\Transformer\Api\{
MediaTransformer,
MediaDraftTransformer,
StatusTransformer,
StatusStatelessTransformer
};
use League\Fractal;
use App\Util\Media\Filter;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Jobs\AvatarPipeline\AvatarOptimize;
use App\Collection;
use App\CollectionItem;
use App\Hashtag;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\VideoPipeline\{
VideoOptimize,
VideoPostProcess,
VideoThumbnail
};
use App\Jobs\VideoPipeline\VideoThumbnail;
use App\Media;
use App\MediaTag;
use App\Models\Poll;
use App\Notification;
use App\Profile;
use App\Services\AccountService;
use App\Services\CollectionService;
use App\Services\NotificationService;
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Services\MediaPathService;
use App\Services\MediaStorageService;
use App\Services\MediaTagService;
use App\Services\StatusService;
use App\Services\SnowflakeService;
use Illuminate\Support\Str;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
use App\Util\Media\License;
use Image;
use App\Services\UserRoleService;
use App\Status;
use App\Transformer\Api\MediaTransformer;
use App\UserFilter;
use App\Util\Lexer\Autolink;
use App\Util\Media\Filter;
use App\Util\Media\License;
use Auth;
use Cache;
use DB;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class ComposeController extends Controller
{
@ -74,30 +53,30 @@ class ComposeController extends Controller
public function mediaUpload(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'file.*' => [
'required_without:file',
'mimetypes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
'mimetypes:'.config_cache('pixelfed.media_types'),
'max:'.config_cache('pixelfed.max_photo_size'),
],
'file' => [
'required_without:file.*',
'mimetypes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
'mimetypes:'.config_cache('pixelfed.media_types'),
'max:'.config_cache('pixelfed.max_photo_size'),
],
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24'
'filter_class' => 'nullable|alpha_dash|max:24',
]);
$user = Auth::user();
$profile = $user->profile;
abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
$limitKey = 'compose:rate-limit:media-upload:'.$user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
return $dailyLimit >= 1250;
@ -105,8 +84,8 @@ class ComposeController extends Controller
abort_if($limitReached == true, 429);
if(config_cache('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
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');
@ -144,24 +123,24 @@ class ComposeController extends Controller
$media->version = 3;
$media->save();
$preview_url = $media->url() . '?v=' . time();
$url = $media->url() . '?v=' . time();
$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)->onQueue('mmo');
break;
ImageOptimize::dispatch($media)->onQueue('mmo');
break;
case 'video/mp4':
VideoThumbnail::dispatch($media)->onQueue('mmo');
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
VideoThumbnail::dispatch($media)->onQueue('mmo');
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
default:
break;
break;
}
Cache::forget($limitKey);
@ -169,6 +148,7 @@ class ComposeController extends Controller
$res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $preview_url;
$res['url'] = $url;
return response()->json($res);
}
@ -176,21 +156,21 @@ class ComposeController extends Controller
{
$this->validate($request, [
'id' => 'required',
'file' => function() {
'file' => function () {
return [
'required',
'mimetypes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
'mimetypes:'.config_cache('pixelfed.media_types'),
'max:'.config_cache('pixelfed.max_photo_size'),
];
},
]);
$user = Auth::user();
abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
$limitKey = 'compose:rate-limit:media-updates:' . $user->id;
$limitKey = 'compose:rate-limit:media-updates:'.$user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
return $dailyLimit >= 1500;
@ -202,9 +182,9 @@ class ComposeController extends Controller
$id = $request->input('id');
$media = Media::whereUserId($user->id)
->whereProfileId($user->profile_id)
->whereNull('status_id')
->findOrFail($id);
->whereProfileId($user->profile_id)
->whereNull('status_id')
->findOrFail($id);
$media->save();
@ -214,47 +194,48 @@ class ComposeController extends Controller
$dir = implode('/', $fragments);
$path = $photo->storePubliclyAs($dir, $name);
$res = [
'url' => $media->url() . '?v=' . time()
'url' => $media->url().'?v='.time(),
];
ImageOptimize::dispatch($media)->onQueue('mmo');
Cache::forget($limitKey);
return $res;
}
public function mediaDelete(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:media,id'
'id' => 'required|integer|min:1|exists:media,id',
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$media = Media::whereNull('status_id')
->whereUserId(Auth::id())
->findOrFail($request->input('id'));
->whereUserId(Auth::id())
->findOrFail($request->input('id'));
MediaStorageService::delete($media, true);
return response()->json([
'msg' => 'Successfully deleted',
'code' => 200
'code' => 200,
]);
}
public function searchTag(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:50'
'q' => 'required|string|min:1|max:50',
]);
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
if (Str::of($q)->startsWith('@')) {
if (strlen($q) < 3) {
return [];
}
$q = mb_substr($q, 1);
@ -262,7 +243,7 @@ class ComposeController extends Controller
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
@ -271,34 +252,34 @@ class ComposeController extends Controller
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
$results = Profile::select('id', 'domain', 'username')
->whereNotIn('id', $blocked)
->whereNull('domain')
->where('username','like','%'.$q.'%')
->where('username', 'like', '%'.$q.'%')
->limit(15)
->get()
->map(function($r) {
->map(function ($r) {
return [
'id' => (string) $r->id,
'name' => $r->username,
'privacy' => true,
'avatar' => $r->avatarUrl()
'avatar' => $r->avatarUrl(),
];
});
});
return $results;
}
public function searchUntag(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'status_id' => 'required',
'profile_id' => 'required'
'profile_id' => 'required',
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$user = $request->user();
$status_id = $request->input('status_id');
@ -310,7 +291,7 @@ class ComposeController extends Controller
->whereProfileId($profile_id)
->first();
if(!$tag) {
if (! $tag) {
return [];
}
Notification::whereItemType('App\MediaTag')
@ -326,37 +307,38 @@ class ComposeController extends Controller
public function searchLocation(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'q' => 'required|string|max:100'
'q' => 'required|string|max:100',
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$pid = $request->user()->profile_id;
abort_if(!$pid, 400);
abort_if(! $pid, 400);
$q = e($request->input('q'));
$popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() {
$popular = Cache::remember('pf:search:location:v1:popular', 1209600, function () {
$minId = SnowflakeService::byDate(now()->subDays(290));
if(config('database.default') == 'pgsql') {
if (config('database.default') == 'pgsql') {
return Status::selectRaw('id, place_id, count(place_id) as pc')
->whereNotNull('place_id')
->where('id', '>', $minId)
->orderByDesc('pc')
->groupBy(['place_id', 'id'])
->limit(400)
->get()
->filter(function($post) {
return $post;
})
->map(function($place) {
return [
'id' => $place->place_id,
'count' => $place->pc
];
})
->unique('id')
->values();
->whereNotNull('place_id')
->where('id', '>', $minId)
->orderByDesc('pc')
->groupBy(['place_id', 'id'])
->limit(400)
->get()
->filter(function ($post) {
return $post;
})
->map(function ($place) {
return [
'id' => $place->place_id,
'count' => $place->pc,
];
})
->unique('id')
->values();
}
return Status::selectRaw('id, place_id, count(place_id) as pc')
->whereNotNull('place_id')
->where('id', '>', $minId)
@ -364,57 +346,58 @@ class ComposeController extends Controller
->orderByDesc('pc')
->limit(400)
->get()
->filter(function($post) {
->filter(function ($post) {
return $post;
})
->map(function($place) {
->map(function ($place) {
return [
'id' => $place->place_id,
'count' => $place->pc
'count' => $place->pc,
];
});
});
$q = '%' . $q . '%';
$q = '%'.$q.'%';
$wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like';
$places = DB::table('places')
->where('name', $wildcard, $q)
->limit((strlen($q) > 5 ? 360 : 30))
->get()
->sortByDesc(function($place, $key) use($popular) {
return $popular->filter(function($p) use($place) {
return $p['id'] == $place->id;
})->map(function($p) use($place) {
return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
})->values();
})
->map(function($r) {
return [
'id' => $r->id,
'name' => $r->name,
'country' => $r->country,
'url' => url('/discover/places/' . $r->id . '/' . $r->slug)
];
})
->values()
->all();
->where('name', $wildcard, $q)
->limit((strlen($q) > 5 ? 360 : 30))
->get()
->sortByDesc(function ($place, $key) use ($popular) {
return $popular->filter(function ($p) use ($place) {
return $p['id'] == $place->id;
})->map(function ($p) use ($place) {
return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
})->values();
})
->map(function ($r) {
return [
'id' => $r->id,
'name' => $r->name,
'country' => $r->country,
'url' => url('/discover/places/'.$r->id.'/'.$r->slug),
];
})
->values()
->all();
return $places;
}
public function searchMentionAutocomplete(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:2|max:50'
'q' => 'required|string|min:2|max:50',
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
if (Str::of($q)->startsWith('@')) {
if (strlen($q) < 3) {
return [];
}
}
@ -426,32 +409,33 @@ class ComposeController extends Controller
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
$results = Profile::select('id', 'domain', 'username')
->whereNotIn('id', $blocked)
->where('username','like','%'.$q.'%')
->where('username', 'like', '%'.$q.'%')
->groupBy('id', 'domain')
->limit(15)
->get()
->map(function($profile) {
->map(function ($profile) {
$username = $profile->domain ? substr($profile->username, 1) : $profile->username;
return [
'key' => '@' . str_limit($username, 30),
'key' => '@'.str_limit($username, 30),
'value' => $username,
];
});
});
return $results;
}
public function searchHashtagAutocomplete(Request $request)
{
abort_if(!$request->user(), 403);
abort_if(! $request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:2|max:50'
'q' => 'required|string|min:2|max:50',
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$q = $request->input('q');
@ -461,12 +445,12 @@ class ComposeController extends Controller
->whereIsBanned(false)
->limit(5)
->get()
->map(function($tag) {
->map(function ($tag) {
return [
'key' => '#' . $tag->slug,
'value' => $tag->slug
'key' => '#'.$tag->slug,
'value' => $tag->slug,
];
});
});
return $results;
}
@ -474,8 +458,8 @@ class ComposeController extends Controller
public function store(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'media.*' => 'required',
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
'media.*' => 'required',
'media.*.id' => 'required|integer|min:1',
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
'media.*.license' => 'nullable|string|max:140',
@ -491,14 +475,14 @@ class ComposeController extends Controller
// 'optimize_media' => 'nullable'
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
if(config('costar.enabled') == true) {
if (config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
if ($blockedKeywords !== null && $request->caption) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($request->caption, $kw) == true) {
foreach ($keywords as $kw) {
if (Str::contains($request->caption, $kw) == true) {
abort(400, 'Invalid object');
}
}
@ -508,9 +492,9 @@ class ComposeController extends Controller
$user = $request->user();
$profile = $user->profile;
$limitKey = 'compose:rate-limit:store:' . $user->id;
$limitKey = 'compose:rate-limit:store:'.$user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
$dailyLimit = Status::whereProfileId($user->profile_id)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
@ -534,12 +518,12 @@ class ComposeController extends Controller
$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')) {
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) {
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;
@ -547,7 +531,7 @@ class ComposeController extends Controller
$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) {
if ($cw == true || $profile->cw == true) {
$m->is_nsfw = $cw;
$status->is_nsfw = $cw;
}
@ -560,19 +544,19 @@ class ComposeController extends Controller
$mediaType = StatusController::mimeTypeCheck($mimes);
if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
if (in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
abort(400, __('exception.compose.invalid.album'));
}
if($place && is_array($place)) {
if ($place && is_array($place)) {
$status->place_id = $place['id'];
}
if($request->filled('comments_disabled')) {
if ($request->filled('comments_disabled')) {
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
if($request->filled('spoiler_text') && $cw) {
if ($request->filled('spoiler_text') && $cw) {
$status->cw_summary = $request->input('spoiler_text');
}
@ -583,7 +567,7 @@ class ComposeController extends Controller
$status->profile_id = $profile->id;
$status->save();
foreach($attachments as $media) {
foreach ($attachments as $media) {
$media->status_id = $status->id;
$media->save();
}
@ -597,7 +581,7 @@ class ComposeController extends Controller
$status->type = $mediaType;
$status->save();
foreach($tagged as $tg) {
foreach ($tagged as $tg) {
$mt = new MediaTag;
$mt->status_id = $status->id;
$mt->media_id = $status->media->first()->id;
@ -612,17 +596,17 @@ class ComposeController extends Controller
MediaTagService::sendNotification($mt);
}
if($request->filled('collections')) {
if ($request->filled('collections')) {
$collections = Collection::whereProfileId($profile->id)
->find($request->input('collections'))
->each(function($collection) use($status) {
->each(function ($collection) use ($status) {
$count = $collection->items()->count();
CollectionItem::firstOrCreate([
'collection_id' => $collection->id,
'object_type' => 'App\Status',
'object_id' => $status->id
'object_id' => $status->id,
], [
'order' => $count
'order' => $count,
]);
CollectionService::addItem(
@ -643,7 +627,7 @@ class ComposeController extends Controller
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('profile:embed:'.$status->profile_id);
Cache::forget($limitKey);
return $status->url();
@ -653,7 +637,7 @@ class ComposeController extends Controller
{
abort_unless(config('exp.top'), 404);
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
@ -661,14 +645,14 @@ class ComposeController extends Controller
'tagged' => 'nullable',
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
if(config('costar.enabled') == true) {
if (config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
if ($blockedKeywords !== null && $request->caption) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($request->caption, $kw) == true) {
foreach ($keywords as $kw) {
if (Str::contains($request->caption, $kw) == true) {
abort(400, 'Invalid object');
}
}
@ -683,11 +667,11 @@ class ComposeController extends Controller
$cw = $request->input('cw');
$tagged = $request->input('tagged');
if($place && is_array($place)) {
if ($place && is_array($place)) {
$status->place_id = $place['id'];
}
if($request->filled('comments_disabled')) {
if ($request->filled('comments_disabled')) {
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
@ -707,11 +691,11 @@ class ComposeController extends Controller
'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) {
foreach ($tagged as $tg) {
$mt = new MediaTag;
$mt->status_id = $status->id;
$mt->media_id = $status->media->first()->id;
@ -726,7 +710,6 @@ class ComposeController extends Controller
MediaTagService::sendNotification($mt);
}
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
@ -737,18 +720,18 @@ class ComposeController extends Controller
public function mediaProcessingCheck(Request $request)
{
$this->validate($request, [
'id' => 'required|integer|min:1'
'id' => 'required|integer|min:1',
]);
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$media = Media::whereUserId($request->user()->id)
->whereNull('status_id')
->findOrFail($request->input('id'));
if(config('pixelfed.media_fast_process')) {
if (config('pixelfed.media_fast_process')) {
return [
'finished' => true
'finished' => true,
];
}
@ -762,27 +745,27 @@ class ComposeController extends Controller
break;
default:
# code...
// code...
break;
}
return [
'finished' => $finished
'finished' => $finished,
];
}
public function composeSettings(Request $request)
{
$uid = $request->user()->id;
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$default = [
'default_license' => 1,
'media_descriptions' => false,
'max_altext_length' => config_cache('pixelfed.max_altext_length')
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
];
$settings = AccountService::settings($uid);
if(isset($settings['other']) && isset($settings['other']['scope'])) {
if (isset($settings['other']) && isset($settings['other']['scope'])) {
$s = $settings['compose_settings'];
$s['default_scope'] = $settings['other']['scope'];
$settings['compose_settings'] = $s;
@ -794,23 +777,22 @@ class ComposeController extends Controller
public function createPoll(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private',
'comments_disabled' => 'nullable',
'expiry' => 'required|in:60,360,1440,10080',
'pollOptions' => 'required|array|min:1|max:4'
'pollOptions' => 'required|array|min:1|max:4',
]);
abort(404);
abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if(Status::whereType('poll')
->whereProfileId($request->user()->profile_id)
->whereCaption($request->input('caption'))
->where('created_at', '>', now()->subDays(2))
->exists()
, 422, 'Duplicate detected.');
->exists(), 422, 'Duplicate detected.');
$status = new Status;
$status->profile_id = $request->user()->profile_id;
@ -827,7 +809,7 @@ class ComposeController extends Controller
$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) {
$poll->cached_tallies = collect($poll->poll_options)->map(function ($o) {
return 0;
})->toArray();
$poll->save();

View File

@ -105,6 +105,7 @@ class CuratedRegisterController extends Controller
'action_required' => true,
]);
CuratedRegister::findOrFail($crid)->update(['user_has_responded' => true]);
$request->session()->pull('cur-reg-con');
$request->session()->pull('cur-reg-con-attempt');

View File

@ -2,366 +2,422 @@
namespace App\Http\Controllers;
use App\{
DiscoverCategory,
Follower,
Hashtag,
HashtagFollow,
Instance,
Like,
Profile,
Status,
StatusHashtag,
UserFilter
};
use Auth, DB, Cache;
use Illuminate\Http\Request;
use App\Hashtag;
use App\Instance;
use App\Like;
use App\Services\AccountService;
use App\Services\AdminShadowFilterService;
use App\Services\BookmarkService;
use App\Services\ConfigCacheService;
use App\Services\FollowerService;
use App\Services\HashtagService;
use App\Services\LikeService;
use App\Services\ReblogService;
use App\Services\StatusHashtagService;
use App\Services\SnowflakeService;
use App\Services\StatusHashtagService;
use App\Services\StatusService;
use App\Services\TrendingHashtagService;
use App\Services\UserFilterService;
use App\Status;
use Auth;
use Cache;
use DB;
use Illuminate\Http\Request;
class DiscoverController extends Controller
{
public function home(Request $request)
{
abort_if(!Auth::check() && config('instance.discover.public') == false, 403);
return view('discover.home');
}
public function home(Request $request)
{
abort_if(! Auth::check() && config('instance.discover.public') == false, 403);
public function showTags(Request $request, $hashtag)
{
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
return view('discover.home');
}
$tag = Hashtag::whereName($hashtag)
->orWhere('slug', $hashtag)
->where('is_banned', '!=', true)
->firstOrFail();
$tagCount = StatusHashtagService::count($tag->id);
return view('discover.tags.show', compact('tag', 'tagCount'));
}
public function showTags(Request $request, $hashtag)
{
if ($request->user()) {
return redirect('/i/web/hashtag/'.$hashtag.'?src=pd');
}
abort_if(! config('instance.discover.tags.is_public') && ! Auth::check(), 403);
public function getHashtags(Request $request)
{
$user = $request->user();
abort_if(!config('instance.discover.tags.is_public') && !$user, 403);
$tag = Hashtag::whereName($hashtag)
->orWhere('slug', $hashtag)
->where('is_banned', '!=', true)
->firstOrFail();
$tagCount = $tag->cached_count ?? 0;
$this->validate($request, [
'hashtag' => 'required|string|min:1|max:124',
'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3)
]);
return view('discover.tags.show', compact('tag', 'tagCount'));
}
$page = $request->input('page') ?? '1';
$end = $page > 1 ? $page * 9 : 0;
$tag = $request->input('hashtag');
public function getHashtags(Request $request)
{
$user = $request->user();
abort_if(! config('instance.discover.tags.is_public') && ! $user, 403);
if(config('database.default') === 'pgsql') {
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
} else {
$hashtag = Hashtag::whereName($tag)->firstOrFail();
}
$this->validate($request, [
'hashtag' => 'required|string|min:1|max:124',
'page' => 'nullable|integer|min:1|max:'.($user ? 29 : 3),
]);
if($hashtag->is_banned == true) {
return [];
}
if($user) {
$res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id);
}
$res['hashtag'] = [
'name' => $hashtag->name,
'url' => $hashtag->url()
];
if($user) {
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
$res['tags'] = collect($tags)
->map(function($tag) use($user) {
$tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']);
$tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']);
$tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']);
return $tag;
})
->filter(function($tag) {
if(!StatusService::get($tag['status']['id'])) {
return false;
}
return true;
})
->values();
} else {
if($page != 1) {
$res['tags'] = [];
return $res;
}
$key = 'discover:tags:public_feed:' . $hashtag->id . ':page:' . $page;
$tags = Cache::remember($key, 43200, function() use($hashtag, $page, $end) {
return collect(StatusHashtagService::get($hashtag->id, $page, $end))
->filter(function($tag) {
if(!$tag['status']['local']) {
return false;
}
return true;
})
->values();
});
$res['tags'] = collect($tags)
->filter(function($tag) {
if(!StatusService::get($tag['status']['id'])) {
return false;
}
return true;
})
->values();
}
return $res;
}
$page = $request->input('page') ?? '1';
$end = $page > 1 ? $page * 9 : 0;
$tag = $request->input('hashtag');
public function profilesDirectory(Request $request)
{
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
}
if (config('database.default') === 'pgsql') {
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
} else {
$hashtag = Hashtag::whereName($tag)->firstOrFail();
}
public function profilesDirectoryApi(Request $request)
{
return ['error' => 'Temporarily unavailable.'];
}
if ($hashtag->is_banned == true) {
return [];
}
if ($user) {
$res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id);
}
$res['hashtag'] = [
'name' => $hashtag->name,
'url' => $hashtag->url(),
];
if ($user) {
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
$res['tags'] = collect($tags)
->map(function ($tag) use ($user) {
$tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']);
$tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']);
$tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']);
public function trendingApi(Request $request)
{
abort_if(config('instance.discover.public') == false && !$request->user(), 403);
return $tag;
})
->filter(function ($tag) {
if (! StatusService::get($tag['status']['id'])) {
return false;
}
$this->validate($request, [
'range' => 'nullable|string|in:daily,monthly,yearly',
]);
return true;
})
->values();
} else {
if ($page != 1) {
$res['tags'] = [];
$range = $request->input('range');
$days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365);
$ttls = [
1 => 1500,
31 => 14400,
365 => 86400
];
$key = ':api:discover:trending:v2.12:range:' . $days;
return $res;
}
$key = 'discover:tags:public_feed:'.$hashtag->id.':page:'.$page;
$tags = Cache::remember($key, 43200, function () use ($hashtag, $page, $end) {
return collect(StatusHashtagService::get($hashtag->id, $page, $end))
->filter(function ($tag) {
if (! $tag['status']['local']) {
return false;
}
$ids = Cache::remember($key, $ttls[$days], function() use($days) {
$min_id = SnowflakeService::byDate(now()->subDays($days));
return DB::table('statuses')
->select(
'id',
'scope',
'type',
'is_nsfw',
'likes_count',
'created_at'
)
->where('id', '>', $min_id)
->whereNull('uri')
->whereScope('public')
->whereIn('type', [
'photo',
'photo:album',
'video'
])
->whereIsNsfw(false)
->orderBy('likes_count','desc')
->take(30)
->pluck('id');
});
return true;
})
->values();
});
$res['tags'] = collect($tags)
->filter(function ($tag) {
if (! StatusService::get($tag['status']['id'])) {
return false;
}
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
return true;
})
->values();
}
$res = $ids->map(function($s) {
return StatusService::get($s);
})->filter(function($s) use($filtered) {
return
$s &&
!in_array($s['account']['id'], $filtered) &&
isset($s['account']);
})->values();
return $res;
}
return response()->json($res);
}
public function profilesDirectory(Request $request)
{
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
}
public function trendingHashtags(Request $request)
{
abort_if(!$request->user(), 403);
public function profilesDirectoryApi(Request $request)
{
return ['error' => 'Temporarily unavailable.'];
}
$res = TrendingHashtagService::getTrending();
return $res;
}
public function trendingApi(Request $request)
{
abort_if(config('instance.discover.public') == false && ! $request->user(), 403);
public function trendingPlaces(Request $request)
{
return [];
}
$this->validate($request, [
'range' => 'nullable|string|in:daily,monthly,yearly',
]);
public function myMemories(Request $request)
{
abort_if(!$request->user(), 404);
$pid = $request->user()->profile_id;
abort_if(!$this->config()['memories']['enabled'], 404);
$type = $request->input('type') ?? 'posts';
$range = $request->input('range');
$days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365);
$ttls = [
1 => 1500,
31 => 14400,
365 => 86400,
];
$key = ':api:discover:trending:v2.12:range:'.$days;
switch($type) {
case 'posts':
$res = Status::whereProfileId($pid)
->whereDay('created_at', date('d'))
->whereMonth('created_at', date('m'))
->whereYear('created_at', '!=', date('Y'))
->whereNull(['reblog_of_id', 'in_reply_to_id'])
->limit(20)
->pluck('id')
->map(function($id) {
return StatusService::get($id, false);
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
break;
$ids = Cache::remember($key, $ttls[$days], function () use ($days) {
$min_id = SnowflakeService::byDate(now()->subDays($days));
case 'liked':
$res = Like::whereProfileId($pid)
->whereDay('created_at', date('d'))
->whereMonth('created_at', date('m'))
->whereYear('created_at', '!=', date('Y'))
->orderByDesc('status_id')
->limit(20)
->pluck('status_id')
->map(function($id) {
$status = StatusService::get($id, false);
$status['favourited'] = true;
return $status;
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
break;
}
return DB::table('statuses')
->select(
'id',
'scope',
'type',
'is_nsfw',
'likes_count',
'created_at'
)
->where('id', '>', $min_id)
->whereNull('uri')
->whereScope('public')
->whereIn('type', [
'photo',
'photo:album',
'video',
])
->whereIsNsfw(false)
->orderBy('likes_count', 'desc')
->take(30)
->pluck('id');
});
return $res;
}
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
public function accountInsightsPopularPosts(Request $request)
{
abort_if(!$request->user(), 404);
$pid = $request->user()->profile_id;
abort_if(!$this->config()['insights']['enabled'], 404);
$posts = Cache::remember('pf:discover:metro2:accinsights:popular:' . $pid, 43200, function() use ($pid) {
return Status::whereProfileId($pid)
->whereNotNull('likes_count')
->orderByDesc('likes_count')
->limit(12)
->pluck('id')
->map(function($id) {
return StatusService::get($id, false);
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
});
$res = $ids->map(function ($s) {
return StatusService::get($s);
})->filter(function ($s) use ($filtered) {
return
$s &&
! in_array($s['account']['id'], $filtered) &&
isset($s['account']);
})->values();
return $posts;
}
return response()->json($res);
}
public function config()
{
$cc = ConfigCacheService::get('config.discover.features');
if($cc) {
return is_string($cc) ? json_decode($cc, true) : $cc;
}
return [
'hashtags' => [
'enabled' => false,
],
'memories' => [
'enabled' => false,
],
'insights' => [
'enabled' => false,
],
'friends' => [
'enabled' => false,
],
'server' => [
'enabled' => false,
'mode' => 'allowlist',
'domains' => []
]
];
}
public function trendingHashtags(Request $request)
{
abort_if(! $request->user(), 403);
public function serverTimeline(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$this->config()['server']['enabled'], 404);
$pid = $request->user()->profile_id;
$domain = $request->input('domain');
$config = $this->config();
$domains = explode(',', $config['server']['domains']);
abort_unless(in_array($domain, $domains), 400);
$res = TrendingHashtagService::getTrending();
$res = Status::whereNotNull('uri')
->where('uri', 'like', 'https://' . $domain . '%')
->whereNull(['in_reply_to_id', 'reblog_of_id'])
->orderByDesc('id')
->limit(12)
->pluck('id')
->map(function($id) {
return StatusService::get($id);
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
return $res;
}
return $res;
}
public function enabledFeatures(Request $request)
{
abort_if(!$request->user(), 404);
return $this->config();
}
public function trendingPlaces(Request $request)
{
return [];
}
public function updateFeatures(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user()->is_admin, 404);
$pid = $request->user()->profile_id;
$this->validate($request, [
'features.friends.enabled' => 'boolean',
'features.hashtags.enabled' => 'boolean',
'features.insights.enabled' => 'boolean',
'features.memories.enabled' => 'boolean',
'features.server.enabled' => 'boolean',
]);
$res = $request->input('features');
if($res['server'] && isset($res['server']['domains']) && !empty($res['server']['domains'])) {
$parts = explode(',', $res['server']['domains']);
$parts = array_filter($parts, function($v) {
$len = strlen($v);
$pos = strpos($v, '.');
$domain = trim($v);
if($pos == false || $pos == ($len + 1)) {
return false;
}
if(!Instance::whereDomain($domain)->exists()) {
return false;
}
return true;
});
$parts = array_slice($parts, 0, 10);
$d = implode(',', array_map('trim', $parts));
$res['server']['domains'] = $d;
}
ConfigCacheService::put('config.discover.features', json_encode($res));
return $res;
}
public function myMemories(Request $request)
{
abort_if(! $request->user(), 404);
$pid = $request->user()->profile_id;
abort_if(! $this->config()['memories']['enabled'], 404);
$type = $request->input('type') ?? 'posts';
switch ($type) {
case 'posts':
$res = Status::whereProfileId($pid)
->whereDay('created_at', date('d'))
->whereMonth('created_at', date('m'))
->whereYear('created_at', '!=', date('Y'))
->whereNull(['reblog_of_id', 'in_reply_to_id'])
->limit(20)
->pluck('id')
->map(function ($id) {
return StatusService::get($id, false);
})
->filter(function ($post) {
return $post && isset($post['account']);
})
->values();
break;
case 'liked':
$res = Like::whereProfileId($pid)
->whereDay('created_at', date('d'))
->whereMonth('created_at', date('m'))
->whereYear('created_at', '!=', date('Y'))
->orderByDesc('status_id')
->limit(20)
->pluck('status_id')
->map(function ($id) {
$status = StatusService::get($id, false);
$status['favourited'] = true;
return $status;
})
->filter(function ($post) {
return $post && isset($post['account']);
})
->values();
break;
}
return $res;
}
public function accountInsightsPopularPosts(Request $request)
{
abort_if(! $request->user(), 404);
$pid = $request->user()->profile_id;
abort_if(! $this->config()['insights']['enabled'], 404);
$posts = Cache::remember('pf:discover:metro2:accinsights:popular:'.$pid, 43200, function () use ($pid) {
return Status::whereProfileId($pid)
->whereNotNull('likes_count')
->orderByDesc('likes_count')
->limit(12)
->pluck('id')
->map(function ($id) {
return StatusService::get($id, false);
})
->filter(function ($post) {
return $post && isset($post['account']);
})
->values();
});
return $posts;
}
public function config()
{
$cc = ConfigCacheService::get('config.discover.features');
if ($cc) {
return is_string($cc) ? json_decode($cc, true) : $cc;
}
return [
'hashtags' => [
'enabled' => false,
],
'memories' => [
'enabled' => false,
],
'insights' => [
'enabled' => false,
],
'friends' => [
'enabled' => false,
],
'server' => [
'enabled' => false,
'mode' => 'allowlist',
'domains' => [],
],
];
}
public function serverTimeline(Request $request)
{
abort_if(! $request->user(), 404);
abort_if(! $this->config()['server']['enabled'], 404);
$pid = $request->user()->profile_id;
$domain = $request->input('domain');
$config = $this->config();
$domains = explode(',', $config['server']['domains']);
abort_unless(in_array($domain, $domains), 400);
$res = Status::whereNotNull('uri')
->where('uri', 'like', 'https://'.$domain.'%')
->whereNull(['in_reply_to_id', 'reblog_of_id'])
->orderByDesc('id')
->limit(12)
->pluck('id')
->map(function ($id) {
return StatusService::get($id);
})
->filter(function ($post) {
return $post && isset($post['account']);
})
->values();
return $res;
}
public function enabledFeatures(Request $request)
{
abort_if(! $request->user(), 404);
return $this->config();
}
public function updateFeatures(Request $request)
{
abort_if(! $request->user(), 404);
abort_if(! $request->user()->is_admin, 404);
$pid = $request->user()->profile_id;
$this->validate($request, [
'features.friends.enabled' => 'boolean',
'features.hashtags.enabled' => 'boolean',
'features.insights.enabled' => 'boolean',
'features.memories.enabled' => 'boolean',
'features.server.enabled' => 'boolean',
]);
$res = $request->input('features');
if ($res['server'] && isset($res['server']['domains']) && ! empty($res['server']['domains'])) {
$parts = explode(',', $res['server']['domains']);
$parts = array_filter($parts, function ($v) {
$len = strlen($v);
$pos = strpos($v, '.');
$domain = trim($v);
if ($pos == false || $pos == ($len + 1)) {
return false;
}
if (! Instance::whereDomain($domain)->exists()) {
return false;
}
return true;
});
$parts = array_slice($parts, 0, 10);
$d = implode(',', array_map('trim', $parts));
$res['server']['domains'] = $d;
}
ConfigCacheService::put('config.discover.features', json_encode($res));
return $res;
}
public function discoverAccountsPopular(Request $request)
{
abort_if(! $request->user(), 403);
$pid = $request->user()->profile_id;
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () {
return DB::table('profiles')
->where('is_private', false)
->whereNull('status')
->orderByDesc('profiles.followers_count')
->limit(30)
->get();
});
$filters = UserFilterService::filters($pid);
$asf = AdminShadowFilterService::getHideFromPublicFeedsList();
$ids = $ids->map(function ($profile) {
return AccountService::get($profile->id, true);
})
->filter(function ($profile) {
return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked'];
})
->filter(function ($profile) use ($pid) {
return $profile['id'] != $pid;
})
->filter(function ($profile) use ($pid) {
return ! FollowerService::follows($pid, $profile['id'], true);
})
->filter(function ($profile) use ($asf) {
return ! in_array($profile['id'], $asf);
})
->filter(function ($profile) use ($filters) {
return ! in_array($profile['id'], $filters);
})
->take(16)
->values();
return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES);
}
}

View File

@ -78,10 +78,11 @@ class PixelfedDirectoryController extends Controller
$res['community_guidelines'] = json_decode($guidelines->v, true);
}
$openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first();
if($openRegistration) {
$res['open_registration'] = (bool) $openRegistration;
}
$openRegistration = (bool) config_cache('pixelfed.open_registration');
$res['open_registration'] = $openRegistration;
$curatedOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$res['curated_onboarding'] = $curatedOnboarding;
$oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first();
if($oauthEnabled) {

View File

@ -2,11 +2,13 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Util\Lexer\Nickname;
use App\Util\Webfinger\WebfingerUrl;
use App\Models\ProfileAlias;
use App\Models\ProfileMigration;
use App\Services\AccountService;
use App\Services\WebfingerService;
use App\Util\Lexer\Nickname;
use Cache;
use Illuminate\Http\Request;
class ProfileAliasController extends Controller
{
@ -18,31 +20,47 @@ class ProfileAliasController extends Controller
public function index(Request $request)
{
$aliases = $request->user()->profile->aliases;
return view('settings.aliases.index', compact('aliases'));
}
public function store(Request $request)
{
$this->validate($request, [
'acct' => 'required'
'acct' => 'required',
]);
$acct = $request->input('acct');
if($request->user()->profile->aliases->count() >= 3) {
$nn = Nickname::normalizeProfileUrl($acct);
if (! $nn) {
return back()->with('error', 'Invalid account alias.');
}
if ($nn['domain'] === config('pixelfed.domain.app')) {
if (strtolower($nn['username']) == ($request->user()->username)) {
return back()->with('error', 'You cannot add an alias to your own account.');
}
}
if ($request->user()->profile->aliases->count() >= 3) {
return back()->with('error', 'You can only add 3 account aliases.');
}
$webfingerService = WebfingerService::lookup($acct);
if(!$webfingerService || !isset($webfingerService['url'])) {
$webfingerUrl = WebfingerService::rawGet($acct);
if (! $webfingerService || ! isset($webfingerService['url']) || ! $webfingerUrl || empty($webfingerUrl)) {
return back()->with('error', 'Invalid account, cannot add alias at this time.');
}
$alias = new ProfileAlias;
$alias->profile_id = $request->user()->profile_id;
$alias->acct = $acct;
$alias->uri = $webfingerService['url'];
$alias->uri = $webfingerUrl;
$alias->save();
Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id);
return back()->with('status', 'Successfully added alias!');
}
@ -50,14 +68,25 @@ class ProfileAliasController extends Controller
{
$this->validate($request, [
'acct' => 'required',
'id' => 'required|exists:profile_aliases'
'id' => 'required|exists:profile_aliases',
]);
$alias = ProfileAlias::where('profile_id', $request->user()->profile_id)
->where('acct', $request->input('acct'))
$pid = $request->user()->profile_id;
$acct = $request->input('acct');
$alias = ProfileAlias::where('profile_id', $pid)
->where('acct', $acct)
->findOrFail($request->input('id'));
$migration = ProfileMigration::whereProfileId($pid)
->whereAcct($acct)
->first();
if ($migration) {
$request->user()->profile->update([
'moved_to_profile_id' => null,
]);
}
$alias->delete();
Cache::forget('pf:activitypub:user-object:by-id:'.$pid);
AccountService::del($pid);
return back()->with('status', 'Successfully deleted alias!');
}

View File

@ -2,356 +2,385 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use Cache;
use DB;
use View;
use App\AccountInterstitial;
use App\Follower;
use App\FollowRequest;
use App\Profile;
use App\Story;
use App\Status;
use App\User;
use App\UserSetting;
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;
use App\Status;
use App\Story;
use App\Transformer\ActivityPub\ProfileTransformer;
use App\User;
use App\UserFilter;
use App\UserSetting;
use Auth;
use Cache;
use Illuminate\Http\Request;
use League\Fractal;
use View;
class ProfileController extends Controller
{
public function show(Request $request, $username)
{
// redirect authed users to Metro 2.0
if($request->user()) {
// unless they force static view
if(!$request->has('fs') || $request->input('fs') != '1') {
$pid = AccountService::usernameToId($username);
if($pid) {
return redirect('/i/web/profile/' . $pid);
}
}
}
public function show(Request $request, $username)
{
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
$user = $this->getCachedUser($username, true);
abort_if(! $user, 404, 'Not found');
$user = Profile::whereNull('domain')
->whereNull('status')
->whereUsername($username)
->firstOrFail();
return $this->showActivityPub($request, $user);
}
// redirect authed users to Metro 2.0
if ($request->user()) {
// unless they force static view
if (! $request->has('fs') || $request->input('fs') != '1') {
$pid = AccountService::usernameToId($username);
if ($pid) {
return redirect('/i/web/profile/'.$pid);
}
}
}
if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
$user = $this->getCachedUser($username);
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $user->id, 86400, function() use($user) {
$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
if($exists) {
return true;
}
abort_unless($user, 404);
return false;
});
if($aiCheck) {
return redirect('/login');
}
return $this->buildProfile($request, $user);
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$user->id, 3600, function () use ($user) {
$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
if ($exists) {
return true;
}
protected function buildProfile(Request $request, $user)
{
$username = $user->username;
$loggedIn = Auth::check();
$isPrivate = false;
$isBlocked = false;
if(!$loggedIn) {
$key = 'profile:settings:' . $user->id;
$ttl = now()->addHours(6);
$settings = Cache::remember($key, $ttl, function() use($user) {
return $user->user->settings;
});
return false;
});
if ($aiCheck) {
return redirect('/login');
}
if ($user->is_private == true) {
$profile = null;
return view('profile.private', compact('user'));
}
return $this->buildProfile($request, $user);
}
$owner = false;
$is_following = false;
protected function buildProfile(Request $request, $user)
{
$username = $user->username;
$loggedIn = Auth::check();
$isPrivate = false;
$isBlocked = false;
if (! $loggedIn) {
$key = 'profile:settings:'.$user->id;
$ttl = now()->addHours(6);
$settings = Cache::remember($key, $ttl, function () use ($user) {
return $user->user->settings;
});
$profile = $user;
$settings = [
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers
]
];
return view('profile.show', compact('profile', 'settings'));
} else {
$key = 'profile:settings:' . $user->id;
$ttl = now()->addHours(6);
$settings = Cache::remember($key, $ttl, function() use($user) {
return $user->user->settings;
});
if ($user->is_private == true) {
$profile = null;
if ($user->is_private == true) {
$isPrivate = $this->privateProfileCheck($user, $loggedIn);
}
return view('profile.private', compact('user'));
}
$isBlocked = $this->blockedProfileCheck($user);
$owner = false;
$is_following = false;
$owner = $loggedIn && Auth::id() === $user->user_id;
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
$profile = $user;
$settings = [
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following,
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers,
],
];
if ($isPrivate == true || $isBlocked == true) {
$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
->whereFollowingId($user->id)
->exists() : false;
return view('profile.private', compact('user', 'is_following', 'requested'));
}
return view('profile.show', compact('profile', 'settings'));
} else {
$key = 'profile:settings:'.$user->id;
$ttl = now()->addHours(6);
$settings = Cache::remember($key, $ttl, function () use ($user) {
return $user->user->settings;
});
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
$profile = $user;
$settings = [
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers
]
];
return view('profile.show', compact('profile', 'settings'));
}
}
if ($user->is_private == true) {
$isPrivate = $this->privateProfileCheck($user, $loggedIn);
}
public function permalinkRedirect(Request $request, $username)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$isBlocked = $this->blockedProfileCheck($user);
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
$owner = $loggedIn && Auth::id() === $user->user_id;
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
return redirect($user->url());
}
if ($isPrivate == true || $isBlocked == true) {
$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
->whereFollowingId($user->id)
->exists() : false;
protected function privateProfileCheck(Profile $profile, $loggedIn)
{
if (!Auth::check()) {
return true;
}
return view('profile.private', compact('user', 'is_following', 'requested'));
}
$user = Auth::user()->profile;
if($user->id == $profile->id || !$profile->is_private) {
return false;
}
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
$profile = $user;
$settings = [
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following,
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers,
],
];
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
if ($follows == false) {
return true;
}
return view('profile.show', compact('profile', 'settings'));
}
}
return false;
}
protected function getCachedUser($username, $withTrashed = false)
{
$val = str_replace(['_', '.', '-'], '', $username);
if (! ctype_alnum($val)) {
return;
}
$hash = ($withTrashed ? 'wt:' : 'wot:').strtolower($username);
public static function accountCheck(Profile $profile)
{
switch ($profile->status) {
case 'disabled':
case 'suspended':
case 'delete':
return view('profile.disabled');
break;
return Cache::remember('pfc:cached-user:'.$hash, ($withTrashed ? 14400 : 900), function () use ($username, $withTrashed) {
if (! $withTrashed) {
return Profile::whereNull(['domain', 'status'])
->whereUsername($username)
->first();
} else {
return Profile::withTrashed()
->whereNull('domain')
->whereUsername($username)
->first();
}
});
}
default:
break;
}
return abort(404);
}
public function permalinkRedirect(Request $request, $username)
{
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
$user = $this->getCachedUser($username, true);
protected function blockedProfileCheck(Profile $profile)
{
$pid = Auth::user()->profile->id;
$blocks = UserFilter::whereUserId($profile->id)
->whereFilterType('block')
->whereFilterableType('App\Profile')
->pluck('filterable_id')
->toArray();
if (in_array($pid, $blocks)) {
return true;
}
return $this->showActivityPub($request, $user);
}
return false;
}
$user = $this->getCachedUser($username);
public function showActivityPub(Request $request, $user)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if($user->domain, 404);
return redirect($user->url());
}
return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) {
$fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
$res = $fractal->createData($resource)->toArray();
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
});
}
protected function privateProfileCheck(Profile $profile, $loggedIn)
{
if (! Auth::check()) {
return true;
}
public function showAtomFeed(Request $request, $user)
{
abort_if(!config('federation.atom.enabled'), 404);
$user = Auth::user()->profile;
if ($user->id == $profile->id || ! $profile->is_private) {
return false;
}
$pid = AccountService::usernameToId($user);
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
if ($follows == false) {
return true;
}
abort_if(!$pid, 404);
return false;
}
$profile = AccountService::get($pid, true);
public static function accountCheck(Profile $profile)
{
switch ($profile->status) {
case 'disabled':
case 'suspended':
case 'delete':
return view('profile.disabled');
break;
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
default:
break;
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile['id'], 86400, function() use($profile) {
$uid = User::whereProfileId($profile['id'])->first();
if(!$uid) {
return true;
}
$exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count();
if($exists) {
return true;
}
return abort(404);
}
return false;
});
protected function blockedProfileCheck(Profile $profile)
{
$pid = Auth::user()->profile->id;
$blocks = UserFilter::whereUserId($profile->id)
->whereFilterType('block')
->whereFilterableType('App\Profile')
->pluck('filterable_id')
->toArray();
if (in_array($pid, $blocks)) {
return true;
}
abort_if($aiCheck, 404);
return false;
}
$enabled = Cache::remember('profile:atom:enabled:' . $profile['id'], 84600, function() use ($profile) {
$uid = User::whereProfileId($profile['id'])->first();
if(!$uid) {
return false;
}
$settings = UserSetting::whereUserId($uid->id)->first();
if(!$settings) {
return false;
}
public function showActivityPub(Request $request, $user)
{
abort_if(! config_cache('federation.activitypub.enabled'), 404);
abort_if(! $user, 404, 'Not found');
abort_if($user->domain, 404);
return $settings->show_atom;
});
return Cache::remember('pf:activitypub:user-object:by-id:'.$user->id, 1800, function () use ($user) {
$fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
$res = $fractal->createData($resource)->toArray();
abort_if(!$enabled, 404);
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
});
}
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) {
$items = Status::whereProfileId($pid)
->whereScope('public')
->whereIn('type', ['photo', 'photo:album'])
->orderByDesc('id')
->take(10)
->get()
->map(function($status) {
return StatusService::get($status->id, true);
})
->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";
$headers = ['Content-Type' => 'application/atom+xml'];
public function showAtomFeed(Request $request, $user)
{
abort_if(! config('federation.atom.enabled'), 404);
if($items && $items->count()) {
$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
}
$pid = AccountService::usernameToId($user);
return compact('items', 'permalink', 'headers');
});
abort_if(!$data || !isset($data['items']) || !isset($data['permalink']), 404);
return response()
->view('atom.user',
[
'profile' => $profile,
'items' => $data['items'],
'permalink' => $data['permalink']
]
)
->withHeaders($data['headers']);
}
abort_if(! $pid, 404);
public function meRedirect()
{
abort_if(!Auth::check(), 404);
return redirect(Auth::user()->url());
}
$profile = AccountService::get($pid, true);
public function embed(Request $request, $username)
{
$res = view('profile.embed-removed');
abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404);
if(!config('instance.embed.profile')) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 86400, function () use ($profile) {
$uid = User::whereProfileId($profile['id'])->first();
if (! $uid) {
return true;
}
$exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count();
if ($exists) {
return true;
}
if(strlen($username) > 15 || strlen($username) < 2) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
return false;
});
$profile = Profile::whereUsername($username)
->whereIsPrivate(false)
->whereNull('status')
->whereNull('domain')
->first();
abort_if($aiCheck, 404);
if(!$profile) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 84600, function () use ($profile) {
$uid = User::whereProfileId($profile['id'])->first();
if (! $uid) {
return false;
}
$settings = UserSetting::whereUserId($uid->id)->first();
if (! $settings) {
return false;
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) {
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
if($exists) {
return true;
}
return $settings->show_atom;
});
return false;
});
abort_if(! $enabled, 404);
if($aiCheck) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$data = Cache::remember('pf:atom:user-feed:by-id:'.$profile['id'], 14400, function () use ($pid, $profile) {
$items = Status::whereProfileId($pid)
->whereScope('public')
->whereIn('type', ['photo', 'photo:album'])
->orderByDesc('id')
->take(10)
->get()
->map(function ($status) {
return StatusService::get($status->id, true);
})
->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";
$headers = ['Content-Type' => 'application/atom+xml'];
if(AccountService::canEmbed($profile->user_id) == false) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
if ($items && $items->count()) {
$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
}
$profile = AccountService::get($profile->id);
$res = view('profile.embed', compact('profile'));
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
return compact('items', 'permalink', 'headers');
});
abort_if(! $data || ! isset($data['items']) || ! isset($data['permalink']), 404);
public function stories(Request $request, $username)
{
abort_if(!config_cache('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);
$exists = Story::whereProfileId($pid)
->whereActive(true)
->exists();
abort_unless($exists, 404);
return view('profile.story', compact('pid', 'profile'));
}
return response()
->view('atom.user',
[
'profile' => $profile,
'items' => $data['items'],
'permalink' => $data['permalink'],
]
)
->withHeaders($data['headers']);
}
public function meRedirect()
{
abort_if(! Auth::check(), 404);
return redirect(Auth::user()->url());
}
public function embed(Request $request, $username)
{
$res = view('profile.embed-removed');
if (! (bool) config_cache('instance.embed.profile')) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
if (strlen($username) > 15 || strlen($username) < 2) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = $this->getCachedUser($username);
if (! $profile || $profile->is_private) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) {
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
if ($exists) {
return true;
}
return false;
});
if ($aiCheck) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
if (AccountService::canEmbed($profile->user_id) == false) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = AccountService::get($profile->id);
$res = view('profile.embed', compact('profile'));
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function stories(Request $request, $username)
{
abort_if(! config_cache('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);
$exists = Story::whereProfileId($pid)
->whereActive(true)
->exists();
abort_unless($exists, 404);
return view('profile.story', compact('pid', 'profile'));
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileMigrationStoreRequest;
use App\Jobs\ProfilePipeline\ProfileMigrationDeliverMoveActivityPipeline;
use App\Jobs\ProfilePipeline\ProfileMigrationMoveFollowersPipeline;
use App\Models\ProfileAlias;
use App\Models\ProfileMigration;
use App\Services\AccountService;
use App\Services\WebfingerService;
use App\Util\ActivityPub\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Bus;
class ProfileMigrationController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index(Request $request)
{
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
if ((bool) config_cache('federation.migration') === false) {
return redirect(route('help.account-migration'));
}
$hasExistingMigration = ProfileMigration::whereProfileId($request->user()->profile_id)
->where('created_at', '>', now()->subDays(30))
->exists();
return view('settings.migration.index', compact('hasExistingMigration'));
}
public function store(ProfileMigrationStoreRequest $request)
{
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
$acct = WebfingerService::rawGet($request->safe()->acct);
if (! $acct) {
return redirect()->back()->withErrors(['acct' => 'The new account you provided is not responding to our requests.']);
}
$newAccount = Helpers::profileFetch($acct);
if (! $newAccount) {
return redirect()->back()->withErrors(['acct' => 'An error occured, please try again later. Code: res-failed-account-fetch']);
}
$user = $request->user();
ProfileAlias::updateOrCreate([
'profile_id' => $user->profile_id,
'acct' => $request->safe()->acct,
'uri' => $acct,
]);
$migration = ProfileMigration::create([
'profile_id' => $request->user()->profile_id,
'acct' => $request->safe()->acct,
'followers_count' => $request->user()->profile->followers_count,
'target_profile_id' => $newAccount['id'],
]);
$user->profile->update([
'moved_to_profile_id' => $newAccount->id,
'indexable' => false,
]);
AccountService::del($user->profile_id);
Bus::batch([
new ProfileMigrationDeliverMoveActivityPipeline($migration, $user->profile, $newAccount),
new ProfileMigrationMoveFollowersPipeline($user->profile_id, $newAccount->id),
])->onQueue('follow')->dispatch();
return redirect()->back()->with(['status' => 'Succesfully migrated account!']);
}
}

View File

@ -84,14 +84,19 @@ trait PrivacySettings
}
$settings->save();
}
Cache::forget('profile:settings:' . $profile->id);
$pid = $profile->id;
Cache::forget('profile:settings:' . $pid);
Cache::forget('user:account:id:' . $profile->user_id);
Cache::forget('profile:follower_count:' . $profile->id);
Cache::forget('profile:following_count:' . $profile->id);
Cache::forget('profile:atom:enabled:' . $profile->id);
Cache::forget('profile:embed:' . $profile->id);
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
Cache::forget('profile:follower_count:' . $pid);
Cache::forget('profile:following_count:' . $pid);
Cache::forget('profile:atom:enabled:' . $pid);
Cache::forget('profile:embed:' . $pid);
Cache::forget('pf:acct:settings:hidden-followers:' . $pid);
Cache::forget('pf:acct:settings:hidden-following:' . $pid);
Cache::forget('pf:acct-trans:hideFollowing:' . $pid);
Cache::forget('pf:acct-trans:hideFollowers:' . $pid);
Cache::forget('pfc:cached-user:wt:' . strtolower($profile->username));
Cache::forget('pfc:cached-user:wot:' . strtolower($profile->username));
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
}

View File

@ -2,166 +2,202 @@
namespace App\Http\Controllers;
use App\Page;
use App\Profile;
use App\Services\FollowerService;
use App\Status;
use App\User;
use App\Util\ActivityPub\Helpers;
use App\Util\Localization\Localization;
use Auth;
use Cache;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App, Auth, Cache, View;
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;
use View;
class SiteController extends Controller
{
public function home(Request $request)
{
if (Auth::check()) {
return $this->homeTimeline($request);
} else {
return $this->homeGuest();
}
}
public function home(Request $request)
{
if (Auth::check()) {
return $this->homeTimeline($request);
} else {
return $this->homeGuest();
}
}
public function homeGuest()
{
return view('site.index');
}
public function homeGuest()
{
return view('site.index');
}
public function homeTimeline(Request $request)
{
if($request->has('force_old_ui')) {
return view('timeline.home', ['layout' => 'feed']);
}
public function homeTimeline(Request $request)
{
if ($request->has('force_old_ui')) {
return view('timeline.home', ['layout' => 'feed']);
}
return redirect('/i/web');
}
return redirect('/i/web');
}
public function changeLocale(Request $request, $locale)
{
// todo: add other locales after pushing new l10n strings
$locales = Localization::languages();
if(in_array($locale, $locales)) {
if($request->user()) {
$user = $request->user();
$user->language = $locale;
$user->save();
}
session()->put('locale', $locale);
}
public function changeLocale(Request $request, $locale)
{
// todo: add other locales after pushing new l10n strings
$locales = Localization::languages();
if (in_array($locale, $locales)) {
if ($request->user()) {
$user = $request->user();
$user->language = $locale;
$user->save();
}
session()->put('locale', $locale);
}
return redirect(route('site.language'));
}
return redirect(route('site.language'));
}
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();
});
}
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;
public function language()
{
return view('site.language');
}
return view('site.about', compact('rules', 'user_count', 'post_count'))->render();
});
}
public function communityGuidelines(Request $request)
{
return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() {
$slug = '/site/kb/community-guidelines';
$page = Page::whereSlug($slug)->whereActive(true)->first();
return View::make('site.help.community-guidelines')->with(compact('page'))->render();
});
}
public function language()
{
return view('site.language');
}
public function privacy(Request $request)
{
$page = Cache::remember('site:privacy', now()->addDays(120), function() {
$slug = '/site/privacy';
return Page::whereSlug($slug)->whereActive(true)->first();
});
return View::make('site.privacy')->with(compact('page'))->render();
}
public function communityGuidelines(Request $request)
{
return Cache::remember('site:help:community-guidelines', now()->addDays(120), function () {
$slug = '/site/kb/community-guidelines';
$page = Page::whereSlug($slug)->whereActive(true)->first();
public function terms(Request $request)
{
$page = Cache::remember('site:terms', now()->addDays(120), function() {
$slug = '/site/terms';
return Page::whereSlug($slug)->whereActive(true)->first();
});
return View::make('site.terms')->with(compact('page'))->render();
}
return View::make('site.help.community-guidelines')->with(compact('page'))->render();
});
}
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'));
}
public function privacy(Request $request)
{
$page = Cache::remember('site:privacy', now()->addDays(120), function () {
$slug = '/site/privacy';
public function followIntent(Request $request)
{
$this->validate($request, [
'user' => 'string|min:1|max:15|exists:users,username',
]);
$profile = Profile::whereUsername($request->input('user'))->firstOrFail();
$user = $request->user();
abort_if($user && $profile->id == $user->profile_id, 404);
$following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false;
return view('site.intents.follow', compact('profile', 'user', 'following'));
}
return Page::whereSlug($slug)->whereActive(true)->first();
});
public function legacyProfileRedirect(Request $request, $username)
{
$username = Str::contains($username, '@') ? '@' . $username : $username;
if(str_contains($username, '@')) {
$profile = Profile::whereUsername($username)
->firstOrFail();
return View::make('site.privacy')->with(compact('page'))->render();
}
if($profile->domain == null) {
$url = "/$profile->username";
} else {
$url = "/i/web/profile/_/{$profile->id}";
}
public function terms(Request $request)
{
$page = Cache::remember('site:terms', now()->addDays(120), function () {
$slug = '/site/terms';
} else {
$profile = Profile::whereUsername($username)
->whereNull('domain')
->firstOrFail();
$url = "/$profile->username";
}
return Page::whereSlug($slug)->whereActive(true)->first();
});
return redirect($url);
}
return View::make('site.terms')->with(compact('page'))->render();
}
public function legacyWebfingerRedirect(Request $request, $username, $domain)
{
$un = '@'.$username.'@'.$domain;
$profile = Profile::whereUsername($un)
->firstOrFail();
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);
if($profile->domain == null) {
$url = "/$profile->username";
} else {
$url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url();
}
return view('site.redirect', compact('url'));
}
return redirect($url);
}
public function followIntent(Request $request)
{
$this->validate($request, [
'user' => 'string|min:1|max:15|exists:users,username',
]);
$profile = Profile::whereUsername($request->input('user'))->firstOrFail();
$user = $request->user();
abort_if($user && $profile->id == $user->profile_id, 404);
$following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false;
public function legalNotice(Request $request)
{
$page = Cache::remember('site:legal-notice', now()->addDays(120), function() {
$slug = '/site/legal-notice';
return Page::whereSlug($slug)->whereActive(true)->first();
});
abort_if(!$page, 404);
return View::make('site.legal-notice')->with(compact('page'))->render();
}
return view('site.intents.follow', compact('profile', 'user', 'following'));
}
public function legacyProfileRedirect(Request $request, $username)
{
$username = Str::contains($username, '@') ? '@'.$username : $username;
if (str_contains($username, '@')) {
$profile = Profile::whereUsername($username)
->firstOrFail();
if ($profile->domain == null) {
$url = "/$profile->username";
} else {
$url = "/i/web/profile/_/{$profile->id}";
}
} else {
$profile = Profile::whereUsername($username)
->whereNull('domain')
->firstOrFail();
$url = "/$profile->username";
}
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);
}
public function legalNotice(Request $request)
{
$page = Cache::remember('site:legal-notice', now()->addDays(120), function () {
$slug = '/site/legal-notice';
return Page::whereSlug($slug)->whereActive(true)->first();
});
abort_if(! $page, 404);
return View::make('site.legal-notice')->with(compact('page'))->render();
}
public function curatedOnboarding(Request $request)
{
if ($request->user()) {
return redirect('/i/web');
}
$regOpen = (bool) config_cache('pixelfed.open_registration');
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$curOnlyClosed = (bool) config('instance.curated_registration.state.only_enabled_on_closed_reg');
if ($regOpen) {
if ($curOnlyClosed) {
return redirect('/register');
}
} else {
if (! $curOnboarding) {
return redirect('/');
}
}
return view('auth.curated-register.index', ['step' => 1]);
}
}

View File

@ -2,458 +2,466 @@
namespace App\Http\Controllers;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\AccountInterstitial;
use App\Jobs\SharePipeline\SharePipeline;
use App\Jobs\SharePipeline\UndoSharePipeline;
use App\AccountInterstitial;
use App\Media;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Profile;
use App\Services\HashidService;
use App\Services\ReblogService;
use App\Services\StatusService;
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 App\Util\Media\License;
use Auth;
use Cache;
use DB;
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)
{
// redirect authed users to Metro 2.0
if($request->user()) {
// unless they force static view
if(!$request->has('fs') || $request->input('fs') != '1') {
return redirect('/i/web/post/' . $id);
}
}
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($user->status != null) {
return ProfileController::accountCheck($user);
}
$status = Status::whereProfileId($user->id)
->whereNull('reblog_of_id')
->whereIn('scope', ['public','unlisted', 'private'])
->findOrFail($id);
if($status->uri || $status->url) {
$url = $status->uri ?? $status->url;
if(ends_with($url, '/activity')) {
$url = str_replace('/activity', '', $url);
}
return redirect($url);
}
if($status->visibility == 'private' || $user->is_private) {
if(!Auth::check()) {
abort(404);
}
$pid = Auth::user()->profile;
if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
abort(404);
}
}
if($status->type == 'archived') {
if(Auth::user()->profile_id !== $status->profile_id) {
abort(404);
}
}
if($request->user() && $request->user()->profile_id != $status->profile_id) {
StatusView::firstOrCreate([
'status_id' => $status->id,
'status_profile_id' => $status->profile_id,
'profile_id' => $request->user()->profile_id
]);
}
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $status);
}
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
return view($template, compact('user', 'status'));
}
public function shortcodeRedirect(Request $request, $id)
{
abort(404);
}
public function showId(int $id)
{
abort(404);
$status = Status::whereNull('reblog_of_id')
->whereIn('scope', ['public', 'unlisted'])
->findOrFail($id);
return redirect($status->url());
}
public function showEmbed(Request $request, $username, int $id)
{
if(!config('instance.embed.post')) {
$res = view('status.embed-removed');
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = Profile::whereNull(['domain','status'])
->whereIsPrivate(false)
->whereUsername($username)
->first();
if(!$profile) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) {
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
if($exists) {
return true;
}
return false;
});
if($aiCheck) {
$res = view('status.embed-removed');
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$status = Status::whereProfileId($profile->id)
->whereNull('uri')
->whereScope('public')
->whereIsNsfw(false)
->whereIn('type', ['photo', 'video','photo:album'])
->find($id);
if(!$status) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$showLikes = $request->filled('likes') && $request->likes == true;
$showCaption = $request->filled('caption') && $request->caption !== false;
$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function showObject(Request $request, $username, int $id)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($user->status != null) {
return ProfileController::accountCheck($user);
}
$status = Status::whereProfileId($user->id)
->whereNotIn('visibility',['draft','direct'])
->findOrFail($id);
abort_if($status->uri, 404);
if($status->visibility == 'private' || $user->is_private) {
if(!Auth::check()) {
abort(403);
}
$pid = Auth::user()->profile;
if($user->followedBy($pid) == false && $user->id !== $pid->id) {
abort(403);
}
}
return $this->showActivityPub($request, $status);
}
public function compose()
{
$this->authCheck();
return view('status.compose');
}
public function store(Request $request)
{
return;
}
public function delete(Request $request)
{
$this->authCheck();
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$status = Status::findOrFail($request->input('item'));
$user = Auth::user();
if($status->profile_id != $user->profile->id &&
$user->is_admin == true &&
$status->uri == null
) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.removed';
$ai->view = 'account.moderation.post.removed';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
if($status->in_reply_to_id) {
$parent = Status::find($status->in_reply_to_id);
if($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) {
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
Cache::forget('profile:status_count:' . $status->profile_id);
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
}
} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) {
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
Cache::forget('profile:status_count:' . $status->profile_id);
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
}
if($request->wantsJson()) {
return response()->json(['Status successfully deleted.']);
} else {
return redirect($user->url());
}
}
public function storeShare(Request $request)
{
$this->authCheck();
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$user = Auth::user();
$profile = $user->profile;
$status = Status::whereScope('public')
->findOrFail($request->input('item'));
$count = $status->reblogs_count;
$exists = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->exists();
if ($exists == true) {
$shares = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->get();
foreach ($shares as $share) {
UndoSharePipeline::dispatch($share);
ReblogService::del($profile->id, $status->id);
$count--;
}
} else {
$share = new Status();
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->in_reply_to_profile_id = $status->profile_id;
$share->type = 'share';
$share->save();
$count++;
SharePipeline::dispatch($share);
ReblogService::add($profile->id, $status->id);
}
Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
StatusService::del($status->id);
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
return $response;
}
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);
$res = $fractal->createData($resource)->toArray();
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function edit(Request $request, $username, $id)
{
$this->authCheck();
$user = Auth::user()->profile;
$status = Status::whereProfileId($user->id)
->with(['media'])
->findOrFail($id);
$licenses = License::get();
return view('status.edit', compact('user', 'status', 'licenses'));
}
public function editStore(Request $request, $username, $id)
{
$this->authCheck();
$user = Auth::user()->profile;
$status = Status::whereProfileId($user->id)
->with(['media'])
->findOrFail($id);
$this->validate($request, [
'license' => 'nullable|integer|min:1|max:16',
]);
$licenseId = $request->input('license');
$status->media->each(function($media) use($licenseId) {
$media->license = $licenseId;
$media->save();
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
});
return redirect($status->url());
}
protected function authCheck()
{
if (Auth::check() == false) {
abort(403);
}
}
protected function validateVisibility($visibility)
{
$allowed = ['public', 'unlisted', 'private'];
return in_array($visibility, $allowed) ? $visibility : 'public';
}
public static function mimeTypeCheck($mimes)
{
$allowed = explode(',', config_cache('pixelfed.media_types'));
$count = count($mimes);
$photos = 0;
$videos = 0;
foreach($mimes as $mime) {
if(in_array($mime, $allowed) == false && $mime !== 'video/mp4') {
continue;
}
if(str_contains($mime, 'image/')) {
$photos++;
}
if(str_contains($mime, 'video/')) {
$videos++;
}
}
if($photos == 1 && $videos == 0) {
return 'photo';
}
if($videos == 1 && $photos == 0) {
return 'video';
}
if($photos > 1 && $videos == 0) {
return 'photo:album';
}
if($videos > 1 && $photos == 0) {
return 'video:album';
}
if($photos >= 1 && $videos >= 1) {
return 'photo:video:album';
}
return 'text';
}
public function toggleVisibility(Request $request) {
$this->authCheck();
$this->validate($request, [
'item' => 'required|string|min:1|max:20',
'disableComments' => 'required|boolean'
]);
$user = Auth::user();
$id = $request->input('item');
$state = $request->input('disableComments');
$status = Status::findOrFail($id);
if($status->profile_id != $user->profile->id && $user->is_admin == false) {
abort(403);
}
$status->comments_disabled = $status->comments_disabled == true ? false : true;
$status->save();
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);
}
public function show(Request $request, $username, $id)
{
// redirect authed users to Metro 2.0
if ($request->user()) {
// unless they force static view
if (! $request->has('fs') || $request->input('fs') != '1') {
return redirect('/i/web/post/'.$id);
}
}
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if ($user->status != null) {
return ProfileController::accountCheck($user);
}
$status = Status::whereProfileId($user->id)
->whereNull('reblog_of_id')
->whereIn('scope', ['public', 'unlisted', 'private'])
->findOrFail($id);
if ($status->uri || $status->url) {
$url = $status->uri ?? $status->url;
if (ends_with($url, '/activity')) {
$url = str_replace('/activity', '', $url);
}
return redirect($url);
}
if ($status->visibility == 'private' || $user->is_private) {
if (! Auth::check()) {
abort(404);
}
$pid = Auth::user()->profile;
if ($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
abort(404);
}
}
if ($status->type == 'archived') {
if (Auth::user()->profile_id !== $status->profile_id) {
abort(404);
}
}
if ($request->user() && $request->user()->profile_id != $status->profile_id) {
StatusView::firstOrCreate([
'status_id' => $status->id,
'status_profile_id' => $status->profile_id,
'profile_id' => $request->user()->profile_id,
]);
}
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $status);
}
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
return view($template, compact('user', 'status'));
}
public function shortcodeRedirect(Request $request, $id)
{
$hid = HashidService::decode($id);
abort_if(! $hid, 404);
return redirect('/i/web/post/'.$hid);
}
public function showId(int $id)
{
abort(404);
$status = Status::whereNull('reblog_of_id')
->whereIn('scope', ['public', 'unlisted'])
->findOrFail($id);
return redirect($status->url());
}
public function showEmbed(Request $request, $username, int $id)
{
if (! (bool) config_cache('instance.embed.post')) {
$res = view('status.embed-removed');
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = Profile::whereNull(['domain', 'status'])
->whereIsPrivate(false)
->whereUsername($username)
->first();
if (! $profile) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) {
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
if ($exists) {
return true;
}
return false;
});
if ($aiCheck) {
$res = view('status.embed-removed');
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$status = Status::whereProfileId($profile->id)
->whereNull('uri')
->whereScope('public')
->whereIsNsfw(false)
->whereIn('type', ['photo', 'video', 'photo:album'])
->find($id);
if (! $status) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$showLikes = $request->filled('likes') && $request->likes == true;
$showCaption = $request->filled('caption') && $request->caption !== false;
$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function showObject(Request $request, $username, int $id)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if ($user->status != null) {
return ProfileController::accountCheck($user);
}
$status = Status::whereProfileId($user->id)
->whereNotIn('visibility', ['draft', 'direct'])
->findOrFail($id);
abort_if($status->uri, 404);
if ($status->visibility == 'private' || $user->is_private) {
if (! Auth::check()) {
abort(403);
}
$pid = Auth::user()->profile;
if ($user->followedBy($pid) == false && $user->id !== $pid->id) {
abort(403);
}
}
return $this->showActivityPub($request, $status);
}
public function compose()
{
$this->authCheck();
return view('status.compose');
}
public function store(Request $request)
{
}
public function delete(Request $request)
{
$this->authCheck();
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$status = Status::findOrFail($request->input('item'));
$user = Auth::user();
if ($status->profile_id != $user->profile->id &&
$user->is_admin == true &&
$status->uri == null
) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.removed';
$ai->view = 'account.moderation.post.removed';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
if ($status->in_reply_to_id) {
$parent = Status::find($status->in_reply_to_id);
if ($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) {
Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
Cache::forget('profile:status_count:'.$status->profile_id);
Cache::forget('profile:embed:'.$status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
}
} elseif ($status->profile_id == $user->profile_id || $user->is_admin == true) {
Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
Cache::forget('profile:status_count:'.$status->profile_id);
Cache::forget('profile:embed:'.$status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
}
if ($request->wantsJson()) {
return response()->json(['Status successfully deleted.']);
} else {
return redirect($user->url());
}
}
public function storeShare(Request $request)
{
$this->authCheck();
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$user = Auth::user();
$profile = $user->profile;
$status = Status::whereScope('public')
->findOrFail($request->input('item'));
$count = $status->reblogs_count;
$exists = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->exists();
if ($exists == true) {
$shares = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->get();
foreach ($shares as $share) {
UndoSharePipeline::dispatch($share);
ReblogService::del($profile->id, $status->id);
$count--;
}
} else {
$share = new Status();
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->in_reply_to_profile_id = $status->profile_id;
$share->type = 'share';
$share->save();
$count++;
SharePipeline::dispatch($share);
ReblogService::add($profile->id, $status->id);
}
Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
StatusService::del($status->id);
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
return $response;
}
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);
$res = $fractal->createData($resource)->toArray();
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
public function edit(Request $request, $username, $id)
{
$this->authCheck();
$user = Auth::user()->profile;
$status = Status::whereProfileId($user->id)
->with(['media'])
->findOrFail($id);
$licenses = License::get();
return view('status.edit', compact('user', 'status', 'licenses'));
}
public function editStore(Request $request, $username, $id)
{
$this->authCheck();
$user = Auth::user()->profile;
$status = Status::whereProfileId($user->id)
->with(['media'])
->findOrFail($id);
$this->validate($request, [
'license' => 'nullable|integer|min:1|max:16',
]);
$licenseId = $request->input('license');
$status->media->each(function ($media) use ($licenseId) {
$media->license = $licenseId;
$media->save();
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
});
return redirect($status->url());
}
protected function authCheck()
{
if (Auth::check() == false) {
abort(403);
}
}
protected function validateVisibility($visibility)
{
$allowed = ['public', 'unlisted', 'private'];
return in_array($visibility, $allowed) ? $visibility : 'public';
}
public static function mimeTypeCheck($mimes)
{
$allowed = explode(',', config_cache('pixelfed.media_types'));
$count = count($mimes);
$photos = 0;
$videos = 0;
foreach ($mimes as $mime) {
if (in_array($mime, $allowed) == false && $mime !== 'video/mp4') {
continue;
}
if (str_contains($mime, 'image/')) {
$photos++;
}
if (str_contains($mime, 'video/')) {
$videos++;
}
}
if ($photos == 1 && $videos == 0) {
return 'photo';
}
if ($videos == 1 && $photos == 0) {
return 'video';
}
if ($photos > 1 && $videos == 0) {
return 'photo:album';
}
if ($videos > 1 && $photos == 0) {
return 'video:album';
}
if ($photos >= 1 && $videos >= 1) {
return 'photo:video:album';
}
return 'text';
}
public function toggleVisibility(Request $request)
{
$this->authCheck();
$this->validate($request, [
'item' => 'required|string|min:1|max:20',
'disableComments' => 'required|boolean',
]);
$user = Auth::user();
$id = $request->input('item');
$state = $request->input('disableComments');
$status = Status::findOrFail($id);
if ($status->profile_id != $user->profile->id && $user->is_admin == false) {
abort(403);
}
$status->comments_disabled = $status->comments_disabled == true ? false : true;
$status->save();
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);
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace App\Http\Requests;
use App\Models\ProfileMigration;
use App\Services\FetchCacheService;
use App\Services\WebfingerService;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class ProfileMigrationStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
if ((bool) config_cache('federation.activitypub.enabled') === false ||
(bool) config_cache('federation.migration') === false) {
return false;
}
if (! $this->user() || $this->user()->status) {
return false;
}
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'acct' => 'required|email',
'password' => 'required|current_password',
];
}
public function after(): array
{
return [
function (Validator $validator) {
$err = $this->validateNewAccount();
if ($err !== 'noerr') {
$validator->errors()->add(
'acct',
$err
);
}
},
];
}
protected function validateNewAccount()
{
if (ProfileMigration::whereProfileId($this->user()->profile_id)->where('created_at', '>', now()->subDays(30))->exists()) {
return 'Error - You have migrated your account in the past 30 days, you can only perform a migration once per 30 days.';
}
$acct = WebfingerService::rawGet($this->acct);
if (! $acct) {
return 'The new account you provided is not responding to our requests.';
}
$pr = FetchCacheService::getJson($acct);
if (! $pr || ! isset($pr['alsoKnownAs'])) {
return 'Invalid account lookup response.';
}
if (! count($pr['alsoKnownAs']) || ! is_array($pr['alsoKnownAs'])) {
return 'The new account does not contain an alias to your current account.';
}
$curAcctUrl = $this->user()->profile->permalink();
if (! in_array($curAcctUrl, $pr['alsoKnownAs'])) {
return 'The new account does not contain an alias to your current account.';
}
return 'noerr';
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Instance;
use App\Services\AccountService;
use App\Services\StatusService;
class AdminRemoteReport extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$instance = parse_url($this->uri, PHP_URL_HOST);
$statuses = [];
if($this->status_ids && count($this->status_ids)) {
foreach($this->status_ids as $sid) {
$s = StatusService::get($sid, false);
if($s && $s['in_reply_to_id'] != null) {
$parent = StatusService::get($s['in_reply_to_id'], false);
if($parent) {
$s['parent'] = $parent;
}
}
if($s) {
$statuses[] = $s;
}
}
}
$res = [
'id' => $this->id,
'instance' => $instance,
'reported' => AccountService::get($this->account_id, true),
'status_ids' => $this->status_ids,
'statuses' => $statuses,
'message' => $this->comment,
'report_meta' => $this->report_meta,
'created_at' => optional($this->created_at)->format('c'),
'action_taken_at' => optional($this->action_taken_at)->format('c'),
];
return $res;
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace App\Jobs\ProfilePipeline;
use App\Transformer\ActivityPub\Verb\Move;
use App\Util\ActivityPub\HttpSignature;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class ProfileMigrationDeliverMoveActivityPipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $migration;
public $oldAccount;
public $newAccount;
public $timeout = 1400;
public $tries = 3;
public $maxExceptions = 1;
public $failOnTimeout = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'profile:migration:deliver-move-followers:id:'.$this->migration->id;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping('profile:migration:deliver-move-followers:id:'.$this->migration->id))->shared()->dontRelease()];
}
/**
* Create a new job instance.
*/
public function __construct($migration, $oldAccount, $newAccount)
{
$this->migration = $migration;
$this->oldAccount = $oldAccount;
$this->newAccount = $newAccount;
}
/**
* Execute the job.
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
$migration = $this->migration;
$profile = $this->oldAccount;
$newAccount = $this->newAccount;
if ($profile->domain || ! $profile->private_key) {
return;
}
$audience = $profile->getAudienceInbox();
$activitypubObject = new Move();
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($migration, $activitypubObject);
$activity = $fractal->createData($resource)->toArray();
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout'),
]);
$version = config('pixelfed.version');
$appUrl = config('app.url');
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
foreach ($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity, [
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => $userAgent,
]);
yield function () use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
],
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
},
]);
$promise = $pool->promise();
$promise->wait();
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Jobs\ProfilePipeline;
use App\Follower;
use App\Profile;
use App\Services\AccountService;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ProfileMigrationMoveFollowersPipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $oldPid;
public $newPid;
public $timeout = 1400;
public $tries = 3;
public $maxExceptions = 1;
public $failOnTimeout = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'profile:migration:move-followers:oldpid-'.$this->oldPid.':newpid-'.$this->newPid;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping('profile:migration:move-followers:oldpid-'.$this->oldPid.':newpid-'.$this->newPid))->shared()->dontRelease()];
}
/**
* Create a new job instance.
*/
public function __construct($oldPid, $newPid)
{
$this->oldPid = $oldPid;
$this->newPid = $newPid;
}
/**
* Execute the job.
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
$og = Profile::find($this->oldPid);
$ne = Profile::find($this->newPid);
if (! $og || ! $ne || $og == $ne) {
return;
}
$ne->followers_count = $og->followers_count;
$ne->save();
$og->followers_count = 0;
$og->save();
foreach (Follower::whereFollowingId($this->oldPid)->lazyById(200, 'id') as $follower) {
try {
$follower->following_id = $this->newPid;
$follower->save();
} catch (Exception $e) {
$follower->delete();
}
}
AccountService::del($this->oldPid);
AccountService::del($this->newPid);
}
}

View File

@ -8,8 +8,14 @@ class MediaTag extends Model
{
protected $guarded = [];
protected $visible = [
'status_id',
'profile_id',
'tagged_username',
];
public function status()
{
return $this->belongsTo(Status::class);
return $this->belongsTo(Status::class);
}
}

View File

@ -9,25 +9,43 @@ class CuratedRegister extends Model
{
use HasFactory;
protected $fillable = [
'user_has_responded'
];
protected $casts = [
'autofollow_account_ids' => 'array',
'admin_notes' => 'array',
'email_verified_at' => 'datetime',
'admin_notified_at' => 'datetime',
'action_taken_at' => 'datetime',
'user_has_responded' => 'boolean',
'is_awaiting_more_info' => 'boolean',
'is_accepted' => 'boolean',
'is_rejected' => 'boolean',
'is_closed' => 'boolean',
];
public function adminStatusLabel()
{
if($this->user_has_responded) {
return '<span class="border border-warning px-3 py-1 rounded text-white font-weight-bold">Awaiting Admin Response</span>';
}
if(!$this->email_verified_at) {
return '<span class="border border-danger px-3 py-1 rounded text-white font-weight-bold">Unverified email</span>';
}
if($this->is_accepted) { return 'Approved'; }
if($this->is_rejected) { return 'Rejected'; }
if($this->is_awaiting_more_info ) {
return '<span class="border border-info px-3 py-1 rounded text-white font-weight-bold">Awaiting Details</span>';
if($this->is_approved) {
return '<span class="badge badge-success bg-success text-dark">Approved</span>';
}
if($this->is_rejected) {
return '<span class="badge badge-danger bg-danger text-white">Rejected</span>';
}
if($this->is_awaiting_more_info ) {
return '<span class="border border-info px-3 py-1 rounded text-white font-weight-bold">Awaiting User Response</span>';
}
if($this->is_closed ) {
return '<span class="border border-muted px-3 py-1 rounded text-white font-weight-bold" style="opacity:0.5">Closed</span>';
}
if($this->is_closed ) { return 'Closed'; }
return '<span class="border border-success px-3 py-1 rounded text-white font-weight-bold">Open</span>';
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CuratedRegisterTemplate extends Model
{
use HasFactory;
protected $fillable = [
'name', 'description', 'content', 'is_active', 'order',
];
protected $casts = [
'is_active' => 'boolean',
];
}

View File

@ -10,6 +10,8 @@ class ProfileAlias extends Model
{
use HasFactory;
protected $guarded = [];
public function profile()
{
return $this->belongsTo(Profile::class);

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Profile;
class ProfileMigration extends Model
{
use HasFactory;
protected $guarded = [];
public function profile()
{
return $this->belongsTo(Profile::class, 'profile_id');
}
public function target()
{
return $this->belongsTo(Profile::class, 'target_profile_id');
}
}

View File

@ -38,6 +38,10 @@ class StatusObserver
*/
public function updated(Status $status)
{
if(!in_array($status->scope, ['public', 'unlisted', 'private'])) {
return;
}
if(config('instance.timeline.home.cached')) {
Cache::forget('pf:timelines:home:' . $status->profile_id);
}
@ -55,6 +59,10 @@ class StatusObserver
*/
public function deleted(Status $status)
{
if(!in_array($status->scope, ['public', 'unlisted', 'private'])) {
return;
}
if(config('instance.timeline.home.cached')) {
Cache::forget('pf:timelines:home:' . $status->profile_id);
}

View File

@ -2,127 +2,143 @@
namespace App\Services;
use Cache;
use Config;
use App\Models\ConfigCache as ConfigCacheModel;
use Cache;
class ConfigCacheService
{
const CACHE_KEY = 'config_cache:_v0-key:';
const CACHE_KEY = 'config_cache:_v0-key:';
public static function get($key)
{
$cacheKey = self::CACHE_KEY . $key;
$ttl = now()->addHours(12);
if(!config('instance.enable_cc')) {
return config($key);
}
public static function get($key)
{
$cacheKey = self::CACHE_KEY.$key;
$ttl = now()->addHours(12);
if (! config('instance.enable_cc')) {
return config($key);
}
return Cache::remember($cacheKey, $ttl, function() use($key) {
return Cache::remember($cacheKey, $ttl, function () use ($key) {
$allowed = [
'app.name',
'app.short_description',
'app.description',
'app.rules',
$allowed = [
'app.name',
'app.short_description',
'app.description',
'app.rules',
'pixelfed.max_photo_size',
'pixelfed.max_album_length',
'pixelfed.image_quality',
'pixelfed.media_types',
'pixelfed.max_photo_size',
'pixelfed.max_album_length',
'pixelfed.image_quality',
'pixelfed.media_types',
'pixelfed.open_registration',
'federation.activitypub.enabled',
'instance.stories.enabled',
'pixelfed.oauth_enabled',
'pixelfed.import.instagram.enabled',
'pixelfed.bouncer.enabled',
'pixelfed.open_registration',
'federation.activitypub.enabled',
'instance.stories.enabled',
'pixelfed.oauth_enabled',
'pixelfed.import.instagram.enabled',
'pixelfed.bouncer.enabled',
'pixelfed.enforce_email_verification',
'pixelfed.max_account_size',
'pixelfed.enforce_account_limit',
'pixelfed.enforce_email_verification',
'pixelfed.max_account_size',
'pixelfed.enforce_account_limit',
'uikit.custom.css',
'uikit.custom.js',
'uikit.show_custom.css',
'uikit.show_custom.js',
'about.title',
'uikit.custom.css',
'uikit.custom.js',
'uikit.show_custom.css',
'uikit.show_custom.js',
'about.title',
'pixelfed.cloud_storage',
'pixelfed.cloud_storage',
'account.autofollow',
'account.autofollow_usernames',
'config.discover.features',
'account.autofollow',
'account.autofollow_usernames',
'config.discover.features',
'instance.has_legal_notice',
'instance.avatar.local_to_cloud',
'instance.has_legal_notice',
'instance.avatar.local_to_cloud',
'pixelfed.directory',
'app.banner_image',
'pixelfed.directory.submission-key',
'pixelfed.directory.submission-ts',
'pixelfed.directory.has_submitted',
'pixelfed.directory.latest_response',
'pixelfed.directory.is_synced',
'pixelfed.directory.testimonials',
'pixelfed.directory',
'app.banner_image',
'pixelfed.directory.submission-key',
'pixelfed.directory.submission-ts',
'pixelfed.directory.has_submitted',
'pixelfed.directory.latest_response',
'pixelfed.directory.is_synced',
'pixelfed.directory.testimonials',
'instance.landing.show_directory',
'instance.landing.show_explore',
'instance.admin.pid',
'instance.banner.blurhash',
'instance.landing.show_directory',
'instance.landing.show_explore',
'instance.admin.pid',
'instance.banner.blurhash',
'autospam.nlp.enabled',
'autospam.nlp.enabled',
'instance.curated_registration.enabled',
// 'system.user_mode'
];
'instance.curated_registration.enabled',
if(!config('instance.enable_cc')) {
return config($key);
}
'federation.migration',
if(!in_array($key, $allowed)) {
return config($key);
}
'pixelfed.max_caption_length',
'pixelfed.max_bio_length',
'pixelfed.max_name_length',
'pixelfed.min_password_length',
'pixelfed.max_avatar_size',
'pixelfed.max_altext_length',
'pixelfed.allow_app_registration',
'pixelfed.app_registration_rate_limit_attempts',
'pixelfed.app_registration_rate_limit_decay',
'pixelfed.app_registration_confirm_rate_limit_attempts',
'pixelfed.app_registration_confirm_rate_limit_decay',
'instance.embed.profile',
'instance.embed.post',
// 'system.user_mode'
];
$v = config($key);
$c = ConfigCacheModel::where('k', $key)->first();
if (! config('instance.enable_cc')) {
return config($key);
}
if($c) {
return $c->v ?? config($key);
}
if (! in_array($key, $allowed)) {
return config($key);
}
if(!$v) {
return;
}
$v = config($key);
$c = ConfigCacheModel::where('k', $key)->first();
$cc = new ConfigCacheModel;
$cc->k = $key;
$cc->v = $v;
$cc->save();
if ($c) {
return $c->v ?? config($key);
}
return $v;
});
}
if (! $v) {
return;
}
public static function put($key, $val)
{
$exists = ConfigCacheModel::whereK($key)->first();
$cc = new ConfigCacheModel;
$cc->k = $key;
$cc->v = $v;
$cc->save();
if($exists) {
$exists->v = $val;
$exists->save();
Cache::put(self::CACHE_KEY . $key, $val, now()->addHours(12));
return self::get($key);
}
return $v;
});
}
$cc = new ConfigCacheModel;
$cc->k = $key;
$cc->v = $val;
$cc->save();
public static function put($key, $val)
{
$exists = ConfigCacheModel::whereK($key)->first();
Cache::put(self::CACHE_KEY . $key, $val, now()->addHours(12));
if ($exists) {
$exists->v = $val;
$exists->save();
Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));
return self::get($key);
}
return self::get($key);
}
$cc = new ConfigCacheModel;
$cc->k = $key;
$cc->v = $val;
$cc->save();
Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));
return self::get($key);
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Services;
use App\Util\ActivityPub\Helpers;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class FetchCacheService
{
const CACHE_KEY = 'pf:fetch_cache_service:getjson:';
public static function getJson($url, $verifyCheck = true, $ttl = 3600, $allowRedirects = true)
{
$vc = $verifyCheck ? 'vc1:' : 'vc0:';
$ar = $allowRedirects ? 'ar1:' : 'ar0';
$key = self::CACHE_KEY.sha1($url).':'.$vc.$ar.$ttl;
if (Cache::has($key)) {
return false;
}
if ($verifyCheck) {
if (! Helpers::validateUrl($url)) {
Cache::put($key, 1, $ttl);
return false;
}
}
$headers = [
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')',
];
if ($allowRedirects) {
$options = [
'allow_redirects' => [
'max' => 2,
'strict' => true,
],
];
} else {
$options = [
'allow_redirects' => false,
];
}
try {
$res = Http::withOptions($options)
->retry(3, function (int $attempt, $exception) {
return $attempt * 500;
})
->acceptJson()
->withHeaders($headers)
->timeout(40)
->get($url);
} catch (RequestException $e) {
Cache::put($key, 1, $ttl);
return false;
} catch (ConnectionException $e) {
Cache::put($key, 1, $ttl);
return false;
} catch (Exception $e) {
Cache::put($key, 1, $ttl);
return false;
}
if (! $res->ok()) {
Cache::put($key, 1, $ttl);
return false;
}
return $res->json();
}
}

View File

@ -2,54 +2,38 @@
namespace App\Services;
use Cache;
class HashidService
{
public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
class HashidService {
public static function encode($id, $minLimit = true)
{
if (! is_numeric($id) || $id > PHP_INT_MAX) {
return null;
}
public const MIN_LIMIT = 15;
public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
$cmap = self::CMAP;
$base = strlen($cmap);
$shortcode = '';
while ($id) {
$id = ($id - ($r = $id % $base)) / $base;
$shortcode = $cmap[$r].$shortcode;
}
public static function encode($id, $minLimit = true)
{
if(!is_numeric($id) || $id > PHP_INT_MAX) {
return null;
}
return $shortcode;
}
if($minLimit && strlen($id) < self::MIN_LIMIT) {
return null;
}
$key = "hashids:{$id}";
return Cache::remember($key, now()->hours(48), function() use($id) {
$cmap = self::CMAP;
$base = strlen($cmap);
$shortcode = '';
while($id) {
$id = ($id - ($r = $id % $base)) / $base;
$shortcode = $cmap[$r] . $shortcode;
}
return $shortcode;
});
}
public static function decode($short)
{
$len = strlen($short);
if($len < 3 || $len > 11) {
return null;
}
$id = 0;
foreach(str_split($short) as $needle) {
$pos = strpos(self::CMAP, $needle);
// if(!$pos) {
// return null;
// }
$id = ($id*64) + $pos;
}
if(strlen($id) < self::MIN_LIMIT) {
return null;
}
return $id;
}
public static function decode($short = false)
{
if (! $short) {
return;
}
$id = 0;
foreach (str_split($short) as $needle) {
$pos = strpos(self::CMAP, $needle);
$id = ($id * 64) + $pos;
}
return $id;
}
}

View File

@ -11,11 +11,16 @@ class SoftwareUpdateService
{
const CACHE_KEY = 'pf:services:software-update:';
public static function cacheKey()
{
return self::CACHE_KEY . 'latest:v1.0.0';
}
public static function get()
{
$curVersion = config('pixelfed.version');
$versions = Cache::remember(self::CACHE_KEY . 'latest:v1.0.0', 1800, function() {
$versions = Cache::remember(self::cacheKey(), 1800, function() {
return self::fetchLatest();
});

View File

@ -2,105 +2,104 @@
namespace App\Services;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
use App\Status;
use App\User;
use App\Services\AccountService;
use App\Util\Site\Nodeinfo;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class LandingService
{
public static function get($json = true)
{
$activeMonth = Nodeinfo::activeUsersMonthly();
public static function get($json = true)
{
$activeMonth = Nodeinfo::activeUsersMonthly();
$totalUsers = Cache::remember('api:nodeinfo:users', 43200, function() {
return User::count();
});
$totalUsers = Cache::remember('api:nodeinfo:users', 43200, function () {
return User::count();
});
$postCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
return Status::whereLocal(true)->count();
});
$postCount = Cache::remember('api:nodeinfo:statuses', 21600, function () {
return Status::whereLocal(true)->count();
});
$contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () {
if(config_cache('instance.admin.pid')) {
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
}
$admin = User::whereIsAdmin(true)->first();
return $admin && isset($admin->profile_id) ?
AccountService::getMastodon($admin->profile_id, true) :
null;
});
$contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () {
if (config_cache('instance.admin.pid')) {
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
}
$admin = User::whereIsAdmin(true)->first();
$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
return config_cache('app.rules') ?
collect(json_decode(config_cache('app.rules'), true))
->map(function($rule, $key) {
$id = $key + 1;
return [
'id' => "{$id}",
'text' => $rule
];
})
->toArray() : [];
});
return $admin && isset($admin->profile_id) ?
AccountService::getMastodon($admin->profile_id, true) :
null;
});
$openReg = (bool) config_cache('pixelfed.open_registration');
$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
return config_cache('app.rules') ?
collect(json_decode(config_cache('app.rules'), true))
->map(function ($rule, $key) {
$id = $key + 1;
$res = [
'name' => config_cache('app.name'),
'url' => config_cache('app.url'),
'domain' => config('pixelfed.domain.app'),
'show_directory' => config_cache('instance.landing.show_directory'),
'show_explore_feed' => config_cache('instance.landing.show_explore'),
'open_registration' => (bool) $openReg,
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'version' => config('pixelfed.version'),
'about' => [
'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'),
'short_description' => config_cache('app.short_description'),
'description' => config_cache('app.description'),
],
'stats' => [
'active_users' => (int) $activeMonth,
'posts_count' => (int) $postCount,
'total_users' => (int) $totalUsers
],
'contact' => [
'account' => $contactAccount,
'email' => config('instance.email')
],
'rules' => $rules,
'uploader' => [
'max_photo_size' => (int) (config('pixelfed.max_photo_size') * 1024),
'max_caption_length' => (int) config('pixelfed.max_caption_length'),
'max_altext_length' => (int) config('pixelfed.max_altext_length', 150),
'album_limit' => (int) config_cache('pixelfed.max_album_length'),
'image_quality' => (int) config_cache('pixelfed.image_quality'),
'max_collection_length' => (int) config('pixelfed.max_collection_length', 18),
'optimize_image' => (bool) config('pixelfed.optimize_image'),
'optimize_video' => (bool) config('pixelfed.optimize_video'),
'media_types' => config_cache('pixelfed.media_types'),
],
'features' => [
'federation' => config_cache('federation.activitypub.enabled'),
'timelines' => [
'local' => true,
'network' => (bool) config('federation.network_timeline'),
],
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
'stories' => (bool) config_cache('instance.stories.enabled'),
'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
]
];
return [
'id' => "{$id}",
'text' => $rule,
];
})
->toArray() : [];
});
if($json) {
return json_encode($res);
}
$openReg = (bool) config_cache('pixelfed.open_registration');
return $res;
}
$res = [
'name' => config_cache('app.name'),
'url' => config_cache('app.url'),
'domain' => config('pixelfed.domain.app'),
'show_directory' => config_cache('instance.landing.show_directory'),
'show_explore_feed' => config_cache('instance.landing.show_explore'),
'open_registration' => (bool) $openReg,
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'version' => config('pixelfed.version'),
'about' => [
'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'),
'short_description' => config_cache('app.short_description'),
'description' => config_cache('app.description'),
],
'stats' => [
'active_users' => (int) $activeMonth,
'posts_count' => (int) $postCount,
'total_users' => (int) $totalUsers,
],
'contact' => [
'account' => $contactAccount,
'email' => config('instance.email'),
],
'rules' => $rules,
'uploader' => [
'max_photo_size' => (int) (config_cache('pixelfed.max_photo_size') * 1024),
'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'),
'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150),
'album_limit' => (int) config_cache('pixelfed.max_album_length'),
'image_quality' => (int) config_cache('pixelfed.image_quality'),
'max_collection_length' => (int) config('pixelfed.max_collection_length', 18),
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'optimize_video' => (bool) config_cache('pixelfed.optimize_video'),
'media_types' => config_cache('pixelfed.media_types'),
],
'features' => [
'federation' => config_cache('federation.activitypub.enabled'),
'timelines' => [
'local' => true,
'network' => (bool) config_cache('federation.network_timeline'),
],
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
'stories' => (bool) config_cache('instance.stories.enabled'),
'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
],
];
if ($json) {
return json_encode($res);
}
return $res;
}
}

View File

@ -2,298 +2,324 @@
namespace App\Services;
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
use App\Notification;
use App\Transformer\Api\NotificationTransformer;
use Cache;
use Illuminate\Support\Facades\Redis;
use App\{
Notification,
Profile
};
use App\Transformer\Api\NotificationTransformer;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
class NotificationService {
class NotificationService
{
const CACHE_KEY = 'pf:services:notifications:ids:';
const CACHE_KEY = 'pf:services:notifications:ids:';
const EPOCH_CACHE_KEY = 'pf:services:notifications:epoch-id:by-months:';
const ITEM_CACHE_TTL = 86400;
const MASTODON_TYPES = [
'follow',
'follow_request',
'mention',
'reblog',
'favourite',
'poll',
'status'
];
const EPOCH_CACHE_KEY = 'pf:services:notifications:epoch-id:by-months:';
public static function get($id, $start = 0, $stop = 400)
{
$res = collect([]);
$key = self::CACHE_KEY . $id;
$stop = $stop > 400 ? 400 : $stop;
$ids = Redis::zrangebyscore($key, $start, $stop);
if(empty($ids)) {
$ids = self::coldGet($id, $start, $stop);
}
foreach($ids as $id) {
$n = self::getNotification($id);
if($n != null) {
$res->push($n);
}
}
return $res;
}
const ITEM_CACHE_TTL = 86400;
public static function getEpochId($months = 6)
{
$epoch = Cache::get(self::EPOCH_CACHE_KEY . $months);
if(!$epoch) {
NotificationEpochUpdatePipeline::dispatch();
return 1;
}
return $epoch;
}
const MASTODON_TYPES = [
'follow',
'follow_request',
'mention',
'reblog',
'favourite',
'poll',
'status',
];
public static function coldGet($id, $start = 0, $stop = 400)
{
$stop = $stop > 400 ? 400 : $stop;
$ids = Notification::where('id', '>', self::getEpochId())
->where('profile_id', $id)
->orderByDesc('id')
->skip($start)
->take($stop)
->pluck('id');
foreach($ids as $key) {
self::set($id, $key);
}
return $ids;
}
public static function get($id, $start = 0, $stop = 400)
{
$res = collect([]);
$key = self::CACHE_KEY.$id;
$stop = $stop > 400 ? 400 : $stop;
$ids = Redis::zrangebyscore($key, $start, $stop);
if (empty($ids)) {
$ids = self::coldGet($id, $start, $stop);
}
foreach ($ids as $id) {
$n = self::getNotification($id);
if ($n != null) {
$res->push($n);
}
}
public static function getMax($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMaxId($id, $start, $limit);
return $res;
}
if(empty($ids)) {
return [];
}
public static function getEpochId($months = 6)
{
$epoch = Cache::get(self::EPOCH_CACHE_KEY.$months);
if (! $epoch) {
NotificationEpochUpdatePipeline::dispatch();
$res = collect([]);
foreach($ids as $id) {
$n = self::getNotification($id);
if($n != null) {
$res->push($n);
}
}
return $res->toArray();
}
return 1;
}
public static function getMin($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMinId($id, $start, $limit);
return $epoch;
}
if(empty($ids)) {
return [];
}
public static function coldGet($id, $start = 0, $stop = 400)
{
$stop = $stop > 400 ? 400 : $stop;
$ids = Notification::where('id', '>', self::getEpochId())
->where('profile_id', $id)
->orderByDesc('id')
->skip($start)
->take($stop)
->pluck('id');
foreach ($ids as $key) {
self::set($id, $key);
}
$res = collect([]);
foreach($ids as $id) {
$n = self::getNotification($id);
if($n != null) {
$res->push($n);
}
}
return $res->toArray();
}
return $ids;
}
public static function getMax($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMaxId($id, $start, $limit);
public static function getMaxMastodon($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMaxId($id, $start, $limit);
if (empty($ids)) {
return [];
}
if(empty($ids)) {
return [];
}
$res = collect([]);
foreach ($ids as $id) {
$n = self::getNotification($id);
if ($n != null) {
$res->push($n);
}
}
$res = collect([]);
foreach($ids as $id) {
$n = self::rewriteMastodonTypes(self::getNotification($id));
if($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
if(isset($n['account'])) {
$n['account'] = AccountService::getMastodon($n['account']['id']);
}
return $res->toArray();
}
if(isset($n['relationship'])) {
unset($n['relationship']);
}
public static function getMin($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMinId($id, $start, $limit);
if(isset($n['status'])) {
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
}
if (empty($ids)) {
return [];
}
$res->push($n);
}
}
return $res->toArray();
}
$res = collect([]);
foreach ($ids as $id) {
$n = self::getNotification($id);
if ($n != null) {
$res->push($n);
}
}
public static function getMinMastodon($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMinId($id, $start, $limit);
return $res->toArray();
}
if(empty($ids)) {
return [];
}
public static function getMaxMastodon($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMaxId($id, $start, $limit);
$res = collect([]);
foreach($ids as $id) {
$n = self::rewriteMastodonTypes(self::getNotification($id));
if($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
if(isset($n['account'])) {
$n['account'] = AccountService::getMastodon($n['account']['id']);
}
if (empty($ids)) {
return [];
}
if(isset($n['relationship'])) {
unset($n['relationship']);
}
$res = collect([]);
foreach ($ids as $id) {
$n = self::rewriteMastodonTypes(self::getNotification($id));
if ($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
if (isset($n['account'])) {
$n['account'] = AccountService::getMastodon($n['account']['id']);
}
if(isset($n['status'])) {
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
}
if (isset($n['relationship'])) {
unset($n['relationship']);
}
$res->push($n);
}
}
return $res->toArray();
}
if ($n['type'] === 'mention' && isset($n['tagged'], $n['tagged']['status_id'])) {
$n['status'] = StatusService::getMastodon($n['tagged']['status_id'], false);
unset($n['tagged']);
}
public static function getRankedMaxId($id = false, $start = null, $limit = 10)
{
if(!$start || !$id) {
return [];
}
if (isset($n['status'])) {
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
}
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, $start, '-inf', [
'withscores' => true,
'limit' => [1, $limit]
]));
}
$res->push($n);
}
}
public static function getRankedMinId($id = false, $end = null, $limit = 10)
{
if(!$end || !$id) {
return [];
}
return $res->toArray();
}
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, '+inf', $end, [
'withscores' => true,
'limit' => [0, $limit]
]));
}
public static function getMinMastodon($id = false, $start = 0, $limit = 10)
{
$ids = self::getRankedMinId($id, $start, $limit);
public static function rewriteMastodonTypes($notification)
{
if(!$notification || !isset($notification['type'])) {
return $notification;
}
if (empty($ids)) {
return [];
}
if($notification['type'] === 'comment') {
$notification['type'] = 'mention';
}
$res = collect([]);
foreach ($ids as $id) {
$n = self::rewriteMastodonTypes(self::getNotification($id));
if ($n != null && in_array($n['type'], self::MASTODON_TYPES)) {
if (isset($n['account'])) {
$n['account'] = AccountService::getMastodon($n['account']['id']);
}
if($notification['type'] === 'share') {
$notification['type'] = 'reblog';
}
if (isset($n['relationship'])) {
unset($n['relationship']);
}
return $notification;
}
if ($n['type'] === 'mention' && isset($n['tagged'], $n['tagged']['status_id'])) {
$n['status'] = StatusService::getMastodon($n['tagged']['status_id'], false);
unset($n['tagged']);
}
public static function set($id, $val)
{
if(self::count($id) > 400) {
Redis::zpopmin(self::CACHE_KEY . $id);
}
return Redis::zadd(self::CACHE_KEY . $id, $val, $val);
}
if (isset($n['status'])) {
$n['status'] = StatusService::getMastodon($n['status']['id'], false);
}
public static function del($id, $val)
{
Cache::forget('service:notification:' . $val);
return Redis::zrem(self::CACHE_KEY . $id, $val);
}
$res->push($n);
}
}
public static function add($id, $val)
{
return self::set($id, $val);
}
return $res->toArray();
}
public static function rem($id, $val)
{
return self::del($id, $val);
}
public static function getRankedMaxId($id = false, $start = null, $limit = 10)
{
if (! $start || ! $id) {
return [];
}
public static function count($id)
{
return Redis::zcount(self::CACHE_KEY . $id, '-inf', '+inf');
}
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, $start, '-inf', [
'withscores' => true,
'limit' => [1, $limit],
]));
}
public static function getNotification($id)
{
$notification = Cache::remember('service:notification:'.$id, self::ITEM_CACHE_TTL, function() use($id) {
$n = Notification::with('item')->find($id);
public static function getRankedMinId($id = false, $end = null, $limit = 10)
{
if (! $end || ! $id) {
return [];
}
if(!$n) {
return null;
}
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$id, '+inf', $end, [
'withscores' => true,
'limit' => [0, $limit],
]));
}
$account = AccountService::get($n->actor_id, true);
public static function rewriteMastodonTypes($notification)
{
if (! $notification || ! isset($notification['type'])) {
return $notification;
}
if(!$account) {
return null;
}
if ($notification['type'] === 'comment') {
$notification['type'] = 'mention';
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($n, new NotificationTransformer());
return $fractal->createData($resource)->toArray();
});
if ($notification['type'] === 'share') {
$notification['type'] = 'reblog';
}
if(!$notification) {
return;
}
if ($notification['type'] === 'tagged') {
$notification['type'] = 'mention';
}
if(isset($notification['account'])) {
$notification['account'] = AccountService::get($notification['account']['id'], true);
}
return $notification;
}
return $notification;
}
public static function set($id, $val)
{
if (self::count($id) > 400) {
Redis::zpopmin(self::CACHE_KEY.$id);
}
public static function setNotification(Notification $notification)
{
return Cache::remember('service:notification:'.$notification->id, self::ITEM_CACHE_TTL, function() use($notification) {
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($notification, new NotificationTransformer());
return $fractal->createData($resource)->toArray();
});
}
return Redis::zadd(self::CACHE_KEY.$id, $val, $val);
}
public static function warmCache($id, $stop = 400, $force = false)
{
if(self::count($id) == 0 || $force == true) {
$ids = Notification::where('profile_id', $id)
->where('id', '>', self::getEpochId())
->orderByDesc('id')
->limit($stop)
->pluck('id');
foreach($ids as $key) {
self::set($id, $key);
}
return 1;
}
return 0;
}
public static function del($id, $val)
{
Cache::forget('service:notification:'.$val);
return Redis::zrem(self::CACHE_KEY.$id, $val);
}
public static function add($id, $val)
{
return self::set($id, $val);
}
public static function rem($id, $val)
{
return self::del($id, $val);
}
public static function count($id)
{
return Redis::zcount(self::CACHE_KEY.$id, '-inf', '+inf');
}
public static function getNotification($id)
{
$notification = Cache::remember('service:notification:'.$id, self::ITEM_CACHE_TTL, function () use ($id) {
$n = Notification::with('item')->find($id);
if (! $n) {
return null;
}
$account = AccountService::get($n->actor_id, true);
if (! $account) {
return null;
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($n, new NotificationTransformer());
return $fractal->createData($resource)->toArray();
});
if (! $notification) {
return;
}
if (isset($notification['account'])) {
$notification['account'] = AccountService::get($notification['account']['id'], true);
}
return $notification;
}
public static function setNotification(Notification $notification)
{
return Cache::remember('service:notification:'.$notification->id, self::ITEM_CACHE_TTL, function () use ($notification) {
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($notification, new NotificationTransformer());
return $fractal->createData($resource)->toArray();
});
}
public static function warmCache($id, $stop = 400, $force = false)
{
if (self::count($id) == 0 || $force == true) {
$ids = Notification::where('profile_id', $id)
->where('id', '>', self::getEpochId())
->orderByDesc('id')
->limit($stop)
->pluck('id');
foreach ($ids as $key) {
self::set($id, $key);
}
return 1;
}
return 0;
}
}

View File

@ -2,28 +2,26 @@
namespace App\Services;
use Cache;
use Illuminate\Support\Facades\Redis;
use App\{Hashtag, Profile, Status};
use App\Hashtag;
use App\Profile;
use App\Status;
use App\Transformer\Api\AccountTransformer;
use App\Transformer\Api\StatusTransformer;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use App\Services\AccountService;
use App\Services\HashtagService;
use App\Services\StatusService;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class SearchApiV2Service
{
private $query;
static $mastodonMode = false;
public static $mastodonMode = false;
public static function query($query, $mastodonMode = false)
{
self::$mastodonMode = $mastodonMode;
return (new self)->run($query);
}
@ -32,51 +30,51 @@ class SearchApiV2Service
$this->query = $query;
$q = urldecode($query->input('q'));
if($query->has('resolve') &&
( Str::startsWith($q, 'https://') ||
if ($query->has('resolve') &&
(Str::startsWith($q, 'https://') ||
Str::substrCount($q, '@') >= 1)
) {
return $this->resolveQuery();
}
if($query->has('type')) {
if ($query->has('type')) {
switch ($query->input('type')) {
case 'accounts':
return [
'accounts' => $this->accounts(),
'hashtags' => [],
'statuses' => []
'statuses' => [],
];
break;
case 'hashtags':
return [
'accounts' => [],
'hashtags' => $this->hashtags(),
'statuses' => []
'statuses' => [],
];
break;
case 'statuses':
return [
'accounts' => [],
'hashtags' => [],
'statuses' => $this->statuses()
'statuses' => $this->statuses(),
];
break;
}
}
if($query->has('account_id')) {
if ($query->has('account_id')) {
return [
'accounts' => [],
'hashtags' => [],
'statuses' => $this->statusesById()
'statuses' => $this->statusesById(),
];
}
return [
'accounts' => $this->accounts(),
'hashtags' => $this->hashtags(),
'statuses' => $this->statuses()
'statuses' => $this->statuses(),
];
}
@ -87,17 +85,17 @@ class SearchApiV2Service
$limit = $this->query->input('limit') ?? 20;
$offset = $this->query->input('offset') ?? 0;
$rawQuery = $initalQuery ? $initalQuery : $this->query->input('q');
$query = $rawQuery . '%';
$query = $rawQuery.'%';
$webfingerQuery = $query;
if(Str::substrCount($rawQuery, '@') == 1 && substr($rawQuery, 0, 1) !== '@') {
$query = '@' . $query;
if (Str::substrCount($rawQuery, '@') == 1 && substr($rawQuery, 0, 1) !== '@') {
$query = '@'.$query;
}
if(substr($webfingerQuery, 0, 1) !== '@') {
$webfingerQuery = '@' . $webfingerQuery;
if (substr($webfingerQuery, 0, 1) !== '@') {
$webfingerQuery = '@'.$webfingerQuery;
}
$banned = InstanceService::getBannedDomains() ?? [];
$domainBlocks = UserFilterService::domainBlocks($user->profile_id);
if($domainBlocks && count($domainBlocks)) {
if ($domainBlocks && count($domainBlocks)) {
$banned = array_unique(
array_values(
array_merge($banned, $domainBlocks)
@ -112,15 +110,15 @@ class SearchApiV2Service
->offset($offset)
->limit($limit)
->get()
->filter(function($profile) use ($banned) {
->filter(function ($profile) use ($banned) {
return in_array($profile->domain, $banned) == false;
})
->map(function($res) use($mastodonMode) {
->map(function ($res) use ($mastodonMode) {
return $mastodonMode ?
AccountService::getMastodon($res['id']) :
AccountService::get($res['id']);
})
->filter(function($account) {
->filter(function ($account) {
return $account && isset($account['id']);
})
->values();
@ -134,31 +132,31 @@ class SearchApiV2Service
$q = $this->query->input('q');
$limit = $this->query->input('limit') ?? 20;
$offset = $this->query->input('offset') ?? 0;
$query = Str::startsWith($q, '#') ? '%' . substr($q, 1) . '%' : '%' . $q . '%';
$query = Str::startsWith($q, '#') ? substr($q, 1).'%' : $q;
$operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
return Hashtag::where('name', $operator, $query)
->orWhere('slug', $operator, $query)
->where(function($q) {
return $q->where('can_search', true)
->orWhereNull('can_search');
})
->orderByDesc('cached_count')
->offset($offset)
->limit($limit)
->get()
->map(function($tag) use($mastodonMode) {
->filter(function ($tag) {
return $tag->can_search != false;
})
->map(function ($tag) use ($mastodonMode) {
$res = [
'name' => $tag->name,
'url' => $tag->url()
'url' => $tag->url(),
];
if(!$mastodonMode) {
if (! $mastodonMode) {
$res['history'] = [];
$res['count'] = HashtagService::count($tag->id);
$res['count'] = $tag->cached_count ?? 0;
}
return $res;
});
})
->values();
}
protected function statuses()
@ -175,7 +173,7 @@ class SearchApiV2Service
protected function resolveQuery()
{
$default = [
$default = [
'accounts' => [],
'hashtags' => [],
'statuses' => [],
@ -185,73 +183,77 @@ class SearchApiV2Service
$query = urldecode($this->query->input('q'));
$banned = InstanceService::getBannedDomains();
$domainBlocks = UserFilterService::domainBlocks($user->profile_id);
if($domainBlocks && count($domainBlocks)) {
if ($domainBlocks && count($domainBlocks)) {
$banned = array_unique(
array_values(
array_merge($banned, $domainBlocks)
)
);
}
if(substr($query, 0, 1) === '@' && !Str::contains($query, '.')) {
if (substr($query, 0, 1) === '@' && ! Str::contains($query, '.')) {
$default['accounts'] = $this->accounts(substr($query, 1));
return $default;
}
if(Helpers::validateLocalUrl($query)) {
if(Str::contains($query, '/p/') || Str::contains($query, 'i/web/post/')) {
if (Helpers::validateLocalUrl($query)) {
if (Str::contains($query, '/p/') || Str::contains($query, 'i/web/post/')) {
return $this->resolveLocalStatus();
} else if(Str::contains($query, 'i/web/profile/')) {
} elseif (Str::contains($query, 'i/web/profile/')) {
return $this->resolveLocalProfileId();
} else {
return $this->resolveLocalProfile();
}
} else {
if(!Helpers::validateUrl($query) && strpos($query, '@') == -1) {
if (! Helpers::validateUrl($query) && strpos($query, '@') == -1) {
return $default;
}
if(!Str::startsWith($query, 'http') && Str::substrCount($query, '@') == 1 && strpos($query, '@') !== 0) {
if (! Str::startsWith($query, 'http') && Str::substrCount($query, '@') == 1 && strpos($query, '@') !== 0) {
try {
$res = WebfingerService::lookup('@' . $query, $mastodonMode);
$res = WebfingerService::lookup('@'.$query, $mastodonMode);
} catch (\Exception $e) {
return $default;
}
if($res && isset($res['id'], $res['url'])) {
if ($res && isset($res['id'], $res['url'])) {
$domain = strtolower(parse_url($res['url'], PHP_URL_HOST));
if(in_array($domain, $banned)) {
if (in_array($domain, $banned)) {
return $default;
}
$default['accounts'][] = $res;
return $default;
} else {
return $default;
}
}
if(Str::substrCount($query, '@') == 2) {
if (Str::substrCount($query, '@') == 2) {
try {
$res = WebfingerService::lookup($query, $mastodonMode);
} catch (\Exception $e) {
return $default;
}
if($res && isset($res['id'])) {
if ($res && isset($res['id'])) {
$domain = strtolower(parse_url($res['url'], PHP_URL_HOST));
if(in_array($domain, $banned)) {
if (in_array($domain, $banned)) {
return $default;
}
$default['accounts'][] = $res;
return $default;
} else {
return $default;
}
}
if($sid = Status::whereUri($query)->first()) {
if ($sid = Status::whereUri($query)->first()) {
$s = StatusService::get($sid->id, false);
if(!$s) {
if (! $s) {
return $default;
}
if(in_array($s['visibility'], ['public', 'unlisted'])) {
if (in_array($s['visibility'], ['public', 'unlisted'])) {
$default['statuses'][] = $s;
return $default;
}
}
@ -259,10 +261,10 @@ class SearchApiV2Service
try {
$res = ActivityPubFetchService::get($query);
if($res) {
if ($res) {
$json = json_decode($res, true);
if(!$json || !isset($json['@context']) || !isset($json['type']) || !in_array($json['type'], ['Note', 'Person'])) {
if (! $json || ! isset($json['@context']) || ! isset($json['type']) || ! in_array($json['type'], ['Note', 'Person'])) {
return [
'accounts' => [],
'hashtags' => [],
@ -270,38 +272,40 @@ class SearchApiV2Service
];
}
switch($json['type']) {
switch ($json['type']) {
case 'Note':
$obj = Helpers::statusFetch($query);
if(!$obj || !isset($obj['id'])) {
if (! $obj || ! isset($obj['id'])) {
return $default;
}
$note = $mastodonMode ?
StatusService::getMastodon($obj['id'], false) :
StatusService::get($obj['id'], false);
if(!$note) {
if (! $note) {
return $default;
}
if(!isset($note['visibility']) || !in_array($note['visibility'], ['public', 'unlisted'])) {
if (! isset($note['visibility']) || ! in_array($note['visibility'], ['public', 'unlisted'])) {
return $default;
}
$default['statuses'][] = $note;
return $default;
break;
break;
case 'Person':
$obj = Helpers::profileFetch($query);
if(!$obj) {
if (! $obj) {
return $default;
}
if(in_array($obj['domain'], $banned)) {
if (in_array($obj['domain'], $banned)) {
return $default;
}
$default['accounts'][] = $mastodonMode ?
AccountService::getMastodon($obj['id'], true) :
AccountService::get($obj['id'], true);
return $default;
break;
break;
default:
return [
@ -309,7 +313,7 @@ class SearchApiV2Service
'hashtags' => [],
'statuses' => [],
];
break;
break;
}
}
} catch (\Exception $e) {
@ -329,18 +333,18 @@ class SearchApiV2Service
$query = urldecode($this->query->input('q'));
$query = last(explode('/', parse_url($query, PHP_URL_PATH)));
$status = StatusService::getMastodon($query, false);
if(!$status || !in_array($status['visibility'], ['public', 'unlisted'])) {
if (! $status || ! in_array($status['visibility'], ['public', 'unlisted'])) {
return [
'accounts' => [],
'hashtags' => [],
'statuses' => []
'statuses' => [],
];
}
$res = [
'accounts' => [],
'hashtags' => [],
'statuses' => [$status]
'statuses' => [$status],
];
return $res;
@ -355,21 +359,22 @@ class SearchApiV2Service
->whereUsername($query)
->first();
if(!$profile) {
if (! $profile) {
return [
'accounts' => [],
'hashtags' => [],
'statuses' => []
'statuses' => [],
];
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
return [
'accounts' => [$fractal->createData($resource)->toArray()],
'hashtags' => [],
'statuses' => []
'statuses' => [],
];
}
@ -380,22 +385,22 @@ class SearchApiV2Service
$profile = Profile::whereNull('status')
->find($query);
if(!$profile) {
if (! $profile) {
return [
'accounts' => [],
'hashtags' => [],
'statuses' => []
'statuses' => [],
];
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
return [
'accounts' => [$fractal->createData($resource)->toArray()],
'hashtags' => [],
'statuses' => []
'statuses' => [],
];
}
}

View File

@ -2,96 +2,97 @@
namespace App\Services;
use Cache;
use Illuminate\Support\Facades\Redis;
use App\{Status, StatusHashtag};
use App\Transformer\Api\StatusHashtagTransformer;
use App\Hashtag;
use App\Status;
use App\StatusHashtag;
use App\Transformer\Api\HashtagTransformer;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
class StatusHashtagService {
class StatusHashtagService
{
const CACHE_KEY = 'pf:services:status-hashtag:collection:';
const CACHE_KEY = 'pf:services:status-hashtag:collection:';
public static function get($id, $page = 1, $stop = 9)
{
if ($page > 20) {
return [];
}
public static function get($id, $page = 1, $stop = 9)
{
if($page > 20) {
return [];
}
$pid = request()->user() ? request()->user()->profile_id : false;
$filtered = $pid ? UserFilterService::filters($pid) : [];
$pid = request()->user() ? request()->user()->profile_id : false;
$filtered = $pid ? UserFilterService::filters($pid) : [];
return StatusHashtag::whereHashtagId($id)
->whereStatusVisibility('public')
->skip($stop)
->latest()
->take(9)
->pluck('status_id')
->map(function ($i, $k) use ($id) {
return self::getStatus($i, $id);
})
->filter(function ($i) use ($filtered) {
return isset($i['status']) &&
! empty($i['status']) && ! in_array($i['status']['account']['id'], $filtered) &&
isset($i['status']['media_attachments']) &&
! empty($i['status']['media_attachments']);
})
->values();
}
return StatusHashtag::whereHashtagId($id)
->whereStatusVisibility('public')
->skip($stop)
->latest()
->take(9)
->pluck('status_id')
->map(function ($i, $k) use ($id) {
return self::getStatus($i, $id);
})
->filter(function ($i) use($filtered) {
return isset($i['status']) &&
!empty($i['status']) && !in_array($i['status']['account']['id'], $filtered) &&
isset($i['status']['media_attachments']) &&
!empty($i['status']['media_attachments']);
})
->values();
}
public static function coldGet($id, $start = 0, $stop = 2000)
{
$stop = $stop > 2000 ? 2000 : $stop;
$ids = StatusHashtag::whereHashtagId($id)
->whereStatusVisibility('public')
->whereHas('media')
->latest()
->skip($start)
->take($stop)
->pluck('status_id');
foreach ($ids as $key) {
self::set($id, $key);
}
public static function coldGet($id, $start = 0, $stop = 2000)
{
$stop = $stop > 2000 ? 2000 : $stop;
$ids = StatusHashtag::whereHashtagId($id)
->whereStatusVisibility('public')
->whereHas('media')
->latest()
->skip($start)
->take($stop)
->pluck('status_id');
foreach($ids as $key) {
self::set($id, $key);
}
return $ids;
}
return $ids;
}
public static function set($key, $val)
{
return 1;
}
public static function set($key, $val)
{
return 1;
}
public static function del($key)
{
return 1;
}
public static function del($key)
{
return 1;
}
public static function count($id)
{
$key = 'pf:services:status-hashtag:count:' . $id;
$ttl = now()->addMinutes(5);
return Cache::remember($key, $ttl, function() use($id) {
return StatusHashtag::whereHashtagId($id)->has('media')->count();
});
}
public static function count($id)
{
$cc = Hashtag::find($id);
if (! $cc) {
return 0;
}
public static function getStatus($statusId, $hashtagId)
{
return ['status' => StatusService::get($statusId)];
}
return $cc->cached_count ?? 0;
}
public static function statusTags($statusId)
{
$status = Status::with('hashtags')->find($statusId);
if(!$status) {
return [];
}
public static function getStatus($statusId, $hashtagId)
{
return ['status' => StatusService::get($statusId)];
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($status->hashtags, new HashtagTransformer());
return $fractal->createData($resource)->toArray();
}
public static function statusTags($statusId)
{
$status = Status::with('hashtags')->find($statusId);
if (! $status) {
return [];
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($status->hashtags, new HashtagTransformer());
return $fractal->createData($resource)->toArray();
}
}

View File

@ -2,69 +2,95 @@
namespace App\Services;
use Cache;
use App\Profile;
use App\Util\ActivityPub\Helpers;
use App\Util\Webfinger\WebfingerUrl;
use Illuminate\Support\Facades\Http;
use App\Util\ActivityPub\Helpers;
use App\Services\AccountService;
class WebfingerService
{
public static function lookup($query, $mastodonMode = false)
{
return (new self)->run($query, $mastodonMode);
}
public static function rawGet($url)
{
$n = WebfingerUrl::get($url);
if (! $n) {
return false;
}
$webfinger = FetchCacheService::getJson($n);
if (! $webfinger) {
return false;
}
protected function run($query, $mastodonMode)
{
if($profile = Profile::whereUsername($query)->first()) {
return $mastodonMode ?
AccountService::getMastodon($profile->id, true) :
AccountService::get($profile->id);
}
$url = WebfingerUrl::generateWebfingerUrl($query);
if(!Helpers::validateUrl($url)) {
return [];
}
if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) {
return false;
}
$link = collect($webfinger['links'])
->filter(function ($link) {
return $link &&
isset($link['rel'], $link['type'], $link['href']) &&
$link['rel'] === 'self' &&
in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
})
->pluck('href')
->first();
try {
$res = Http::retry(3, 100)
->acceptJson()
->withHeaders([
'User-Agent' => '(Pixelfed/' . config('pixelfed.version') . '; +' . config('app.url') . ')'
])
->timeout(20)
->get($url);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
return [];
}
return $link;
}
if(!$res->successful()) {
return [];
}
public static function lookup($query, $mastodonMode = false)
{
return (new self)->run($query, $mastodonMode);
}
$webfinger = $res->json();
if(!isset($webfinger['links']) || !is_array($webfinger['links']) || empty($webfinger['links'])) {
return [];
}
protected function run($query, $mastodonMode)
{
if ($profile = Profile::whereUsername($query)->first()) {
return $mastodonMode ?
AccountService::getMastodon($profile->id, true) :
AccountService::get($profile->id);
}
$url = WebfingerUrl::generateWebfingerUrl($query);
if (! Helpers::validateUrl($url)) {
return [];
}
$link = collect($webfinger['links'])
->filter(function($link) {
return $link &&
isset($link['rel'], $link['type'], $link['href']) &&
$link['rel'] === 'self' &&
in_array($link['type'], ['application/activity+json','application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
})
->pluck('href')
->first();
try {
$res = Http::retry(3, 100)
->acceptJson()
->withHeaders([
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')',
])
->timeout(20)
->get($url);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
return [];
}
$profile = Helpers::profileFetch($link);
if(!$profile) {
return;
}
return $mastodonMode ?
AccountService::getMastodon($profile->id, true) :
AccountService::get($profile->id);
}
if (! $res->successful()) {
return [];
}
$webfinger = $res->json();
if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) {
return [];
}
$link = collect($webfinger['links'])
->filter(function ($link) {
return $link &&
isset($link['rel'], $link['type'], $link['href']) &&
$link['rel'] === 'self' &&
in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
})
->pluck('href')
->first();
$profile = Helpers::profileFetch($link);
if (! $profile) {
return;
}
return $mastodonMode ?
AccountService::getMastodon($profile->id, true) :
AccountService::get($profile->id);
}
}

View File

@ -3,67 +3,80 @@
namespace App\Transformer\ActivityPub;
use App\Profile;
use League\Fractal;
use App\Services\AccountService;
use League\Fractal;
class ProfileTransformer extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
$res = [
'@context' => [
'https://w3id.org/security/v1',
'https://www.w3.org/ns/activitystreams',
[
'toot' => 'http://joinmastodon.org/ns#',
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'alsoKnownAs' => [
'@id' => 'as:alsoKnownAs',
'@type' => '@id'
],
'movedTo' => [
'@id' => 'as:movedTo',
'@type' => '@id'
],
'indexable' => 'toot:indexable',
'@context' => [
'https://w3id.org/security/v1',
'https://www.w3.org/ns/activitystreams',
[
'toot' => 'http://joinmastodon.org/ns#',
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'alsoKnownAs' => [
'@id' => 'as:alsoKnownAs',
'@type' => '@id',
],
'movedTo' => [
'@id' => 'as:movedTo',
'@type' => '@id',
],
'indexable' => 'toot:indexable',
'suspended' => 'toot:suspended',
],
],
],
'id' => $profile->permalink(),
'type' => 'Person',
'following' => $profile->permalink('/following'),
'followers' => $profile->permalink('/followers'),
'inbox' => $profile->permalink('/inbox'),
'outbox' => $profile->permalink('/outbox'),
'preferredUsername' => $profile->username,
'name' => $profile->name,
'summary' => $profile->bio,
'url' => $profile->url(),
'manuallyApprovesFollowers' => (bool) $profile->is_private,
'indexable' => (bool) $profile->indexable,
'published' => $profile->created_at->format('Y-m-d') . 'T00:00:00Z',
'publicKey' => [
'id' => $profile->permalink().'#main-key',
'owner' => $profile->permalink(),
'publicKeyPem' => $profile->public_key,
],
'icon' => [
'type' => 'Image',
'mediaType' => 'image/jpeg',
'url' => $profile->avatarUrl(),
],
'endpoints' => [
'sharedInbox' => config('app.url') . '/f/inbox'
]
];
'id' => $profile->permalink(),
'type' => 'Person',
'following' => $profile->permalink('/following'),
'followers' => $profile->permalink('/followers'),
'inbox' => $profile->permalink('/inbox'),
'outbox' => $profile->permalink('/outbox'),
'preferredUsername' => $profile->username,
'name' => $profile->name,
'summary' => $profile->bio,
'url' => $profile->url(),
'manuallyApprovesFollowers' => (bool) $profile->is_private,
'indexable' => (bool) $profile->indexable,
'published' => $profile->created_at->format('Y-m-d').'T00:00:00Z',
'publicKey' => [
'id' => $profile->permalink().'#main-key',
'owner' => $profile->permalink(),
'publicKeyPem' => $profile->public_key,
],
'icon' => [
'type' => 'Image',
'mediaType' => 'image/jpeg',
'url' => $profile->avatarUrl(),
],
'endpoints' => [
'sharedInbox' => config('app.url').'/f/inbox',
],
];
if($profile->aliases->count()) {
$res['alsoKnownAs'] = $profile->aliases->map(fn($alias) => $alias->uri);
}
if ($profile->status === 'delete' || $profile->deleted_at != null) {
$res['suspended'] = true;
$res['name'] = '';
unset($res['icon']);
$res['summary'] = '';
$res['indexable'] = false;
$res['manuallyApprovesFollowers'] = false;
} else {
if ($profile->aliases->count()) {
$res['alsoKnownAs'] = $profile->aliases->map(fn ($alias) => $alias->uri);
}
if($profile->moved_to_profile_id) {
$res['movedTo'] = AccountService::get($profile->moved_to_profile_id)['url'];
}
if ($profile->moved_to_profile_id) {
$movedTo = AccountService::get($profile->moved_to_profile_id);
if ($movedTo && isset($movedTo['url'], $movedTo['id'])) {
$res['movedTo'] = $movedTo['url'];
}
}
}
return $res;
return $res;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Transformer\ActivityPub\Verb;
use App\Profile;
use League\Fractal;
class DeleteActor extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
return [
'@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()
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Transformer\ActivityPub\Verb;
use App\Models\ProfileMigration;
use League\Fractal;
class Move extends Fractal\TransformerAbstract
{
public function transform(ProfileMigration $migration)
{
$objUrl = $migration->target->permalink();
$id = $migration->target->permalink('#moves/'.$migration->id);
$to = $migration->target->permalink('/followers');
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $id,
'actor' => $objUrl,
'type' => 'Move',
'object' => $objUrl,
'target' => $migration->profile->permalink(),
'to' => $to,
];
}
}

View File

@ -2,12 +2,13 @@
namespace App\Transformer\Api;
use Auth;
use Cache;
use App\Profile;
use App\User;
use League\Fractal;
use App\Services\AccountService;
use App\Services\PronounService;
use App\User;
use App\UserSetting;
use Cache;
use League\Fractal;
class AccountTransformer extends Fractal\TransformerAbstract
{
@ -15,47 +16,77 @@ class AccountTransformer extends Fractal\TransformerAbstract
// 'relationship',
];
public function transform(Profile $profile)
{
if(!$profile) {
return [];
}
public function transform(Profile $profile)
{
if (! $profile) {
return [];
}
$adminIds = Cache::remember('pf:admin-ids', 604800, function() {
return User::whereIsAdmin(true)->pluck('profile_id')->toArray();
});
$adminIds = Cache::remember('pf:admin-ids', 604800, function () {
return User::whereIsAdmin(true)->pluck('profile_id')->toArray();
});
$local = $profile->private_key != null;
$is_admin = !$local ? false : in_array($profile->id, $adminIds);
$acct = $local ? $profile->username : substr($profile->username, 1);
$username = $local ? $profile->username : explode('@', $acct)[0];
return [
'id' => (string) $profile->id,
'username' => $username,
'acct' => $acct,
'display_name' => $profile->name,
'discoverable' => true,
'locked' => (bool) $profile->is_private,
'followers_count' => (int) $profile->followers_count,
'following_count' => (int) $profile->following_count,
'statuses_count' => (int) $profile->status_count,
'note' => $profile->bio ?? '',
'note_text' => $profile->bio ? strip_tags($profile->bio) : null,
'url' => $profile->url(),
'avatar' => $profile->avatarUrl(),
'website' => $profile->website,
'local' => (bool) $local,
'is_admin' => (bool) $is_admin,
'created_at' => $profile->created_at->toJSON(),
'header_bg' => $profile->header_bg,
'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
'pronouns' => PronounService::get($profile->id),
'location' => $profile->location
];
}
$local = $profile->private_key != null;
$local = $profile->user_id && $profile->private_key != null;
$hideFollowing = false;
$hideFollowers = false;
if ($local) {
$hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:'.$profile->id, 2592000, function () use ($profile) {
$settings = UserSetting::whereUserId($profile->user_id)->first();
if (! $settings) {
return false;
}
protected function includeRelationship(Profile $profile)
{
return $this->item($profile, new RelationshipTransformer());
}
return $settings->show_profile_following_count == false;
});
$hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:'.$profile->id, 2592000, function () use ($profile) {
$settings = UserSetting::whereUserId($profile->user_id)->first();
if (! $settings) {
return false;
}
return $settings->show_profile_follower_count == false;
});
}
$is_admin = ! $local ? false : in_array($profile->id, $adminIds);
$acct = $local ? $profile->username : substr($profile->username, 1);
$username = $local ? $profile->username : explode('@', $acct)[0];
$res = [
'id' => (string) $profile->id,
'username' => $username,
'acct' => $acct,
'display_name' => $profile->name,
'discoverable' => true,
'locked' => (bool) $profile->is_private,
'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count,
'following_count' => $hideFollowing ? 0 : (int) $profile->following_count,
'statuses_count' => (int) $profile->status_count,
'note' => $profile->bio ?? '',
'note_text' => $profile->bio ? strip_tags($profile->bio) : null,
'url' => $profile->url(),
'avatar' => $profile->avatarUrl(),
'website' => $profile->website,
'local' => (bool) $local,
'is_admin' => (bool) $is_admin,
'created_at' => $profile->created_at->toJSON(),
'header_bg' => $profile->header_bg,
'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
'pronouns' => PronounService::get($profile->id),
'location' => $profile->location,
];
if ($profile->moved_to_profile_id) {
$mt = AccountService::getMastodon($profile->moved_to_profile_id, true);
if ($mt) {
$res['moved'] = $mt;
}
}
return $res;
}
protected function includeRelationship(Profile $profile)
{
return $this->item($profile, new RelationshipTransformer());
}
}

View File

@ -4,78 +4,81 @@ namespace App\Transformer\Api;
use App\Notification;
use App\Services\AccountService;
use App\Services\HashidService;
use App\Services\RelationshipService;
use App\Services\StatusService;
use League\Fractal;
class NotificationTransformer extends Fractal\TransformerAbstract
{
public function transform(Notification $notification)
{
$res = [
'id' => (string) $notification->id,
'type' => $this->replaceTypeVerb($notification->action),
'created_at' => (string) str_replace('+00:00', 'Z', $notification->created_at->format(DATE_RFC3339_EXTENDED)),
];
public function transform(Notification $notification)
{
$res = [
'id' => (string) $notification->id,
'type' => $this->replaceTypeVerb($notification->action),
'created_at' => (string) str_replace('+00:00', 'Z', $notification->created_at->format(DATE_RFC3339_EXTENDED)),
];
$n = $notification;
$n = $notification;
if($n->actor_id) {
$res['account'] = AccountService::get($n->actor_id);
if($n->profile_id != $n->actor_id) {
$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
}
}
if ($n->actor_id) {
$res['account'] = AccountService::get($n->actor_id);
if ($n->profile_id != $n->actor_id) {
$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
}
}
if($n->item_id && $n->item_type == 'App\Status') {
$res['status'] = StatusService::get($n->item_id, false);
}
if ($n->item_id && $n->item_type == 'App\Status') {
$res['status'] = StatusService::get($n->item_id, false);
}
if($n->item_id && $n->item_type == 'App\ModLog') {
$ml = $n->item;
if($ml && $ml->object_uid) {
$res['modlog'] = [
'id' => $ml->object_uid,
'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
];
}
}
if ($n->item_id && $n->item_type == 'App\ModLog') {
$ml = $n->item;
if ($ml && $ml->object_uid) {
$res['modlog'] = [
'id' => $ml->object_uid,
'url' => url('/i/admin/users/modlogs/'.$ml->object_uid),
];
}
}
if($n->item_id && $n->item_type == 'App\MediaTag') {
$ml = $n->item;
if($ml && $ml->tagged_username) {
$res['tagged'] = [
'username' => $ml->tagged_username,
'post_url' => '/p/'.HashidService::encode($ml->status_id)
];
}
}
if ($n->item_id && $n->item_type == 'App\MediaTag') {
$ml = $n->item;
if ($ml && $ml->tagged_username) {
$np = StatusService::get($ml->status_id, false);
if ($np && isset($np['id'])) {
$res['tagged'] = [
'username' => $ml->tagged_username,
'post_url' => $np['url'],
'status_id' => $ml->status_id,
'profile_id' => $ml->profile_id,
];
}
}
}
return $res;
}
return $res;
}
public function replaceTypeVerb($verb)
{
$verbs = [
'dm' => 'direct',
'follow' => 'follow',
'mention' => 'mention',
'reblog' => 'share',
'share' => 'share',
'like' => 'favourite',
'group:like' => 'favourite',
'comment' => 'comment',
'admin.user.modlog.comment' => 'modlog',
'tagged' => 'tagged',
'story:react' => 'story:react',
'story:comment' => 'story:comment',
];
public function replaceTypeVerb($verb)
{
$verbs = [
'dm' => 'direct',
'follow' => 'follow',
'mention' => 'mention',
'reblog' => 'share',
'share' => 'share',
'like' => 'favourite',
'comment' => 'comment',
'admin.user.modlog.comment' => 'modlog',
'tagged' => 'tagged',
'story:react' => 'story:react',
'story:comment' => 'story:comment',
];
if(!isset($verbs[$verb])) {
return $verb;
}
if (! isset($verbs[$verb])) {
return $verb;
}
return $verbs[$verb];
}
return $verbs[$verb];
}
}

View File

@ -423,7 +423,7 @@ class Inbox
$status->uri = $activity['id'];
$status->object_url = $activity['id'];
$status->in_reply_to_profile_id = $profile->id;
$status->saveQuietly();
$status->save();
$dm = new DirectMessage;
$dm->to_id = $profile->id;
@ -1243,7 +1243,14 @@ class Inbox
return;
}
$content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null;
$content = null;
if(isset($this->payload['content'])) {
if(strlen($this->payload['content']) > 5000) {
$content = Purify::clean(substr($this->payload['content'], 0, 5000) . ' ... (truncated message due to exceeding max length)');
} else {
$content = Purify::clean($this->payload['content']);
}
}
$object = $this->payload['object'];
if(empty($object) || (!is_array($object) && !is_string($object))) {
@ -1259,7 +1266,7 @@ class Inbox
foreach($object as $objectUrl) {
if(!Helpers::validateLocalUrl($objectUrl)) {
continue;
return;
}
if(str_contains($objectUrl, '/users/')) {
@ -1276,10 +1283,27 @@ class Inbox
}
}
if(!$accountId || !$objects->count()) {
if(!$accountId && !$objects->count()) {
return;
}
if($objects->count()) {
$obc = $objects->count();
if($obc > 25) {
if($obc > 30) {
return;
} else {
$objLimit = $objects->take(20);
$objects = collect($objLimit->all());
$obc = $objects->count();
}
}
$count = Status::whereProfileId($accountId)->find($objects)->count();
if($obc !== $count) {
return;
}
}
$instanceHost = parse_url($id, PHP_URL_HOST);
$instance = Instance::updateOrCreate([

View File

@ -5,32 +5,34 @@ namespace App\Util\Site;
use Cache;
use Illuminate\Support\Str;
class Config {
class Config
{
const CACHE_KEY = 'api:site:configuration:_v0.8';
public static function get() {
return Cache::remember(self::CACHE_KEY, 900, function() {
public static function get()
{
return Cache::remember(self::CACHE_KEY, 900, function () {
$hls = [
'enabled' => config('media.hls.enabled'),
];
if(config('media.hls.enabled')) {
if (config('media.hls.enabled')) {
$hls = [
'enabled' => true,
'debug' => (bool) config('media.hls.debug'),
'p2p' => (bool) config('media.hls.p2p'),
'p2p_debug' => (bool) config('media.hls.p2p_debug'),
'tracker' => config('media.hls.tracker'),
'ice' => config('media.hls.ice')
'ice' => config('media.hls.ice'),
];
}
return [
'version' => config('pixelfed.version'),
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'uploader' => [
'max_photo_size' => (int) config('pixelfed.max_photo_size'),
'max_caption_length' => (int) config('pixelfed.max_caption_length'),
'max_altext_length' => (int) config('pixelfed.max_altext_length', 150),
'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'),
'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150),
'album_limit' => (int) config_cache('pixelfed.max_album_length'),
'image_quality' => (int) config_cache('pixelfed.image_quality'),
@ -41,12 +43,12 @@ class Config {
'media_types' => config_cache('pixelfed.media_types'),
'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [],
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit')
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
],
'activitypub' => [
'enabled' => (bool) config_cache('federation.activitypub.enabled'),
'remote_follow' => config('federation.activitypub.remoteFollow')
'remote_follow' => config('federation.activitypub.remoteFollow'),
],
'ab' => config('exp'),
@ -54,8 +56,8 @@ class Config {
'site' => [
'name' => config_cache('app.name'),
'domain' => config('pixelfed.domain.app'),
'url' => config('app.url'),
'description' => config_cache('app.short_description')
'url' => config('app.url'),
'description' => config_cache('app.short_description'),
],
'account' => [
@ -63,15 +65,15 @@ class Config {
'max_bio_length' => config('pixelfed.max_bio_length'),
'max_name_length' => config('pixelfed.max_name_length'),
'min_password_length' => config('pixelfed.min_password_length'),
'max_account_size' => config('pixelfed.max_account_size')
'max_account_size' => config('pixelfed.max_account_size'),
],
'username' => [
'remote' => [
'formats' => config('instance.username.remote.formats'),
'format' => config('instance.username.remote.format'),
'custom' => config('instance.username.remote.custom')
]
'custom' => config('instance.username.remote.custom'),
],
],
'features' => [
@ -85,22 +87,29 @@ class Config {
'import' => [
'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'),
'mastodon' => false,
'pixelfed' => false
'pixelfed' => false,
],
'label' => [
'covid' => [
'enabled' => (bool) config('instance.label.covid.enabled'),
'org' => config('instance.label.covid.org'),
'url' => config('instance.label.covid.url'),
]
],
],
'hls' => $hls
]
'hls' => $hls,
],
];
});
}
public static function json() {
public static function refresh()
{
Cache::forget(self::CACHE_KEY);
return self::get();
}
public static function json()
{
return json_encode(self::get(), JSON_FORCE_OBJECT);
}
}

View File

@ -3,16 +3,28 @@
namespace App\Util\Webfinger;
use App\Util\Lexer\Nickname;
use App\Services\InstanceService;
class WebfingerUrl
{
public static function get($url)
{
$n = Nickname::normalizeProfileUrl($url);
if(!$n || !isset($n['domain'], $n['username'])) {
return false;
}
if(in_array($n['domain'], InstanceService::getBannedDomains())) {
return false;
}
return 'https://' . $n['domain'] . '/.well-known/webfinger?resource=acct:' . $n['username'] . '@' . $n['domain'];
}
public static function generateWebfingerUrl($url)
{
$url = Nickname::normalizeProfileUrl($url);
$domain = $url['domain'];
$username = $url['username'];
$path = "https://{$domain}/.well-known/webfinger?resource=acct:{$username}@{$domain}";
return $path;
}
}

View File

@ -16,7 +16,7 @@
"bacon/bacon-qr-code": "^2.0.3",
"beyondcode/laravel-websockets": "^1.13",
"brick/math": "^0.9.3",
"buzz/laravel-h-captcha": "1.0.4",
"buzz/laravel-h-captcha": "^1.0.4",
"doctrine/dbal": "^3.0",
"intervention/image": "^2.4",
"jenssegers/agent": "^2.6",
@ -46,6 +46,7 @@
"require-dev": {
"brianium/paratest": "^6.1",
"fakerphp/faker": "^1.20",
"laravel/pint": "^1.14",
"laravel/telescope": "^4.14",
"mockery/mockery": "^1.0",
"nunomaduro/collision": "^6.1",

474
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -57,4 +57,6 @@ return [
// max size in bytes, default is 2mb
'max_size' => env('CUSTOM_EMOJI_MAX_SIZE', 2000000),
],
'migration' => env('PF_ACCT_MIGRATION_ENABLED', true),
];

View File

@ -72,7 +72,7 @@ return [
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'visibility' => 'public',
'visibility' => env('AWS_VISIBILITY', 'public'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),

View File

@ -23,7 +23,7 @@ return [
| This value is the version of your Pixelfed instance.
|
*/
'version' => '0.11.12',
'version' => '0.11.13',
/*
|--------------------------------------------------------------------------

View File

@ -1,35 +0,0 @@
upstream fe {
server 127.0.0.1:8080;
}
server {
server_name real.domain;
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/real.domain/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/real.domain/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
proxy_redirect off;
proxy_pass http://fe/;
}
}
server {
if ($host = real.domain) {
return 301 https://$host$request_uri;
}
listen 80;
listen [::]:80;
server_name real.domain;
return 404;
}

View File

@ -1,100 +0,0 @@
FROM php:8.1-apache-bullseye
ENV COMPOSER_MEMORY_LIMIT=-1
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /var/www/
# Get Composer binary
COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer
# Install package dependencies
RUN apt-get update \
&& apt-get upgrade -y \
# && apt-get install -y --no-install-recommends apt-utils \
&& apt-get install -y --no-install-recommends \
## Standard
locales \
locales-all \
git \
gosu \
zip \
unzip \
libzip-dev \
libcurl4-openssl-dev \
## Image Optimization
optipng \
pngquant \
jpegoptim \
gifsicle \
## Image Processing
libjpeg62-turbo-dev \
libpng-dev \
libmagickwand-dev \
# Required for GD
libxpm4 \
libxpm-dev \
libwebp7 \
libwebp-dev \
## Video Processing
ffmpeg \
## Database
# libpq-dev \
# libsqlite3-dev \
mariadb-client \
# Locales Update
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
&& locale-gen \
&& update-locale \
# Install PHP extensions
&& docker-php-source extract \
#PHP Imagemagick extensions
&& pecl install imagick \
&& docker-php-ext-enable imagick \
# PHP GD extensions
&& docker-php-ext-configure gd \
--with-freetype \
--with-jpeg \
--with-webp \
--with-xpm \
&& docker-php-ext-install -j$(nproc) gd \
#PHP Redis extensions
&& pecl install redis \
&& docker-php-ext-enable redis \
#PHP Database extensions
&& docker-php-ext-install pdo_mysql \
#pdo_pgsql pdo_sqlite \
#PHP extensions (dependencies)
&& docker-php-ext-configure intl \
&& docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \
#APACHE Bootstrap
&& a2enmod rewrite remoteip \
&& {\
echo RemoteIPHeader X-Real-IP ;\
echo RemoteIPTrustedProxy 10.0.0.0/8 ;\
echo RemoteIPTrustedProxy 172.16.0.0/12 ;\
echo RemoteIPTrustedProxy 192.168.0.0/16 ;\
echo SetEnvIf X-Forwarded-Proto "https" HTTPS=on ;\
} > /etc/apache2/conf-available/remoteip.conf \
&& a2enconf remoteip \
#Cleanup
&& docker-php-source delete \
&& apt-get autoremove --purge -y \
&& apt-get clean \
&& rm -rf /var/cache/apt \
&& rm -rf /var/lib/apt/lists/
# Use the default production configuration
COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini"
COPY . /var/www/
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
RUN cp -r storage storage.skel \
&& composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \
&& rm -rf html && ln -s public html \
&& chown -R www-data:www-data /var/www
RUN php artisan horizon:publish
VOLUME /var/www/storage /var/www/bootstrap
CMD ["/var/www/contrib/docker/start.apache.sh"]

View File

@ -1,90 +0,0 @@
FROM php:8.1-fpm-bullseye
ENV COMPOSER_MEMORY_LIMIT=-1
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /var/www/
# Get Composer binary
COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer
# Install package dependencies
RUN apt-get update \
&& apt-get upgrade -y \
# && apt-get install -y --no-install-recommends apt-utils \
&& apt-get install -y --no-install-recommends \
## Standard
locales \
locales-all \
git \
gosu \
zip \
unzip \
libzip-dev \
libcurl4-openssl-dev \
## Image Optimization
optipng \
pngquant \
jpegoptim \
gifsicle \
## Image Processing
libjpeg62-turbo-dev \
libpng-dev \
libmagickwand-dev \
# Required for GD
libxpm4 \
libxpm-dev \
libwebp7 \
libwebp-dev \
## Video Processing
ffmpeg \
## Database
# libpq-dev \
# libsqlite3-dev \
mariadb-client \
# Locales Update
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
&& locale-gen \
&& update-locale \
# Install PHP extensions
&& docker-php-source extract \
#PHP Imagemagick extensions
&& pecl install imagick \
&& docker-php-ext-enable imagick \
# PHP GD extensions
&& docker-php-ext-configure gd \
--with-freetype \
--with-jpeg \
--with-webp \
--with-xpm \
&& docker-php-ext-install -j$(nproc) gd \
#PHP Redis extensions
&& pecl install redis \
&& docker-php-ext-enable redis \
#PHP Database extensions
&& docker-php-ext-install pdo_mysql \
#pdo_pgsql pdo_sqlite \
#PHP extensions (dependencies)
&& docker-php-ext-configure intl \
&& docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \
#Cleanup
&& docker-php-source delete \
&& apt-get autoremove --purge -y \
&& apt-get clean \
&& rm -rf /var/cache/apt \
&& rm -rf /var/lib/apt/lists/
# Use the default production configuration
COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini"
COPY . /var/www/
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
RUN cp -r storage storage.skel \
&& composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \
&& rm -rf html && ln -s public html \
&& chown -R www-data:www-data /var/www
RUN php artisan horizon:publish
VOLUME /var/www/storage /var/www/bootstrap
CMD ["/var/www/contrib/docker/start.fpm.sh"]

View File

@ -1,15 +0,0 @@
#!/bin/bash
# Create the storage tree if needed and fix permissions
cp -r storage.skel/* storage/
chown -R www-data:www-data storage/ bootstrap/
# Refresh the environment
php artisan config:cache
php artisan storage:link
php artisan horizon:publish
php artisan route:cache
php artisan view:cache
# Finally run Apache
apache2-foreground

View File

@ -1,15 +0,0 @@
#!/bin/bash
# Create the storage tree if needed and fix permissions
cp -r storage.skel/* storage/
chown -R www-data:www-data storage/ bootstrap/
# Refresh the environment
php artisan config:cache
php artisan storage:link
php artisan horizon:publish
php artisan route:cache
php artisan view:cache
# Finally run FPM
php-fpm

View File

@ -1,67 +0,0 @@
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name pixelfed.example; # change this to your fqdn
root /home/pixelfed/public; # path to repo/public
ssl_certificate /etc/nginx/ssl/server.crt; # generate your own
ssl_certificate_key /etc/nginx/ssl/server.key; # or use letsencrypt
ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES;
ssl_prefer_server_ciphers on;
#add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
client_max_body_size 15M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param HTTP_PROXY "";
}
location ~ /\.(?!well-known).* {
deny all;
}
}
server { # Redirect http to https
server_name pixelfed.example; # change this to your fqdn
listen 80;
listen [::]:80;
return 301 https://$host$request_uri;
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('curated_registers', function (Blueprint $table) {
$table->boolean('user_has_responded')->default(false)->index()->after('is_awaiting_more_info');
});
CuratedRegisterActivity::whereFromUser(true)->get()->each(function($cra) {
$cr = CuratedRegister::find($cra->register_id);
$cr->user_has_responded = true;
$cr->save();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('curated_registers', function (Blueprint $table) {
$table->dropColumn('user_has_responded');
});
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('curated_register_templates', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->text('description')->nullable();
$table->text('content')->nullable();
$table->boolean('is_active')->default(false)->index();
$table->tinyInteger('order')->default(10)->unsigned()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('curated_register_templates');
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('profile_migrations', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('profile_id');
$table->string('acct')->nullable();
$table->unsignedBigInteger('followers_count')->default(0);
$table->unsignedBigInteger('target_profile_id')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('profile_migrations');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('instances', function (Blueprint $table) {
$table->string('shared_inbox')->nullable()->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('instances', function (Blueprint $table) {
$table->dropColumn('shared_inbox');
});
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Instance;
use App\Profile;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
foreach(Instance::lazyById(50, 'id') as $instance) {
$si = Profile::whereDomain($instance->domain)->whereNotNull('sharedInbox')->first();
if($si && $si->sharedInbox) {
$instance->shared_inbox = $si->sharedInbox;
$instance->save();
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};

View File

@ -0,0 +1,42 @@
---
version: "3"
services:
migrate:
image: "secoresearch/rsync"
entrypoint: ""
working_dir: /migrate
command: 'bash -c "exit 1"'
restart: never
volumes:
################################
# Storage volume
################################
# OLD
- "app-storage:/migrate/app-storage/old"
# NEW
- "${DOCKER_APP_HOST_STORAGE_PATH}:/migrate/app-storage/new"
################################
# MySQL/DB volume
################################
# OLD
- "db-data:/migrate/db-data/old"
# NEW
- "${DOCKER_DB_HOST_DATA_PATH}:/migrate/db-data/new"
################################
# Redis volume
################################
# OLD
- "redis-data:/migrate/redis-data/old"
# NEW
- "${DOCKER_REDIS_HOST_DATA_PATH}:/migrate/redis-data/new"
# Volumes from the old [docker-compose.yml] file
# https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76
volumes:
db-data:
redis-data:
app-storage:
app-bootstrap:

View File

@ -1,82 +1,218 @@
---
version: '3'
# Require 3.8 to ensure people use a recent version of Docker + Compose
version: "3.8"
# In order to set configuration, please use a .env file in
# your compose project directory (the same directory as your
# docker-compose.yml), and set database options, application
# name, key, and other settings there.
# A list of available settings is available in .env.example
#
# The services should scale properly across a swarm cluster
# if the volumes are properly shared between cluster members.
###############################################################
# Please see docker/README.md for usage information
###############################################################
services:
## App and Worker
app:
# Comment to use dockerhub image
image: pixelfed/pixelfed:latest
# HTTP/HTTPS proxy
#
# Sits in front of the *real* webserver and manages SSL and (optionally)
# load-balancing between multiple web servers
#
# You can disable this service by setting [DOCKER_PROXY_PROFILE="disabled"]
# in your [.env] file - the setting is near the bottom of the file.
#
# This also disables the [proxy-acme] service, if this is not desired, change the
# [DOCKER_PROXY_ACME_PROFILE] setting to an empty string [""]
#
# See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs
proxy:
image: nginxproxy/nginx-proxy:1.4
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
restart: unless-stopped
env_file:
- .env.docker
profiles:
- ${DOCKER_PROXY_PROFILE:-}
environment:
DOCKER_SERVICE_NAME: "proxy"
volumes:
- app-storage:/var/www/storage
- app-bootstrap:/var/www/bootstrap
- "./.env.docker:/var/www/.env"
networks:
- external
- internal
- "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/tmp/docker.sock:ro"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs"
- "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html"
ports:
- "8080:80"
- "${DOCKER_PROXY_HOST_PORT_HTTP}:80"
- "${DOCKER_PROXY_HOST_PORT_HTTPS}:443"
healthcheck:
test: "curl --fail https://${APP_DOMAIN}/api/service/health-check"
interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL}"
retries: 2
timeout: 5s
# Proxy companion for managing letsencrypt SSL certificates
#
# You can disable this service by setting [DOCKER_PROXY_ACME_PROFILE="disabled"]
# in your [.env] file - the setting is near the bottom of the file.
#
# See: https://github.com/nginx-proxy/acme-companion/tree/main/docs
proxy-acme:
image: nginxproxy/acme-companion
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy-acme"
restart: unless-stopped
profiles:
- ${DOCKER_PROXY_ACME_PROFILE:-}
environment:
DEBUG: 0
DEFAULT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}"
NGINX_PROXY_CONTAINER: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
depends_on:
- proxy
volumes:
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy-acme:/etc/acme.sh"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d"
- "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html"
- "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock:ro"
web:
image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}"
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-web"
restart: unless-stopped
profiles:
- ${DOCKER_WEB_PROFILE:-}
build:
target: ${DOCKER_APP_RUNTIME}-runtime
cache_from:
- "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}"
args:
APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}"
PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}"
PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}"
PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}"
PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}"
PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}"
environment:
# Used by Pixelfed Docker init script
DOCKER_SERVICE_NAME: "web"
DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0}
ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-}
# Used by [proxy] service
LETSENCRYPT_HOST: "${DOCKER_PROXY_LETSENCRYPT_HOST:?error}"
LETSENCRYPT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}"
LETSENCRYPT_TEST: "${DOCKER_PROXY_LETSENCRYPT_TEST:-}"
VIRTUAL_HOST: "${APP_DOMAIN}"
VIRTUAL_PORT: "80"
volumes:
- "./.env:/var/www/.env"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d"
- "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache"
- "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro"
- "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage"
labels:
com.github.nginx-proxy.nginx-proxy.keepalive: 30
com.github.nginx-proxy.nginx-proxy.http2.enable: true
com.github.nginx-proxy.nginx-proxy.http3.enable: true
ports:
- "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80"
depends_on:
- db
- redis
healthcheck:
test: 'curl --header "Host: ${APP_DOMAIN}" --fail http://localhost/api/service/health-check'
interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL}"
retries: 2
timeout: 5s
worker:
image: pixelfed/pixelfed:latest
restart: unless-stopped
env_file:
- .env.docker
volumes:
- app-storage:/var/www/storage
- app-bootstrap:/var/www/bootstrap
networks:
- external
- internal
image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}"
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker"
command: gosu www-data php artisan horizon
restart: unless-stopped
stop_signal: SIGTERM
profiles:
- ${DOCKER_WORKER_PROFILE:-}
build:
target: ${DOCKER_APP_RUNTIME}-runtime
cache_from:
- "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}"
args:
APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}"
PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}"
PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}"
PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}"
PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}"
PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}"
environment:
# Used by Pixelfed Docker init script
DOCKER_SERVICE_NAME: "worker"
DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0}
ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-}
volumes:
- "./.env:/var/www/.env"
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d"
- "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache"
- "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro"
- "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage"
depends_on:
- db
- redis
healthcheck:
test: gosu www-data php artisan horizon:status | grep running
interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL:?error}"
timeout: 5s
retries: 2
## DB and Cache
db:
image: mariadb:jammy
image: ${DOCKER_DB_IMAGE:?error}
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-db"
command: ${DOCKER_DB_COMMAND:-}
restart: unless-stopped
networks:
- internal
command: --default-authentication-plugin=mysql_native_password
env_file:
- .env.docker
profiles:
- ${DOCKER_DB_PROFILE:-}
environment:
TZ: "${TZ:?error}"
# MySQL (Oracle) - "Environment Variables" at https://hub.docker.com/_/mysql
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD:?error}"
MYSQL_USER: "${DB_USERNAME:?error}"
MYSQL_PASSWORD: "${DB_PASSWORD:?error}"
MYSQL_DATABASE: "${DB_DATABASE:?error}"
# MySQL (MariaDB) - "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb
MARIADB_ROOT_PASSWORD: "${DB_PASSWORD:?error}"
MARIADB_USER: "${DB_USERNAME:?error}"
MARIADB_PASSWORD: "${DB_PASSWORD:?error}"
MARIADB_DATABASE: "${DB_DATABASE:?error}"
# PostgreSQL - "Environment Variables" at https://hub.docker.com/_/postgres
POSTGRES_USER: "${DB_USERNAME:?error}"
POSTGRES_PASSWORD: "${DB_PASSWORD:?error}"
POSTGRES_DB: "${DB_DATABASE:?error}"
volumes:
- "db-data:/var/lib/mysql"
- "${DOCKER_DB_HOST_DATA_PATH:?error}:${DOCKER_DB_CONTAINER_DATA_PATH:?error}"
ports:
- "${DOCKER_DB_HOST_PORT:?error}:${DOCKER_DB_CONTAINER_PORT:?error}"
healthcheck:
test:
[
"CMD",
"healthcheck.sh",
"--su-mysql",
"--connect",
"--innodb_initialized",
]
interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL:?error}"
retries: 2
timeout: 5s
redis:
image: redis:5-alpine
image: redis:${DOCKER_REDIS_VERSION}
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-redis"
restart: unless-stopped
env_file:
- .env.docker
command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'"
profiles:
- ${DOCKER_REDIS_PROFILE:-}
environment:
TZ: "${TZ:?error}"
REDISCLI_AUTH: ${REDIS_PASSWORD:-}
volumes:
- "redis-data:/data"
networks:
- internal
volumes:
db-data:
redis-data:
app-storage:
app-bootstrap:
networks:
internal:
internal: true
external:
driver: bridge
- "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis:/etc/redis"
- "${DOCKER_REDIS_HOST_DATA_PATH}:/data"
ports:
- "${DOCKER_REDIS_HOST_PORT}:6379"
healthcheck:
test: ["CMD", "redis-cli", "-p", "6379", "ping"]
interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL:?error}"
retries: 2
timeout: 5s

5
docker/README.md Normal file
View File

@ -0,0 +1,5 @@
# Pixelfed + Docker + Docker Compose
Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage.
The docs can be [reviewed in the pixelfed/docs-next](https://github.com/pixelfed/docs-next/pull/1) repository.

View File

@ -0,0 +1,8 @@
RemoteIPHeader X-Real-IP
# All private IPs as outlined in rfc1918
#
# See: https://datatracker.ietf.org/doc/html/rfc1918
RemoteIPTrustedProxy 10.0.0.0/8
RemoteIPTrustedProxy 172.16.0.0/12
RemoteIPTrustedProxy 192.168.0.0/16

11
docker/artisan Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
declare service="${PF_SERVICE:=worker}"
declare user="${PF_USER:=www-data}"
exec docker compose exec \
--user "${user}" \
--env TERM \
--env COLORTERM \
"${service}" \
php artisan "${@}"

45
docker/dottie Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
set -e -o errexit -o nounset -o pipefail
declare project_root="${PWD}"
declare user="${PF_USER:=www-data}"
if command -v git &>/dev/null; then
project_root=$(git rev-parse --show-toplevel)
fi
declare -r release="${DOTTIE_VERSION:-latest}"
declare -r update_check_file="/tmp/.dottie-update-check" # file to check age of since last update
declare -i update_check_max_age=$((8 * 60 * 60)) # 8 hours between checking for dottie version
declare -i update_check_cur_age=$((update_check_max_age + 1)) # by default the "update" event should happen
# default [docker run] flags
declare -a flags=(
--rm
--interactive
--tty
--user "${user}"
--env TERM
--env COLORTERM
--volume "${project_root}:/var/www"
--workdir /var/www
)
# if update file exists, find its age since last modification
if [[ -f "${update_check_file}" ]]; then
now=$(date +%s)
changed=$(date -r "${update_check_file}" +%s)
update_check_cur_age=$((now - changed))
fi
# if update file is older than max allowed poll for new version of dottie
if [[ $update_check_cur_age -gt $update_check_max_age ]]; then
flags+=(--pull always)
touch "${update_check_file}"
fi
# run dottie
exec docker run "${flags[@]}" "ghcr.io/jippi/dottie:${release}" "$@"

0
docker/fpm/root/.gitkeep Normal file
View File

2
docker/nginx/Procfile Normal file
View File

@ -0,0 +1,2 @@
fpm: php-fpm
nginx: nginx -g "daemon off;"

View File

@ -0,0 +1,49 @@
server {
listen 80 default_server;
server_name {{ getenv "APP_DOMAIN" }};
root /var/www/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
access_log /dev/stdout;
error_log /dev/stderr warn;
index index.html index.htm index.php;
charset utf-8;
client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }};
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.(?!well-known).* {
deny all;
}
}

View File

@ -0,0 +1,41 @@
# This is changed from the original "nginx" in upstream to work properly
# with permissions within pixelfed when serving static files.
user www-data;
worker_processes auto;
# Ensure the PID is writable
# Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged
pid /tmp/nginx.pid;
# Write error log to stderr (/proc/self/fd/2 -> /dev/stderr)
error_log /proc/self/fd/2 notice;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';
# Write error log to stdout (/proc/self/fd/1 -> /dev/stdout)
access_log /proc/self/fd/1 main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
gzip on;
# Ensure all temp paths are in a writable by "www-data" user.
# Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/conf.d/*.conf;
}

View File

@ -0,0 +1,31 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
# Ensure the Docker volumes and required files are owned by the runtime user as other scripts
# will be writing to these
run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env"
run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache"
run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage"
run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "./storage/docker"
# Optionally fix ownership of configured paths
: "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}"
declare -a ensure_ownership_paths=()
IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}"
if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then
log-info "No paths has been configured for ownership fixes via [\$DOCKER_APP_ENSURE_OWNERSHIP_PATHS]."
exit 0
fi
for path in "${ensure_ownership_paths[@]}"; do
log-info "Ensure ownership of [${path}] is correct"
stream-prefix-command-output run-as-current-user chown --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "${path}"
done

View File

@ -0,0 +1,21 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
# Validating dot-env files for any issues
for file in "${dot_env_files[@]}"; do
if ! file-exists "${file}"; then
log-warning "Could not source file [${file}]: does not exists"
continue
fi
# We ignore 'dir' + 'file' rules since they are validate *host* paths
# which do not (and should not) exists inside the container
#
# We disable fixer since its not interactive anyway
run-as-current-user dottie validate --file "${file}" --ignore-rule dir,file --exclude-prefix APP_KEY --no-fix
done

View File

@ -0,0 +1,33 @@
#!/bin/bash
# NOTE:
#
# This file is *sourced* not run by the entrypoint runner
# so any environment values set here will be accessible to all sub-processes
# and future entrypoint.d scripts
#
# We also don't need to source `helpers.sh` since it's already available
entrypoint-set-script-name "${BASH_SOURCE[0]}"
load-config-files
: "${MAX_PHOTO_SIZE:=15000}"
: "${MAX_ALBUM_LENGTH:=4}"
# We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead
: "${POST_MAX_SIZE_BUFFER:=1M}"
log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]"
buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}")
log-info "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]"
# Automatically calculate the [post_max_size] value for [php.ini] and [nginx]
log-info "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]"
log-info " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}"
log-info " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}"
log-info " POST_MAX_SIZE_BUFFER=${buffer}"
: "${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((MAX_PHOTO_SIZE * MAX_ALBUM_LENGTH) + buffer)))}"
log-info "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]"
# NOTE: must export the value so it's available in other scripts!
export POST_MAX_SIZE

View File

@ -0,0 +1,60 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
# Show [git diff] of templates being rendered (will help verify output)
: "${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1}"
# Directory where templates can be found
: "${ENTRYPOINT_TEMPLATE_DIR:=/docker/templates/}"
# Root path to write template template_files to (default is '', meaning it will be written to /<path>)
: "${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX:=}"
declare template_file relative_template_file_path output_file_dir
# load all dot-env config files
load-config-files
# export all dot-env variables so they are available in templating
#
# shellcheck disable=SC2068
export ${seen_dot_env_variables[@]}
find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r template_file; do
# Example: template_file=/docker/templates/usr/local/etc/php/php.ini
# The file path without the template dir prefix ($ENTRYPOINT_TEMPLATE_DIR)
#
# Example: /usr/local/etc/php/php.ini
relative_template_file_path="${template_file#"${ENTRYPOINT_TEMPLATE_DIR}"}"
# Adds optional prefix to the output file path
#
# Example: /usr/local/etc/php/php.ini
output_file_path="${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX}/${relative_template_file_path}"
# Remove the file from the path
#
# Example: /usr/local/etc/php
output_file_dir=$(dirname "${output_file_path}")
# Ensure the output directory is writable
if ! is-writable "${output_file_dir}"; then
log-error-and-exit "${output_file_dir} is not writable"
fi
# Create the output directory if it doesn't exists
ensure-directory-exists "${output_file_dir}"
# Render the template
log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]"
gomplate <"${template_file}" >"${output_file_path}"
# Show the diff from the envsubst command
if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then
git --no-pager diff --color=always "${template_file}" "${output_file_path}" || : # ignore diff exit code
fi
done

View File

@ -0,0 +1,13 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
# Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions
run-as-runtime-user cp --force --recursive storage.skel/. ./storage/
# Ensure storage linkk are correctly configured
run-as-runtime-user php artisan storage:link

View File

@ -0,0 +1,38 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
load-config-files
# Allow automatic applying of outstanding/new migrations on startup
: "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS:=1}"
if is-false "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS}"; then
log-warning "Automatic run of the 'One-time setup tasks' is disabled."
log-warning "Please set [DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS=1] in your [.env] file to enable this."
exit 0
fi
await-database-ready
# Following https://docs.pixelfed.org/running-pixelfed/installation/#one-time-setup-tasks
#
# NOTE: Caches happens in [30-cache.sh]
only-once "key:generate" run-as-runtime-user php artisan key:generate
only-once "storage:link" run-as-runtime-user php artisan storage:link
only-once "initial:migrate" run-as-runtime-user php artisan migrate --force
only-once "import:cities" run-as-runtime-user php artisan import:cities
if is-true "${ACTIVITY_PUB:-false}"; then
only-once "instance:actor" run-as-runtime-user php artisan instance:actor
fi
if is-true "${OAUTH_ENABLED:-false}"; then
only-once "passport:keys" run-as-runtime-user php artisan passport:keys
fi

View File

@ -0,0 +1,42 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
# Allow automatic applying of outstanding/new migrations on startup
: "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0}"
# Wait for the database to be ready
await-database-ready
# Run the migrate:status command and capture output
output=$(run-as-runtime-user php artisan migrate:status || :)
# By default we have no new migrations
declare -i new_migrations=0
# Detect if any new migrations are available by checking for "No" in the output
echo "$output" | grep No && new_migrations=1
if is-false "${new_migrations}"; then
log-info "No new migrations detected"
exit 0
fi
log-warning "New migrations available"
# Print the output
echo "$output"
if is-false "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then
log-info "Automatic applying of new database migrations is disabled"
log-info "Please set [DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this."
exit 0
fi
run-as-runtime-user php artisan migrate --force

View File

@ -0,0 +1,9 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
run-as-runtime-user php artisan horizon:publish

View File

@ -0,0 +1,11 @@
#!/bin/bash
: "${ENTRYPOINT_ROOT:="/docker"}"
# shellcheck source=SCRIPTDIR/../helpers.sh
source "${ENTRYPOINT_ROOT}/helpers.sh"
entrypoint-set-script-name "$0"
run-as-runtime-user php artisan config:cache
run-as-runtime-user php artisan route:cache
run-as-runtime-user php artisan view:cache

Some files were not shown because too many files have changed in this diff Show More