Porównaj commity

...

1149 Commity
v0.11.8 ... dev

Autor SHA1 Wiadomość Data
daniel ab383ba55b
Merge pull request #5080 from pixelfed/staging
Staging
2024-05-07 04:23:54 -06:00
Daniel Supernault 29e472d6ca
Update changelog 2024-05-07 04:23:19 -06:00
Daniel Supernault 9a5e3471d4
Update AdminCuratedRegisterController, increase message length from 1000 to 3000 2024-05-07 04:22:28 -06:00
Daniel Supernault 61d105fd25
Update DirectMessageController, add 72 hour delay for new accounts before they can send a DM 2024-05-07 04:21:20 -06:00
daniel 101e620bf5
Merge pull request #5078 from pixelfed/staging
Bump version to v0.12.1
2024-05-07 01:15:50 -06:00
Daniel Supernault 2d3f1df003
Bump version to v0.12.1 2024-05-07 01:11:26 -06:00
Daniel Supernault 7c19baf5dc
Update changelog 2024-05-07 01:11:09 -06:00
Daniel Supernault 326bb93b8b
Update composer deps 2024-05-07 01:08:49 -06:00
daniel 64631c7233
Merge pull request #5077 from pixelfed/staging
Staging
2024-05-07 01:06:12 -06:00
Daniel Supernault 902572ed51
Update changelog 2024-05-07 01:01:17 -06:00
Daniel Supernault 60a62b59c9
Update ConfigCacheService, fix database race condition and fallback to file config and enable by default 2024-05-07 01:00:32 -06:00
daniel 3e59dd2868
Merge pull request #5067 from pixelfed/staging
Update ApiV1Dot1Controller, fix in app registration bug that prevents…
2024-04-30 23:29:37 -06:00
Daniel Supernault cbf996c9b6
Update ApiV1Dot1Controller, fix in app registration bug that prevents proper auth flow due to missing oauth scopes 2024-04-30 23:05:38 -06:00
daniel cf10fca74f
Merge pull request #5061 from pixelfed/staging
Bump version to v0.12.0
2024-04-29 03:12:32 -06:00
Daniel Supernault aa94dde376
Bump version to v0.12.0 2024-04-29 03:03:40 -06:00
daniel eecc7bef61
Merge pull request #5060 from pixelfed/staging
Update Like model, increase max likes per day from 500 to 1500
2024-04-29 02:47:45 -06:00
Daniel Supernault 3c877523b1
Update changelog 2024-04-29 02:47:17 -06:00
Daniel Supernault 4223119f58
Update Like model, increase max likes per day from 500 to 1500 2024-04-29 02:46:14 -06:00
daniel 74a3d5c6c0
Merge pull request #5056 from pixelfed/staging
Update CustomEmojiService, only return local emoji
2024-04-23 05:55:10 -06:00
Daniel Supernault 7f8bba4415
Update CustomEmojiService, only return local emoji 2024-04-23 05:54:24 -06:00
daniel 6263e90a13
Merge pull request #5054 from pixelfed/staging
Disable config cache by default
2024-04-21 14:10:21 -06:00
Daniel Supernault e46bd6cc06
Disable config cache by default 2024-04-21 14:09:48 -06:00
daniel ae60c99679
Merge pull request #5052 from pixelfed/staging
Update profile embed, fix height bug
2024-04-20 05:59:25 -06:00
Daniel Supernault a54b4fb038
Update profile embed, fix height bug 2024-04-20 05:59:01 -06:00
daniel 2f7481205c
Merge pull request #5051 from pixelfed/staging
Staging
2024-04-20 05:03:29 -06:00
Daniel Supernault cde17f5af7
Update changelog 2024-04-20 05:03:10 -06:00
Daniel Supernault 433bc4c286
Update embed.js 2024-04-20 05:02:58 -06:00
daniel 5d407ededf
Merge pull request #5050 from pixelfed/staging
Update profile embed view, fix height bug
2024-04-20 05:01:50 -06:00
Daniel Supernault 65166570c5
Update profile embed view, fix height bug 2024-04-20 05:01:04 -06:00
daniel 610326e7b0
Merge pull request #5049 from pixelfed/staging
Refactor embeds
2024-04-20 04:36:08 -06:00
Daniel Supernault 6fc066a213
Update changelog 2024-04-20 04:35:10 -06:00
Daniel Supernault 8b8b1ffc5c
Update ProfileController, refactor profile embeds 2024-04-20 04:33:47 -06:00
Daniel Supernault 9a7acc12a6
Update StatusController, refactor status embeds 2024-04-20 04:26:47 -06:00
Daniel Supernault 51b6fe7dc8
Refactor embeds 2024-04-20 04:25:22 -06:00
daniel c4ffa62242
Merge pull request #5048 from pixelfed/staging
Update webpack config
2024-04-20 02:01:19 -06:00
Daniel Supernault 87ee0633fe
Update assets, move presenters 2024-04-20 01:26:51 -06:00
Daniel Supernault ded660b2c4
Update webpack config 2024-04-20 00:55:21 -06:00
daniel 4d04227d41
Merge pull request #5043 from pixelfed/staging
Staging
2024-04-12 04:44:12 -06:00
Daniel Supernault 26f92c93ce
Update compiled assets 2024-04-12 04:43:38 -06:00
Daniel Supernault 81566987e4
Update changelog 2024-04-12 04:43:21 -06:00
Daniel Supernault 8af2360779
Update VideoPlayer component, add playsinline attribute to video element 2024-04-12 04:42:57 -06:00
daniel b3fb69c5b1
Merge pull request #5042 from pixelfed/staging
Staging
2024-04-12 04:03:20 -06:00
Daniel Supernault f30f7d79fb
Update changelog 2024-04-12 04:00:46 -06:00
Daniel Supernault 2deb65d874
Update compiled assets 2024-04-12 04:00:25 -06:00
Daniel Supernault ad03291699
Update VideoPresenter component, add webkit-playsinline attribute to video element to prevent the full screen video player 2024-04-12 03:56:33 -06:00
daniel 6be21891d5
Merge pull request #5041 from pixelfed/staging
Staging
2024-04-11 20:44:15 -06:00
daniel 141f6d38a7
Merge pull request #5035 from jippi/jippi-fork
Docker fixes
2024-04-11 20:43:52 -06:00
Daniel Supernault 4608c66c0b
Re-add .env.example 2024-04-11 20:43:24 -06:00
Christian Winther e227ee1bd5 use DOCKER_DB_HOST_PORT when checking if database is ready or not 2024-04-06 10:53:48 +00:00
Christian Winther dbc5df849f use ENABLE_CONFIG_CACHE when dumping composer autoload 2024-04-06 10:06:07 +00:00
Daniel Supernault 6bdf73de4d
Update UnfollowPipeline, fix follower count cache bug 2024-04-06 03:29:17 -06:00
daniel 5b3d0206ae
Merge pull request #5034 from pixelfed/staging
Update docker env, fix config_cache. Fixes #5033
2024-04-06 03:20:31 -06:00
Daniel Supernault 858fcbf606
Update docker env, fix config_cache. Fixes #5033 2024-04-06 03:19:50 -06:00
daniel 57df06a0b6
Merge pull request #5032 from pixelfed/staging
Staging
2024-04-06 02:56:48 -06:00
Daniel Supernault db1a4c9f8e
Update changelog 2024-04-06 02:48:31 -06:00
Daniel Supernault ce4beab9c8
Update composer deps 2024-04-06 02:48:03 -06:00
Daniel Supernault 9d5479de39
Update compiled assets 2024-04-06 02:47:39 -06:00
Daniel Supernault 512518d319
Update npm deps 2024-04-06 02:47:19 -06:00
Daniel Supernault b06a3455c2
Update compiled assets 2024-04-06 02:28:27 -06:00
Daniel Supernault 94a6e8614a
Update styles 2024-04-06 02:27:55 -06:00
Daniel Supernault 81d1e0fdab
Update context menu, add mute/block/unfollow actions and update relationship store accordingly 2024-04-06 02:27:22 -06:00
Daniel Supernault b8e96a5ff3
Update ApiV1Controller, improve refresh relations logic when (un)muting or (un)blocking 2024-04-06 01:24:09 -06:00
Daniel Supernault b7322b6874
Update PrivacySettings controller, refresh RelationshipService when unmute/unblocking 2024-04-06 01:22:52 -06:00
daniel 142db2c41e
Merge pull request #5031 from pixelfed/staging
Staging
2024-04-06 00:49:02 -06:00
Daniel Supernault 8c6936409d
Update relationships view, fix unfollow hashtag bug. Fixes #5008 2024-04-05 22:38:13 -06:00
daniel e2c2952fda
Merge pull request #5011 from ThisIsMissEm/feat/add-api-cors
Adjust CORS configuration to support API & OAuth Routes
2024-04-05 22:11:20 -06:00
daniel dfab7e945a
Merge pull request #5002 from jippi/docker-check-requirements
Docker: Script to check requirements
2024-04-05 22:09:59 -06:00
daniel 363196883d
Merge pull request #4985 from jippi/allow-setting-db-root-password
Docker: Allow setting the DB root password separately
2024-04-05 22:08:18 -06:00
daniel f1eaaa80be
Merge pull request #4984 from jippi/fix-rsync-arm-image
Docker: Use rsync container image that supports arm64 and amd64
2024-04-05 22:07:02 -06:00
daniel 0b162dc15e
Merge pull request #5005 from pixelfed/staging
Update Admin Settings
2024-04-05 22:05:09 -06:00
Daniel Supernault a9e54aa540
Add BeagleService 2024-04-05 22:02:51 -06:00
Daniel Supernault 3871a80391
Update compiled assets 2024-04-05 22:01:17 -06:00
Daniel Supernault 4147f7c521
Update spa sass, fix timestamp dark mode bug 2024-04-05 22:00:16 -06:00
Daniel Supernault 039dfaa6c3
Update circleci config 2024-03-31 16:10:28 -06:00
Daniel Supernault f318bd7a30
Update circleci config 2024-03-31 16:05:45 -06:00
Daniel Supernault d946afcc5c
Update AdminSettings, use better validation for user integer settings 2024-03-18 06:13:27 -06:00
Daniel Supernault 2dcbc1d5ef
Update AdminSettings, add max_account_size support 2024-03-18 06:09:36 -06:00
Daniel Supernault ec2fdd61f7
Update AdminInstances component 2024-03-18 05:58:06 -06:00
Daniel Supernault aba1e13d43
Update AdminSettings component, fix user settings 2024-03-18 05:57:21 -06:00
Daniel Supernault dcc5f416ef
Update AdminSettingsController, add AdminSettingsService 2024-03-18 05:55:38 -06:00
Emelia Smith 1eadff9d2e
Adjust CORS configuration to support API & OAuth Routes
Fixes #4411 and #3381
2024-03-17 21:43:26 +01:00
Daniel Supernault ac1f074889
Update AdminSettingsController, add user filter max limit settings 2024-03-16 05:13:06 -06:00
Daniel Supernault 5162c0704a
Update RemoteFollowImportRecent, use MediaPathService 2024-03-16 03:58:24 -06:00
Daniel Supernault 3628b4625c
Update ConfigCacheService, encrypt keys at rest 2024-03-14 05:49:02 -06:00
Daniel Supernault 674e560f04
Update admin settings, refactor to vue component 2024-03-14 05:17:18 -06:00
Daniel Supernault eb4871237b
Add admin settings partials 2024-03-14 05:07:49 -06:00
Daniel Supernault 704e7b12e0
Update AdminReadMore component, add .prevent to click action 2024-03-14 05:06:28 -06:00
Daniel Supernault cee979eda8
Update hashtag component 2024-03-14 05:05:27 -06:00
Daniel Supernault 828a456f36
Update web-admin routes, add setting api routies ;) 2024-03-14 05:04:14 -06:00
Daniel Supernault 087b27916f
Update filesystems config, add to config_cache 2024-03-14 05:03:19 -06:00
Daniel Supernault 6ce513f8c3
Update user_filters, use config_cache 2024-03-14 00:06:33 -06:00
Daniel Supernault 949e99798e
Update UserObserver, fix type casting 2024-03-13 23:36:26 -06:00
Daniel Supernault 911446c03e
Update app.name config, use config_cache 2024-03-12 06:42:12 -06:00
Daniel Supernault a76cb5f4f8
Update autospam config, use config_cache 2024-03-12 06:20:26 -06:00
Daniel Supernault da0e0ffabf
Update ig import, use config_cache 2024-03-12 06:11:20 -06:00
Daniel Supernault 2d113de536
Update config_cache, fix type casting 2024-03-12 04:27:24 -06:00
Daniel Supernault d1adb109de
Update stories config, use config_cache 2024-03-12 04:15:05 -06:00
Daniel Supernault ce228f7fa4
Update oauth setting, use config_cache 2024-03-12 03:55:51 -06:00
Daniel Supernault 5071aaf408
Update activitpub setting, use config_cache() 2024-03-12 02:20:37 -06:00
Daniel Supernault 40478f258a
Update landing settings, use config_cache 2024-03-12 02:05:22 -06:00
Daniel Supernault d670de175e
Update media_types, use config_cache 2024-03-12 01:28:08 -06:00
Daniel Supernault fecbe1897b
Update pixelfed.max_album_length, use config_cache 2024-03-12 01:20:24 -06:00
Daniel Supernault 665581d80c
Update cloud storage, use config_cache 2024-03-12 01:03:33 -06:00
Daniel Supernault a72188a7db
Update image pipeline, use config_cache 2024-03-12 00:35:33 -06:00
Daniel Supernault ad506e901d
Update AdminDirectoryController, fix type casting 2024-03-12 00:03:10 -06:00
Daniel Supernault f2f2a8097c
Update PixelfedDirectoryController, use cached stats 2024-03-12 00:02:15 -06:00
Daniel Supernault f08aab2231
Update PixelfedDirectoryController, fix boolean cast bug 2024-03-11 23:43:23 -06:00
Daniel Supernault 8a0c456edc
Update admin css, use font-display:swap for nucleo icons 2024-03-11 23:27:43 -06:00
Daniel Supernault 75081e609a
Update ProfileController, handle permalink redirect bug 2024-03-11 23:26:30 -06:00
Daniel Supernault 481314cd23
Update custom emoji, add config_cache support 2024-03-11 22:42:26 -06:00
Daniel Supernault 8a89e3c963
Update captcha, use config_cache helper 2024-03-11 21:25:04 -06:00
Daniel Supernault c96167f2f7
Update config_cache 2024-03-11 00:29:55 -06:00
Christian Winther ad382f8f55 docker: cleanup script 2024-03-10 15:16:25 +00:00
Christian Winther 3a1f4789e6 docker: I => Pixelfed 2024-03-10 15:15:08 +00:00
Christian Winther 4942f7fbd4 docker: docs 2024-03-10 15:13:45 +00:00
Christian Winther cc8c5ccd37 docker: include convenience script reference 2024-03-10 15:11:54 +00:00
Christian Winther 56d47dd1bc hide jippi-fork 2024-03-10 15:00:31 +00:00
Christian Winther 1892f68ebd Add helper script to check minimum requirements 2024-03-10 14:53:57 +00:00
Christian Winther ca7c2d34f2 Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-03-10 13:44:16 +00:00
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
Christian Winther 091de696c2 Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork 2024-03-07 19:43:30 +00: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
Christian Winther dd5878b256 Allow setting the DB root password seperately
Fixes https://github.com/pixelfed/pixelfed/issues/4980
2024-03-06 20:49:36 +00:00
Christian Winther ae645ddd15 Use rsync container image that supports arm64 and amd64
fixes https://github.com/pixelfed/pixelfed/issues/4979
2024-03-06 20:45:04 +00:00
Christian Winther e38aa65dad keep building for jippi-fork branch 2024-03-06 19:38:34 +00: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
daniel 1f6dc94a34
Merge pull request #4946 from pixelfed/staging
Add Curated Onboarding
2024-02-19 05:57:04 -07:00
Daniel Supernault e1715d40b8
Update changelog 2024-02-19 05:44:34 -07:00
Daniel Supernault 0ad3654da3
Update compiled assets 2024-02-19 04:35:11 -07:00
Daniel Supernault 06655c3a8b
Update LandingService, add curated onboarding parameter 2024-02-19 04:34:57 -07:00
Daniel Supernault cae26c666d
Update landing nav, fix curated onboarding state 2024-02-19 04:33:29 -07:00
Daniel Supernault 8355d5d00c
Add AdminCuratedRegisterController 2024-02-19 04:03:39 -07:00
Daniel Supernault 8dac2caf1d
Add Curated Onboarding 2024-02-19 04:00:31 -07:00
daniel 4057ee3bb3
Merge pull request #4944 from pixelfed/staging
Update ApiV1Controller, implement better limit logic to gracefully ha…
2024-02-19 01:48:28 -07:00
Daniel Supernault 9409c569bd
Update compiled assets 2024-02-19 01:47:57 -07:00
Daniel Supernault b6c97d1d26
Update npm deps 2024-02-19 01:46:45 -07:00
Daniel Supernault eb0e76f8e2
Update changelog 2024-02-19 01:39:52 -07:00
Daniel Supernault 1f74a95d0c
Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max 2024-02-19 01:36:09 -07:00
daniel 6c0c61e45a
Merge pull request #4943 from pixelfed/staging
Staging
2024-02-18 22:19:21 -07:00
daniel 8cb7ebdd8b
Merge branch 'dev' into staging 2024-02-18 22:19:00 -07:00
Daniel Supernault ce9c0e0b24
Update changelog 2024-02-18 22:16:46 -07:00
Daniel Supernault 545f7d5e70
Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 2024-02-18 22:14:23 -07:00
daniel f97afcf47e
Merge pull request #4938 from ThisIsMissEm/patch-2
Update .gitattributes to collapse diffs on generated files
2024-02-18 19:21:14 -07:00
daniel b339f4a5b0
Merge pull request #4942 from pixelfed/staging
Staging
2024-02-18 18:11:07 -07:00
Daniel Supernault 147113cc95
Update changelog 2024-02-18 18:02:08 -07:00
Daniel Supernault ea6b162340
Update cache config, use predis as default redis driver client 2024-02-18 18:01:14 -07:00
Daniel Supernault 4c26f59cd0
Update composer deps, add php 8.3 support 2024-02-18 18:00:43 -07: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
daniel 4eb0c36480
Merge pull request #4941 from pixelfed/staging
Update federation config, increase default timeline days falloff to 9…
2024-02-18 13:08:00 -07:00
Daniel Supernault 17027c3487
Update changelog 2024-02-18 13:07:26 -07:00
Daniel Supernault 011834f473
Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 2024-02-18 13:05:49 -07: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
Emelia Smith 9978b2b959
Update .gitattributes to collapse diffs on generated files 2024-02-16 17:56:13 +01:00
daniel 01a86009e6
Merge pull request #4936 from pixelfed/staging
Update Inbox, cast live filters to lowercase
2024-02-16 06:58:13 -07:00
Daniel Supernault d835e0adaa
Update Inbox, cast live filters to lowercase 2024-02-16 06:57:04 -07:00
daniel f45d293707
Merge pull request #4935 from pixelfed/staging
Bump version to 0.11.12
2024-02-16 03:09:23 -07:00
Daniel Supernault 66f6640072
Bump version to 0.11.12 2024-02-16 03:08:50 -07:00
daniel 25dd2e46fe
Merge pull request #4934 from pixelfed/staging
Add Software Update banner to admin home feeds
2024-02-16 03:02:36 -07:00
Daniel Supernault f07843a0f2
Update compiled assets 2024-02-16 03:01:51 -07:00
Daniel Supernault 56b736c325
Update changelog 2024-02-16 03:00:51 -07:00
Daniel Supernault b0fb198829
Add Software Update banner to admin home feeds 2024-02-16 02:58:27 -07:00
daniel e1d579b10b
Merge pull request #4933 from pixelfed/staging
Autospam Live Filters - block remote activities based on comma separated keywords
2024-02-16 02:20:23 -07:00
Daniel Supernault 83eadbb811
Update changelog 2024-02-16 02:20:11 -07:00
Daniel Supernault 40b45b2a11
Update Autospam, add live filters to block remote activities based on comma separated keywords 2024-02-16 02:15:39 -07:00
daniel ccbba91e70
Merge pull request #4932 from pixelfed/staging
Update routes
2024-02-15 23:36:51 -07:00
Daniel Supernault bc4d223714
Update routes 2024-02-15 22:20:40 -07:00
daniel 0032415459
Merge pull request #4931 from pixelfed/staging
Staging
2024-02-15 21:42:15 -07:00
Daniel Supernault 70fc44dfe5
Update changelog 2024-02-15 21:41:40 -07:00
Daniel Supernault 0f3ca19461
Update status view, fix unlisted/private scope bug 2024-02-15 21:41:18 -07:00
daniel 0dc54e9ac0
Merge pull request #4930 from pixelfed/staging
Staging
2024-02-15 21:23:29 -07:00
Daniel Supernault df5e61266c
Update changelog 2024-02-15 21:23:00 -07:00
Daniel Supernault 1232cfc86a
Update ActivityPubFetchService, enforce stricter Content-Type validation 2024-02-15 21:22:41 -07:00
daniel af935c729b
Merge pull request #4929 from pixelfed/staging
Staging
2024-02-15 20:59:35 -07:00
Daniel Supernault 4c6ec20e36
Update changelog 2024-02-15 20:59:08 -07:00
Daniel Supernault fb0bb9a34f
Update Federation, use proper Content-Type headers for following/follower collections 2024-02-15 20:58:43 -07:00
Christian Winther d3bbfdb6e0 Merge branch 'staging' of github.com:jippi/pixelfed into jippi-fork 2024-02-13 00:52:18 +00:00
daniel b10c60584b
Merge pull request #4924 from pixelfed/staging
Update public/network timelines, fix non-redis response and fix reblo…
2024-02-11 20:26:31 -07:00
Daniel Supernault 221fe43638
Update compiled assets 2024-02-11 20:25:18 -07:00
Daniel Supernault 97c131fdf2
Update AccountImport component 2024-02-11 20:24:27 -07:00
Daniel Supernault 78da12004f
Update changelog 2024-02-11 20:23:19 -07:00
Daniel Supernault 8b4ac5cc0b
Update public/network timelines, fix non-redis response and fix reblogs in home feed 2024-02-11 20:21:46 -07:00
daniel bbd3688333
Merge pull request #4923 from pixelfed/staging
Update ApiV1Controller, fix network timeline
2024-02-11 14:30:46 -07:00
Daniel Supernault 7b8977e9cc
Update changelog 2024-02-11 14:27:38 -07:00
Daniel Supernault 0faf59e3b7
Update ApiV1Controller, fix network timeline 2024-02-11 14:26:45 -07: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
daniel 8fa6ae421b
Merge pull request #4915 from pixelfed/staging
Bump version to v0.11.11
2024-02-09 20:58:43 -07:00
Daniel Supernault e5bbe9340a
Bump version to v0.11.11 2024-02-09 20:57:45 -07:00
Daniel Supernault 7a7b4bc717
Update AuthServiceProvider 2024-02-09 20:54:17 -07:00
daniel 8ab9951909
Merge pull request #4914 from pixelfed/staging
Fix api endpoints
2024-02-09 20:52:09 -07:00
Daniel Supernault 62b9eef805
Fix api endpoints 2024-02-09 20:51:37 -07:00
daniel 67167a5b90
Merge pull request #4913 from pixelfed/staging
Fix api endpoints
2024-02-09 20:45:44 -07:00
Daniel Supernault fd7f5dbba1
Fix api endpoints 2024-02-09 20:45:10 -07:00
daniel 0649bb4754
Merge pull request #4912 from pixelfed/staging
Fix api endpoints
2024-02-09 20:41:42 -07:00
Daniel Supernault e354750808
Fix api endpoints 2024-02-09 20:41:12 -07:00
daniel 0dbbc6a6b4
Merge pull request #4911 from pixelfed/staging
Staging
2024-02-09 20:06:20 -07:00
Daniel Supernault 607b239c1a
Bump version to v0.11.10 2024-02-09 20:05:22 -07:00
Daniel Supernault 2e6100f275
Update BearerTokenResponse, fix scope bug 2024-02-09 20:04:15 -07:00
daniel 7e47d6dccb
Merge pull request from GHSA-gccq-h3xj-jgvf
[staging] Implement proper OAuth authorization on API endpoints
2024-02-09 19:44:27 -07: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
Emelia Smith 0f8e45fe75
Implement proper OAuth authorization on API endpoints 2024-02-09 02:28:08 +01:00
Emelia Smith 9330cd02f7
Implement proper OAuth authorization on Admin API endpoints 2024-02-09 02:28:08 +01:00
Emelia Smith 7b0a6060b2
Return access tokens' scopes, not hardcoded list 2024-02-09 02:28:08 +01:00
Christian Winther d374d73ba7
Merge branch 'staging' into jippi-fork 2024-02-08 01:17:25 +01:00
daniel 73b4dab9a8
Merge pull request #4904 from pixelfed/staging
Update ApiV2Controller, add vapid key to instance object. Thanks this…
2024-02-07 06:00:20 -07:00
Daniel Supernault 2becd273c4
Update changelog 2024-02-07 06:00:09 -07:00
Daniel Supernault 4d02d6f12e
Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! 2024-02-07 05:59:12 -07:00
daniel a1b0d3d3c0
Merge pull request #4903 from pixelfed/staging
Add migration
2024-02-07 04:50:33 -07:00
Daniel Supernault 97b7cb2719
Add migration 2024-02-07 04:49:44 -07:00
daniel 6ea20716bc
Merge pull request #4902 from pixelfed/staging
Staging
2024-02-07 04:45:47 -07:00
Daniel Supernault 1f3f0cae65
Update changelog 2024-02-07 04:43:32 -07:00
Daniel Supernault 6921d3568e
Add InstanceMananger command 2024-02-07 04:42:27 -07:00
Daniel Supernault 5b284cacea
Update ApiV1Controller, enforce blocked instance domain logic 2024-02-07 04:41:12 -07:00
Daniel Supernault 01b33fb37e
Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints 2024-02-07 03:43:20 -07:00
Daniel Supernault 1e3acadefb
Update horizon.php config 2024-02-07 02:52:37 -07:00
Daniel Supernault ac01f51ab6
Update FetchNodeinfoPipeline, use more efficient dispatch 2024-02-07 02:51:50 -07:00
Daniel Supernault 289cad470b
Update Instance model, add entity casts 2024-02-07 02:49:29 -07:00
Daniel Supernault 240e6bbe4f
Update NodeinfoService, disable redirects 2024-02-07 02:47:34 -07:00
daniel 111ba70473
Merge pull request #4897 from pixelfed/staging
Staging
2024-02-04 07:20:53 -07:00
Daniel Supernault 80e0ada946
Update changelog 2024-02-04 07:18:54 -07:00
Daniel Supernault fa97a1f38e
Update notification pipelines, fix non-local saving 2024-02-04 07:18:05 -07:00
Daniel Supernault 4d4013896c
Update NotificationEpochUpdatePipeline, use more efficient query 2024-02-04 07:11:05 -07:00
daniel 3a557d7ffc
Merge pull request #4896 from pixelfed/staging
Update AP helpers, fix sensitive bug
2024-02-04 03:19:08 -07:00
Daniel Supernault 152b6eab9a
Update changelog 2024-02-04 03:18:58 -07:00
Daniel Supernault 00ed330cf3
Update AP helpers, fix sensitive bug 2024-02-04 03:16:57 -07:00
daniel 2483832754
Merge pull request #4895 from pixelfed/staging
Update AP helpers, refactor post count decrement logic
2024-02-04 03:03:16 -07:00
Daniel Supernault 09ca96cc2b
Update changelog 2024-02-04 02:54:37 -07:00
Daniel Supernault 8b843d620c
Update ProfilePipeline jobs 2024-02-04 02:53:40 -07:00
Daniel Supernault b81ae5773f
Update AP helpers, refactor post count decrement logic 2024-02-04 02:50:48 -07:00
daniel 5d89fe8130
Merge pull request #4894 from pixelfed/staging
Update AP helpers, more efficently update post counts
2024-02-04 02:41:08 -07:00
Daniel Supernault ddf7f09ad4
Update changelog 2024-02-04 02:40:59 -07:00
Daniel Supernault 7caed381fb
Update AP helpers, more efficently update post counts 2024-02-04 02:40:04 -07:00
daniel 3d5fd48a22
Merge pull request #4893 from pixelfed/staging
Update TransformImports command, fix import service condition
2024-02-03 13:31:21 -07:00
Daniel Supernault d3ff89e538
Update changelog 2024-02-03 13:31:11 -07:00
Daniel Supernault 32c59f0440
Update TransformImports command, fix import service condition 2024-02-03 13:30:02 -07:00
daniel 4050055e5e
Merge pull request #4891 from pixelfed/staging
Add S3 IG Import Media Storage
2024-02-02 05:50:09 -07:00
Daniel Supernault 04c5e550a5
Update changelog 2024-02-02 05:46:28 -07:00
Daniel Supernault 081360b905
Update console kernel, add ig import s3 job 2024-02-02 05:45:06 -07:00
Daniel Supernault edbb07cc37
Add import video thumbnail job 2024-02-02 05:30:32 -07:00
Daniel Supernault 622e9cee97
Add S3 IG Import Media Storage 2024-02-02 02:29:33 -07:00
daniel 9dcb25c8e2
Merge pull request #4890 from pixelfed/staging
Update AccountImport.vue, fix new IG export format
2024-02-01 23:21:03 -07:00
Daniel Supernault 5b7111c56f
Update changelog 2024-02-01 23:20:12 -07:00
Daniel Supernault cf00542336
Update compiled assets 2024-02-01 23:19:25 -07:00
Daniel Supernault 59aa6a4b02
Update AccountImport.vue, fix new IG export format 2024-02-01 22:49:04 -07:00
daniel 33d1faf734
Merge pull request #4889 from pixelfed/staging
Staging
2024-02-01 22:47:18 -07:00
Daniel Supernault 339857ffa2
Update changelog 2024-02-01 22:46:39 -07:00
Daniel Supernault 0aff126aa0
Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 2024-02-01 22:46:13 -07: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
daniel 29785b5654
Merge pull request #4884 from pixelfed/staging
Staging
2024-01-29 22:25:32 -07:00
Daniel Supernault 8a9a7c0e47
Fix parental_controls migration 2024-01-29 22:24:50 -07:00
Daniel Supernault 61b1523368
Fix newsroom migration 2024-01-29 22:13:09 -07:00
Daniel Supernault 92ff114d2d
Update migrations, fixes #4883 2024-01-29 21:45:59 -07:00
daniel 6d8ba64e0b
Merge pull request #4879 from shleeable/patch-14
typo
2024-01-26 20:57:10 -07:00
daniel 44f92f4888
Merge pull request #4856 from nexryai/patch-1
Fix syntax error of lang/vendor/backup/ja/notifications.php
2024-01-26 20:56:35 -07:00
daniel d4e4c4e1dd
Merge pull request #4859 from mbliznikova/4845_provide_error_message_when_account_size_limit_reached
Provide an informative error message when account size limit is reached
2024-01-26 20:55:44 -07: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
Shlee 934f2ffdb4
Update home.blade.php 2024-01-24 12:45:21 +10:30
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
daniel 51c6935e05
Merge pull request #4877 from pixelfed/staging
Update UserEmailForgotController
2024-01-22 06:00:37 -07:00
Daniel Supernault 6167ebc654
Update UserEmailForgotController 2024-01-22 05:59:37 -07:00
daniel c77e427fa3
Merge pull request #4876 from pixelfed/staging
Update forgot mail template, urlencode email
2024-01-22 05:58:15 -07:00
Daniel Supernault efe8e89046
Update forgot mail template, urlencode email 2024-01-22 05:57:40 -07:00
daniel 1f6577c947
Merge pull request #4875 from pixelfed/staging
Update forgot email captcha config
2024-01-22 05:52:27 -07:00
Daniel Supernault 96366ab3de
Update forgot email captcha config 2024-01-22 05:51:50 -07:00
daniel dbf59367df
Merge pull request #4874 from pixelfed/staging
Add forgot email feature
2024-01-22 05:42:39 -07:00
Daniel Supernault c26a3d2817
Add migration 2024-01-22 05:35:30 -07:00
Daniel Supernault 5afe7abdfb
Update changelog 2024-01-22 05:26:57 -07:00
Daniel Supernault 67c650b195
Add forgot email feature 2024-01-22 05:26:01 -07:00
Daniel Supernault 0325e17115
Update LoginController, fix captcha validation error message 2024-01-22 03:06:14 -07:00
daniel ca05279bb6
Merge pull request #4872 from pixelfed/staging
Update login view, add email prefill logic
2024-01-22 02:05:43 -07:00
Daniel Supernault 74423b52ca
Update changelog 2024-01-22 02:05:01 -07:00
Daniel Supernault d76f01685c
Update login view, add email prefill logic 2024-01-22 02:03:39 -07: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
daniel 84b63f8aa9
Merge pull request #4866 from pixelfed/staging
Update migration, fixes #4863
2024-01-15 22:43:38 -07:00
Daniel Supernault 979aa55135
Update migration, fixes #4863 2024-01-15 22:43:09 -07: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
daniel 187d1e1af9
Merge pull request #4862 from pixelfed/staging
Add Parental Controls feature
2024-01-11 07:08:38 -07:00
Daniel Supernault 85a612742d
Update changelog 2024-01-11 06:54:20 -07:00
Daniel Supernault c91f1c595a
Update ParentalControlsController, prevent children from adding accounts 2024-01-11 06:52:12 -07:00
Daniel Supernault db1b466792
Update instance config 2024-01-11 06:45:32 -07:00
Daniel Supernault c7ed684a5c
Update ParentalControlsController 2024-01-11 06:31:19 -07:00
Daniel Supernault 71c148c61e
Update StoryController, add parental controls support 2024-01-11 05:46:02 -07:00
Daniel Supernault fe30cd25d1
Update DirectMessageController, add parental controls support 2024-01-11 05:25:23 -07:00
Daniel Supernault fd9b5ad443
Update api controllers, add parental control support 2024-01-11 04:50:11 -07:00
Daniel Supernault 9d365d07f9
Update ParentalControls, map updated saved permissions/roles 2024-01-11 04:41:38 -07:00
Daniel Supernault 2dcfc81495
Update ComposeController, add parental controls support 2024-01-11 04:40:25 -07:00
Daniel Supernault 1a16ec2078
Update BookmarkController, add parental control support 2024-01-11 03:22:35 -07:00
Daniel Supernault 42298a2e9c
Apply dangerZone middleware to parental controls routes 2024-01-11 02:40:52 -07:00
Daniel Supernault 58745a8808
Update settings sidebar 2024-01-11 02:37:13 -07:00
Daniel Supernault 5f6ed85770
Update settings sidebar 2024-01-11 02:34:43 -07:00
Daniel Supernault 319a20b473
Update ParentalControlsController, redirect to new custom error page on active session when attempting to use child invite link so as to not overwrite parent active session with child session 2024-01-11 02:12:54 -07:00
Daniel Supernault ef57d471e5
Update migration 2024-01-11 01:50:51 -07:00
Daniel Supernault c53894fe16
Add Parental Controls feature 2024-01-11 01:35:15 -07:00
mbliznikova 4e567e3411 Provide an informative error message when account size limit is reached 2024-01-09 04:49:01 +00:00
Christian Winther 6f0a6aeb3d fix hadolint path 2024-01-07 14:54:28 +00:00
nexryai 19e8037c85
Fix lang/vendor/backup/ja/notifications.php 2024-01-07 21:05:04 +09:00
daniel 0a556d1ac1
Merge pull request #4855 from pixelfed/staging
Update ApiV1Controller, update favourites max limit. Fixes #4854
2024-01-06 11:45:12 -07:00
Daniel Supernault d25209f74a
Update ApiV1Controller, update favourites max limit. Fixes #4854 2024-01-06 11:43:56 -07: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
daniel d8a5dc00bb
Merge pull request #4847 from pixelfed/staging
Staging
2024-01-03 04:13:11 -07:00
Daniel Supernault bca2484994
Update Webfinger util, add avatar entity. Fixes #1629 2024-01-03 04:11:29 -07:00
daniel 5f5cb0616d
Merge pull request #3919 from shleeable/patch-3
Update NotificationCard.vue to popover image preview on comment/share
2024-01-03 02:59:56 -07:00
daniel d7efe1a7ee
Merge pull request #3894 from vanlueckn/feat-add-emoji-cli
Add a command to import emoji archives to pixelfed
2024-01-03 02:54:43 -07:00
daniel d0f7865508
Merge pull request #4846 from pixelfed/staging
Update meta tags, improve descriptions and seo/og tags
2024-01-03 02:03:19 -07:00
Daniel Supernault 5087a87885
Update changelog 2024-01-03 02:00:47 -07:00
Daniel Supernault fd44c80ce9
Update meta tags, improve descriptions and seo/og tags 2024-01-03 01:54:30 -07:00
daniel 5b4214cb80
Merge pull request #4843 from pixelfed/staging
Add Roles & Parental Controls
2024-01-02 23:05:23 -07:00
Daniel Supernault 0ef6812709
Update UserRoleService, add useDefaultFallback parameter 2024-01-02 22:07:42 -07:00
Daniel Supernault cbe75ce871
Update UserRolesController 2024-01-02 22:06:54 -07:00
Daniel Supernault 75b0f2dda0
Update ComposeController, add permissions check 2024-01-02 22:06:18 -07:00
Daniel Supernault d39946b045
Update ApiV1Controller, add permissions check 2024-01-02 22:04:27 -07:00
Daniel Supernault 7b6c9c7428
Update migrations 2024-01-01 16:19:24 -07:00
Christian Winther 98211d3620 refactor Dockerfile and Docker workflow 2023-12-28 23:46:59 +00:00
Daniel Supernault 7dbdbf15a5
Add Roles & Parental Controls 2023-12-27 02:51:47 -07:00
daniel 238f646306
Merge pull request #4842 from pixelfed/staging
Update AP ProfileTransformer, add published attribute
2023-12-25 00:31:15 -07:00
Daniel Supernault f66b9fe74e
Update changelog 2023-12-25 00:31:05 -07:00
Daniel Supernault adfaa2b140
Update AP ProfileTransformer, add published attribute 2023-12-25 00:30:05 -07:00
daniel 25a4289dc3
Merge pull request #4836 from pixelfed/staging
Fix StatusHashtag delete bug
2023-12-21 06:19:43 -07:00
Daniel Supernault 1be21c76f3
Fix StatusHashtag delete bug 2023-12-21 06:18:51 -07:00
daniel eebed73a5e
Merge pull request #4834 from pixelfed/staging
Add User Domain Blocks
2023-12-21 05:25:46 -07:00
Daniel Supernault 73a0f528ab
Update user domain block commands 2023-12-21 05:00:35 -07:00
Daniel Supernault d8f46f47a1
Update changelog 2023-12-21 04:48:56 -07:00
Daniel Supernault fa0380ac3b
Update UserObserver, add default domain blocks logic 2023-12-21 04:47:09 -07:00
Daniel Supernault 519c7a3735
Update domain block commands 2023-12-21 03:48:08 -07:00
Daniel Supernault f3f0175c84
Add DefaultDomainBlock model + migration 2023-12-21 03:47:23 -07:00
Daniel Supernault 3e28cf661b
Add user domain block commands 2023-12-21 03:35:47 -07:00
Daniel Supernault e98df1196f
Add migration 2023-12-21 03:34:31 -07:00
Daniel Supernault 6c39df7fb3
Update Inbox, import AccountService 2023-12-21 02:08:44 -07:00
Daniel Supernault 5169936062
Update MarkerService, fix php deprecation warning 2023-12-21 02:05:26 -07:00
Daniel Supernault 89b8e87477
Update ApiV1Controller, apply user domain blocks filtering to hashtag timelines 2023-12-21 02:03:15 -07:00
Daniel Supernault fcbcd7ec73
Update Delete pipelines, delete status hashtags quietly 2023-12-21 01:53:49 -07:00
Daniel Supernault c3f16c87a3
Update SearchApiV2Service, add user domain blocks filtering 2023-12-21 01:05:49 -07:00
Daniel Supernault 21947835f8
Update ApiV1Controller, use domainBlock filtering on public/network feeds 2023-12-21 00:46:24 -07:00
Daniel Supernault 6d81214138
Update DomainBlockController, purge domainBlocks cache 2023-12-21 00:44:54 -07:00
Daniel Supernault 6d55cb27ee
Update UserFilterService, add domainBlocks method 2023-12-21 00:42:26 -07:00
Daniel Supernault b3148b788e
Update HomeTimelineService, add domain blocks filtering to warmCache method 2023-12-21 00:21:33 -07:00
Daniel Supernault 29aa87c282
Update HomeFeedPipeline jobs, add domain block filtering 2023-12-21 00:17:20 -07:00
Daniel Supernault 0455dd1996
Update UserFilter model, add user relation 2023-12-20 23:55:26 -07:00
Daniel Supernault ae1db1e3ab
Update migration 2023-12-20 23:17:27 -07:00
Daniel Supernault dd16189fc8
Update ImageResize job, add more logging 2023-12-20 23:10:57 -07:00
Daniel Supernault 795132df18
Update changelog 2023-12-19 06:25:39 -07:00
Daniel Supernault 87bba03d23
Update DomainBlockController, dispatch jobies 2023-12-19 06:24:51 -07:00
Daniel Supernault 54adbeb059
Update FeedRemoveDomainPipeline, make batchable 2023-12-19 06:04:03 -07:00
Daniel Supernault 9d621108b0
Add ProfilePurgeNotificationsByDomain pipeline job 2023-12-19 06:03:36 -07:00
Daniel Supernault 484a377a44
Add ProfilePurgeFollowersByDomain pipeline job 2023-12-19 06:02:58 -07:00
Daniel Supernault 1664a5bc52
Update FollowerService, add $silent param to remove method to more efficently purge relationships 2023-12-19 05:46:06 -07:00
Daniel Supernault a492a95a0e
Update AdminShadowFilter, fix deleted profile bug 2023-12-19 04:01:41 -07:00
Daniel Supernault 5c1591fdff
Add job batches migration 2023-12-19 01:20:14 -07:00
Daniel Supernault 819e7d3b32
Add FeedRemoveDomainPipeline 2023-12-19 01:10:48 -07:00
Daniel Supernault 8a0ceaf801
Update Inbox, add user domain blocks to Story reaction handlers 2023-12-18 22:57:53 -07:00
Daniel Supernault 491468612f
Update Inbox, add user domain blocks to Undo handler 2023-12-18 22:49:31 -07:00
Daniel Supernault e32e50da7b
Update Inbox, add user domain blocks to Like handler 2023-12-18 22:47:03 -07:00
Daniel Supernault 3fbf8f159e
Update Inbox, add user domain blocks to Accept handler 2023-12-18 22:46:09 -07:00
Daniel Supernault 279fb28e2a
Update Inbox, add user domain blocks to Announce handler 2023-12-18 22:42:21 -07:00
Daniel Supernault c89dc45e8d
Update Inbox, add user domain blocks to Follow handler 2023-12-18 22:37:34 -07:00
Daniel Supernault a7f96d8194
Update Inbox, add user domain blocks to Direct Message handler 2023-12-18 22:34:53 -07:00
Daniel Supernault e7c08fbbb2
Update AccountService, add blocksDomain method 2023-12-18 22:32:48 -07:00
Daniel Supernault 7016d19520
Update Privacy Settings view, change button to Blocked Domains and add l10n 2023-12-16 17:04:16 -07:00
Daniel Supernault 60e053c936
Update ApiV1Controller, update discoverAccountsPopular method 2023-12-16 06:22:56 -07:00
Daniel Supernault d3f032b2ec
Update FollowerService, add quickCheck to follows method for non cold-boot checks 2023-12-16 06:11:13 -07:00
Daniel Supernault e5d789e0ab
Add domain blocks setting view 2023-12-16 06:01:43 -07:00
Daniel Supernault 28da107f66
Add DomainBlockController 2023-12-16 05:56:37 -07:00
Daniel Supernault 63c9ebe81f
Update api routes 2023-12-16 05:43:37 -07:00
Daniel Supernault 28da44beec
Update PrivacySettings, add domainBlocks 2023-12-16 05:42:56 -07:00
Daniel Supernault cef451e588
Update routes 2023-12-16 05:36:59 -07:00
Daniel Supernault 2438324369
Add template-vue settings blade view 2023-12-16 05:32:21 -07:00
Daniel Supernault 2136ffe3d8
Add localization 2023-12-16 05:30:52 -07:00
Daniel Supernault 5cea5aab3c
Add Domain Blocks 2023-12-16 04:56:22 -07:00
daniel c2a535bfa1
Merge pull request #4828 from pixelfed/staging
Staging
2023-12-13 06:50:58 -07:00
Daniel Supernault f22a36fe30
Update CHANGELOG 2023-12-13 06:49:51 -07:00
Daniel Supernault b641954549
Update ApiV1Controller, set last_active_at 2023-12-13 06:49:00 -07:00
Daniel Supernault ebbd98e743
Update AccountService, add setLastActive method 2023-12-13 06:30:05 -07:00
daniel 5e6658de25
Merge pull request #4827 from pixelfed/staging
Update cache/session config
2023-12-13 06:08:00 -07:00
Daniel Supernault 85839b220a
Update cache/session config 2023-12-13 04:46:49 -07:00
daniel 11eef54b0c
Merge pull request #4826 from pixelfed/staging
Allow Import from IG media to be stored on S3
2023-12-12 23:19:34 -07:00
Daniel Supernault ff92015c87
Add migration 2023-12-12 23:07:25 -07:00
daniel a5a2f77871
Merge pull request #4822 from pixelfed/staging
Update Inbox, improve tombstone query efficiency
2023-12-11 04:13:52 -07:00
Daniel Supernault 8d98e3dc97
Update changelog 2023-12-11 04:10:13 -07:00
Daniel Supernault 759a439334
Update Inbox, improve tombstone query efficiency 2023-12-11 04:09:33 -07:00
daniel 1c40762921
Merge pull request #4820 from pixelfed/staging
Add Mutual Followers API endpoint
2023-12-11 01:43:36 -07:00
Daniel Supernault 6dceb6f05b
Update changelog 2023-12-11 01:37:11 -07:00
Daniel Supernault 33dbbe467d
Add Mutual Followers API endpoint 2023-12-11 01:34:46 -07:00
daniel 7ac1e398b8
Merge pull request #4817 from pixelfed/staging
Update HomeFeedPipeline, fix StatusService validation
2023-12-09 23:53:58 -07:00
Daniel Supernault 041c01359b
Update HomeFeedPipeline, fix StatusService validation 2023-12-09 23:53:02 -07:00
daniel d66cf5d028
Merge pull request #4806 from pixelfed/staging
Update DirectMessageController
2023-12-08 05:48:38 -07:00
Daniel Supernault 38fee418a9
Update DirectMessageController 2023-12-08 05:48:04 -07:00
daniel abcaa19ff1
Merge pull request #4805 from pixelfed/staging
Staging
2023-12-08 05:27:07 -07:00
Daniel Supernault ed5e956a54
Update changelog 2023-12-08 05:25:44 -07:00
Daniel Supernault 9c43e7e265
Update Timeline.vue, improve CHT pagination 2023-12-08 05:25:03 -07:00
Daniel Supernault 822e9888bb
Update PhotoAlbumPresenter.vue, fix fullscreen mode 2023-12-08 05:01:04 -07:00
Daniel Supernault 0a0681199f
Update ComposeModal, fix missing alttext post state 2023-12-08 04:56:21 -07:00
Daniel Supernault 4c3823b0c4
Update Notifications.vue, fix deprecated DM action links for story activities 2023-12-08 04:51:29 -07:00
Daniel Supernault 4c95306f12
Update StatusPipeline, fix Direct and Story notification deletion 2023-12-08 04:44:45 -07:00
Daniel Supernault 9818656425
Update DirectMessageController, dispatch local deletes to pipeline 2023-12-08 04:27:09 -07:00
daniel f01f4bf23e
Merge pull request #4804 from pixelfed/staging
Staging
2023-12-08 03:27:52 -07:00
Daniel Supernault 93a6f1e224
formatting 2023-12-08 03:26:32 -07:00
Daniel Supernault 957bbbc2bd
Update FeedInsertPipeline 2023-12-08 03:25:53 -07:00
Daniel Supernault 06bee36c52
Update Inbox, improve story attribute collection 2023-12-08 03:24:09 -07:00
daniel 4bb97e1547
Merge pull request #4803 from pixelfed/staging
Update DirectMessageController, revert delete delivery to sharedInbox
2023-12-08 02:41:37 -07:00
Daniel Supernault d1c297d1ad
Update DirectMessageController, revert delete delivery to sharedInbox 2023-12-08 02:36:56 -07:00
daniel 128415dbf8
Merge pull request #4802 from pixelfed/staging
Staging
2023-12-08 02:09:41 -07:00
Daniel Supernault 7f462a8055
Update DirectMessageController, dispatch deliver and delete actions to the job queue 2023-12-08 02:07:26 -07:00
Daniel Supernault d848792ad4
Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox 2023-12-08 01:38:17 -07:00
daniel a4030faa9d
Merge pull request #4801 from pixelfed/staging
Update Inbox handler, fix missing object_url and uri fields for direc…
2023-12-08 01:16:50 -07:00
Daniel Supernault 4cc66a838d
Update changelog 2023-12-08 01:15:41 -07:00
Daniel Supernault a0157fce0c
Update Inbox handler, fix missing object_url and uri fields for direct statuses 2023-12-08 01:13:04 -07:00
daniel b74e813cba
Merge pull request #4796 from pixelfed/staging
Update FederationController, add proper statuses counts
2023-12-05 00:56:12 -07:00
Daniel Supernault dec061f5ae
Update FederationController, add proper statuses counts 2023-12-05 00:55:41 -07:00
daniel f9badbf4dd
Merge pull request #4795 from pixelfed/staging
Staging
2023-12-05 00:50:51 -07:00
Daniel Supernault 3204fb9669
Update FederationController, add proper following/follower counts 2023-12-05 00:48:14 -07:00
daniel 6ffc964371
Merge pull request #4792 from mbliznikova/4790_4791_add_recently_deleted_post_to_collection_no_page_reloading_invalidate_cache_after_adding
4790 4791 add recently deleted post to collection no page reloading invalidate cache after adding
2023-12-05 00:28:42 -07:00
daniel baa653d7de
Merge pull request #4750 from mbliznikova/3698_make_unlisted_photos_visible_in_collections
3698 make unlisted photos visible in collections
2023-12-05 00:27:30 -07:00
daniel cdd153d385
Merge pull request #4591 from Happyfeet01/dev
Updating Libwebp6 to libwebp7
2023-12-05 00:23:46 -07:00
daniel c2ce63ecd3
Merge branch 'staging' into dev 2023-12-05 00:23:26 -07:00
daniel d83df5cd64
Merge pull request #4794 from pixelfed/staging
Add WebPush
2023-12-05 00:18:19 -07:00
Daniel Supernault 4a1363b929
Add WebPush 2023-12-03 22:18:38 -07:00
daniel 7a6ef5fcbc
Merge pull request #4787 from pixelfed/staging
 Enhanced Places/Location tagging
2023-12-03 03:09:40 -07:00
Daniel Supernault fadb4d6ea4
Update changelog 2023-12-03 03:07:58 -07:00
Daniel Supernault 8548294c7a
Update HomeFeedPipeline, observe mutes/blocks during fanout 2023-12-03 03:06:32 -07:00
Daniel Supernault fe9b4c5a37
Update FollowServiceWarmCache 2023-12-03 03:05:00 -07:00
mbliznikova 7cb075dbf9 #4790 User experience: add a post to a collection just right after deleting it from there 2023-11-30 00:20:08 +00:00
mbliznikova a7320535e9 #4791 Invalidate cache after adding a collection item for data consistency 2023-11-30 00:19:04 +00:00
Daniel Supernault 1ef885c1a1
Add migration to add state and other fields to places table 2023-11-26 04:30:48 -07:00
daniel 66dc955d11
Merge pull request #4783 from pixelfed/staging
Update StoryApiV1Controller, add self-carousel endpoint. Fixes #4352
2023-11-18 01:14:07 -07:00
Daniel Supernault b0e8810a91
Update changelog 2023-11-18 01:13:55 -07:00
Daniel Supernault bcb88d5b0a
Update StoryApiV1Controller, add self-carousel endpoint. Fixes #4352 2023-11-18 01:11:12 -07:00
daniel 6eb256860c
Merge pull request #4782 from pixelfed/staging
Update app:hashtag-related-generate command, add existing confirmation
2023-11-17 22:45:26 -07:00
Daniel Supernault e5e3be0598
Update app:hashtag-related-generate command, add existing confirmation 2023-11-17 22:45:04 -07:00
daniel 54b6c96112
Merge pull request #4777 from pixelfed/staging
Add Related Hashtags
2023-11-17 22:25:06 -07:00
Daniel Supernault d62a60a4ee
Update changelog 2023-11-17 22:22:49 -07:00
Daniel Supernault 176b4ed793
Add app:hashtag-related-generate command 2023-11-17 22:21:55 -07:00
Daniel Supernault aa166ab11a
Update ApiV1Controller, move tags endpoints to TagsController 2023-11-17 22:10:03 -07:00
Daniel Supernault 287f903bf3
Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic 2023-11-17 20:50:07 -07:00
Daniel Supernault 175203089b
Add Related Hashtags 2023-11-16 06:06:22 -07:00
daniel 47b0354e16
Merge pull request #4776 from pixelfed/staging
Update HashtagService, reduce cached_count cache ttl
2023-11-16 03:27:11 -07:00
Daniel Supernault 051eb962e1
Update changelog 2023-11-16 02:54:30 -07:00
Daniel Supernault 15f29f7d79
Update HashtagService, reduce cached_count cache ttl 2023-11-16 02:53:22 -07:00
daniel 0899f909d8
Merge pull request #4775 from pixelfed/staging
Add app:hashtag-cached-count-update command to update cached_count of…
2023-11-16 02:44:32 -07:00
Daniel Supernault 4aca04729b
Update changelog 2023-11-16 02:44:21 -07:00
Daniel Supernault 1e31fee6a6
Add app:hashtag-cached-count-update command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour 2023-11-16 02:43:11 -07:00
daniel 7f6d64b517
Merge pull request #4774 from pixelfed/staging
Staging
2023-11-16 00:50:09 -07:00
Daniel Supernault e6d3c7f4d7
Update changelog 2023-11-16 00:49:51 -07:00
Daniel Supernault f105f4e8f6
Update HomeFeedPipeline, fix tag filtering 2023-11-16 00:47:49 -07:00
Daniel Supernault e5401f8558
Update StatusHashtagService, remove problemaatic cache layer 2023-11-16 00:29:10 -07:00
daniel dfbc453b03
Merge pull request #4773 from pixelfed/staging
Update AP helpers, fix fanout scope
2023-11-15 23:44:23 -07:00
Daniel Supernault 33a60e767d
Update AP helpers, fix fanout scope 2023-11-15 23:43:57 -07:00
daniel 23dea20024
Merge pull request #4772 from pixelfed/staging
Update HashtagService, improve count perf
2023-11-15 23:24:46 -07:00
Daniel Supernault e1b39bcf6f
Update changelog 2023-11-15 23:24:32 -07:00
Daniel Supernault 3327a008fa
Update HashtagService, improve count perf 2023-11-15 23:23:17 -07:00
daniel 8b8fb5f6c0
Merge pull request #4771 from pixelfed/staging
Updaet HashtagUnfollowPipeline, fix typo
2023-11-15 22:37:46 -07:00
Daniel Supernault 3e96fa8a56
Updaet HashtagUnfollowPipeline, fix typo 2023-11-15 22:37:21 -07:00
daniel 0d48cf1c2e
Merge pull request #4770 from pixelfed/staging
Update Experimental Home Feed, fix remote posts, shares and reblogs
2023-11-15 22:17:15 -07:00
Daniel Supernault 19233cc976
Update HashtagFollowObserver 2023-11-15 22:16:23 -07:00
Daniel Supernault c6a6b3ae30
Update Experimental Home Feed, fix remote posts, shares and reblogs 2023-11-15 21:57:13 -07:00
daniel 57584391a4
Merge pull request #4769 from pixelfed/staging
Update ApiV1Controller
2023-11-15 00:18:02 -07:00
Daniel Supernault b365aa7e06
Update ApiV1Controller 2023-11-15 00:17:31 -07:00
daniel 40651c036a
Merge pull request #4768 from pixelfed/staging
Update HashtagUnfollowPipeline
2023-11-15 00:03:01 -07:00
Daniel Supernault c8092116e5
Update HashtagUnfollowPipeline 2023-11-15 00:02:17 -07:00
daniel c5cb2c0a1c
Merge pull request #4767 from pixelfed/staging
Update HashtagFollowService, fix cache invalidation bug
2023-11-14 23:56:58 -07:00
Daniel Supernault 84f4e88573
Update HashtagFollowService, fix cache invalidation bug 2023-11-14 23:55:53 -07:00
daniel f203d0540f
Merge pull request #4766 from pixelfed/staging
Update HashtagUnfollowPipeline
2023-11-14 23:06:49 -07:00
Daniel Supernault d8fbb4ff32
Update HashtagUnfollowPipeline 2023-11-14 23:05:17 -07:00
daniel 4071a4687d
Merge pull request #4759 from pixelfed/staging
Update IncrementPostCount job
2023-11-13 06:34:27 -07:00
Daniel Supernault cf50618696
Update FeedInsertPipeline, self fanout, oof 2023-11-13 06:33:52 -07:00
Daniel Supernault a5204f3e67
Update changelog 2023-11-13 06:12:30 -07:00
Daniel Supernault b2c9cc2318
Update IncrementPostCount job 2023-11-13 06:11:39 -07:00
daniel cac7c6bf3c
Merge pull request #4758 from pixelfed/staging
Update hashtag following
2023-11-13 05:32:47 -07:00
Daniel Supernault dde858bd5f
Update changelog 2023-11-13 05:32:36 -07:00
Daniel Supernault 015b1b80b4
Update hashtag following 2023-11-13 05:29:38 -07:00
daniel 2e2a200659
Merge pull request #4757 from pixelfed/staging
Update notification epoch generation
2023-11-13 02:01:07 -07:00
Daniel Supernault 446ca3a878
Update notification epoch generation 2023-11-13 01:59:38 -07:00
daniel 0a2a3b996d
Merge pull request #4756 from pixelfed/staging
Update mail config
2023-11-13 01:02:11 -07:00
Daniel Supernault 06bf0c14bf
Update changelog 2023-11-13 01:01:55 -07:00
Daniel Supernault 0e43127197
Update mail config 2023-11-13 01:00:53 -07:00
daniel e8439358cb
Merge pull request #4752 from pixelfed/staging
Experimental home feed
2023-11-13 00:50:26 -07:00
Daniel Supernault 05d646c034
Update changelog 2023-11-13 00:00:53 -07:00
Daniel Supernault c39b9afbfd
Update HomeTimelineService, apply filters to feed warm logic 2023-11-12 23:53:22 -07:00
Daniel Supernault 386e64d5e8
Update StatusEntityLexer, skip reblogs on FeedInsertPipeline 2023-11-12 23:52:44 -07:00
Daniel Supernault 125208fb9e
Update UserFilterObserver, dispatch FeedFollowPipeline jobs 2023-11-12 23:52:10 -07:00
Daniel Supernault e917341651
Update ApiV1Controller 2023-11-12 23:32:45 -07:00
Daniel Supernault 7deaaed4dd
Add migration 2023-11-12 23:32:14 -07:00
Daniel Supernault 43443503a1
Update FeedFollowPipeline, use more efficient query 2023-11-12 23:00:33 -07:00
Daniel Supernault 115a9d2dec
Update HomeTimelineService 2023-11-12 22:45:09 -07:00
Daniel Supernault 73cb8b43b3
Update HomeFeedPipeline, add follow/unfollow 2023-11-12 22:44:15 -07:00
Daniel Supernault 24c370ee22
Update ApiV1Controller, add experimental home timeline support to v1/timelines/home 2023-11-12 21:13:08 -07:00
Daniel Supernault 2a8a299058
Update HomeTimelineService 2023-11-12 21:09:06 -07:00
Daniel Supernault ce63c4997b
Add Feed fanout 2023-11-12 20:54:32 -07:00
Daniel Supernault de2b5ba4e9
Update FollowerService, reduce localFollowerIds ttl 2023-11-12 16:36:02 -07:00
Daniel Supernault df1f98d5f7
Add FeedInsertPipeline job 2023-11-11 07:23:42 -07:00
Daniel Supernault 20a560bfd1
Update FollowerService, add localFollowerIds method 2023-11-11 07:23:11 -07:00
Daniel Supernault 0fce5de6cd
Update composer deps 2023-11-11 05:53:16 -07:00
Daniel Supernault c806bbce3f
Update composer deps 2023-11-11 05:51:10 -07:00
Daniel Supernault 6aa65b9a21
Add FeedWarmCachePipeline 2023-11-11 05:49:41 -07:00
Daniel Supernault 1cd96ced2a
Update StatusHashtagObserver 2023-11-11 05:47:52 -07:00
Daniel Supernault 9dfc377322
Add HomeTimelineService 2023-11-11 05:46:37 -07:00
Daniel Supernault 448c061070
Update HomeFeedPipeline, add hashtag jobs 2023-11-11 05:43:33 -07:00
Daniel Supernault 1f35da0d4b
Update HashtagServices 2023-11-11 05:25:23 -07:00
Daniel Supernault ce54d29c69
Update delete pipelines, properly invoke StatusHashtag delete events 2023-11-11 03:40:59 -07:00
mbliznikova 2c6edf37a7 oFix #3698, make unlisted photos visible in collections 2023-11-11 04:08:29 +00:00
mbliznikova 170f877c26 Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging 2023-11-09 18:05:09 +00:00
daniel d20efd2c61
Merge pull request #4746 from pixelfed/staging
Update AP helpers, improve preferredUsername validation
2023-11-09 02:50:28 -07:00
Daniel Supernault d24c60576f
Update changelog 2023-11-09 02:50:09 -07:00
Daniel Supernault 21218c794b
Update AP helpers, improve preferredUsername validation 2023-11-09 02:47:20 -07:00
mbliznikova 439c8fc0ea Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging 2023-11-07 17:00:49 +00:00
daniel 1bdd0b3609
Merge pull request #4740 from pixelfed/staging
Update ApiV1Controller, fix mutes in home feed
2023-11-07 02:28:35 -07:00
Daniel Supernault ff272292ef
Update changelog 2023-11-07 02:27:16 -07:00
Daniel Supernault ddc217147c
Update ApiV1Controller, fix mutes in home feed 2023-11-07 02:24:52 -07:00
daniel c0575ae3bf
Merge pull request #4739 from pixelfed/staging
Add S3 command to rewrite media urls
2023-11-07 00:59:42 -07:00
Daniel Supernault d84c84c1e2
Update changelog 2023-11-07 00:54:01 -07:00
Daniel Supernault 5b3a56102f
Add S3 command to rewrite media urls 2023-11-07 00:49:36 -07:00
mbliznikova 3425821b55 Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging 2023-11-06 21:02:25 +00:00
daniel aaa0c7f76c
Merge pull request #4737 from pixelfed/staging
Update http client
2023-11-06 02:09:21 -07:00
Daniel Supernault c7b304ef20
Update http client 2023-11-06 02:08:51 -07:00
daniel 091fa1a62b
Merge pull request #4735 from pixelfed/staging
Update vue components, fix typos
2023-11-03 21:38:02 -06:00
Daniel Supernault a3fd0b032b
Update vue components, fix typos 2023-11-03 21:37:13 -06:00
daniel e45ede5a12
Merge pull request #4730 from pixelfed/staging
Staging
2023-11-02 04:37:39 -06:00
Daniel Supernault 960594f90d
Update changelog 2023-11-02 04:37:17 -06:00
Daniel Supernault c09a7d1127
Update compiled assets 2023-11-02 04:36:42 -06:00
Daniel Supernault 9c24157ab3
Update ImportPostController, fix IG bug with missing spaces between hashtags 2023-11-02 04:31:59 -06:00
Daniel Supernault 5a2d7e3eca
Update AccountImport 2023-11-02 04:31:15 -06:00
daniel e6301bfa51
Merge pull request #4726 from paulexyz/insta-import-utf8
fix: Instagram import broken UTF8 characters
2023-11-02 04:27:19 -06:00
mbliznikova 3269481148 Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging 2023-11-01 21:57:53 +00:00
paule 950baef58b fix: Instagram import broken UTF8 characters 2023-11-01 06:20:23 +01:00
daniel 4b9d0dc6ef
Merge pull request #4725 from pixelfed/staging
Update LikePipeline, dispatch to feed queue. Fixes #4723
2023-10-31 02:12:32 -06:00
Daniel Supernault da510089e2
Update LikePipeline, dispatch to feed queue. Fixes #4723 2023-10-30 21:16:28 -06:00
mbliznikova 770409c4a4 Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging 2023-10-30 20:47:18 +00:00
daniel 7960ab9222
Merge pull request #4721 from pixelfed/staging
Staging
2023-10-29 07:34:41 -06:00
Daniel Supernault eb291efe00
Update changelog 2023-10-29 05:29:56 -06:00
Daniel Supernault 4c6a0719ca
Update ApiV1Dot1Controller, add configurable app confirm rate limit ttl 2023-10-29 05:29:30 -06:00
Daniel Supernault 1686fc68e8
Update pixelfed config 2023-10-29 05:28:13 -06:00
Daniel Supernault 7cd9fa6e5b
Update pixelfed config 2023-10-29 05:27:12 -06:00
daniel 12fc8fd0c0
Merge pull request #4720 from pixelfed/staging
Update ApiV1Dot1Controller, update iar redirect url format
2023-10-29 04:50:58 -06:00
Daniel Supernault 3249695066
Update ApiV1Dot1Controller, update iar redirect url format 2023-10-29 04:50:20 -06:00
daniel e0208a7dd9
Merge pull request #4719 from pixelfed/staging
Update ApiV1Dot1Controller, update iar redirect url format
2023-10-29 04:07:59 -06:00
Daniel Supernault 432acb491a
Update ApiV1Dot1Controller, update iar redirect url format 2023-10-29 04:07:04 -06:00
daniel 81db60df32
Merge pull request #4718 from pixelfed/staging
Update ApiV1Dot1Controller, add domain to iar redirect
2023-10-29 03:49:47 -06:00
Daniel Supernault 1f82d47ce5
Update ApiV1Dot1Controller, add domain to iar redirect 2023-10-29 03:47:06 -06:00
daniel 4bac21d5d5
Merge pull request #4717 from pixelfed/staging
Staging
2023-10-29 03:08:19 -06:00
Daniel Supernault 28a808031b
Update ApiV1Dot1Controller, allow iar rate limits to be configurable 2023-10-29 03:06:10 -06:00
Daniel Supernault b58ed0ad01
Update pixelfed config 2023-10-29 03:04:48 -06:00
mbliznikova a8f78aa2ab Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging 2023-10-25 18:10:47 +00:00
daniel 2b17cc2c0d
Merge pull request #4709 from mbliznikova/check_if_collection_empty_in_edit
Add check if collection is empty in Edit Collection before publishing
2023-10-24 20:00:09 -06:00
daniel 1be012e439
Merge pull request #4698 from aneillans/strip-profile-tags-embed
Strip tags from bio in embeds
2023-10-24 19:59:28 -06:00
daniel 381e23e172
Merge pull request #4684 from mbliznikova/3596_provide_error_message_for_too_large_files
Provide the error message if a file to upload is too large, issue #3596
2023-10-24 19:56:28 -06:00
daniel d85c0c3d0a
Merge pull request #4676 from viviicat/fix-post-follow
Update Post component, adding follow and unfollow methods.
2023-10-24 19:54:28 -06:00
mbliznikova e3de4c3e68 Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging 2023-10-23 18:46:11 +00:00
daniel 42fb713092
Merge pull request #4713 from pixelfed/staging
Add WebP2P support for Video
2023-10-23 03:38:32 -06:00
Daniel Supernault 31fafb1b68
Update changelog 2023-10-23 02:47:29 -06:00
Daniel Supernault 7edfea0951
Update hls pipeline, improve version check 2023-10-23 02:46:56 -06:00
Daniel Supernault 6ab7e37a48
Update ffmpeg config 2023-10-23 02:31:54 -06:00
Daniel Supernault 0405ef1248
Update compiled assets 2023-10-23 01:58:16 -06:00
Daniel Supernault c63707b3ec
Update npm deps 2023-10-23 01:35:19 -06:00
Daniel Supernault f11ce7009f
Update PostContent, add new video-player component 2023-10-23 01:28:06 -06:00
Daniel Supernault e3f8cfb49e
Add hls/p2p video player 2023-10-23 01:27:11 -06:00
Daniel Supernault 5c358010b0
Update Config util, add hls attributes 2023-10-23 01:15:02 -06:00
Daniel Supernault 6cf4363c50
Update MediaService, remove hls_manifest attribute for MastoAPI entities 2023-10-23 01:14:33 -06:00
Daniel Supernault f0ba2dfc69
Update VideoThumbnail job, dispatch HLS job when applicable 2023-10-23 01:13:09 -06:00
Daniel Supernault 3f292459ff
Update VideoPipeline, add VideoHlsPipeline job for HLS generation 2023-10-23 01:12:40 -06:00
Daniel Supernault f9bbb05575
Update MediaDeletePipeline, handle HLS deletion 2023-10-23 01:09:16 -06:00
Daniel Supernault fac7c3c5e7
Update MediaTransformer, add hls_manifest attribute 2023-10-23 00:38:37 -06:00
Daniel Supernault 4e3e23db36
Add js debounce util 2023-10-23 00:15:53 -06:00
Daniel Supernault a144301085
Add RegisterForm component 2023-10-23 00:11:46 -06:00
Daniel Supernault 4cd53247a6
Add MediaHlsService 2023-10-22 23:42:25 -06:00
Daniel Supernault 82fc36b2b3
Update npm deps, add webp2p libs. Thanks @peertube <3 2023-10-22 23:41:03 -06:00
Daniel Supernault 00823545a5
Add WebP2P support for Video 2023-10-22 23:21:50 -06:00
daniel b4a918ef42
Merge pull request #4712 from pixelfed/staging
Update ComposeModal component, fix multi filter bug and allow media r…
2023-10-22 21:15:12 -06:00
Daniel Supernault 56e315f69f
Update ComposeModal component, fix multi filter bug and allow media re-ordering before upload/posting 2023-10-22 21:13:55 -06:00
Happyfeet01 2a0ef7620d
Merge branch 'pixelfed:dev' into dev 2023-10-22 21:03:36 +02:00
mbliznikova b838f90b77 Add check if collection is empty in Edit Collection before publishing 2023-10-20 21:16:19 +00:00
mbliznikova fdb51d1f5a Add check if collection is empty before publishing 2023-10-20 21:09:29 +00:00
daniel 352786144b
Merge pull request #4707 from pixelfed/staging
Update StatusTransformer
2023-10-20 00:18:21 -06:00
Daniel Supernault 65a048cdd5
Update StatusTransformer 2023-10-20 00:17:24 -06:00
daniel 7cbdac7adb
Merge pull request #4702 from pixelfed/staging
Update StatusTransformer, generate autolink on request
2023-10-16 06:30:16 -06:00
Daniel Supernault dfe2379b93
Update StatusTransformer, generate autolink on request 2023-10-16 06:28:57 -06:00
daniel 9f968d134e
Merge pull request #4700 from pixelfed/staging
Staging
2023-10-15 03:53:14 -06:00
Daniel Supernault 9677791bef
Update changelog 2023-10-15 03:52:20 -06:00
Daniel Supernault 778e83d398
Update lexer regex, fix mention regex and add more tests 2023-10-15 03:51:45 -06:00
daniel a1b280ec33
Merge pull request #4699 from pixelfed/staging
Update nodeinfo
2023-10-12 21:30:30 -06:00
Daniel Supernault 36df0d8373
Update nodeinfo 2023-10-12 21:29:51 -06:00
Andy Neillans e9d9c4d8cc Strip tags from bio in embeds 2023-10-11 19:08:22 +01:00
daniel 2fa595d2cf
Merge pull request #4697 from pixelfed/staging
Update RemoteStatusDelete, fix include
2023-10-11 05:25:10 -06:00
Daniel Supernault b76ad7cfe0
Update RemoteStatusDelete, fix include 2023-10-11 05:24:34 -06:00
daniel c906dbb26b
Merge pull request #4696 from pixelfed/staging
Update IncrementPostCount pipeline
2023-10-11 05:12:32 -06:00
Daniel Supernault 0d35f1a3e5
Update IncrementPostCount pipeline 2023-10-11 05:10:32 -06:00
daniel 627be42b36
Merge pull request #4695 from pixelfed/staging
Staging
2023-10-11 05:06:24 -06:00
Daniel Supernault f481f3d248
Update RemoteStatusDelete pipeline 2023-10-11 04:56:39 -06:00
Daniel Supernault edbcf3ed79
Update RemoteStatusDelete and DecrementPostCount pipelines 2023-10-11 04:42:40 -06:00
daniel 8f4f64d737
Merge pull request #4694 from pixelfed/staging
Update AvatarPipeline, improve refresh logic and garbage collection
2023-10-11 03:42:21 -06:00
Daniel Supernault 4e35f0d32e
Update changelog 2023-10-11 03:39:35 -06:00
Daniel Supernault c37b7cde30
Add `avatar:storage-deep-clean` command to dispatch avatar storage cleanup jobs 2023-10-11 03:38:31 -06:00
Daniel Supernault 319ced4054
Update CreateAvatar job, add processing constraints and set is_remote attribute 2023-10-11 01:32:04 -06:00
Daniel Supernault 95a1eddcb2
Update changelog 2023-10-11 00:48:56 -06:00
Daniel Supernault 82798b5ea3
Update AvatarPipeline, improve refresh logic and garbage collection to purge old avatars 2023-10-11 00:48:30 -06:00
daniel ffa44c4fad
Merge pull request #4693 from pixelfed/staging
Update AP helpers, adjust RemoteAvatarFetch ttl from 24h to 3 months
2023-10-10 21:28:02 -06:00
Daniel Supernault 36b23fe34e
Update AP helpers, adjust RemoteAvatarFetch ttl from 24h to 3 months 2023-10-10 20:55:20 -06:00
daniel 6f314fa0d2
Merge pull request #4692 from pixelfed/staging
Update user:admin command, improve logic. Fixes #2465
2023-10-10 20:23:16 -06:00
Daniel Supernault 01bac51104
Update user:admin command, improve logic. Fixes #2465 2023-10-10 20:20:18 -06:00
daniel dadb6ab416
Merge pull request #4691 from pixelfed/staging
Staging
2023-10-10 20:08:56 -06:00
Daniel Supernault 7bfe43095b
Update changelog 2023-10-10 20:08:16 -06:00
Daniel Supernault c6408fd79d
Add user:2fa command to easily disable 2FA for given account 2023-10-10 20:00:12 -06:00
daniel a276b2ce70
Merge pull request #4690 from pixelfed/staging
Update NotificationService, handle empty epoch. Fixes #4689
2023-10-10 19:14:12 -06:00
Daniel Supernault 457d5454f8
Update NotificationService, handle empty epoch. Fixes #4689 2023-10-10 19:13:19 -06:00
daniel f38226c527
Merge pull request #4688 from pixelfed/staging
Update AdminReportController, add `profile_id` to group by. Fixes #4685
2023-10-09 14:59:59 -06:00
Daniel Supernault e4d3b19642
Update AdminReportController, add `profile_id` to group by. Fixes #4685 2023-10-09 13:44:25 -06:00
daniel d679ae4f11
Merge pull request #4687 from pixelfed/staging
Update ApiV1Controller, hydrate reblog interactions. Fixes #4686
2023-10-09 13:08:27 -06:00
Daniel Supernault 135798eb68
Update ApiV1Controller, hydrate reblog interactions. Fixes #4686 2023-10-09 13:06:46 -06:00
mbliznikova 6c1e56fcb2 Provide the error message if a file to upload is too large 2023-10-06 02:05:22 +00:00
daniel ed20344c3e
Merge pull request #4682 from pixelfed/staging
Update profile embeds, filter sensitive posts
2023-09-30 14:49:12 -06:00
Daniel Supernault ede5ec3bf4
Update profile embeds, filter sensitive posts 2023-09-30 14:45:24 -06:00
Vivianne Langdon 4508697563 Update Post component, adding follow and unfollow methods. 2023-09-28 00:20:09 -07:00
daniel eb517aa8bf
Merge pull request #4674 from pixelfed/staging
Update Sign-in with Mastodon, allow usage when registrations are closed
2023-09-27 01:38:23 -06:00
Daniel Supernault a1e162f095
Update changelog 2023-09-27 01:38:05 -06:00
Daniel Supernault 895dc4fa9e
Update Sign-in with Mastodon, allow usage when registrations are closed 2023-09-27 01:33:39 -06:00
daniel 705f30b865
Merge pull request #4672 from pixelfed/staging
Staging
2023-09-26 23:17:19 -06:00
Daniel Supernault fcb4933369
Update changelog 2023-09-26 23:14:42 -06:00
Daniel Supernault 8c96919119
Update ap helpers, store media attachment width and height if present 2023-09-26 23:14:19 -06:00
Daniel Supernault ce1afe2711
Update Note and CreateNote transformers, include attachment blurhash, width and height 2023-09-26 23:10:20 -06:00
daniel d5baf2627a
Merge pull request #4670 from pixelfed/staging
Update StatusTagsPipeline, fix object tags slug query
2023-09-25 05:20:38 -06:00
Daniel Supernault 79b378cdb1
Update StatusTagsPipeline, fix object tags slug query 2023-09-25 05:20:04 -06:00
daniel dcc6f65e33
Merge pull request #4669 from pixelfed/staging
Update StatusTagsPipeline, fix object tags slug query
2023-09-25 05:14:22 -06:00
Daniel Supernault 9989d6c66f
Update StatusTagsPipeline, fix object tags slug query 2023-09-25 05:13:09 -06:00
daniel 7fdb87ef9b
Merge pull request #4668 from pixelfed/staging
Staging
2023-09-25 04:33:03 -06:00
Daniel Supernault bf5b72f082
Update changelog 2023-09-25 04:23:28 -06:00
Daniel Supernault d295e6059b
Update StatusTagsPipeline, fix object tags and slug normalization 2023-09-25 04:23:04 -06:00
daniel b2195ca837
Merge pull request #4667 from pixelfed/staging
Update Status model, allow unlisted thumbnails
2023-09-25 02:11:33 -06:00
Daniel Supernault 1f0a45b7f4
Update Status model, allow unlisted thumbnails 2023-09-25 02:10:19 -06:00
daniel 7b5999496e
Merge pull request #4666 from pixelfed/staging
Staging
2023-09-25 02:00:12 -06:00
Daniel Supernault 2d428f43e8
Update changelog 2023-09-25 01:58:09 -06:00
Daniel Supernault d969a97360
Update Status model, improve thumb logic 2023-09-25 01:57:43 -06:00
daniel 8a89570b4a
Merge pull request #4665 from pixelfed/staging
Add Resilient Media Storage
2023-09-25 01:14:11 -06:00
Daniel Supernault 439638f7d7
Update changelog 2023-09-25 01:13:14 -06:00
Daniel Supernault fb1deb6e28
Add Resilient Media Storage 2023-09-25 00:59:24 -06:00
daniel b91d263237
Merge pull request #4662 from pixelfed/staging
Update profile embed, fix resize
2023-09-21 06:48:16 -06:00
Daniel Supernault dcdfb28dcd
Update changelog 2023-09-21 06:47:59 -06:00
Daniel Supernault dc23c21db0
Update profile embed, fix resize 2023-09-21 06:42:10 -06:00
daniel 5a9a159708
Merge pull request #4656 from pixelfed/staging
Update NotificationService, fix order bug
2023-09-18 00:25:28 -06:00
Daniel Supernault 0210f8aa2a
Update NotificationService, fix order bug 2023-09-18 00:25:01 -06:00
daniel 19015f18b0
Merge pull request #4655 from pixelfed/staging
Update StatusService, fix logic check
2023-09-18 00:17:58 -06:00
Daniel Supernault 61d235b797
Update StatusService, fix logic check 2023-09-18 00:17:22 -06:00
daniel 4112ab5f83
Merge pull request #4654 from pixelfed/staging
Update NotificationService, improve cache warming query
2023-09-18 00:00:46 -06:00
Daniel Supernault 5ab7f9958c
Update changelog 2023-09-17 23:57:52 -06:00
Daniel Supernault 223661ecb2
Update StatusService, hydrate accounts on request instead of caching them along with status objects 2023-09-17 23:54:31 -06:00
Daniel Supernault 2496386d9b
Update NotificationService, improve cache warming query 2023-09-17 23:51:42 -06:00
daniel 155e1704ff
Merge pull request #4649 from pixelfed/staging
Add AdminShadowFilter model/migration
2023-09-14 22:39:29 -06:00
Daniel Supernault 33ed7a8c91
Add AdminShadowFilter feature 2023-09-14 22:32:37 -06:00
Daniel Supernault a510c3e89c
Add AdminShadowFilter model/migration 2023-09-14 22:23:46 -06:00
daniel e36d7da841
Merge pull request #4644 from pixelfed/staging
Staging
2023-09-10 01:18:10 -06:00
Daniel Supernault 3979e33b57
Update changelog 2023-09-10 01:14:08 -06:00
Daniel Supernault 8fa2afe016
Remove unused resource 2023-09-10 01:13:45 -06:00
Daniel Supernault 941736ce6c
Update StoryApiV1Controller, add viewers route to view story viewers 2023-09-10 01:12:27 -06:00
daniel c4843e823e
Merge pull request #4642 from b2cc/bugfix-4518-unable-to-view-reports-in-admin
[Bugfix] #4518: SQL query that generates the report list in the admin view needs to include the 'id' field
2023-09-09 20:48:18 -06:00
daniel 6ec4077549
Merge pull request #4643 from ThisIsMissEm/fix/memory-leak-in-blurhash-calculation
Fix potential memory leak due to not calling imagedestroy on GdImage objects
2023-09-09 15:31:11 -06:00
Emelia Smith 74ad26fee6
Fix potential memory leak due to not calling imagedestroy on GdImage objects 2023-09-09 22:55:06 +02:00
David Gabriel 2e5c141724 Fix similar SQL error which triggers when mentioning people in new posts 2023-09-09 20:46:50 +02:00
David Gabriel 480394f3d8 [Bugfix] Fix for #4518: SQL query that generates the report list in the admin view needs to include the 'id' field 2023-09-09 19:50:45 +02:00
daniel e286f98762
Merge pull request #4624 from pixelfed/staging
Update ProfileTransformer, fix Mastodon indexable context
2023-08-27 21:43:38 -06:00
Daniel Supernault 83900a3b00
Update ProfileTransformer, fix Mastodon indexable context 2023-08-27 21:43:11 -06:00
daniel 5dc397ec1f
Merge pull request #4613 from pixelfed/staging
Update Profile AP transformer, fix context
2023-08-24 23:55:09 -06:00
Daniel Supernault 817b494703
Update Profile AP transformer, fix context 2023-08-24 23:54:08 -06:00
daniel 24db7d71cf
Merge pull request #4612 from pixelfed/staging
Add support for Mastodon indexable search flag
2023-08-24 23:41:14 -06:00
Daniel Supernault 23bc985b36
Update changelog 2023-08-24 23:37:11 -06:00
Daniel Supernault fbdcdd9dbc
Update AP Helpers, consume actor `indexable` attribute 2023-08-24 23:36:50 -06:00
Daniel Supernault fc24630eba
Update Privacy Settings, add support for Mastodon indexable search flag 2023-08-24 23:31:33 -06:00
daniel 28bca423d7
Merge pull request #4603 from pixelfed/staging
Prepare for groups
2023-08-23 03:57:22 -06:00
Daniel Supernault a3696dac95
Update changelog 2023-08-23 03:54:00 -06:00
Daniel Supernault 61a6d90403
Update FollowServiceWarmCache, improve handling larger following/follower lists 2023-08-23 03:45:51 -06:00
Daniel Supernault 93c7ad9779
Update groups migration 2023-08-23 02:45:41 -06:00
Daniel Supernault 347e4f59a3
Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging 2023-08-23 02:39:16 -06:00
Daniel Supernault a04ba18113
Add Groups migrations 2023-08-22 00:25:15 -06:00
daniel 3cb50af8a3
Merge pull request #4601 from pixelfed/staging
v0.11.9 🚀
2023-08-21 22:45:48 -06:00
Daniel Supernault a7e4305043
Bump version to 0.11.9 2023-08-21 22:45:13 -06:00
Daniel Supernault ca746717cb
Update ApiV1Controller, add bookmarked to timeline entities 2023-08-21 22:43:30 -06:00
Happyfeet01 1ea65db70d
Update Dockerfile.fpm
update libwp6 to libwp7
2023-08-16 11:05:55 +02:00
Happyfeet01 a6a0333170
Update Dockerfile.apache
Update libwp6 to libwp7
2023-08-16 11:05:11 +02:00
daniel b6c3ac4b13
Merge pull request #4578 from pixelfed/staging
Add Account Migrations
2023-08-08 00:03:18 -06:00
Daniel Supernault 526807f01c
Update web routes 2023-08-08 00:02:10 -06:00
Daniel Supernault 781d3c0ec3
Update changelog 2023-08-08 00:00:39 -06:00
Daniel Supernault 47e5c07061
Add ProfileAliasController, hello account migrations! 2023-08-08 00:00:24 -06:00
Daniel Supernault dc7973de62
Update Profile model, add aliases relation 2023-08-07 23:59:29 -06:00
Daniel Supernault 9378c65396
Add AccountMigration ActivityPub support 2023-08-07 23:59:10 -06:00
Daniel Supernault eab16e7fd8
Add Help Center Documentation for Account Migration 2023-08-07 23:58:21 -06:00
Daniel Supernault 3103af2fe4
Add Account Migration setting views 2023-08-07 23:58:02 -06:00
Daniel Supernault 3c60362648
Add moved_to_profile_id migration 2023-08-07 23:56:01 -06:00
Daniel Supernault a9220e4e01
Add Account Migrations 2023-08-07 23:48:58 -06:00
daniel 211a9057fc
Merge pull request #4576 from pixelfed/staging
Update RemoteStatusPipeline, fix reply check
2023-08-06 21:52:35 -06:00
Daniel Supernault 618b67271a
Update RemoteStatusPipeline, fix reply check 2023-08-06 21:52:01 -06:00
daniel 268804856b
Merge pull request #4575 from pixelfed/staging
Staging
2023-08-06 21:31:47 -06:00
Daniel Supernault fab8f25e9b
Update RemoteStatusDelete pipeline 2023-08-06 15:31:37 -06:00
Daniel Supernault 4f19a58b2c
Add AdminProfileActionPipeline 2023-08-01 05:48:43 -06:00
Daniel Supernault 6161cf45aa
Add AdminProfile resource 2023-08-01 05:48:27 -06:00
Daniel Supernault 71e92261f4
Update RemoteStatusDelete pipeline 2023-08-01 05:07:58 -06:00
Daniel Supernault 45be6e10b8
Update RemoteStatusDelete pipeline 2023-08-01 04:40:39 -06:00
Daniel Supernault ed87ddb923
Update RemoteStatusDelete pipeline 2023-08-01 04:30:50 -06:00
Daniel Supernault 3d1b6516fe
Update ActivityPubFetchService, add validateUrl parameter to bypass url validation to fetch content from blocked instances 2023-08-01 03:16:08 -06:00
daniel 2a63ff1d40
Merge pull request #4570 from pixelfed/staging
Staging
2023-07-31 21:22:03 -06:00
Daniel Supernault f2dfe12ac3
Update changelog 2023-07-31 21:21:37 -06:00
Daniel Supernault 3e90f6cee5
Update compiled assets 2023-07-31 21:21:23 -06:00
Daniel Supernault 911504fa54
Update PostContent, add text cw warning 2023-07-31 21:15:50 -06:00
daniel 5cdf076527
Merge pull request #4566 from pixelfed/staging
Add Photo reblogs only setting
2023-07-30 06:35:18 -06:00
Daniel Supernault c527858ac4
Update changelog 2023-07-30 06:31:17 -06:00
Daniel Supernault 75bfd21104
Update compiled assets 2023-07-30 06:31:09 -06:00
Daniel Supernault e2705b9ae9
Update timeline settings, add photo reblogs only option 2023-07-30 06:29:52 -06:00
Daniel Supernault dccec7d5a9
Update SettingsController, add photo_reblogs_only setting 2023-07-30 06:29:12 -06:00
daniel 2cdf8917da
Merge pull request #4565 from pixelfed/staging
Staging
2023-07-30 06:03:24 -06:00
Daniel Supernault 74a6b169d3
Update changelog 2023-07-30 06:02:38 -06:00
Daniel Supernault f4d46d8148
Update compiled assets 2023-07-30 06:02:30 -06:00
Daniel Supernault 29de91e5d0
Update Timeline component, improve reblog support 2023-07-30 06:01:12 -06:00
Daniel Supernault ec2a1ed99c
Update SharePipeline 2023-07-30 05:50:39 -06:00
Daniel Supernault 5a19daabce
Update SharePipeline 2023-07-30 05:45:14 -06:00
Daniel Supernault 13bdaa2ed4
Update ApiV1Controller, hydrate reblog state in home timeline 2023-07-30 05:16:51 -06:00
daniel e559187411
Merge pull request #4563 from pixelfed/staging
Reblogs in feeds
2023-07-30 04:11:47 -06:00
Daniel Supernault 685d45a8df
Update changelog 2023-07-30 04:11:25 -06:00
Daniel Supernault b86d47bfec
Update compiled assets 2023-07-30 04:07:49 -06:00
Daniel Supernault 8efb4047b1
Update Timeline component 2023-07-30 04:04:49 -06:00
Daniel Supernault 3b885709b8
Update ApiV1Dot1Controller 2023-07-30 04:01:29 -06:00
Daniel Supernault f54cf0b2d7
Update timeline settings 2023-07-30 03:59:35 -06:00
Daniel Supernault 0eca48f1a4
Update SettingsController 2023-07-30 03:55:51 -06:00
Daniel Supernault 1c13b518be
Update StatusStatelessTransformer, allow unlisted reblogs 2023-07-30 02:25:49 -06:00
Daniel Supernault c469d47552
Update admin users view, fix website value. Closes #4557 2023-07-30 00:58:31 -06:00
daniel acadd1b473
Merge pull request #4562 from pixelfed/staging
Update ProfileController, allow albums in atom feed. Closes #4561. Fi…
2023-07-30 00:51:11 -06:00
Daniel Supernault 1c105a6ce3
Update ProfileController, allow albums in atom feed. Closes #4561. Fixes #4526 2023-07-30 00:49:23 -06:00
daniel 93b2dbab17
Merge pull request #4554 from pixelfed/staging
Staging
2023-07-22 03:51:46 -06:00
Daniel Supernault 6c36995083
Update changelog 2023-07-22 03:50:43 -06:00
Daniel Supernault 59b643789f
Update StatusService, reduce cache ttl from 7 days to 6 hours 2023-07-22 03:50:23 -06:00
daniel 8e7963c0c5
Merge pull request #4549 from pixelfed/staging
Add Health check endpoint at /api/service/health-check
2023-07-18 02:12:59 -06:00
Daniel Supernault ecc697a241
Update changelog 2023-07-18 02:12:10 -06:00
Daniel Supernault ff58f9707f
Add Health check endpoint at /api/service/health-check 2023-07-18 02:11:06 -06:00
daniel 1a3176c996
Merge pull request #4548 from pixelfed/staging
Staging
2023-07-18 00:12:43 -06:00
Daniel Supernault 3f22640644
Update changelog 2023-07-17 23:44:43 -06:00
Daniel Supernault 95fb893f95
Update compiled assets 2023-07-17 23:44:30 -06:00
Daniel Supernault acabf603f0
Update Remote Auth feature, fix custom domain bug and enforce banned domains 2023-07-17 23:43:19 -06:00
daniel 8911ace102
Merge pull request #4545 from pixelfed/staging
Sign-in with Mastodon
2023-07-16 23:43:36 -06:00
Daniel Supernault 780e78f21a
Update changelog 2023-07-16 23:00:08 -06:00
Daniel Supernault a3dd7c95df
Update compiled assets 2023-07-16 22:59:52 -06:00
Daniel Supernault 852dbd8d34
Add remote_auth.js 2023-07-16 22:58:21 -06:00
Daniel Supernault 9cfa89dab4
Update routes and add RemoteAuthController 2023-07-16 22:54:02 -06:00
Daniel Supernault 0b90d629d5
Update User, fix last_active_at guard 2023-07-16 18:42:43 -06:00
Daniel Supernault 45b9404ec1
Add Sign-in with Mastodon 2023-07-16 07:09:15 -06:00
daniel 45090a0e76
Merge pull request #4544 from pixelfed/staging
Update MediaStorageService, improve head header handling
2023-07-16 05:41:49 -06:00
Daniel Supernault ce02f05718
Update changelog 2023-07-16 04:29:35 -06:00
Daniel Supernault 0d802c313b
Update FanoutDeletePipeline, fix AP object 2023-07-16 04:27:14 -06:00
Daniel Supernault 7a431af93a
Update Media model 2023-07-16 04:24:58 -06:00
Daniel Supernault ff2c16fe74
Update admin user view, improve previews 2023-07-16 04:23:41 -06:00
Daniel Supernault 3590adbd87
Update MediaStorageService, improve head header handling 2023-07-16 04:17:23 -06:00
daniel 1a30e488f9
Merge pull request #4541 from pixelfed/staging
Update user model
2023-07-14 04:33:41 -06:00
Daniel Supernault 4b611be2d9
Update user model 2023-07-14 04:33:05 -06:00
daniel ef58c3b304
Merge pull request #4540 from pixelfed/staging
Staging
2023-07-14 04:30:00 -06:00
Daniel Supernault c07233a1c1
Update changelog 2023-07-14 04:29:43 -06:00
Daniel Supernault e0b48b2976
Update admin users blade view, show last_active_at and other info 2023-07-14 04:28:37 -06:00
Daniel Supernault 2bef3e415d
Update AP Helpers, improve url validation and add optional dns verification, disabled by default 2023-07-14 03:10:48 -06:00
daniel 57582b1b2e
Merge pull request #4539 from pixelfed/staging
Staging
2023-07-14 01:50:53 -06:00
Daniel Supernault a00a520bf3
Update composer deps 2023-07-14 01:50:43 -06:00
Daniel Supernault 84669ac614
Update changelog 2023-07-14 01:48:34 -06:00
Daniel Supernault ba7551d8a9
Update TransformImports command, increment status_count on profile model 2023-07-14 01:46:51 -06:00
Daniel Supernault 0b5157675f
Update FollowPipeline, improve follower/following count calculation 2023-07-14 01:38:36 -06:00
Daniel Supernault c61d0b915f
Update SearchApiV2Service, improve resolve query logic to better handle remote posts/profiles and local posts/profiles 2023-07-14 01:22:49 -06:00
Daniel Supernault 0704c7e05e
Update AP Helpers, preserve admin unlisted state before adding to NetworkTimelineService 2023-07-14 01:20:26 -06:00
Daniel Supernault 9fa6b3f7aa
Update Inbox, allow storing Create->Note activities without any local followers, disabled by default 2023-07-13 23:11:19 -06:00
Daniel Supernault 4b2c66f557
Update Services, use zpopmin on predis 2023-07-13 22:06:21 -06:00
Daniel Supernault 1cc6274ac0
Update rate limits, fixes #4537 2023-07-13 21:38:18 -06:00
Daniel Supernault 9233cd8f5b
Add migration 2023-07-11 02:27:08 -06:00
daniel cc561c0522
Merge pull request #4533 from pixelfed/staging
Staging
2023-07-11 00:09:56 -06:00
Daniel Supernault 947847898a
Update changelog 2023-07-10 23:41:40 -06:00
Daniel Supernault 37fd03428a
Update ApiV1Controller, add include_reblogs attribute to home timeline 2023-07-10 23:41:17 -06:00
Daniel Supernault c244d8b5c8
Update composer 2023-07-10 18:49:52 -06:00
daniel 1809cb217c
Merge pull request #4515 from pixelfed/staging
Staging
2023-07-01 22:39:29 -06:00
Daniel Supernault 4276d3f248
Update changelog 2023-07-01 22:38:58 -06:00
Daniel Supernault 73b35d3231
Update compiled assets 2023-07-01 22:38:53 -06:00
Daniel Supernault 625a76a51d
Update AccountImport, add select first 100 posts button 2023-07-01 22:38:16 -06:00
Daniel Supernault 2d959fb354
Update ComposeModal.vue, fix scroll issue and dont hide scrollbar 2023-07-01 22:36:22 -06:00
daniel 924ce5eea9
Merge pull request #4510 from pixelfed/staging
Update StatusRemoteUpdatePipeline, fix missing mime and size attribut…
2023-06-28 06:41:58 -06:00
Daniel Supernault 43b101dbfa
Add FetchMissingMediaMimeType command 2023-06-28 06:41:32 -06:00
Daniel Supernault 72844c0715
Update changelog 2023-06-28 06:29:33 -06:00
Daniel Supernault ea54413e95
Update StatusRemoteUpdatePipeline, fix missing mime and size attributes that cause empty media previews on our mobile app 2023-06-28 06:28:13 -06:00
daniel f041f5f60a
Merge pull request #4509 from pixelfed/staging
Staging
2023-06-28 05:03:56 -06:00
Daniel Supernault 60747bfb15
Update changelog 2023-06-28 05:03:06 -06:00
Daniel Supernault 04f4f8baf1
Update FixStatusCount, improve command and support remote count resync 2023-06-28 05:02:41 -06:00
daniel da90bf630a
Merge pull request #4505 from pixelfed/staging
Update IG Import commands, fix stalled import queue
2023-06-26 05:53:45 -06:00
Daniel Supernault 2f2e446c1f
Update ImportService 2023-06-26 05:39:16 -06:00
Daniel Supernault fe6123c820
Update ImportPostController 2023-06-26 05:38:29 -06:00
Daniel Supernault 6fd53a3001
Update changelog 2023-06-26 04:47:43 -06:00
Daniel Supernault 10dd348c28
Update ImportService, filter deleted posts from getImportedPosts endpoint 2023-06-26 04:46:57 -06:00
Daniel Supernault d6d60a8574
Update changelog 2023-06-26 04:34:12 -06:00
Daniel Supernault afe6948da8
Update console kernel, add import upload gc 2023-06-26 04:33:47 -06:00
Daniel Supernault b47e8f8e3e
Update changelog 2023-06-26 04:18:39 -06:00
Daniel Supernault 892907d5d1
Update TransformImports command, improve handling of imported posts that already exist or are from deleted accounts 2023-06-26 04:16:49 -06:00
Daniel Supernault b18f3fba8b
Update IG Import commands, fix stalled import queue 2023-06-26 03:21:38 -06:00
daniel 48cd829572
Merge pull request #4504 from pixelfed/staging
Update ActivityPubFetchService, fix authorized_fetch support
2023-06-25 23:11:20 -06:00
Daniel Supernault c64c4aa1cb
Update ActivityPubFetchService, fix authorized_fetch compatibility. Closes #1850, #2713, #2935 2023-06-25 23:10:36 -06:00
Daniel Supernault 63a7879c29
Update ActivityPubFetchService 2023-06-25 23:02:02 -06:00
Daniel Supernault b89c4f1cdc
Update ActivityPubFetchService, fix authorized_fetch support 2023-06-25 22:26:04 -06:00
daniel 4412a6b5dd
Merge pull request #4500 from pixelfed/staging
Staging
2023-06-22 06:48:23 -06:00
Daniel Supernault 763ce19a0a
Update AdminApiController, improve admin moderation tools 2023-06-22 05:43:42 -06:00
Daniel Supernault 71ad7d5d43
Update AdminUser resource 2023-06-22 01:12:53 -06:00
Daniel Supernault 4f850e54ad
Update AdminApiController, include more data for getUser method 2023-06-22 01:08:15 -06:00
daniel 8606040858
Merge pull request #4492 from pixelfed/staging
Staging
2023-06-20 05:12:16 -06:00
Daniel Supernault 52f9999fcc
wip 2023-06-20 05:11:00 -06:00
Daniel Supernault 5c5541fc01
Update AdminApiController 2023-06-20 05:09:15 -06:00
Daniel Supernault a6d10f0389
Update RegisterController 2023-06-20 05:03:49 -06:00
Daniel Supernault 89c3710d3c
Update AdminApiController, add instance stats endpoint 2023-06-20 00:34:02 -06:00
daniel 2dcfd68f01
Merge pull request #4468 from pixelfed/staging
Update ImportPostController, fix typo
2023-06-12 06:20:01 -06:00
Daniel Supernault ba58aaba36
Update ImportPostController, fix typo 2023-06-12 06:19:09 -06:00
daniel 29554d20b7
Merge pull request #4467 from pixelfed/staging
Update sidebar/settings
2023-06-12 06:09:33 -06:00
Daniel Supernault c394fb76c6
Update sidebar/settings 2023-06-12 06:08:25 -06:00
daniel d76ae33eb9
Merge pull request #4466 from pixelfed/staging
Import from Instagram
2023-06-12 05:56:27 -06:00
Daniel Supernault 6d0c5994da
Update TransformImports command, bump limit from 10 to 100 2023-06-12 05:53:29 -06:00
Daniel Supernault 477986abdd
Add import scheduler 2023-06-12 05:46:56 -06:00
Daniel Supernault aabc20dd2e
Update changelog 2023-06-12 05:32:42 -06:00
Daniel Supernault cf3078c569
Update compiled assets 2023-06-12 05:31:23 -06:00
Daniel Supernault 7de67650c7
Update import blade view 2023-06-12 05:30:18 -06:00
Daniel Supernault 5d8599a497
Update npm deps 2023-06-12 05:29:16 -06:00
Daniel Supernault 828f369373
Add AccountImport component 2023-06-12 05:26:24 -06:00
Daniel Supernault 9f3e809f26
Add Import Help Center page 2023-06-12 05:16:20 -06:00
Daniel Supernault 7e0335b246
Update routes 2023-06-12 05:14:03 -06:00
Daniel Supernault 49e5703198
Add ImportPostController 2023-06-12 05:13:50 -06:00
Daniel Supernault b64af89d40
Update migration and service 2023-06-12 04:56:18 -06:00
Daniel Supernault 7dd45c23b7
Add ImportStatus Resource 2023-06-12 02:52:00 -06:00
Daniel Supernault 8c9f4da48a
Add ImportPost model, migration and service 2023-06-12 02:51:29 -06:00
Daniel Supernault c50e0966db
Add import config 2023-06-12 02:22:00 -06:00
daniel 1dd9617da2
Merge pull request #4465 from pixelfed/staging
Add missing vue components + spa.js
2023-06-11 16:57:49 -06:00
Daniel Supernault 7217c962bf
Update webpack 2023-06-11 16:47:23 -06:00
Daniel Supernault 1f6d11736a
Add partial components 2023-06-11 16:38:28 -06:00
Daniel Supernault cbf086ccb4
Add spa.js 2023-06-11 16:24:37 -06:00
Daniel Supernault adf015be43
Update webpack 2023-06-11 16:20:05 -06:00
Daniel Supernault d48e8d9832
Add NavMenu component 2023-06-11 15:38:14 -06:00
Daniel Supernault 67bf3d10e0
Update components.js 2023-06-11 15:36:00 -06:00
Daniel Supernault 36f5f2e8b4
Update admin.js, add autospam + reports components 2023-06-11 15:35:34 -06:00
Daniel Supernault bffd8f0771
Add section components 2023-06-11 15:35:06 -06:00
Daniel Supernault 9817025578
Add Profile components 2023-06-11 15:32:44 -06:00
Daniel Supernault f5dbc8281a
Add partial components 2023-06-11 15:23:23 -06:00
Daniel Supernault 5361082026
Add partial components 2023-06-11 15:16:20 -06:00
Daniel Supernault fff692a25c
Add NotFound component 2023-06-11 15:05:06 -06:00
Daniel Supernault 751f30d845
Add Language component 2023-06-11 15:04:31 -06:00
Daniel Supernault add6cf0fe1
Add Home component 2023-06-11 15:03:36 -06:00
Daniel Supernault b12b956b7d
Add Hashtag component 2023-06-11 15:01:44 -06:00
Daniel Supernault b447db082f
Add Discover components 2023-06-11 15:01:02 -06:00
Daniel Supernault bb97b55c66
Add Direct+DirectMessage components 2023-06-11 14:57:06 -06:00
Daniel Supernault 495b78afba
Add changelog component 2023-06-11 14:55:43 -06:00
Daniel Supernault 163cf5b3c9
Add compose component 2023-06-11 14:54:40 -06:00
Daniel Supernault ab2a11341c
Add admin components 2023-06-11 14:53:37 -06:00
daniel 398a42b383
Merge pull request #4463 from pixelfed/staging
Update admin dashboard, improve users section
2023-06-10 05:09:27 -06:00
Daniel Supernault 7cd59f75e4
Update admin dashboard, improve users section 2023-06-10 05:08:16 -06:00
daniel 7736324ee7
Merge pull request #4462 from pixelfed/staging
Update admin dashboard, improve users section
2023-06-10 03:41:32 -06:00
Daniel Supernault 8be4582fc4
Update changelog 2023-06-10 03:41:23 -06:00
Daniel Supernault 36b6bf480e
Update admin dashboard, improve users section 2023-06-10 03:40:31 -06:00
daniel 341a032949
Merge pull request #4456 from pixelfed/staging
Update profile
2023-06-06 06:21:57 -06:00
Daniel Supernault 2cbf1acec3
Update profile 2023-06-06 06:21:24 -06:00
daniel 61346b104c
Merge pull request #4455 from pixelfed/staging
Handle Update.Person activities
2023-06-06 06:14:03 -06:00
Daniel Supernault 0f72b33c0e
Add tests 2023-06-06 06:12:24 -06:00
Daniel Supernault e3be04db2b
Update HandleUpdateActivity 2023-06-06 06:08:00 -06:00
Daniel Supernault e8d4ce1888
Update MediaStorageService 2023-06-06 06:01:49 -06:00
Daniel Supernault d6374cfe70
Update MediaStorageService 2023-06-06 05:54:49 -06:00
Daniel Supernault ab9a8ba314
Update Profile 2023-06-06 05:50:08 -06:00
Daniel Supernault 5bea903409
Update HandleUpdateActivity 2023-06-06 05:47:45 -06:00
Daniel Supernault 1bf3ad7ed9
Update HandleUpdateActivity 2023-06-06 05:47:17 -06:00
Daniel Supernault 31afaba3d0
Update ProfilePipeline 2023-06-06 05:21:44 -06:00
Daniel Supernault 0eb51ed74d
Update RemoteAvatarFetch command 2023-06-06 05:07:14 -06:00
Daniel Supernault 29961c4a80
Update ap inbox 2023-06-06 04:53:09 -06:00
daniel 2e79a89706
Merge pull request #4454 from pixelfed/staging
Staging
2023-06-06 01:57:34 -06:00
Daniel Supernault 8cc91babd7
Update changelog 2023-06-06 01:56:51 -06:00
Daniel Supernault a8453e7719
Update api routes, add DeprecatedEndpoint middleware 2023-06-06 01:55:28 -06:00
daniel 498d7fec80
Merge pull request #4450 from pixelfed/staging
Update nginx config
2023-06-04 08:15:12 -06:00
Daniel Supernault b49310a2d3
Update changelog 2023-06-04 08:14:51 -06:00
Daniel Supernault fbdc635829
Update nginx config 2023-06-04 08:13:08 -06:00
daniel 4c0c29ee37
Merge pull request #4445 from pixelfed/staging
Staging
2023-06-02 06:46:35 -06:00
Daniel Supernault fccd927c0b
Update changelog 2023-06-02 06:45:49 -06:00
Daniel Supernault fe8728c0ba
Update Direct Messages, fix api endpoint 2023-06-02 06:45:14 -06:00
daniel 97ce98b234
Merge pull request #4444 from pixelfed/staging
Staging
2023-06-02 05:37:29 -06:00
Daniel Supernault eaff1a7607
Update compiled assets 2023-06-02 05:36:37 -06:00
Daniel Supernault 6197a7b1e1
Update Notifications.vue component, fix filtering logic to prevent endless spinner 2023-06-02 05:36:00 -06:00
daniel ad88a07118
Merge pull request #4443 from pixelfed/staging
Update Notifications.vue component, fix filtering logic to prevent en…
2023-06-02 05:32:57 -06:00
Daniel Supernault 69a53d0fa1
Update changelog 2023-06-02 05:32:25 -06:00
Daniel Supernault b495918b3c
Update compiled assets 2023-06-02 05:32:10 -06:00
Daniel Supernault 3df9b53f4e
Update Notifications.vue component, fix filtering logic to prevent endless spinner 2023-06-02 05:30:11 -06:00
Shlee 031290d987
Update NotificationCard.vue 2022-12-12 00:31:34 +10:30
Shlee 312bc06685
Update NotificationCard.vue 2022-12-11 15:09:29 +10:30
Nils van Lück c96bcd559d Check imported emojis for mimetype
Signed-off-by: Nils van Lück <nils@vanlueck.dev>
2022-12-08 21:10:10 +01:00
Nils van Lück af28aecf21 Add a command to import emoji archives 2022-12-04 22:54:27 +01:00
675 zmienionych plików z 72302 dodań i 30643 usunięć

Wyświetl plik

@ -21,7 +21,12 @@ jobs:
steps:
- checkout
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
- run:
name: "Create Environment file and generate app key"
command: |
mv .env.testing .env
- run: sudo apt install zlib1g-dev libsqlite3-dev
# Download and cache dependencies
@ -36,18 +41,17 @@ jobs:
- run: composer install -n --prefer-dist
- save_cache:
key: composer-v2-{{ checksum "composer.lock" }}
key: v2-dependencies-{{ checksum "composer.json" }}
paths:
- vendor
- run: cp .env.testing .env
- run: php artisan config:cache
- run: php artisan route:clear
- run: php artisan storage:link
- run: php artisan key:generate
# run tests with phpunit or codecept
- run: ./vendor/bin/phpunit
- run: php artisan test
- store_test_results:
path: tests/_output
- store_artifacts:

Wyświetl plik

@ -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 "$@"

Wyświetl plik

@ -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

Wyświetl plik

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

Plik diff jest za duży Load Diff

Wyświetl plik

@ -8,6 +8,7 @@ OPEN_REGISTRATION="false"
ENFORCE_EMAIL_VERIFICATION="false"
PF_MAX_USERS="1000"
OAUTH_ENABLED="true"
ENABLE_CONFIG_CACHE=true
# Media Configuration
PF_OPTIMIZE_IMAGES="true"

Wyświetl plik

@ -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

7
.gitattributes vendored
Wyświetl plik

@ -3,3 +3,10 @@
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
# Collapse diffs for generated files:
public/**/*.js text -diff
public/**/*.json text -diff
public/**/*.css text -diff
public/img/* binary -diff
public/fonts/* binary -diff

Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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
Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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.

Wyświetl plik

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

12
.shellcheckrc 100644
Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -1,8 +1,307 @@
# Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.8...dev)
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.1...dev)
- ([](https://github.com/pixelfed/pixelfed/commit/))
### Updates
- Update DirectMessageController, add 72 hour delay for new accounts before they can send a DM ([61d105fd](https://github.com/pixelfed/pixelfed/commit/61d105fd))
- Update AdminCuratedRegisterController, increase message length from 1000 to 3000 ([9a5e3471](https://github.com/pixelfed/pixelfed/commit/))
- ([](https://github.com/pixelfed/pixelfed/commit/9a5e3471))
## [v0.12.1 (2024-05-07)](https://github.com/pixelfed/pixelfed/compare/v0.12.0...v0.12.1)
### Updates
- Update ApiV1Dot1Controller, fix in app registration bug that prevents proper auth flow due to missing oauth scopes ([cbf996c9](https://github.com/pixelfed/pixelfed/commit/cbf996c9))
- Update ConfigCacheService, fix database race condition and fallback to file config and enable by default ([60a62b59](https://github.com/pixelfed/pixelfed/commit/60a62b59))
## [v0.12.0 (2024-04-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.13...v0.12.0)
### Updates
- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6))
- Update Post.vue, fix cache bug ([3a27e637](https://github.com/pixelfed/pixelfed/commit/3a27e637))
- Update StatusHashtagService, use more efficient cached count ([592c8412](https://github.com/pixelfed/pixelfed/commit/592c8412))
- Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a))
- Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c))
- Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8))
- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7))
- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06))
- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9))
- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4))
- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595))
- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f))
- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843))
- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6))
- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f))
- Update web-api popular accounts route to its own method to remove the breaking oauth scope bug ([a4bc5ce3](https://github.com/pixelfed/pixelfed/commit/a4bc5ce3))
- Update config cache ([5e4d4eff](https://github.com/pixelfed/pixelfed/commit/5e4d4eff))
- Update Config, use config_cache ([7785a2da](https://github.com/pixelfed/pixelfed/commit/7785a2da))
- Update ApiV1Dot1Controller, use config_cache for in-app registration ([b0cb4456](https://github.com/pixelfed/pixelfed/commit/b0cb4456))
- Update captcha, use config_cache helper ([8a89e3c9](https://github.com/pixelfed/pixelfed/commit/8a89e3c9))
- Update custom emoji, add config_cache support ([481314cd](https://github.com/pixelfed/pixelfed/commit/481314cd))
- Update ProfileController, fix permalink redirect bug ([75081e60](https://github.com/pixelfed/pixelfed/commit/75081e60))
- Update admin css, use font-display:swap for nucleo icons ([8a0c456e](https://github.com/pixelfed/pixelfed/commit/8a0c456e))
- Update PixelfedDirectoryController, fix boolean cast bug ([f08aab22](https://github.com/pixelfed/pixelfed/commit/f08aab22))
- Update PixelfedDirectoryController, use cached stats ([f2f2a809](https://github.com/pixelfed/pixelfed/commit/f2f2a809))
- Update AdminDirectoryController, fix type casting ([ad506e90](https://github.com/pixelfed/pixelfed/commit/ad506e90))
- Update image pipeline, use config_cache ([a72188a7](https://github.com/pixelfed/pixelfed/commit/a72188a7))
- Update cloud storage, use config_cache ([665581d8](https://github.com/pixelfed/pixelfed/commit/665581d8))
- Update pixelfed.max_album_length, use config_cache ([fecbe189](https://github.com/pixelfed/pixelfed/commit/fecbe189))
- Update media_types, use config_cache ([d670de17](https://github.com/pixelfed/pixelfed/commit/d670de17))
- Update landing settings, use config_cache ([40478f25](https://github.com/pixelfed/pixelfed/commit/40478f25))
- Update activitypub setting, use config_cache ([5071aaf4](https://github.com/pixelfed/pixelfed/commit/5071aaf4))
- Update oauth setting, use config_cache ([ce228f7f](https://github.com/pixelfed/pixelfed/commit/ce228f7f))
- Update stories config, use config_cache ([d1adb109](https://github.com/pixelfed/pixelfed/commit/d1adb109))
- Update ig import, use config_cache ([da0e0ffa](https://github.com/pixelfed/pixelfed/commit/da0e0ffa))
- Update autospam config, use config_cache ([a76cb5f4](https://github.com/pixelfed/pixelfed/commit/a76cb5f4))
- Update app.name config, use config_cache ([911446c0](https://github.com/pixelfed/pixelfed/commit/911446c0))
- Update UserObserver, fix type casting ([949e9979](https://github.com/pixelfed/pixelfed/commit/949e9979))
- Update user_filters, use config_cache ([6ce513f8](https://github.com/pixelfed/pixelfed/commit/6ce513f8))
- Update filesystems config, add to config_cache ([087b2791](https://github.com/pixelfed/pixelfed/commit/087b2791))
- Update web-admin routes, add setting api routes ([828a456f](https://github.com/pixelfed/pixelfed/commit/828a456f))
- Update hashtag component ([cee979ed](https://github.com/pixelfed/pixelfed/commit/cee979ed))
- Update AdminReadMore component, add .prevent to click action ([704e7b12](https://github.com/pixelfed/pixelfed/commit/704e7b12))
- Update admin dashboard, add admin settings partials ([eb487123](https://github.com/pixelfed/pixelfed/commit/eb487123))
- Update admin settings, refactor to vue component ([674e560f](https://github.com/pixelfed/pixelfed/commit/674e560f))
- Update ConfigCacheService, encrypt keys at rest ([3628b462](https://github.com/pixelfed/pixelfed/commit/3628b462))
- Update RemoteFollowImportRecent, use MediaPathService ([5162c070](https://github.com/pixelfed/pixelfed/commit/5162c070))
- Update AdminSettingsController, add user filter max limit settings ([ac1f0748](https://github.com/pixelfed/pixelfed/commit/ac1f0748))
- Update AdminSettingsController, add AdminSettingsService ([dcc5f416](https://github.com/pixelfed/pixelfed/commit/dcc5f416))
- Update AdminSettings component, fix user settings ([aba1e13d](https://github.com/pixelfed/pixelfed/commit/aba1e13d))
- Update AdminInstances component ([ec2fdd61](https://github.com/pixelfed/pixelfed/commit/ec2fdd61))
- Update AdminSettings, add max_account_size support ([2dcbc1d5](https://github.com/pixelfed/pixelfed/commit/2dcbc1d5))
- Update AdminSettings, use better validation for user integer settings ([d946afcc](https://github.com/pixelfed/pixelfed/commit/d946afcc))
- Update spa sass, fix timestamp dark mode bug ([4147f7c5](https://github.com/pixelfed/pixelfed/commit/4147f7c5))
- Update relationships view, fix unfollow hashtag bug. Fixes #5008 ([8c693640](https://github.com/pixelfed/pixelfed/commit/8c693640))
- Update PrivacySettings controller, refresh RelationshipService when unmute/unblocking ([b7322b68](https://github.com/pixelfed/pixelfed/commit/b7322b68))
- Update ApiV1Controller, improve refresh relations logic when (un)muting or (un)blocking ([b8e96a5f](https://github.com/pixelfed/pixelfed/commit/b8e96a5f))
- Update context menu, add mute/block/unfollow actions and update relationship store accordingly ([81d1e0fd](https://github.com/pixelfed/pixelfed/commit/81d1e0fd))
- Update docker env, fix config_cache. Fixes #5033 ([858fcbf6](https://github.com/pixelfed/pixelfed/commit/858fcbf6))
- Update UnfollowPipeline, fix follower count cache bug ([6bdf73de](https://github.com/pixelfed/pixelfed/commit/6bdf73de))
- Update VideoPresenter component, add webkit-playsinline attribute to video element to prevent the full screen video player ([ad032916](https://github.com/pixelfed/pixelfed/commit/ad032916))
- Update VideoPlayer component, add playsinline attribute to video element ([8af23607](https://github.com/pixelfed/pixelfed/commit/8af23607))
- Update StatusController, refactor status embeds ([9a7acc12](https://github.com/pixelfed/pixelfed/commit/9a7acc12))
- Update ProfileController, refactor profile embeds ([8b8b1ffc](https://github.com/pixelfed/pixelfed/commit/8b8b1ffc))
- Update profile embed view, fix height bug ([65166570](https://github.com/pixelfed/pixelfed/commit/65166570))
- Update CustomEmojiService, only return local emoji ([7f8bba44](https://github.com/pixelfed/pixelfed/commit/7f8bba44))
- Update Like model, increase max likes per day from 500 to 1500 ([4223119f](https://github.com/pixelfed/pixelfed/commit/4223119f))
## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)
### Features
- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212))
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f))
### Updates
- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641))
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607))
- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106))
- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997))
- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b))
- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655))
- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58))
- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3))
- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7))
- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599))
- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a))
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
### Features
- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
### Updates
- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))
- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3))
- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8))
- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194))
## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11)
### Fixes
- Fix api endpoints ([fd7f5dbb](https://github.com/pixelfed/pixelfed/commit/fd7f5dbb))
## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10)
### Added
- Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6))
- Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12))
- Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7))
- Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde))
- Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610))
- Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb))
- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6))
- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7))
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1))
- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9))
### Federation
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
- Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d))
### Updates
- Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59))
- Update FollowServiceWarmCache, improve handling larger following/follower lists ([61a6d904](https://github.com/pixelfed/pixelfed/commit/61a6d904))
- Update StoryApiV1Controller, add viewers route to view story viewers ([941736ce](https://github.com/pixelfed/pixelfed/commit/941736ce))
- Update NotificationService, improve cache warming query ([2496386d](https://github.com/pixelfed/pixelfed/commit/2496386d))
- Update StatusService, hydrate accounts on request instead of caching them along with status objects ([223661ec](https://github.com/pixelfed/pixelfed/commit/223661ec))
- Update profile embed, fix resize ([dc23c21d](https://github.com/pixelfed/pixelfed/commit/dc23c21d))
- Update Status model, improve thumb logic ([d969a973](https://github.com/pixelfed/pixelfed/commit/d969a973))
- Update Status model, allow unlisted thumbnails ([1f0a45b7](https://github.com/pixelfed/pixelfed/commit/1f0a45b7))
- Update StatusTagsPipeline, fix object tags and slug normalization ([d295e605](https://github.com/pixelfed/pixelfed/commit/d295e605))
- Update Note and CreateNote transformers, include attachment blurhash, width and height ([ce1afe27](https://github.com/pixelfed/pixelfed/commit/ce1afe27))
- Update ap helpers, store media attachment width and height if present ([8c969191](https://github.com/pixelfed/pixelfed/commit/8c969191))
- Update Sign-in with Mastodon, allow usage when registrations are closed ([895dc4fa](https://github.com/pixelfed/pixelfed/commit/895dc4fa))
- Update profile embeds, filter sensitive posts ([ede5ec3b](https://github.com/pixelfed/pixelfed/commit/ede5ec3b))
- Update ApiV1Controller, hydrate reblog interactions. Fixes ([#4686](https://github.com/pixelfed/pixelfed/issues/4686)) ([135798eb](https://github.com/pixelfed/pixelfed/commit/135798eb))
- Update AdminReportController, add `profile_id` to group by. Fixes ([#4685](https://github.com/pixelfed/pixelfed/issues/4685)) ([e4d3b196](https://github.com/pixelfed/pixelfed/commit/e4d3b196))
- Update user:admin command, improve logic. Fixes ([#2465](https://github.com/pixelfed/pixelfed/issues/2465)) ([01bac511](https://github.com/pixelfed/pixelfed/commit/01bac511))
- Update AP helpers, adjust RemoteAvatarFetch ttl from 24h to 3 months ([36b23fe3](https://github.com/pixelfed/pixelfed/commit/36b23fe3))
- Update AvatarPipeline, improve refresh logic and garbage collection to purge old avatars ([82798b5e](https://github.com/pixelfed/pixelfed/commit/82798b5e))
- Update CreateAvatar job, add processing constraints and set `is_remote` attribute ([319ced40](https://github.com/pixelfed/pixelfed/commit/319ced40))
- Update RemoteStatusDelete and DecrementPostCount pipelines ([edbcf3ed](https://github.com/pixelfed/pixelfed/commit/edbcf3ed))
- Update lexer regex, fix mention regex and add more tests ([778e83d3](https://github.com/pixelfed/pixelfed/commit/778e83d3))
- Update StatusTransformer, generate autolink on request ([dfe2379b](https://github.com/pixelfed/pixelfed/commit/dfe2379b))
- Update ComposeModal component, fix multi filter bug and allow media re-ordering before upload/posting ([56e315f6](https://github.com/pixelfed/pixelfed/commit/56e315f6))
- Update ApiV1Dot1Controller, allow iar rate limits to be configurable ([28a80803](https://github.com/pixelfed/pixelfed/commit/28a80803))
- Update ApiV1Dot1Controller, add domain to iar redirect ([1f82d47c](https://github.com/pixelfed/pixelfed/commit/1f82d47c))
- Update ApiV1Dot1Controller, add configurable app confirm rate limit ttl ([4c6a0719](https://github.com/pixelfed/pixelfed/commit/4c6a0719))
- Update LikePipeline, dispatch to feed queue. Fixes ([#4723](https://github.com/pixelfed/pixelfed/issues/4723)) ([da510089](https://github.com/pixelfed/pixelfed/commit/da510089))
- Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e))
- Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a))
- Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714))
- Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79))
- Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c))
- Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271))
- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80))
- Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23))
- Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885))
- Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae))
- Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008))
- Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85))
- Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8))
- Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d))
- Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b))
- Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b))
- Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a))
- Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c))
- Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
- Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
- Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce))
- Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox ([d848792a](https://github.com/pixelfed/pixelfed/commit/d848792a))
- Update DirectMessageController, dispatch deliver and delete actions to the job queue ([7f462a80](https://github.com/pixelfed/pixelfed/commit/7f462a80))
- Update Inbox, improve story attribute collection ([06bee36c](https://github.com/pixelfed/pixelfed/commit/06bee36c))
- Update DirectMessageController, dispatch local deletes to pipeline ([98186564](https://github.com/pixelfed/pixelfed/commit/98186564))
- Update StatusPipeline, fix Direct and Story notification deletion ([4c95306f](https://github.com/pixelfed/pixelfed/commit/4c95306f))
- Update Notifications.vue, fix deprecated DM action links for story activities ([4c3823b0](https://github.com/pixelfed/pixelfed/commit/4c3823b0))
- Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119))
- Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888))
- Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2))
- Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135))
- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393))
- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7))
- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545))
- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a))
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1))
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168))
- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171))
- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a))
- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b))
- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04))
- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381))
- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577))
- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c))
- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389))
- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3))
- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe))
- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47))
- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a))
- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade))
- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1))
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
### Added
- Import from Instagram ([#4466](https://github.com/pixelfed/pixelfed/pull/4466)) ([cf3078c5](https://github.com/pixelfed/pixelfed/commit/cf3078c5))
- Sign-in with Mastodon ([#4545](https://github.com/pixelfed/pixelfed/pull/4545)) ([45b9404e](https://github.com/pixelfed/pixelfed/commit/45b9404e))
- Health check endpoint at /api/service/health-check ([ff58f970](https://github.com/pixelfed/pixelfed/commit/ff58f970))
- Reblogs in home feed ([#4563](https://github.com/pixelfed/pixelfed/pull/4563)) ([b86d47bf](https://github.com/pixelfed/pixelfed/commit/b86d47bf))
- Account Migrations ([#4578](https://github.com/pixelfed/pixelfed/pull/4578)) ([a9220e4e](https://github.com/pixelfed/pixelfed/commit/a9220e4e))
### Updates
- Update Notifications.vue component, fix filtering logic to prevent endless spinner ([3df9b53f](https://github.com/pixelfed/pixelfed/commit/3df9b53f))
- Update Direct Messages, fix api endpoint ([fe8728c0](https://github.com/pixelfed/pixelfed/commit/fe8728c0))
- Update nginx config ([fbdc6358](https://github.com/pixelfed/pixelfed/commit/fbdc6358))
- Update api routes, add DeprecatedEndpoint middleware. For more info, visit [pixelfed.org/kb/10404](https://pixelfed.org/kb/10404) ([a8453e77](https://github.com/pixelfed/pixelfed/commit/a8453e77))
- Update admin dashboard, improve users section ([36b6bf48](https://github.com/pixelfed/pixelfed/commit/36b6bf48))
- Update AdminApiController, add instance stats endpoint ([89c3710d](https://github.com/pixelfed/pixelfed/commit/89c3710d))
- Update config, re-add `PF_MAX_USERS` .env variable to limit max users to 1000 by default ([a6d10f03](https://github.com/pixelfed/pixelfed/commit/a6d10f03))
- Update AdminApiController, fix stats ([5c5541fc](https://github.com/pixelfed/pixelfed/commit/5c5541fc))
- Update AdminApiController, include more data for getUser method ([4f850e54](https://github.com/pixelfed/pixelfed/commit/4f850e54))
- Update AdminApiController, improve admin moderation tools ([763ce19a](https://github.com/pixelfed/pixelfed/commit/763ce19a))
- Update ActivityPubFetchService, fix authorized_fetch compatibility. Closes #1850, #2713, #2935 ([63a7879c](https://github.com/pixelfed/pixelfed/commit/63a7879c))
- Update IG Import commands, fix stalled import queue ([b18f3fba](https://github.com/pixelfed/pixelfed/commit/b18f3fba))
- Update TransformImports command, improve handling of imported posts that already exist or are from deleted accounts ([892907d5](https://github.com/pixelfed/pixelfed/commit/892907d5))
- Update console kernel, add import upload gc ([afe6948d](https://github.com/pixelfed/pixelfed/commit/afe6948d))
- Update ImportService, filter deleted posts from getImportedPosts endpoint ([10dd348c](https://github.com/pixelfed/pixelfed/commit/10dd348c))
- Update FixStatusCount, improve command and support remote count resync ([04f4f8ba](https://github.com/pixelfed/pixelfed/commit/04f4f8ba))
- Update StatusRemoteUpdatePipeline, fix missing mime and size attributes that cause empty media previews on our mobile app ([ea54413e](https://github.com/pixelfed/pixelfed/commit/ea54413e))
- Update ComposeModal.vue, fix scroll issue and dont hide scrollbar ([2d959fb3](https://github.com/pixelfed/pixelfed/commit/2d959fb3))
- Update AccountImport, add select first 100 posts button ([625a76a5](https://github.com/pixelfed/pixelfed/commit/625a76a5))
- Update ApiV1Controller, add include_reblogs attribute to home timeline ([37fd0342](https://github.com/pixelfed/pixelfed/commit/37fd0342))
- Update rate limits, fixes #4537 ([1cc6274a](https://github.com/pixelfed/pixelfed/commit/1cc6274a))
- Update Services, use zpopmin on predis ([4b2c66f5](https://github.com/pixelfed/pixelfed/commit/4b2c66f5))
- Update Inbox, allow storing Create->Note activities without any local followers, disabled by default ([9fa6b3f7](https://github.com/pixelfed/pixelfed/commit/9fa6b3f7))
- Update AP Helpers, preserve admin unlisted state before adding to NetworkTimelineService ([0704c7e0](https://github.com/pixelfed/pixelfed/commit/0704c7e0))
- Update SearchApiV2Service, improve resolve query logic to better handle remote posts/profiles and local posts/profiles ([c61d0b91](https://github.com/pixelfed/pixelfed/commit/c61d0b91))
- Update FollowPipeline, improve follower/following count calculation ([0b515767](https://github.com/pixelfed/pixelfed/commit/0b515767))
- Update TransformImports command, increment status_count on profile model ([ba7551d8](https://github.com/pixelfed/pixelfed/commit/ba7551d8))
- Update AP Helpers, improve url validation and add optional dns verification, disabled by default ([2bef3e41](https://github.com/pixelfed/pixelfed/commit/2bef3e41))
- Update admin users blade view, show last_active_at and other info ([e0b48b29](https://github.com/pixelfed/pixelfed/commit/e0b48b29))
- Update MediaStorageService, improve head header handling ([3590adbd](https://github.com/pixelfed/pixelfed/commit/3590adbd))
- Update admin user view, improve previews ([ff2c16fe](https://github.com/pixelfed/pixelfed/commit/ff2c16fe))
- Update FanoutDeletePipeline, fix AP object ([0d802c31](https://github.com/pixelfed/pixelfed/commit/0d802c31))
- Update Remote Auth feature, fix custom domain bug and enforce banned domains ([acabf603](https://github.com/pixelfed/pixelfed/commit/acabf603))
- Update StatusService, reduce cache ttl from 7 days to 6 hours ([59b64378](https://github.com/pixelfed/pixelfed/commit/59b64378))
- Update ProfileController, allow albums in atom feed. Closes #4561. Fixes #4526 ([1c105a6c](https://github.com/pixelfed/pixelfed/commit/1c105a6c))
- Update admin users view, fix website value. Closes #4557 ([c469d475](https://github.com/pixelfed/pixelfed/commit/c469d475))
- Update StatusStatelessTransformer, allow unlisted reblogs ([1c13b518](https://github.com/pixelfed/pixelfed/commit/1c13b518))
- Update ApiV1Controller, hydrate reblog state in home timeline ([13bdaa2e](https://github.com/pixelfed/pixelfed/commit/13bdaa2e))
- Update Timeline component, improve reblog support ([29de91e5](https://github.com/pixelfed/pixelfed/commit/29de91e5))
- Update timeline settings, add photo reblogs only option ([e2705b9a](https://github.com/pixelfed/pixelfed/commit/e2705b9a))
- Update PostContent, add text cw warning ([911504fa](https://github.com/pixelfed/pixelfed/commit/911504fa))
- Update ActivityPubFetchService, add validateUrl parameter to bypass url validation to fetch content from blocked instances ([3d1b6516](https://github.com/pixelfed/pixelfed/commit/3d1b6516))
- Update RemoteStatusDelete pipeline ([71e92261](https://github.com/pixelfed/pixelfed/commit/71e92261))
- Update RemoteStatusDelete pipeline ([fab8f25e](https://github.com/pixelfed/pixelfed/commit/fab8f25e))
- Update RemoteStatusPipeline, fix reply check ([618b6727](https://github.com/pixelfed/pixelfed/commit/618b6727))
- Update ApiV1Controller, add bookmarked to timeline entities ([ca746717](https://github.com/pixelfed/pixelfed/commit/ca746717))
## [v0.11.8 (2023-05-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.7...v0.11.8)

18
CODEOWNERS 100644
Wyświetl plik

@ -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 100644
Wyświetl plik

@ -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 \
&& ENABLE_CONFIG_CACHE=false 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"]

Wyświetl plik

@ -18,8 +18,7 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke
protected function getExtraParams(AccessTokenEntityInterface $accessToken)
{
return [
'created_at' => time(),
'scope' => 'read write follow push'
'created_at' => time(),
];
}
}

Wyświetl plik

@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\AccountService;
use App\Services\Account\AccountStatService;
use App\Status;
use App\Profile;
class AccountPostCountStatUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:account-post-count-stat-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update post counts from recent activities';
/**
* Execute the console command.
*/
public function handle()
{
$ids = AccountStatService::getAllPostCountIncr();
if(!$ids || !count($ids)) {
return;
}
foreach($ids as $id) {
$acct = AccountService::get($id, true);
if(!$acct) {
AccountStatService::removeFromPostCount($id);
continue;
}
$statusCount = Status::whereProfileId($id)->count();
if($statusCount != $acct['statuses_count']) {
$profile = Profile::find($id);
if(!$profile) {
AccountStatService::removeFromPostCount($id);
continue;
}
$profile->status_count = $statusCount;
$profile->save();
AccountService::del($id);
}
AccountStatService::removeFromPostCount($id);
}
return;
}
}

Wyświetl plik

@ -0,0 +1,106 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\User;
use App\Models\DefaultDomainBlock;
use App\Models\UserDomainBlock;
use function Laravel\Prompts\text;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\progress;
class AddUserDomainBlock extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:add-user-domain-block';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Apply a domain block to all users';
/**
* Execute the console command.
*/
public function handle()
{
$domain = text('Enter domain you want to block');
$domain = strtolower($domain);
$domain = $this->validateDomain($domain);
if(!$domain || empty($domain)) {
$this->error('Invalid domain');
return;
}
$this->processBlocks($domain);
return;
}
protected function validateDomain($domain)
{
if(!strpos($domain, '.')) {
return;
}
if(str_starts_with($domain, 'https://')) {
$domain = str_replace('https://', '', $domain);
}
if(str_starts_with($domain, 'http://')) {
$domain = str_replace('http://', '', $domain);
}
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
if(!$valid) {
return;
}
if($domain === config('pixelfed.domain.app')) {
$this->error('Invalid domain');
return;
}
$confirmed = confirm('Are you sure you want to block ' . $domain . '?');
if(!$confirmed) {
return;
}
return $domain;
}
protected function processBlocks($domain)
{
DefaultDomainBlock::updateOrCreate([
'domain' => $domain
]);
progress(
label: 'Updating user domain blocks...',
steps: User::lazyById(500),
callback: fn ($user) => $this->performTask($user, $domain),
);
}
protected function performTask($user, $domain)
{
if(!$user->profile_id || $user->delete_after) {
return;
}
if($user->status != null && $user->status != 'disabled') {
return;
}
UserDomainBlock::updateOrCreate([
'profile_id' => $user->profile_id,
'domain' => $domain
]);
}
}

Wyświetl plik

@ -82,7 +82,7 @@ class AvatarStorage extends Command
$this->line(' ');
if(config_cache('pixelfed.cloud_storage')) {
if((bool) config_cache('pixelfed.cloud_storage')) {
$this->info('✅ - Cloud storage configured');
$this->line(' ');
}
@ -92,7 +92,7 @@ class AvatarStorage extends Command
$this->line(' ');
}
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
if((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
$disk = Storage::disk(config_cache('filesystems.cloud'));
$exists = $disk->exists('cache/avatars/default.jpg');
$state = $exists ? '✅' : '❌';
@ -100,7 +100,7 @@ class AvatarStorage extends Command
$this->info($msg);
}
$options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
$options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
[
'Cancel',
'Upload default avatar to cloud',
@ -164,7 +164,7 @@ class AvatarStorage extends Command
protected function uploadAvatarsToCloud()
{
if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
if(!(bool) config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
$this->error('Enable cloud storage and avatar cloud storage to perform this action');
return;
}
@ -213,7 +213,7 @@ class AvatarStorage extends Command
return;
}
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
if((bool) config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
$this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
return;
}

Wyświetl plik

@ -0,0 +1,115 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Cache;
use Storage;
use App\Avatar;
use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
class AvatarStorageDeepClean extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'avatar:storage-deep-clean';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Cleanup avatar storage';
protected $shouldKeepRunning = true;
protected $counter = 0;
/**
* Execute the console command.
*/
public function handle(): void
{
$this->info(' ____ _ ______ __ ');
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
$this->info(' ');
$this->info(' Pixelfed Avatar Deep Cleaner');
$this->line(' ');
$this->info(' Purge/delete old and outdated avatars from remote accounts');
$this->line(' ');
$storage = [
'cloud' => (bool) config_cache('pixelfed.cloud_storage'),
'local' => boolval(config_cache('federation.avatars.store_local'))
];
if(!$storage['cloud'] && !$storage['local']) {
$this->error('Remote avatars are not cached locally, there is nothing to purge. Aborting...');
exit;
}
$start = 0;
if(!$this->confirm('Are you sure you want to proceed?')) {
$this->error('Aborting...');
exit;
}
if(!$this->activeCheck()) {
$this->info('Found existing deep cleaning job');
if(!$this->confirm('Do you want to continue where you left off?')) {
$this->error('Aborting...');
exit;
} else {
$start = Cache::has('cmd:asdp') ? (int) Cache::get('cmd:asdp') : (int) Storage::get('avatar-deep-clean.json');
if($start && $start < 1 || $start > PHP_INT_MAX) {
$this->error('Error fetching cached value');
$this->error('Aborting...');
exit;
}
}
}
$count = Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->count();
$bar = $this->output->createProgressBar($count);
foreach(Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->lazyById(10, 'id') as $avatar) {
usleep(random_int(50, 1000));
$this->counter++;
$this->handleAvatar($avatar);
$bar->advance();
}
$bar->finish();
}
protected function updateCache($id)
{
Cache::put('cmd:asdp', $id);
if($this->counter % 5 === 0) {
Storage::put('avatar-deep-clean.json', $id);
}
}
protected function activeCheck()
{
if(Storage::exists('avatar-deep-clean.json') || Cache::has('cmd:asdp')) {
return false;
}
return true;
}
protected function handleAvatar($avatar)
{
$this->updateCache($avatar->id);
$queues = ['feed', 'mmo', 'feed', 'mmo', 'feed', 'feed', 'mmo', 'low'];
$queue = $queues[random_int(0, 7)];
AvatarStorageCleanup::dispatch($avatar)->onQueue($queue);
}
}

Wyświetl plik

@ -35,12 +35,16 @@ class CloudMediaMigrate extends Command
*/
public function handle()
{
$enabled = config('pixelfed.cloud_storage');
$enabled = (bool) config_cache('pixelfed.cloud_storage');
if(!$enabled) {
$this->error('Cloud storage not enabled. Exiting...');
return;
}
if(!$this->confirm('Are you sure you want to proceed?')) {
return;
}
$limit = $this->option('limit');
$hugeMode = $this->option('huge');

Wyświetl plik

@ -0,0 +1,96 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\User;
use App\Models\DefaultDomainBlock;
use App\Models\UserDomainBlock;
use function Laravel\Prompts\text;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\progress;
class DeleteUserDomainBlock extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:delete-user-domain-block';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove a domain block for all users';
/**
* Execute the console command.
*/
public function handle()
{
$domain = text('Enter domain you want to unblock');
$domain = strtolower($domain);
$domain = $this->validateDomain($domain);
if(!$domain || empty($domain)) {
$this->error('Invalid domain');
return;
}
$this->processUnblocks($domain);
return;
}
protected function validateDomain($domain)
{
if(!strpos($domain, '.')) {
return;
}
if(str_starts_with($domain, 'https://')) {
$domain = str_replace('https://', '', $domain);
}
if(str_starts_with($domain, 'http://')) {
$domain = str_replace('http://', '', $domain);
}
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
if(!$valid) {
return;
}
if($domain === config('pixelfed.domain.app')) {
return;
}
$confirmed = confirm('Are you sure you want to unblock ' . $domain . '?');
if(!$confirmed) {
return;
}
return $domain;
}
protected function processUnblocks($domain)
{
DefaultDomainBlock::whereDomain($domain)->delete();
if(!UserDomainBlock::whereDomain($domain)->count()) {
$this->info('No results found!');
return;
}
progress(
label: 'Updating user domain blocks...',
steps: UserDomainBlock::whereDomain($domain)->lazyById(500),
callback: fn ($domainBlock) => $this->performTask($domainBlock),
);
}
protected function performTask($domainBlock)
{
$domainBlock->deleteQuietly();
}
}

Wyświetl plik

@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use App\Media;
use App\Services\MediaService;
use App\Services\StatusService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class FetchMissingMediaMimeType extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:fetch-missing-media-mime-type';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
foreach (Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) {
$res = Http::retry(2, 100, throw: false)->head($media->remote_url);
if (! $res->successful()) {
continue;
}
if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
continue;
}
$media->mime = $res->header('content-type');
if ($res->hasHeader('content-length')) {
$media->size = $res->header('content-length');
}
$media->save();
MediaService::del($media->status_id);
StatusService::del($media->status_id);
$this->info('mid:'.$media->id.' ('.$res->header('content-type').':'.$res->header('content-length').' bytes)');
}
}
}

Wyświetl plik

@ -37,7 +37,7 @@ class FixMediaDriver extends Command
return Command::SUCCESS;
}
if(config_cache('pixelfed.cloud_storage') == false) {
if((bool) config_cache('pixelfed.cloud_storage') == false) {
$this->error('Cloud storage not enabled, exiting...');
return Command::SUCCESS;
}

Wyświetl plik

@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Profile;
use App\Services\AccountService;
class FixStatusCount extends Command
{
@ -12,7 +13,7 @@ class FixStatusCount extends Command
*
* @var string
*/
protected $signature = 'fix:statuscount';
protected $signature = 'fix:statuscount {--remote} {--resync} {--remote-only} {--dlog}';
/**
* The console command description.
@ -38,18 +39,100 @@ class FixStatusCount extends Command
*/
public function handle()
{
Profile::whereNull('domain')
->chunk(50, function($profiles) {
foreach($profiles as $profile) {
$profile->status_count = $profile->statuses()
->getQuery()
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->count();
$profile->save();
if(!$this->confirm('Are you sure you want to run the fix status command?')) {
return;
}
$this->line(' ');
$this->info('Running fix status command...');
$now = now();
$nulls = ['domain', 'status', 'last_fetched_at'];
$resync = $this->option('resync');
$resync24hours = false;
if($resync) {
$resyncChoices = ['Only resync accounts that havent been synced in 24 hours', 'Resync all accounts'];
$rsc = $this->choice(
'Do you want to resync all accounts, or just accounts that havent been resynced for 24 hours?',
$resyncChoices,
0
);
$rsci = array_search($rsc, $resyncChoices);
if($rsci === 0) {
$resync24hours = true;
$nulls = ['status', 'domain', 'last_fetched_at'];
} else {
$resync24hours = false;
$nulls = ['status', 'domain'];
}
});
}
$remote = $this->option('remote');
if($remote) {
$ni = array_search('domain', $nulls);
unset($nulls[$ni]);
$ni = array_search('last_fetched_at', $nulls);
unset($nulls[$ni]);
}
$remoteOnly = $this->option('remote-only');
if($remoteOnly) {
$ni = array_search('domain', $nulls);
unset($nulls[$ni]);
$ni = array_search('last_fetched_at', $nulls);
unset($nulls[$ni]);
$nulls[] = 'user_id';
}
$dlog = $this->option('dlog');
$nulls = array_values($nulls);
foreach(
Profile::when($resync24hours, function($query, $resync24hours) use($nulls) {
if(in_array('domain', $nulls)) {
return $query->whereNull('domain')
->whereNull('last_fetched_at')
->orWhere('last_fetched_at', '<', now()->subHours(24));
} else {
return $query->whereNull('last_fetched_at')
->orWhere('last_fetched_at', '<', now()->subHours(24));
}
})
->when($remoteOnly, function($query, $remoteOnly) {
return $query->whereNull('last_fetched_at')
->orWhere('last_fetched_at', '<', now()->subHours(24));
})
->whereNull($nulls)
->lazyById(50, 'id') as $profile
) {
$ogc = $profile->status_count;
$upc = $profile->statuses()
->getQuery()
->whereIn('scope', ['public', 'private', 'unlisted'])
->count();
if($ogc != $upc) {
$profile->status_count = $upc;
$profile->last_fetched_at = $now;
$profile->save();
AccountService::del($profile->id);
if($dlog) {
$this->info($profile->id . ':' . $profile->username . ' : ' . $upc);
}
} else {
$profile->last_fetched_at = $now;
$profile->save();
if($dlog) {
$this->info($profile->id . ':' . $profile->username . ' : ' . $upc);
}
}
}
$this->line(' ');
$this->info('Finished fix status count command!');
return 0;
}

Wyświetl plik

@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Hashtag;
use App\StatusHashtag;
use DB;
class HashtagCachedCountUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:hashtag-cached-count-update {--limit=100}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update cached counter of hashtags';
/**
* Execute the console command.
*/
public function handle()
{
$limit = $this->option('limit');
$tags = Hashtag::whereNull('cached_count')->limit($limit)->get();
$count = count($tags);
if(!$count) {
return;
}
$bar = $this->output->createProgressBar($count);
$bar->start();
foreach($tags as $tag) {
$count = DB::table('status_hashtags')->whereHashtagId($tag->id)->count();
if(!$count) {
$tag->cached_count = 0;
$tag->saveQuietly();
$bar->advance();
continue;
}
$tag->cached_count = $count;
$tag->saveQuietly();
$bar->advance();
}
$bar->finish();
$this->line(' ');
return;
}
}

Wyświetl plik

@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Hashtag;
use App\StatusHashtag;
use App\Models\HashtagRelated;
use App\Services\HashtagRelatedService;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\confirm;
class HashtagRelatedGenerate extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:hashtag-related-generate {tag}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'tag' => 'Which hashtag should we generate related tags for?',
];
}
/**
* Execute the console command.
*/
public function handle()
{
$tag = $this->argument('tag');
$hashtag = Hashtag::whereName($tag)->orWhere('slug', $tag)->first();
if(!$hashtag) {
$this->error('Hashtag not found, aborting...');
exit;
}
$exists = HashtagRelated::whereHashtagId($hashtag->id)->exists();
if($exists) {
$confirmed = confirm('Found existing related tags, do you want to regenerate them?');
if(!$confirmed) {
$this->error('Aborting...');
exit;
}
}
$this->info('Looking up #' . $tag . '...');
$tags = StatusHashtag::whereHashtagId($hashtag->id)->count();
if(!$tags || $tags < 100) {
$this->error('Not enough posts found to generate related hashtags!');
exit;
}
$this->info('Found ' . $tags . ' posts that use that hashtag');
$related = collect(HashtagRelatedService::fetchRelatedTags($tag));
$selected = multiselect(
label: 'Which tags do you want to generate?',
options: $related->pluck('name'),
required: true,
);
$filtered = $related->filter(fn($i) => in_array($i['name'], $selected))->all();
$agg_score = $related->filter(fn($i) => in_array($i['name'], $selected))->sum('related_count');
HashtagRelated::updateOrCreate([
'hashtag_id' => $hashtag->id,
], [
'related_tags' => array_values($filtered),
'agg_score' => $agg_score,
'last_calculated_at' => now()
]);
$this->info('Finished!');
}
}

Wyświetl plik

@ -0,0 +1,118 @@
<?php
namespace App\Console\Commands;
use App\Models\CustomEmoji;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
class ImportEmojis extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:emojis
{path : Path to a tar.gz archive with the emojis}
{--prefix : Define a prefix for the emjoi shortcode}
{--suffix : Define a suffix for the emjoi shortcode}
{--overwrite : Overwrite existing emojis}
{--disabled : Import all emojis as disabled}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import emojis to the database';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$path = $this->argument('path');
if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') {
$this->error('Path does not exist or is not a tarfile');
return Command::FAILURE;
}
$imported = 0;
$skipped = 0;
$failed = 0;
$tar = new \PharData($path);
$tar->decompress();
foreach (new \RecursiveIteratorIterator($tar) as $entry) {
$this->line("Processing {$entry->getFilename()}");
if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) {
$failed++;
continue;
}
$filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME);
$extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION);
// Skip macOS shadow files
if (str_starts_with($filename, '._')) {
continue;
}
$shortcode = implode('', [
$this->option('prefix'),
$filename,
$this->option('suffix'),
]);
$customEmoji = CustomEmoji::whereShortcode($shortcode)->first();
if ($customEmoji && !$this->option('overwrite')) {
$skipped++;
continue;
}
$emoji = $customEmoji ?? new CustomEmoji();
$emoji->shortcode = $shortcode;
$emoji->domain = config('pixelfed.domain.app');
$emoji->disabled = $this->option('disabled');
$emoji->save();
$fileName = $emoji->id . '.' . $extension;
Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName);
$emoji->media_path = 'emoji/' . $fileName;
$emoji->save();
$imported++;
Cache::forget('pf:custom_emoji');
}
$this->line("Imported: {$imported}");
$this->line("Skipped: {$skipped}");
$this->line("Failed: {$failed}");
//delete file
unlink(str_replace('.tar.gz', '.tar', $path));
return Command::SUCCESS;
}
private function isImage($file)
{
$image = getimagesize($file->getPathname());
return $image !== false;
}
private function isEmoji($filename)
{
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
$mimeType = mime_content_type($filename);
return in_array($mimeType, $allowedMimeTypes);
}
}

Wyświetl plik

@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use App\User;
use App\Models\ImportPost;
use App\Services\ImportService;
class ImportRemoveDeletedAccounts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-remove-deleted-accounts';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
const CACHE_KEY = 'pf:services:import:gc-accounts:skip_min_id';
/**
* Execute the console command.
*/
public function handle()
{
$skipMinId = Cache::remember(self::CACHE_KEY, 864000, function() {
return 1;
});
$deletedIds = User::withTrashed()
->whereNotNull('status')
->whereIn('status', ['deleted', 'delete'])
->where('id', '>', $skipMinId)
->limit(500)
->pluck('id');
if(!$deletedIds || !$deletedIds->count()) {
return;
}
foreach($deletedIds as $did) {
if(Storage::exists('imports/' . $did)) {
Storage::deleteDirectory('imports/' . $did);
}
ImportPost::where('user_id', $did)->delete();
$skipMinId = $did;
}
Cache::put(self::CACHE_KEY, $skipMinId, 864000);
}
}

Wyświetl plik

@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use Storage;
use App\Services\ImportService;
use App\User;
class ImportUploadCleanStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-upload-clean-storage';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$dirs = Storage::allDirectories('imports');
foreach($dirs as $dir) {
$uid = last(explode('/', $dir));
$skip = User::whereNull('status')->find($uid);
if(!$skip) {
Storage::deleteDirectory($dir);
}
}
}
}

Wyświetl plik

@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use Storage;
use App\Services\ImportService;
class ImportUploadGarbageCollection extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-upload-garbage-collection';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
if(!config('import.instagram.enabled')) {
return;
}
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', true)->take(100)->get();
if(!$ips->count()) {
return;
}
foreach($ips as $ip) {
$pid = $ip->profile_id;
$ip->delete();
ImportService::getPostCount($pid, true);
ImportService::clearAttempts($pid);
ImportService::getImportedFiles($pid, true);
}
}
}

Wyświetl plik

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use App\Jobs\ImportPipeline\ImportMediaToCloudPipeline;
use function Laravel\Prompts\progress;
class ImportUploadMediaToCloudStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-upload-media-to-cloud-storage {--limit=500}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate media imported from Instagram to S3 cloud storage.';
/**
* Execute the console command.
*/
public function handle()
{
if(
(bool) config('import.instagram.storage.cloud.enabled') === false ||
(bool) config_cache('pixelfed.cloud_storage') === false
) {
$this->error('Aborted. Cloud storage is not enabled for IG imports.');
return;
}
$limit = $this->option('limit');
$progress = progress(label: 'Migrating import media', steps: $limit);
$progress->start();
$posts = ImportPost::whereUploadedToS3(false)->take($limit)->get();
foreach($posts as $post) {
ImportMediaToCloudPipeline::dispatch($post)->onQueue('low');
$progress->advance();
}
$progress->finish();
}
}

Wyświetl plik

@ -0,0 +1,298 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Instance;
use App\Profile;
use App\Services\InstanceService;
use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
use function Laravel\Prompts\select;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\progress;
use function Laravel\Prompts\search;
use function Laravel\Prompts\table;
class InstanceManager extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:instance-manager';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Manage Instances';
/**
* Execute the console command.
*/
public function handle()
{
$action = select(
'What action do you want to perform?',
[
'Recalculate Stats',
'Ban Instance',
'Unlist Instance',
'Unlisted Instances',
'Banned Instances',
'Unban Instance',
'Relist Instance',
],
);
switch($action) {
case 'Recalculate Stats':
return $this->recalculateStats();
break;
case 'Unlisted Instances':
return $this->viewUnlistedInstances();
break;
case 'Banned Instances':
return $this->viewBannedInstances();
break;
case 'Unlist Instance':
return $this->unlistInstance();
break;
case 'Ban Instance':
return $this->banInstance();
break;
case 'Unban Instance':
return $this->unbanInstance();
break;
case 'Relist Instance':
return $this->relistInstance();
break;
}
}
protected function recalculateStats()
{
$instanceCount = Instance::count();
$confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?');
if(!$confirmed) {
$this->error('Aborting...');
exit;
}
$users = progress(
label: 'Updating instance stats...',
steps: Instance::all(),
callback: fn ($instance) => $this->updateInstanceStats($instance),
);
}
protected function updateInstanceStats($instance)
{
FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
}
protected function unlistInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to unlist this instance?');
if(!$confirmed) {
$this->error('Aborting instance unlisting');
exit;
}
$instance->unlisted = true;
$instance->save();
InstanceService::refresh();
$this->info('Successfully unlisted ' . $instance->domain . '!');
exit;
}
protected function relistInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to re-list this instance?');
if(!$confirmed) {
$this->error('Aborting instance re-listing');
exit;
}
$instance->unlisted = false;
$instance->save();
InstanceService::refresh();
$this->info('Successfully re-listed ' . $instance->domain . '!');
exit;
}
protected function banInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to ban this instance?');
if(!$confirmed) {
$this->error('Aborting instance ban');
exit;
}
$instance->banned = true;
$instance->save();
InstanceService::refresh();
$this->info('Successfully banned ' . $instance->domain . '!');
exit;
}
protected function unbanInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to unban this instance?');
if(!$confirmed) {
$this->error('Aborting instance unban');
exit;
}
$instance->banned = false;
$instance->save();
InstanceService::refresh();
$this->info('Successfully un-banned ' . $instance->domain . '!');
exit;
}
protected function viewBannedInstances()
{
$data = Instance::whereBanned(true)
->get(['domain', 'user_count', 'status_count'])
->map(function($d) {
return [
'domain' => $d->domain,
'user_count' => number_format($d->user_count),
'status_count' => number_format($d->status_count),
];
})
->toArray();
table(
['Domain', 'User Count', 'Status Count'],
$data
);
}
protected function viewUnlistedInstances()
{
$data = Instance::whereUnlisted(true)
->get(['domain', 'user_count', 'status_count', 'banned'])
->map(function($d) {
return [
'domain' => $d->domain,
'user_count' => number_format($d->user_count),
'status_count' => number_format($d->status_count),
'banned' => $d->banned ? '✅' : null
];
})
->toArray();
table(
['Domain', 'User Count', 'Status Count', 'Banned'],
$data
);
}
}

Wyświetl plik

@ -0,0 +1,140 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Media;
use Cache, Storage;
use Illuminate\Contracts\Console\PromptsForMissingInput;
class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'media:cloud-url-rewrite {oldDomain} {newDomain}';
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'oldDomain' => 'The old S3 domain',
'newDomain' => 'The new S3 domain'
];
}
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rewrite S3 media urls from local users';
/**
* Execute the console command.
*/
public function handle()
{
$this->preflightCheck();
$this->bootMessage();
$this->confirmCloudUrl();
}
protected function preflightCheck()
{
if(!(bool) config_cache('pixelfed.cloud_storage')) {
$this->info('Error: Cloud storage is not enabled!');
$this->error('Aborting...');
exit;
}
}
protected function bootMessage()
{
$this->info(' ____ _ ______ __ ');
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
$this->info(' ');
$this->info(' Media Cloud Url Rewrite Tool');
$this->info(' ===');
$this->info(' Old S3: ' . trim($this->argument('oldDomain')));
$this->info(' New S3: ' . trim($this->argument('newDomain')));
$this->info(' ');
}
protected function confirmCloudUrl()
{
$disk = Storage::disk(config('filesystems.cloud'))->url('test');
$domain = parse_url($disk, PHP_URL_HOST);
if(trim($this->argument('newDomain')) !== $domain) {
$this->error('Error: The new S3 domain you entered is not currently configured');
exit;
}
if(!$this->confirm('Confirm this is correct')) {
$this->error('Aborting...');
exit;
}
$this->updateUrls();
}
protected function updateUrls()
{
$this->info('Updating urls...');
$oldDomain = trim($this->argument('oldDomain'));
$newDomain = trim($this->argument('newDomain'));
$disk = Storage::disk(config('filesystems.cloud'));
$count = Media::whereNotNull('cdn_url')->count();
$bar = $this->output->createProgressBar($count);
$counter = 0;
$bar->start();
foreach(Media::whereNotNull('cdn_url')->lazyById(1000, 'id') as $media) {
if(strncmp($media->media_path, 'http', 4) === 0) {
$bar->advance();
continue;
}
$cdnHost = parse_url($media->cdn_url, PHP_URL_HOST);
if($oldDomain != $cdnHost || $newDomain == $cdnHost) {
$bar->advance();
continue;
}
$media->cdn_url = str_replace($oldDomain, $newDomain, $media->cdn_url);
if($media->thumbnail_url != null) {
$thumbHost = parse_url($media->thumbnail_url, PHP_URL_HOST);
if($thumbHost == $oldDomain) {
$thumbUrl = $disk->url($media->thumbnail_path);
$media->thumbnail_url = $thumbUrl;
}
}
if($media->optimized_url != null) {
$optiHost = parse_url($media->optimized_url, PHP_URL_HOST);
if($optiHost == $oldDomain) {
$optiUrl = str_replace($oldDomain, $newDomain, $media->optimized_url);
$media->optimized_url = $optiUrl;
}
}
$media->save();
$counter++;
$bar->advance();
}
$bar->finish();
$this->line(' ');
$this->info('Finished! Updated ' . $counter . ' total records!');
$this->line(' ');
$this->info('Tip: Run `php artisan cache:clear` to purge cached urls');
}
}

Wyświetl plik

@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command
*/
public function handle()
{
$enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']);
$enabled = (bool) config_cache('pixelfed.cloud_storage');
if(!$enabled) {
$this->error('Cloud storage not enabled. Exiting...');
return;

Wyświetl plik

@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
class NotificationEpochUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:notification-epoch-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update notification epoch';
/**
* Execute the console command.
*/
public function handle()
{
NotificationEpochUpdatePipeline::dispatch();
}
}

Wyświetl plik

@ -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!');
}
}

Wyświetl plik

@ -0,0 +1,153 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use App\Services\ImportService;
use App\Media;
use App\Profile;
use App\Status;
use Storage;
use App\Services\AccountService;
use App\Services\MediaPathService;
use Illuminate\Support\Str;
use App\Util\Lexer\Autolink;
class TransformImports extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:transform-imports';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Transform imports into statuses';
/**
* Execute the console command.
*/
public function handle()
{
if(!config('import.instagram.enabled')) {
return;
}
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get();
if(!$ips->count()) {
return;
}
foreach($ips as $ip) {
$id = $ip->user_id;
$pid = $ip->profile_id;
$profile = Profile::find($pid);
if(!$profile) {
$ip->skip_missing_media = true;
$ip->save();
continue;
}
$exists = ImportPost::whereUserId($id)
->whereNotNull('status_id')
->where('filename', $ip->filename)
->where('creation_year', $ip->creation_year)
->where('creation_month', $ip->creation_month)
->where('creation_day', $ip->creation_day)
->exists();
if($exists == true) {
$ip->skip_missing_media = true;
$ip->save();
continue;
}
$idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day);
if(!$idk) {
$ip->skip_missing_media = true;
$ip->save();
continue;
}
if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) {
ImportService::clearAttempts($profile->id);
ImportService::getPostCount($profile->id, true);
$ip->skip_missing_media = true;
$ip->save();
continue;
}
$missingMedia = false;
foreach($ip->media as $ipm) {
$fileName = last(explode('/', $ipm['uri']));
$og = 'imports/' . $id . '/' . $fileName;
if(!Storage::exists($og)) {
$missingMedia = true;
}
}
if($missingMedia === true) {
$ip->skip_missing_media = true;
$ip->save();
continue;
}
$caption = $ip->caption;
$status = new Status;
$status->profile_id = $pid;
$status->caption = $caption;
$status->rendered = strlen(trim($caption)) ? Autolink::create()->autolink($ip->caption) : null;
$status->type = $ip->post_type;
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->id = $idk['id'];
$status->created_at = now()->parse($ip->creation_date);
$status->save();
foreach($ip->media as $ipm) {
$fileName = last(explode('/', $ipm['uri']));
$ext = last(explode('.', $fileName));
$basePath = MediaPathService::get($profile);
$og = 'imports/' . $id . '/' . $fileName;
if(!Storage::exists($og)) {
$ip->skip_missing_media = true;
$ip->save();
continue;
}
$size = Storage::size($og);
$mime = Storage::mimeType($og);
$newFile = Str::random(40) . '.' . $ext;
$np = $basePath . '/' . $newFile;
Storage::move($og, $np);
$media = new Media;
$media->profile_id = $pid;
$media->user_id = $id;
$media->status_id = $status->id;
$media->media_path = $np;
$media->mime = $mime;
$media->size = $size;
$media->save();
}
$ip->status_id = $status->id;
$ip->creation_id = $idk['incr'];
$ip->save();
$profile->status_count = $profile->status_count + 1;
$profile->save();
AccountService::del($profile->id);
ImportService::clearAttempts($profile->id);
ImportService::getPostCount($profile->id, true);
}
}
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -3,16 +3,17 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use App\User;
class UserAdmin extends Command
class UserAdmin extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:admin {id}';
protected $signature = 'user:admin {username}';
/**
* The console command description.
@ -22,13 +23,15 @@ class UserAdmin extends Command
protected $description = 'Make a user an admin, or remove admin privileges.';
/**
* Create a new command instance.
* Prompt for missing input arguments using the returned questions.
*
* @return void
* @return array
*/
public function __construct()
protected function promptForMissingArgumentsUsing()
{
parent::__construct();
return [
'username' => 'Which username should we toggle admin privileges for?',
];
}
/**
@ -38,16 +41,15 @@ class UserAdmin extends Command
*/
public function handle()
{
$id = $this->argument('id');
if(ctype_digit($id) == true) {
$user = User::find($id);
} else {
$user = User::whereUsername($id)->first();
}
$id = $this->argument('username');
$user = User::whereUsername($id)->first();
if(!$user) {
$this->error('Could not find any user with that username or id.');
exit;
}
$this->info('Found username: ' . $user->username);
$state = $user->is_admin ? 'Remove admin privileges from this user?' : 'Add admin privileges to this user?';
$confirmed = $this->confirm($state);

Wyświetl plik

@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use App\User;
class UserToggle2FA extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:2fa {username}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Disable two factor authentication for given username';
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'username' => 'Which username should we disable 2FA for?',
];
}
/**
* Execute the console command.
*/
public function handle()
{
$user = User::whereUsername($this->argument('username'))->first();
if(!$user) {
$this->error('Could not find any user with that username');
exit;
}
if(!$user->{'2fa_enabled'}) {
$this->info('User did not have 2FA enabled!');
return;
}
$user->{'2fa_enabled'} = false;
$user->{'2fa_secret'} = null;
$user->{'2fa_backup_codes'} = null;
$user->save();
$this->info('Successfully disabled 2FA on this account!');
}
}

Wyświetl plik

@ -25,17 +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 ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) {
$schedule->command('media:s3gc')->hourlyAt(15);
}
if (config('import.instagram.enabled')) {
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
}
}
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
}
/**
@ -45,7 +60,7 @@ class Kernel extends ConsoleKernel
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}

Wyświetl plik

@ -157,7 +157,7 @@ class AccountController extends Controller
$pid = $request->user()->profile_id;
$count = UserFilterService::muteCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
$maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)->count();
@ -260,7 +260,7 @@ class AccountController extends Controller
]);
$pid = $request->user()->profile_id;
$count = UserFilterService::blockCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
$maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();

Wyświetl plik

@ -2,30 +2,20 @@
namespace App\Http\Controllers\Admin;
use DB, Cache;
use App\{
DiscoverCategory,
DiscoverCategoryHashtag,
Hashtag,
Media,
Profile,
Status,
StatusHashtag,
User
};
use App\Http\Controllers\PixelfedDirectoryController;
use App\Models\ConfigCache;
use App\Services\AccountService;
use App\Services\ConfigCacheService;
use App\Services\StatusService;
use Carbon\Carbon;
use App\Status;
use App\User;
use Cache;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use League\ISO3166\ISO3166;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Http;
use App\Http\Controllers\PixelfedDirectoryController;
use Illuminate\Support\Str;
use League\ISO3166\ISO3166;
trait AdminDirectoryController
{
@ -41,40 +31,41 @@ trait AdminDirectoryController
$res['countries'] = collect((new ISO3166)->all())->pluck('name');
$res['admins'] = User::whereIsAdmin(true)
->where('2fa_enabled', true)
->get()->map(function($user) {
return [
'uid' => (string) $user->id,
'pid' => (string) $user->profile_id,
'username' => $user->username,
'created_at' => $user->created_at
];
});
->get()->map(function ($user) {
return [
'uid' => (string) $user->id,
'pid' => (string) $user->profile_id,
'username' => $user->username,
'created_at' => $user->created_at,
];
});
$config = ConfigCache::whereK('pixelfed.directory')->first();
if($config) {
if ($config) {
$data = $config->v ? json_decode($config->v, true) : [];
$res = array_merge($res, $data);
}
if(empty($res['summary'])) {
if (empty($res['summary'])) {
$summary = ConfigCache::whereK('app.short_description')->pluck('v');
$res['summary'] = $summary ? $summary[0] : null;
}
if(isset($res['banner_image']) && !empty($res['banner_image'])) {
if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
$res['banner_image'] = url(Storage::url($res['banner_image']));
}
if(isset($res['favourite_posts'])) {
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
if (isset($res['favourite_posts'])) {
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
return StatusService::get($id);
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
->filter(function ($post) {
return $post && isset($post['account']);
})
->values();
}
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
$res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled');
$res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
$res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
@ -83,22 +74,22 @@ trait AdminDirectoryController
$res['feature_config'] = [
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => config_cache('pixelfed.optimize_image'),
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
'max_account_size' => config_cache('pixelfed.max_account_size'),
'max_album_length' => config_cache('pixelfed.max_album_length'),
'account_deletion' => config_cache('pixelfed.account_deletion'),
'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
];
if(config_cache('pixelfed.directory.testimonials')) {
$testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true))
->map(function($t) {
if (config_cache('pixelfed.directory.testimonials')) {
$testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
->map(function ($t) {
return [
'profile' => AccountService::get($t['profile_id']),
'body' => $t['body']
'body' => $t['body'],
];
});
$res['testimonials'] = $testimonials;
@ -107,8 +98,8 @@ trait AdminDirectoryController
$validator = Validator::make($res['feature_config'], [
'media_types' => [
'required',
function ($attribute, $value, $fail) {
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
function ($attribute, $value, $fail) {
if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
$fail('You must enable image/jpeg and image/png support.');
}
},
@ -119,12 +110,12 @@ trait AdminDirectoryController
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
'max_album_length' => 'required|integer|min:4|max:20',
'account_deletion' => 'required|accepted',
'max_caption_length' => 'required|integer|min:500|max:10000'
'max_caption_length' => 'required|integer|min:500|max:10000',
]);
$res['requirements_validator'] = $validator->errors();
$res['is_eligible'] = $res['open_registration'] &&
$res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) &&
$res['oauth_enabled'] &&
$res['activitypub_enabled'] &&
count($res['requirements_validator']) === 0 &&
@ -145,11 +136,11 @@ trait AdminDirectoryController
foreach (new \DirectoryIterator($path) as $io) {
$name = $io->getFilename();
$skip = ['vendor'];
if($io->isDot() || in_array($name, $skip)) {
if ($io->isDot() || in_array($name, $skip)) {
continue;
}
if($io->isDir()) {
if ($io->isDir()) {
$langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
}
}
@ -158,25 +149,26 @@ trait AdminDirectoryController
$res['primary_locale'] = config('app.locale');
$submissionState = Http::withoutVerifying()
->post('https://pixelfed.org/api/v1/directory/check-submission', [
'domain' => config('pixelfed.domain.app')
]);
->post('https://pixelfed.org/api/v1/directory/check-submission', [
'domain' => config('pixelfed.domain.app'),
]);
$res['submission_state'] = $submissionState->json();
return $res;
}
protected function validVal($res, $val, $count = false, $minLen = false)
{
if(!isset($res[$val])) {
if (! isset($res[$val])) {
return false;
}
if($count) {
if ($count) {
return count($res[$val]) >= $count;
}
if($minLen) {
if ($minLen) {
return strlen($res[$val]) >= $minLen;
}
@ -193,11 +185,11 @@ trait AdminDirectoryController
'favourite_posts' => 'array|max:12',
'favourite_posts.*' => 'distinct',
'privacy_pledge' => 'sometimes',
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000'
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000',
]);
$config = ConfigCache::firstOrNew([
'k' => 'pixelfed.directory'
'k' => 'pixelfed.directory',
]);
$res = $config->v ? json_decode($config->v, true) : [];
@ -207,27 +199,28 @@ trait AdminDirectoryController
$res['contact_email'] = $request->input('contact_email');
$res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
if($request->filled('location')) {
if ($request->filled('location')) {
$exists = (new ISO3166)->name($request->location);
if($exists) {
if ($exists) {
$res['location'] = $request->input('location');
}
}
if($request->hasFile('banner_image')) {
if ($request->hasFile('banner_image')) {
collect(Storage::files('public/headers'))
->filter(function($name) {
$protected = [
'public/headers/.gitignore',
'public/headers/default.jpg',
'public/headers/missing.png'
];
return !in_array($name, $protected);
})
->each(function($name) {
Storage::delete($name);
});
$path = $request->file('banner_image')->store('public/headers');
->filter(function ($name) {
$protected = [
'public/headers/.gitignore',
'public/headers/default.jpg',
'public/headers/missing.png',
];
return ! in_array($name, $protected);
})
->each(function ($name) {
Storage::delete($name);
});
$path = $request->file('banner_image')->storePublicly('public/headers');
$res['banner_image'] = $path;
ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
@ -239,9 +232,10 @@ trait AdminDirectoryController
ConfigCacheService::put('pixelfed.directory', $config->v);
$updated = json_decode($config->v, true);
if(isset($updated['banner_image'])) {
if (isset($updated['banner_image'])) {
$updated['banner_image'] = url(Storage::url($updated['banner_image']));
}
return $updated;
}
@ -249,9 +243,10 @@ trait AdminDirectoryController
{
$reqs = [];
$reqs['feature_config'] = [
'open_registration' => config_cache('pixelfed.open_registration'),
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
'oauth_enabled' => (bool) config_cache('pixelfed.oauth_enabled'),
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => config_cache('pixelfed.optimize_image'),
@ -265,13 +260,14 @@ trait AdminDirectoryController
];
$validator = Validator::make($reqs['feature_config'], [
'open_registration' => 'required|accepted',
'open_registration' => 'required_unless:curated_onboarding,true',
'curated_onboarding' => 'required_unless:open_registration,true',
'activitypub_enabled' => 'required|accepted',
'oauth_enabled' => 'required|accepted',
'media_types' => [
'required',
function ($attribute, $value, $fail) {
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
function ($attribute, $value, $fail) {
if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
$fail('You must enable image/jpeg and image/png support.');
}
},
@ -282,10 +278,10 @@ trait AdminDirectoryController
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
'max_album_length' => 'required|integer|min:4|max:20',
'account_deletion' => 'required|accepted',
'max_caption_length' => 'required|integer|min:500|max:10000'
'max_caption_length' => 'required|integer|min:500|max:10000',
]);
if(!$validator->validate()) {
if (! $validator->validate()) {
return response()->json($validator->errors(), 422);
}
@ -294,6 +290,7 @@ trait AdminDirectoryController
$data = (new PixelfedDirectoryController())->buildListing();
$res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
return 200;
}
@ -301,7 +298,7 @@ trait AdminDirectoryController
{
$bannerImage = ConfigCache::whereK('app.banner_image')->first();
$directory = ConfigCache::whereK('pixelfed.directory')->first();
if(!$bannerImage && !$directory || empty($directory->v)) {
if (! $bannerImage && ! $directory || empty($directory->v)) {
return;
}
$directoryArr = json_decode($directory->v, true);
@ -309,12 +306,12 @@ trait AdminDirectoryController
$protected = [
'public/headers/.gitignore',
'public/headers/default.jpg',
'public/headers/missing.png'
'public/headers/missing.png',
];
if(!$path || in_array($path, $protected)) {
if (! $path || in_array($path, $protected)) {
return;
}
if(Storage::exists($directoryArr['banner_image'])) {
if (Storage::exists($directoryArr['banner_image'])) {
Storage::delete($directoryArr['banner_image']);
}
@ -325,12 +322,13 @@ trait AdminDirectoryController
$bannerImage->save();
Cache::forget('api:v1:instance-data-response-v1');
ConfigCacheService::put('pixelfed.directory', $directory);
return $bannerImage->v;
}
public function directoryGetPopularPosts(Request $request)
{
$ids = Cache::remember('admin:api:popular_posts', 86400, function() {
$ids = Cache::remember('admin:api:popular_posts', 86400, function () {
return Status::whereLocal(true)
->whereScope('public')
->whereType('photo')
@ -340,21 +338,21 @@ trait AdminDirectoryController
->pluck('id');
});
$res = $ids->map(function($id) {
$res = $ids->map(function ($id) {
return StatusService::get($id);
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
->filter(function ($post) {
return $post && isset($post['account']);
})
->values();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
public function directoryGetAddPostByIdSearch(Request $request)
{
$this->validate($request, [
'q' => 'required|integer'
'q' => 'required|integer',
]);
$id = $request->input('q');
@ -377,11 +375,12 @@ trait AdminDirectoryController
$profile_id = $request->input('profile_id');
$testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail();
$existing = collect(json_decode($testimonials->v, true))
->filter(function($t) use($profile_id) {
->filter(function ($t) use ($profile_id) {
return $t['profile_id'] !== $profile_id;
})
->values();
ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
return $existing;
}
@ -389,13 +388,13 @@ trait AdminDirectoryController
{
$this->validate($request, [
'username' => 'required',
'body' => 'required|string|min:5|max:500'
'body' => 'required|string|min:5|max:500',
]);
$user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
$configCache = ConfigCache::firstOrCreate([
'k' => 'pixelfed.directory.testimonials'
'k' => 'pixelfed.directory.testimonials',
]);
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
@ -406,7 +405,7 @@ trait AdminDirectoryController
$testimonials->push([
'profile_id' => (string) $user->profile_id,
'username' => $request->input('username'),
'body' => $request->input('body')
'body' => $request->input('body'),
]);
$configCache->v = json_encode($testimonials->toArray());
@ -414,8 +413,9 @@ trait AdminDirectoryController
ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
$res = [
'profile' => AccountService::get($user->profile_id),
'body' => $request->input('body')
'body' => $request->input('body'),
];
return $res;
}
@ -423,7 +423,7 @@ trait AdminDirectoryController
{
$this->validate($request, [
'profile_id' => 'required',
'body' => 'required|string|min:5|max:500'
'body' => 'required|string|min:5|max:500',
]);
$profile_id = $request->input('profile_id');
@ -431,18 +431,19 @@ trait AdminDirectoryController
$user = User::whereProfileId($profile_id)->firstOrFail();
$configCache = ConfigCache::firstOrCreate([
'k' => 'pixelfed.directory.testimonials'
'k' => 'pixelfed.directory.testimonials',
]);
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
$updated = $testimonials->map(function($t) use($profile_id, $body) {
if($t['profile_id'] == $profile_id) {
$updated = $testimonials->map(function ($t) use ($profile_id, $body) {
if ($t['profile_id'] == $profile_id) {
$t['body'] = $body;
}
return $t;
})
->values();
->values();
$configCache->v = json_encode($updated);
$configCache->save();

Wyświetl plik

@ -11,6 +11,7 @@ use App\Mail\AdminMessage;
use Illuminate\Support\Facades\Mail;
use App\Services\ModLogService;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Services\AccountService;
trait AdminUserController
{
@ -25,7 +26,7 @@ trait AdminUserController
'next' => $offset + 1,
'query' => $search ? '&a=search&q=' . $search : null
];
$users = User::select('id', 'username', 'status', 'profile_id')
$users = User::select('id', 'username', 'status', 'profile_id', 'is_admin')
->orderBy($col, $dir)
->when($search, function($q, $search) {
return $q->where('username', 'like', "%{$search}%");
@ -34,7 +35,11 @@ trait AdminUserController
return $q->offset(($offset * 10));
})
->limit(10)
->get();
->get()
->map(function($u) {
$u['account'] = AccountService::get($u->profile_id, true);
return $u;
});
return view('admin.users.home', compact('users', 'pagination'));
}

Wyświetl plik

@ -424,7 +424,7 @@ class AdminController extends Controller
public function customEmojiHome(Request $request)
{
if(!config('federation.custom_emoji.enabled')) {
if(!(bool) config_cache('federation.custom_emoji.enabled')) {
return view('admin.custom-emoji.not-enabled');
}
$this->validate($request, [
@ -497,7 +497,7 @@ class AdminController extends Controller
public function customEmojiToggleActive(Request $request, $id)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id);
$emoji->disabled = !$emoji->disabled;
$emoji->save();
@ -508,13 +508,13 @@ class AdminController extends Controller
public function customEmojiAdd(Request $request)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
return view('admin.custom-emoji.add');
}
public function customEmojiStore(Request $request)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$this->validate($request, [
'shortcode' => [
'required',
@ -545,7 +545,7 @@ class AdminController extends Controller
public function customEmojiDelete(Request $request, $id)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id);
Storage::delete("public/{$emoji->media_path}");
Cache::forget('pf:custom_emoji');
@ -555,7 +555,7 @@ class AdminController extends Controller
public function customEmojiShowDuplicates(Request $request, $id)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));

Wyświetl plik

@ -0,0 +1,335 @@
<?php
namespace App\Http\Controllers;
use App\Mail\CuratedRegisterAcceptUser;
use App\Mail\CuratedRegisterRejectUser;
use App\Mail\CuratedRegisterRequestDetailsFromUser;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use App\Models\CuratedRegisterTemplate;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class AdminCuratedRegisterController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'admin']);
}
public function index(Request $request)
{
$this->validate($request, [
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses',
'sort' => 'sometimes|in:asc,desc',
]);
$filter = $request->input('filter', 'open');
$sort = $request->input('sort', 'asc');
$records = CuratedRegister::when($filter, function ($q, $filter) {
if ($filter === 'open') {
return $q->where('is_rejected', false)
->where(function ($query) {
return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false);
})
->whereNotNull('email_verified_at')
->whereIsClosed(false);
} elseif ($filter === 'all') {
return $q;
} elseif ($filter === 'responses') {
return $q->whereIsClosed(false)
->whereNotNull('email_verified_at')
->where('user_has_responded', true)
->where('is_awaiting_more_info', true);
} elseif ($filter === 'awaiting') {
return $q->whereIsClosed(false)
->where('is_rejected', false)
->where('is_approved', false)
->where('user_has_responded', false)
->where('is_awaiting_more_info', true);
} elseif ($filter === 'approved') {
return $q->whereIsClosed(true)->whereIsApproved(true);
} elseif ($filter === 'rejected') {
return $q->whereIsClosed(true)->whereIsRejected(true);
}
})
->when($sort, function ($query, $sort) {
return $query->orderBy('id', $sort);
})
->paginate(10)
->withQueryString();
return view('admin.curated-register.index', compact('records', 'filter'));
}
public function show(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return view('admin.curated-register.show', compact('record'));
}
public function apiActivityLog(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
$res = collect([
[
'id' => 1,
'action' => 'created',
'title' => 'Onboarding application created',
'message' => null,
'link' => null,
'timestamp' => $record->created_at,
],
]);
if ($record->email_verified_at) {
$res->push([
'id' => 3,
'action' => 'email_verified_at',
'title' => 'Applicant successfully verified email address',
'message' => null,
'link' => null,
'timestamp' => $record->email_verified_at,
]);
}
$activities = CuratedRegisterActivity::whereRegisterId($record->id)->get();
$idx = 4;
$userResponses = collect([]);
foreach ($activities as $activity) {
$idx++;
if ($activity->type === 'user_resend_email_confirmation') {
continue;
}
if ($activity->from_user) {
$userResponses->push($activity);
continue;
}
$res->push([
'id' => $idx,
'aid' => $activity->id,
'action' => $activity->type,
'title' => $activity->from_admin ? 'Admin requested info' : 'User responded',
'message' => $activity->message,
'link' => $activity->adminReviewUrl(),
'timestamp' => $activity->created_at,
]);
}
foreach ($userResponses as $ur) {
$res = $res->map(function ($r) use ($ur) {
if (! isset($r['aid'])) {
return $r;
}
if ($ur->reply_to_id === $r['aid']) {
$r['user_response'] = $ur;
return $r;
}
return $r;
});
}
if ($record->is_approved) {
$idx++;
$res->push([
'id' => $idx,
'action' => 'approved',
'title' => 'Application Approved',
'message' => null,
'link' => null,
'timestamp' => $record->action_taken_at,
]);
} elseif ($record->is_rejected) {
$idx++;
$res->push([
'id' => $idx,
'action' => 'rejected',
'title' => 'Application Rejected',
'message' => null,
'link' => null,
'timestamp' => $record->action_taken_at,
]);
}
return $res->reverse()->values();
}
public function apiMessagePreviewStore(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return $request->all();
}
public function apiMessageSendStore(Request $request, $id)
{
$this->validate($request, [
'message' => 'required|string|min:5|max:3000',
]);
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity;
$activity->register_id = $record->id;
$activity->admin_id = $request->user()->id;
$activity->secret_code = Str::random(32);
$activity->type = 'request_details';
$activity->from_admin = true;
$activity->message = $request->input('message');
$activity->save();
$record->is_awaiting_more_info = true;
$record->user_has_responded = false;
$record->save();
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
return $request->all();
}
public function previewDetailsMessageShow(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity;
$activity->message = $request->input('message');
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
}
public function previewMessageShow(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$record->message = $request->input('message');
return new \App\Mail\CuratedRegisterSendMessage($record);
}
public function apiHandleReject(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:reject-email,reject-silent',
]);
$action = $request->input('action');
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
$record->is_rejected = true;
$record->is_closed = true;
$record->action_taken_at = now();
$record->save();
if ($action === 'reject-email') {
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
}
return [200];
}
public function apiHandleApprove(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
$record->is_approved = true;
$record->is_closed = true;
$record->action_taken_at = now();
$record->save();
$user = User::create([
'name' => $record->username,
'username' => $record->username,
'email' => $record->email,
'password' => $record->password,
'app_register_ip' => $record->ip_address,
'email_verified_at' => now(),
'register_source' => 'cur_onboarding',
]);
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
return [200];
}
public function templates(Request $request)
{
$templates = CuratedRegisterTemplate::paginate(10);
return view('admin.curated-register.templates', compact('templates'));
}
public function templateCreate(Request $request)
{
return view('admin.curated-register.template-create');
}
public function templateEdit(Request $request, $id)
{
$template = CuratedRegisterTemplate::findOrFail($id);
return view('admin.curated-register.template-edit', compact('template'));
}
public function templateEditStore(Request $request, $id)
{
$this->validate($request, [
'name' => 'required|string|max:30',
'content' => 'required|string|min:5|max:3000',
'description' => 'nullable|sometimes|string|max:1000',
'active' => 'sometimes',
]);
$template = CuratedRegisterTemplate::findOrFail($id);
$template->name = $request->input('name');
$template->content = $request->input('content');
$template->description = $request->input('description');
$template->is_active = $request->boolean('active');
$template->save();
return redirect()->back()->with('status', 'Successfully updated template!');
}
public function templateDelete(Request $request, $id)
{
$template = CuratedRegisterTemplate::findOrFail($id);
$template->delete();
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!');
}
public function templateStore(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:30',
'content' => 'required|string|min:5|max:3000',
'description' => 'nullable|sometimes|string|max:1000',
'active' => 'sometimes',
]);
CuratedRegisterTemplate::create([
'name' => $request->input('name'),
'content' => $request->input('content'),
'description' => $request->input('description'),
'is_active' => $request->boolean('active'),
]);
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!');
}
public function getActiveTemplates(Request $request)
{
$templates = CuratedRegisterTemplate::whereIsActive(true)
->orderBy('order')
->get()
->map(function ($tmp) {
return [
'name' => $tmp->name,
'content' => $tmp->content,
];
});
return response()->json($templates);
}
}

Wyświetl plik

@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\AdminShadowFilter;
use App\Profile;
use App\Services\AccountService;
use App\Services\AdminShadowFilterService;
class AdminShadowFilterController extends Controller
{
public function __construct()
{
$this->middleware(['auth','admin']);
}
public function home(Request $request)
{
$filter = $request->input('filter');
$searchQuery = $request->input('q');
$filters = AdminShadowFilter::whereHas('profile')
->when($filter, function($q, $filter) {
if($filter == 'all') {
return $q;
} else if($filter == 'inactive') {
return $q->whereActive(false);
} else {
return $q;
}
}, function($q, $filter) {
return $q->whereActive(true);
})
->when($searchQuery, function($q, $searchQuery) {
$ids = Profile::where('username', 'like', '%' . $searchQuery . '%')
->limit(100)
->pluck('id')
->toArray();
return $q->where('item_type', 'App\Profile')->whereIn('item_id', $ids);
})
->latest()
->paginate(10)
->withQueryString();
return view('admin.asf.home', compact('filters'));
}
public function create(Request $request)
{
return view('admin.asf.create');
}
public function edit(Request $request, $id)
{
$filter = AdminShadowFilter::findOrFail($id);
$profile = AccountService::get($filter->item_id);
return view('admin.asf.edit', compact('filter', 'profile'));
}
public function store(Request $request)
{
$this->validate($request, [
'username' => 'required',
'active' => 'sometimes',
'note' => 'sometimes',
'hide_from_public_feeds' => 'sometimes'
]);
$profile = Profile::whereUsername($request->input('username'))->first();
if(!$profile) {
return back()->withErrors(['Invalid account']);
}
if($profile->user && $profile->user->is_admin) {
return back()->withErrors(['Cannot filter an admin account']);
}
$active = $request->has('active') && $request->has('hide_from_public_feeds');
AdminShadowFilter::updateOrCreate([
'item_id' => $profile->id,
'item_type' => get_class($profile)
], [
'is_local' => $profile->domain === null,
'note' => $request->input('note'),
'hide_from_public_feeds' => $request->has('hide_from_public_feeds'),
'admin_id' => $request->user()->profile_id,
'active' => $active
]);
AdminShadowFilterService::refresh();
return redirect('/i/admin/asf/home');
}
public function storeEdit(Request $request, $id)
{
$this->validate($request, [
'active' => 'sometimes',
'note' => 'sometimes',
'hide_from_public_feeds' => 'sometimes'
]);
$filter = AdminShadowFilter::findOrFail($id);
$profile = Profile::findOrFail($filter->item_id);
if($profile->user && $profile->user->is_admin) {
return back()->withErrors(['Cannot filter an admin account']);
}
$active = $request->has('active');
$filter->active = $active;
$filter->hide_from_public_feeds = $request->has('hide_from_public_feeds');
$filter->note = $request->input('note');
$filter->save();
AdminShadowFilterService::refresh();
return redirect('/i/admin/asf/home');
}
}

Wyświetl plik

@ -18,31 +18,42 @@ use App\{
Status,
User
};
use App\Models\Conversation;
use App\Models\RemoteReport;
use App\Services\AccountService;
use App\Services\AdminStatsService;
use App\Services\ConfigCacheService;
use App\Services\InstanceService;
use App\Services\ModLogService;
use App\Services\SnowflakeService;
use App\Services\StatusService;
use App\Services\PublicTimelineService;
use App\Services\NetworkTimelineService;
use App\Services\NotificationService;
use App\Http\Resources\AdminInstance;
use App\Http\Resources\AdminUser;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
class AdminApiController extends Controller
{
public function supported(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
return response()->json(['supported' => true]);
}
public function getStats(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$res = AdminStatsService::summary();
$res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
@ -53,8 +64,10 @@ class AdminApiController extends Controller
public function autospam(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$appeals = AccountInterstitial::whereType('post.autospam')
->whereNull('appeal_handled_at')
@ -88,11 +101,13 @@ class AdminApiController extends Controller
public function autospamHandle(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all',
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account',
'id' => 'required'
]);
@ -104,14 +119,53 @@ class AdminApiController extends Controller
$now = now();
$res = ['status' => 'success'];
$meta = json_decode($appeal->meta);
$user = $appeal->user;
$profile = $user->profile;
if($action == 'dismiss') {
$appeal->is_spam = true;
$appeal->appeal_handled_at = $now;
$appeal->save();
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $profile->id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $profile->id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
if($action == 'delete-post') {
$appeal->appeal_handled_at = now();
$appeal->is_spam = true;
$appeal->save();
ModLogService::boot()
->objectUid($profile->id)
->objectId($appeal->status->id)
->objectType('App\Status::class')
->user($request->user())
->action('admin.status.delete')
->accessLevel('admin')
->save();
PublicTimelineService::deleteByProfileId($profile->id);
StatusDelete::dispatch($appeal->status)->onQueue('high');
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
if($action == 'delete-account') {
abort_if($user->is_admin, 400, 'Cannot delete an admin account.');
$appeal->appeal_handled_at = now();
$appeal->is_spam = true;
$appeal->save();
ModLogService::boot()
->objectUid($profile->id)
->objectId($profile->id)
->objectType('App\User::class')
->user($request->user())
->action('admin.user.delete')
->accessLevel('admin')
->save();
PublicTimelineService::deleteByProfileId($profile->id);
DeleteAccountPipeline::dispatch($appeal->user)->onQueue('high');
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -141,13 +195,13 @@ class AdminApiController extends Controller
StatusService::del($status->id);
Notification::whereAction('autospam.warning')
->whereProfileId($appeal->user->profile_id)
->get()
->each(function($n) use($appeal) {
NotificationService::del($appeal->user->profile_id, $n->id);
$n->forceDelete();
});
Notification::whereAction('autospam.warning')
->whereProfileId($appeal->user->profile_id)
->get()
->each(function($n) use($appeal) {
NotificationService::del($appeal->user->profile_id, $n->id);
$n->forceDelete();
});
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
@ -174,13 +228,13 @@ class AdminApiController extends Controller
StatusService::del($status->id, true);
}
Notification::whereAction('autospam.warning')
->whereProfileId($report->user->profile_id)
->get()
->each(function($n) use($report) {
NotificationService::del($report->user->profile_id, $n->id);
$n->forceDelete();
});
Notification::whereAction('autospam.warning')
->whereProfileId($report->user->profile_id)
->get()
->each(function($n) use($report) {
NotificationService::del($report->user->profile_id, $n->id);
$n->forceDelete();
});
});
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
@ -193,8 +247,10 @@ class AdminApiController extends Controller
public function modReports(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$reports = Report::whereNull('admin_seen')
->orderBy('created_at','desc')
@ -239,8 +295,10 @@ class AdminApiController extends Controller
public function modReportHandle(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'action' => 'required|string',
@ -297,8 +355,11 @@ class AdminApiController extends Controller
public function getConfiguration(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
abort_unless(config('instance.enable_cc'), 400);
return collect([
@ -340,8 +401,11 @@ class AdminApiController extends Controller
public function updateConfiguration(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
abort_unless(config('instance.enable_cc'), 400);
$this->validate($request, [
@ -402,8 +466,14 @@ class AdminApiController extends Controller
public function getUsers(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$this->validate($request, [
'sort' => 'sometimes|in:asc,desc',
]);
$q = $request->input('q');
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
$res = User::whereNull('status')
@ -417,31 +487,47 @@ class AdminApiController extends Controller
public function getUser(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$id = $request->input('user_id');
$user = User::findOrFail($id);
$profile = $user->profile;
$account = AccountService::get($user->profile_id, true);
return (new AdminUser($user))->additional(['meta' => [
'account' => $account,
'moderation' => [
'unlisted' => (bool) $profile->unlisted,
'cw' => (bool) $profile->cw,
'no_autolink' => (bool) $profile->no_autolink
]
]]);
$key = 'pf-admin-api:getUser:byId:' . $id;
if($request->has('refresh')) {
Cache::forget($key);
}
return Cache::remember($key, 86400, function() use($id) {
$user = User::findOrFail($id);
$profile = $user->profile;
$account = AccountService::get($user->profile_id, true);
$res = (new AdminUser($user))->additional(['meta' => [
'cached_at' => str_replace('+00:00', 'Z', now()->format(DATE_RFC3339_EXTENDED)),
'account' => $account,
'dms_sent' => Conversation::whereFromId($profile->id)->count(),
'report_count' => Report::where('object_id', $profile->id)->orWhere('reported_profile_id', $profile->id)->count(),
'remote_report_count' => RemoteReport::whereAccountId($profile->id)->count(),
'moderation' => [
'unlisted' => (bool) $profile->unlisted,
'cw' => (bool) $profile->cw,
'no_autolink' => (bool) $profile->no_autolink
]
]]);
return $res;
});
}
public function userAdminAction(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'id' => 'required',
'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email',
'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email,delete',
'value' => 'sometimes'
]);
@ -452,7 +538,59 @@ class AdminApiController extends Controller
abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
if($action === 'refresh_stats') {
if($action === 'delete') {
if(config('pixelfed.account_deletion') == false) {
abort(404);
}
abort_if($user->is_admin, 400, 'Cannot delete an admin account.');
$ts = now()->addMonth();
$user->status = 'delete';
$user->delete_after = $ts;
$user->save();
$profile->status = 'delete';
$profile->delete_after = $ts;
$profile->save();
ModLogService::boot()
->objectUid($profile->id)
->objectId($profile->id)
->objectType('App\Profile::class')
->user($request->user())
->action('admin.user.delete')
->accessLevel('admin')
->save();
PublicTimelineService::deleteByProfileId($profile->id);
NetworkTimelineService::deleteByProfileId($profile->id);
if($profile->user_id) {
DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
$user->email = $user->id;
$user->password = '';
$user->status = 'delete';
$user->save();
$profile->status = 'delete';
$profile->delete_after = now()->addMonth();
$profile->save();
AccountService::del($profile->id);
DeleteAccountPipeline::dispatch($user)->onQueue('high');
} else {
$profile->status = 'delete';
$profile->delete_after = now()->addMonth();
$profile->save();
AccountService::del($profile->id);
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
}
return [
'status' => 200,
'msg' => 'deleted',
];
} else if($action === 'refresh_stats') {
$profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count();
$profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
$statusCount = Status::whereProfileId($user->profile_id)
@ -478,6 +616,51 @@ class AdminApiController extends Controller
])
->accessLevel('admin')
->save();
} else if($action === 'unlisted') {
ModLogService::boot()
->objectUid($profile->id)
->objectId($profile->id)
->objectType('App\Profile::class')
->user($request->user())
->action('admin.user.moderate')
->metadata([
'action' => $action,
'message' => 'Success!'
])
->accessLevel('admin')
->save();
$profile->unlisted = !$profile->unlisted;
$profile->save();
} else if($action === 'cw') {
ModLogService::boot()
->objectUid($profile->id)
->objectId($profile->id)
->objectType('App\Profile::class')
->user($request->user())
->action('admin.user.moderate')
->metadata([
'action' => $action,
'message' => 'Success!'
])
->accessLevel('admin')
->save();
$profile->cw = !$profile->cw;
$profile->save();
} else if($action === 'no_autolink') {
ModLogService::boot()
->objectUid($profile->id)
->objectId($profile->id)
->objectType('App\Profile::class')
->user($request->user())
->action('admin.user.moderate')
->metadata([
'action' => $action,
'message' => 'Success!'
])
->accessLevel('admin')
->save();
$profile->no_autolink = !$profile->no_autolink;
$profile->save();
} else {
$profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
$profile->save();
@ -511,8 +694,10 @@ class AdminApiController extends Controller
public function instances(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'q' => 'sometimes',
@ -549,8 +734,10 @@ class AdminApiController extends Controller
public function getInstance(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$id = $request->input('id');
$res = Instance::findOrFail($id);
@ -560,8 +747,10 @@ class AdminApiController extends Controller
public function moderateInstance(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'id' => 'required',
@ -584,8 +773,10 @@ class AdminApiController extends Controller
public function refreshInstanceStats(Request $request)
{
abort_if(!$request->user(), 404);
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'id' => 'required',
@ -599,4 +790,64 @@ class AdminApiController extends Controller
return new AdminInstance($instance);
}
public function getAllStats(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_unless($request->user()->is_admin === 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
if($request->has('refresh')) {
Cache::forget('admin-api:instance-all-stats-v1');
}
return Cache::remember('admin-api:instance-all-stats-v1', 1209600, function() {
$days = range(1, 7);
$res = [
'cached_at' => now()->format('c'),
];
$minStatusId = SnowflakeService::byDate(now()->subDays(7));
foreach($days as $day) {
$label = now()->subDays($day)->format('D');
$labelShort = substr($label, 0, 1);
$res['users']['days'][] = [
'date' => now()->subDays($day)->format('M j Y'),
'label_full' => $label,
'label' => $labelShort,
'count' => User::whereDate('created_at', now()->subDays($day))->count()
];
$res['posts']['days'][] = [
'date' => now()->subDays($day)->format('M j Y'),
'label_full' => $label,
'label' => $labelShort,
'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count()
];
$res['instances']['days'][] = [
'date' => now()->subDays($day)->format('M j Y'),
'label_full' => $label,
'label' => $labelShort,
'count' => Instance::whereDate('created_at', now()->subDays($day))->count()
];
}
$res['users']['total'] = DB::table('users')->count();
$res['users']['min'] = collect($res['users']['days'])->min('count');
$res['users']['max'] = collect($res['users']['days'])->max('count');
$res['users']['change'] = collect($res['users']['days'])->sum('count');;
$res['posts']['total'] = DB::table('statuses')->whereNull('uri')->count();
$res['posts']['min'] = collect($res['posts']['days'])->min('count');
$res['posts']['max'] = collect($res['posts']['days'])->max('count');
$res['posts']['change'] = collect($res['posts']['days'])->sum('count');
$res['instances']['total'] = DB::table('instances')->count();
$res['instances']['min'] = collect($res['instances']['days'])->min('count');
$res['instances']['max'] = collect($res['instances']['days'])->max('count');
$res['instances']['change'] = collect($res['instances']['days'])->sum('count');
return $res;
});
}
}

Wyświetl plik

@ -17,308 +17,323 @@ use App\Services\SearchApiV2Service;
use App\Util\Media\Filter;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
use App\Jobs\VideoPipeline\{
VideoOptimize,
VideoPostProcess,
VideoThumbnail
VideoOptimize,
VideoPostProcess,
VideoThumbnail
};
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Transformer\Api\Mastodon\v1\{
AccountTransformer,
MediaTransformer,
NotificationTransformer,
StatusTransformer,
AccountTransformer,
MediaTransformer,
NotificationTransformer,
StatusTransformer,
};
use App\Transformer\Api\{
RelationshipTransformer,
RelationshipTransformer,
};
use App\Util\Site\Nodeinfo;
use App\Services\UserRoleService;
class ApiV2Controller extends Controller
{
const PF_API_ENTITY_KEY = "_pe";
const PF_API_ENTITY_KEY = "_pe";
public function json($res, $code = 200, $headers = [])
{
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
}
public function json($res, $code = 200, $headers = [])
{
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
}
public function instance(Request $request)
{
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
if(config_cache('instance.admin.pid')) {
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
}
$admin = User::whereIsAdmin(true)->first();
return $admin && isset($admin->profile_id) ?
AccountService::getMastodon($admin->profile_id, true) :
null;
});
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
if(config_cache('instance.admin.pid')) {
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
}
$admin = User::whereIsAdmin(true)->first();
return $admin && isset($admin->profile_id) ?
AccountService::getMastodon($admin->profile_id, true) :
null;
});
$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() : [];
});
$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() : [];
});
$res = [
'domain' => config('pixelfed.domain.app'),
'title' => config_cache('app.name'),
'version' => config('pixelfed.version'),
'source_url' => 'https://github.com/pixelfed/pixelfed',
'description' => config_cache('app.short_description'),
'usage' => [
'users' => [
'active_month' => (int) Cache::remember('api:nodeinfo:am', 172800, function() {
return User::select('last_active_at', 'created_at')
->where('last_active_at', '>', now()->subMonths(1))
->orWhere('created_at', '>', now()->subMonths(1))
->count();
})
]
],
'thumbnail' => [
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
'blurhash' => InstanceService::headerBlurhash(),
'versions' => [
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
]
],
'languages' => [config('app.locale')],
'configuration' => [
'urls' => [
'streaming' => 'wss://' . config('pixelfed.domain.app'),
'status' => null
],
'accounts' => [
'max_featured_tags' => 0,
],
'statuses' => [
'max_characters' => (int) config('pixelfed.max_caption_length'),
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
'characters_reserved_per_url' => 23
],
'media_attachments' => [
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
'image_matrix_limit' => 3686400,
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
'video_frame_rate_limit' => 240,
'video_matrix_limit' => 3686400
],
'polls' => [
'max_options' => 4,
'max_characters_per_option' => 50,
'min_expiration' => 300,
'max_expiration' => 2629746,
],
'translation' => [
'enabled' => false,
],
],
'registrations' => [
'enabled' => (bool) config_cache('pixelfed.open_registration'),
'approval_required' => false,
'message' => null
],
'contact' => [
'email' => config('instance.email'),
'account' => $contact
],
'rules' => $rules
];
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
return [
'domain' => config('pixelfed.domain.app'),
'title' => config_cache('app.name'),
'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
'source_url' => 'https://github.com/pixelfed/pixelfed',
'description' => config_cache('app.short_description'),
'usage' => [
'users' => [
'active_month' => (int) Nodeinfo::activeUsersMonthly()
]
],
'thumbnail' => [
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
'blurhash' => InstanceService::headerBlurhash(),
'versions' => [
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
]
],
'languages' => [config('app.locale')],
'configuration' => [
'urls' => [
'streaming' => null,
'status' => null
],
'vapid' => [
'public_key' => config('webpush.vapid.public_key'),
],
'accounts' => [
'max_featured_tags' => 0,
],
'statuses' => [
'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
'characters_reserved_per_url' => 23
],
'media_attachments' => [
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
'image_matrix_limit' => 3686400,
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
'video_frame_rate_limit' => 240,
'video_matrix_limit' => 3686400
],
'polls' => [
'max_options' => 0,
'max_characters_per_option' => 0,
'min_expiration' => 0,
'max_expiration' => 0,
],
'translation' => [
'enabled' => false,
],
],
'registrations' => [
'enabled' => null,
'approval_required' => false,
'message' => null,
'url' => null,
],
'contact' => [
'email' => config('instance.email'),
'account' => $contact
],
'rules' => $rules
];
});
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
}
/**
* GET /api/v2/search
*
*
* @return array
*/
public function search(Request $request)
{
abort_if(!$request->user(), 403);
/**
* GET /api/v2/search
*
*
* @return array
*/
public function search(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:100',
'account_id' => 'nullable|string',
'max_id' => 'nullable|string',
'min_id' => 'nullable|string',
'type' => 'nullable|in:accounts,hashtags,statuses',
'exclude_unreviewed' => 'nullable',
'resolve' => 'nullable',
'limit' => 'nullable|integer|max:40',
'offset' => 'nullable|integer',
'following' => 'nullable'
]);
$this->validate($request, [
'q' => 'required|string|min:1|max:100',
'account_id' => 'nullable|string',
'max_id' => 'nullable|string',
'min_id' => 'nullable|string',
'type' => 'nullable|in:accounts,hashtags,statuses',
'exclude_unreviewed' => 'nullable',
'resolve' => 'nullable',
'limit' => 'nullable|integer|max:40',
'offset' => 'nullable|integer',
'following' => 'nullable'
]);
$mastodonMode = !$request->has('_pe');
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
}
if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) {
return [
'accounts' => [],
'hashtags' => [],
'statuses' => []
];
}
/**
* GET /api/v2/streaming/config
*
*
* @return object
*/
public function getWebsocketConfig()
{
return config('broadcasting.default') === 'pusher' ? [
'host' => config('broadcasting.connections.pusher.options.host'),
'port' => config('broadcasting.connections.pusher.options.port'),
'key' => config('broadcasting.connections.pusher.key'),
'cluster' => config('broadcasting.connections.pusher.options.cluster')
] : [];
}
$mastodonMode = !$request->has('_pe');
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
}
/**
* POST /api/v2/media
*
*
* @return MediaTransformer
*/
public function mediaUploadV2(Request $request)
{
abort_if(!$request->user(), 403);
/**
* GET /api/v2/streaming/config
*
*
* @return object
*/
public function getWebsocketConfig()
{
return config('broadcasting.default') === 'pusher' ? [
'host' => config('broadcasting.connections.pusher.options.host'),
'port' => config('broadcasting.connections.pusher.options.port'),
'key' => config('broadcasting.connections.pusher.key'),
'cluster' => config('broadcasting.connections.pusher.options.cluster')
] : [];
}
$this->validate($request, [
'file.*' => [
'required_without:file',
'mimetypes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
],
'file' => [
'required_without:file.*',
'mimetypes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
],
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24',
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
'replace_id' => 'sometimes'
]);
/**
* POST /api/v2/media
*
*
* @return MediaTransformer
*/
public function mediaUploadV2(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$user = $request->user();
$this->validate($request, [
'file.*' => [
'required_without:file',
'mimetypes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
],
'file' => [
'required_without:file.*',
'mimetypes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
],
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24',
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
'replace_id' => 'sometimes'
]);
if($user->last_active_at == null) {
return [];
}
$user = $request->user();
if(empty($request->file('file'))) {
return response('', 422);
}
if($user->last_active_at == null) {
return [];
}
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
if(empty($request->file('file'))) {
return response('', 422);
}
return $dailyLimit >= 250;
});
abort_if($limitReached == true, 429);
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
$profile = $user->profile;
return $dailyLimit >= 1250;
});
abort_if($limitReached == true, 429);
if(config_cache('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config_cache('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$profile = $user->profile;
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
if(config_cache('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config_cache('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$photo = $request->file('file');
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
$mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
abort(403, 'Invalid or unsupported mime type.');
}
$photo = $request->file('file');
$storagePath = MediaPathService::get($user, 2);
$path = $photo->storePublicly($storagePath);
$hash = \hash_file('sha256', $photo);
$license = null;
$mime = $photo->getMimeType();
$mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
abort(403, 'Invalid or unsupported mime type.');
}
$settings = UserSetting::whereUserId($user->id)->first();
$storagePath = MediaPathService::get($user, 2);
$path = $photo->storePublicly($storagePath);
$hash = \hash_file('sha256', $photo);
$license = null;
$mime = $photo->getMimeType();
if($settings && !empty($settings->compose_settings)) {
$compose = $settings->compose_settings;
$settings = UserSetting::whereUserId($user->id)->first();
if(isset($compose['default_license']) && $compose['default_license'] != 1) {
$license = $compose['default_license'];
}
}
if($settings && !empty($settings->compose_settings)) {
$compose = $settings->compose_settings;
abort_if(MediaBlocklistService::exists($hash) == true, 451);
if(isset($compose['default_license']) && $compose['default_license'] != 1) {
$license = $compose['default_license'];
}
}
if($request->has('replace_id')) {
$rpid = $request->input('replace_id');
$removeMedia = Media::whereNull('status_id')
->whereUserId($user->id)
->whereProfileId($profile->id)
->where('created_at', '>', now()->subHours(2))
->find($rpid);
if($removeMedia) {
MediaDeletePipeline::dispatch($removeMedia)
->onQueue('mmo')
->delay(now()->addMinutes(15));
}
}
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media();
$media->status_id = null;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $mime;
$media->caption = $request->input('description');
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
if($license) {
$media->license = $license;
}
$media->save();
if($request->has('replace_id')) {
$rpid = $request->input('replace_id');
$removeMedia = Media::whereNull('status_id')
->whereUserId($user->id)
->whereProfileId($profile->id)
->where('created_at', '>', now()->subHours(2))
->find($rpid);
if($removeMedia) {
MediaDeletePipeline::dispatch($removeMedia)
->onQueue('mmo')
->delay(now()->addMinutes(15));
}
}
switch ($media->mime) {
case 'image/jpeg':
case 'image/png':
ImageOptimize::dispatch($media)->onQueue('mmo');
break;
$media = new Media();
$media->status_id = null;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $mime;
$media->caption = $request->input('description');
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
if($license) {
$media->license = $license;
}
$media->save();
case 'video/mp4':
VideoThumbnail::dispatch($media)->onQueue('mmo');
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
}
switch ($media->mime) {
case 'image/jpeg':
case 'image/png':
ImageOptimize::dispatch($media)->onQueue('mmo');
break;
Cache::forget($limitKey);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
$res = $fractal->createData($resource)->toArray();
$res['preview_url'] = $media->url(). '?v=' . time();
$res['url'] = null;
return $this->json($res, 202);
}
case 'video/mp4':
VideoThumbnail::dispatch($media)->onQueue('mmo');
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
}
Cache::forget($limitKey);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
$res = $fractal->createData($resource)->toArray();
$res['preview_url'] = $media->url(). '?v=' . time();
$res['url'] = null;
return $this->json($res, 202);
}
}

Wyświetl plik

@ -99,6 +99,7 @@ class BaseApiController extends Controller
public function avatarUpdate(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
]);
@ -134,9 +135,10 @@ class BaseApiController extends Controller
public function verifyCredentials(Request $request)
{
abort_if(!$request->user(), 403);
$user = $request->user();
abort_if(!$user, 403);
if($user->status != null) {
if ($user->status != null) {
Auth::logout();
abort(403);
}
@ -147,6 +149,7 @@ class BaseApiController extends Controller
public function accountLikes(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'page' => 'sometimes|int|min:1|max:20',
'limit' => 'sometimes|int|min:1|max:10'

Wyświetl plik

@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\UserDomainBlock;
use App\Util\ActivityPub\Helpers;
use App\Services\UserFilterService;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
use App\Jobs\HomeFeedPipeline\FeedRemoveDomainPipeline;
use App\Jobs\ProfilePipeline\ProfilePurgeNotificationsByDomain;
use App\Jobs\ProfilePipeline\ProfilePurgeFollowersByDomain;
class DomainBlockController extends Controller
{
public function json($res, $code = 200, $headers = [])
{
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
}
public function index(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'limit' => 'sometimes|integer|min:1|max:200'
]);
$limit = $request->input('limit', 100);
$id = $request->user()->profile_id;
$filters = UserDomainBlock::whereProfileId($id)->orderByDesc('id')->cursorPaginate($limit);
$links = null;
$headers = [];
if($filters->nextCursor()) {
$links .= '<'.$filters->nextPageUrl().'&limit='.$limit.'>; rel="next"';
}
if($filters->previousCursor()) {
if($links != null) {
$links .= ', ';
}
$links .= '<'.$filters->previousPageUrl().'&limit='.$limit.'>; rel="prev"';
}
if($links) {
$headers = ['Link' => $links];
}
return $this->json($filters->pluck('domain'), 200, $headers);
}
public function store(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'domain' => 'required|active_url|min:1|max:120'
]);
$pid = $request->user()->profile_id;
$domain = trim($request->input('domain'));
if(Helpers::validateUrl($domain) == false) {
return abort(500, 'Invalid domain or already blocked by server admins');
}
$domain = strtolower(parse_url($domain, PHP_URL_HOST));
abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server');
$existingCount = UserDomainBlock::whereProfileId($pid)->count();
$maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks');
$errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]);
abort_if($existingCount >= $maxLimit, 400, $errorMsg);
$block = UserDomainBlock::updateOrCreate([
'profile_id' => $pid,
'domain' => $domain
]);
if($block->wasRecentlyCreated) {
Bus::batch([
[
new FeedRemoveDomainPipeline($pid, $domain),
new ProfilePurgeNotificationsByDomain($pid, $domain),
new ProfilePurgeFollowersByDomain($pid, $domain)
]
])->allowFailures()->onQueue('feed')->dispatch();
Cache::forget('profile:following:' . $pid);
UserFilterService::domainBlocks($pid, true);
}
return $this->json([]);
}
public function delete(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'domain' => 'required|min:1|max:120'
]);
$pid = $request->user()->profile_id;
$domain = strtolower(trim($request->input('domain')));
$filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete();
UserFilterService::domainBlocks($pid, true);
return $this->json([]);
}
}

Wyświetl plik

@ -0,0 +1,209 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Hashtag;
use App\HashtagFollow;
use App\StatusHashtag;
use App\Services\AccountService;
use App\Services\HashtagService;
use App\Services\HashtagFollowService;
use App\Services\HashtagRelatedService;
use App\Http\Resources\MastoApi\FollowedTagResource;
use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline;
use App\Jobs\HomeFeedPipeline\HashtagUnfollowPipeline;
class TagsController extends Controller
{
const PF_API_ENTITY_KEY = "_pe";
public function json($res, $code = 200, $headers = [])
{
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
}
/**
* GET /api/v1/tags/:id/related
*
*
* @return array
*/
public function relatedTags(Request $request, $tag)
{
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$tag = Hashtag::whereSlug($tag)->firstOrFail();
return HashtagRelatedService::get($tag->id);
}
/**
* POST /api/v1/tags/:id/follow
*
*
* @return object
*/
public function followHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
abort_if(!$tag, 422, 'Unknown hashtag');
abort_if(
HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT,
422,
'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.'
);
$follows = HashtagFollow::updateOrCreate(
[
'profile_id' => $account['id'],
'hashtag_id' => $tag->id
],
[
'user_id' => $request->user()->id
]
);
HashtagService::follow($pid, $tag->id);
HashtagFollowService::add($tag->id, $pid);
return response()->json(FollowedTagResource::make($follows)->toArray($request));
}
/**
* POST /api/v1/tags/:id/unfollow
*
*
* @return object
*/
public function unfollowHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
abort_if(!$tag, 422, 'Unknown hashtag');
$follows = HashtagFollow::whereProfileId($pid)
->whereHashtagId($tag->id)
->first();
if(!$follows) {
return [
'name' => $tag->name,
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
'history' => [],
'following' => false
];
}
if($follows) {
HashtagService::unfollow($pid, $tag->id);
HashtagFollowService::unfollow($tag->id, $pid);
HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed');
$follows->delete();
}
$res = FollowedTagResource::make($follows)->toArray($request);
$res['following'] = false;
return response()->json($res);
}
/**
* GET /api/v1/tags/:id
*
*
* @return object
*/
public function getHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
if(!$tag) {
return [
'name' => $id,
'url' => config('app.url') . '/i/web/hashtag/' . $id,
'history' => [],
'following' => false
];
}
$res = [
'name' => $tag->name,
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
'history' => [],
'following' => HashtagService::isFollowing($pid, $tag->id)
];
if($request->has(self::PF_API_ENTITY_KEY)) {
$res['count'] = HashtagService::count($tag->id);
}
return $this->json($res);
}
/**
* GET /api/v1/followed_tags
*
*
* @return array
*/
public function getFollowedTags(Request $request)
{
abort_if(!$request->user(), 403);
$account = AccountService::get($request->user()->profile_id);
$this->validate($request, [
'cursor' => 'sometimes',
'limit' => 'sometimes|integer|min:1|max:200'
]);
$limit = $request->input('limit', 100);
$res = HashtagFollow::whereProfileId($account['id'])
->orderByDesc('id')
->cursorPaginate($limit)
->withQueryString();
$pagination = false;
$prevPage = $res->nextPageUrl();
$nextPage = $res->previousPageUrl();
if($nextPage && $prevPage) {
$pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"';
} else if($nextPage && !$prevPage) {
$pagination = '<' . $nextPage . '>; rel="next"';
} else if(!$nextPage && $prevPage) {
$pagination = '<' . $prevPage . '>; rel="prev"';
}
if($pagination) {
return response()->json(FollowedTagResource::collection($res)->collection)
->header('Link', $pagination);
}
return response()->json(FollowedTagResource::collection($res)->collection);
}
}

Wyświetl plik

@ -62,7 +62,7 @@ class ForgotPasswordController extends Controller
usleep(random_int(100000, 3000000));
if(config('captcha.enabled')) {
if((bool) config_cache('captcha.enabled')) {
$rules = [
'email' => 'required|email',
'h-captcha-response' => 'required|captcha'

Wyświetl plik

@ -71,20 +71,21 @@ class LoginController extends Controller
$this->username() => 'required|email',
'password' => 'required|string|min:6',
];
$messages = [];
if(
config('captcha.enabled') ||
config('captcha.active.login') ||
(bool) config_cache('captcha.enabled') &&
(bool) config_cache('captcha.active.login') ||
(
config('captcha.triggers.login.enabled') &&
(bool) config_cache('captcha.triggers.login.enabled') &&
request()->session()->has('login_attempts') &&
request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
)
) {
$rules['h-captcha-response'] = 'required|filled|captcha|min:5';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules);
$request->validate($rules, $messages);
}
/**

Wyświetl plik

@ -60,7 +60,7 @@ class RegisterController extends Controller
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
public function validator(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
@ -137,7 +137,7 @@ class RegisterController extends Controller
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
];
if(config('captcha.enabled') || config('captcha.active.register')) {
if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) {
$rules['h-captcha-response'] = 'required|captcha';
}
@ -151,7 +151,7 @@ class RegisterController extends Controller
*
* @return \App\User
*/
protected function create(array $data)
public function create(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
@ -174,12 +174,13 @@ class RegisterController extends Controller
*/
public function showRegistrationForm()
{
if(config_cache('pixelfed.open_registration')) {
if((bool) config_cache('pixelfed.open_registration')) {
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp(request()->ip()), 404);
}
$limit = config('pixelfed.max_users');
if($limit) {
$hasLimit = config('pixelfed.enforce_max_users');
if($hasLimit) {
$limit = config('pixelfed.max_users');
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
if($limit <= $count) {
return redirect(route('help.instance-max-users-limit'));
@ -190,7 +191,11 @@ class RegisterController extends Controller
return view('auth.register');
}
} else {
abort(404);
if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
return redirect('/auth/sign_up');
} else {
abort(404);
}
}
}
@ -208,13 +213,17 @@ class RegisterController extends Controller
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
$limit = config('pixelfed.max_users');
$hasLimit = config('pixelfed.enforce_max_users');
if($hasLimit) {
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
$limit = config('pixelfed.max_users');
if(false == config_cache('pixelfed.open_registration') || $limit && $limit <= $count) {
return redirect(route('help.instance-max-users-limit'));
if($limit && $limit <= $count) {
return redirect(route('help.instance-max-users-limit'));
}
}
$this->validator($request->all())->validate();
event(new Registered($user = $this->create($request->all())));

Wyświetl plik

@ -50,7 +50,7 @@ class ResetPasswordController extends Controller
{
usleep(random_int(100000, 3000000));
if(config('captcha.enabled')) {
if((bool) config_cache('captcha.enabled')) {
return [
'token' => 'required',
'email' => 'required|email',

Wyświetl plik

@ -8,60 +8,56 @@ use Auth;
use Illuminate\Http\Request;
use App\Services\BookmarkService;
use App\Services\FollowerService;
use App\Services\UserRoleService;
class BookmarkController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function __construct()
{
$this->middleware('auth');
}
public function store(Request $request)
{
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
public function store(Request $request)
{
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$profile = Auth::user()->profile;
$status = Status::findOrFail($request->input('item'));
$user = $request->user();
$status = Status::findOrFail($request->input('item'));
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
if($status->scope == 'private') {
if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) {
if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) {
BookmarkService::del($profile->id, $status->id);
$exists->delete();
if($status->scope == 'private') {
if($user->profile_id !== $status->profile_id && !FollowerService::follows($user->profile_id, $status->profile_id)) {
if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) {
BookmarkService::del($user->profile_id, $status->id);
$exists->delete();
if ($request->ajax()) {
return ['code' => 200, 'msg' => 'Bookmark removed!'];
} else {
return redirect()->back();
}
}
abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
}
}
if ($request->ajax()) {
return ['code' => 200, 'msg' => 'Bookmark removed!'];
} else {
return redirect()->back();
}
}
abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
}
}
$bookmark = Bookmark::firstOrCreate(
['status_id' => $status->id], ['profile_id' => $profile->id]
);
$bookmark = Bookmark::firstOrCreate(
['status_id' => $status->id], ['profile_id' => $user->profile_id]
);
if (!$bookmark->wasRecentlyCreated) {
BookmarkService::del($profile->id, $status->id);
$bookmark->delete();
} else {
BookmarkService::add($profile->id, $status->id);
}
if (!$bookmark->wasRecentlyCreated) {
BookmarkService::del($user->profile_id, $status->id);
$bookmark->delete();
} else {
BookmarkService::add($user->profile_id, $status->id);
}
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
} else {
$response = redirect()->back();
}
return $response;
}
return $request->expectsJson() ? ['code' => 200, 'msg' => 'Bookmark saved!'] : redirect()->back();
}
}

Wyświetl plik

@ -153,7 +153,7 @@ class CollectionController extends Controller
abort(400, 'You can only add '.$max.' posts per collection');
}
$status = Status::whereScope('public')
$status = Status::whereIn('scope', ['public', 'unlisted'])
->whereProfileId($profileId)
->whereIn('type', ['photo', 'photo:album', 'video'])
->findOrFail($postId);
@ -166,17 +166,13 @@ class CollectionController extends Controller
'order' => $count,
]);
CollectionService::addItem(
$collection->id,
$status->id,
$count
);
CollectionService::deleteCollection($collection->id);
$collection->updated_at = now();
$collection->save();
CollectionService::setCollection($collection->id, $collection);
return StatusService::get($status->id);
return StatusService::get($status->id, false);
}
public function getCollection(Request $request, $id)
@ -226,10 +222,10 @@ class CollectionController extends Controller
return collect($items)
->map(function($id) {
return StatusService::get($id);
return StatusService::get($id, false);
})
->filter(function($item) {
return $item && isset($item['account'], $item['media_attachments']);
return $item && ($item['visibility'] == 'public' || $item['visibility'] == 'unlisted') && isset($item['account'], $item['media_attachments']);
})
->values();
}
@ -298,7 +294,7 @@ class CollectionController extends Controller
abort(400, 'You cannot delete the only post of a collection!');
}
$status = Status::whereScope('public')
$status = Status::whereIn('scope', ['public', 'unlisted'])
->whereIn('type', ['photo', 'photo:album', 'video'])
->findOrFail($postId);

Wyświetl plik

@ -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();

Wyświetl plik

@ -0,0 +1,399 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\User;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use App\Services\EmailService;
use App\Services\BouncerService;
use App\Util\Lexer\RestrictedNames;
use App\Mail\CuratedRegisterConfirmEmail;
use App\Mail\CuratedRegisterNotifyAdmin;
use Illuminate\Support\Facades\Mail;
use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
class CuratedRegisterController extends Controller
{
public function __construct()
{
abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
if((bool) config_cache('pixelfed.open_registration')) {
abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
} else {
abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
}
}
public function index(Request $request)
{
abort_if($request->user(), 404);
return view('auth.curated-register.index', ['step' => 1]);
}
public function concierge(Request $request)
{
abort_if($request->user(), 404);
$emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
$request->has('next') &&
$request->session()->has('cur-reg-con.cr-id');
return view('auth.curated-register.concierge', compact('emailConfirmed'));
}
public function conciergeResponseSent(Request $request)
{
return view('auth.curated-register.user_response_sent');
}
public function conciergeFormShow(Request $request)
{
abort_if($request->user(), 404);
abort_unless(
$request->session()->has('cur-reg-con.email-confirmed') &&
$request->session()->has('cur-reg-con.cr-id') &&
$request->session()->has('cur-reg-con.ac-id'), 404);
$crid = $request->session()->get('cur-reg-con.cr-id');
$arid = $request->session()->get('cur-reg-con.ac-id');
$showCaptcha = config('instance.curated_registration.captcha_enabled');
if($attempts = $request->session()->get('cur-reg-con-attempt')) {
$showCaptcha = $attempts && $attempts >= 2;
} else {
$showCaptcha = false;
}
$activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
}
public function conciergeFormStore(Request $request)
{
abort_if($request->user(), 404);
$request->session()->increment('cur-reg-con-attempt');
abort_unless(
$request->session()->has('cur-reg-con.email-confirmed') &&
$request->session()->has('cur-reg-con.cr-id') &&
$request->session()->has('cur-reg-con.ac-id'), 404);
$attempts = $request->session()->get('cur-reg-con-attempt');
$messages = [];
$rules = [
'response' => 'required|string|min:5|max:1000',
'crid' => 'required|integer|min:1',
'acid' => 'required|integer|min:1'
];
if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$crid = $request->session()->get('cur-reg-con.cr-id');
$acid = $request->session()->get('cur-reg-con.ac-id');
abort_if((string) $crid !== $request->input('crid'), 404);
abort_if((string) $acid !== $request->input('acid'), 404);
if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
}
$act = CuratedRegisterActivity::create([
'register_id' => $crid,
'reply_to_id' => $acid,
'type' => 'user_response',
'message' => $request->input('response'),
'from_user' => true,
'action_required' => true,
]);
CuratedRegister::findOrFail($crid)->update(['user_has_responded' => true]);
$request->session()->pull('cur-reg-con');
$request->session()->pull('cur-reg-con-attempt');
return view('auth.curated-register.user_response_sent');
}
public function conciergeStore(Request $request)
{
abort_if($request->user(), 404);
$rules = [
'sid' => 'required_if:action,email|integer|min:1|max:20000000',
'id' => 'required_if:action,email|integer|min:1|max:20000000',
'code' => 'required_if:action,email',
'action' => 'required|string|in:email,message',
'email' => 'required_if:action,email|email',
'response' => 'required_if:action,message|string|min:20|max:1000',
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$action = $request->input('action');
$sid = $request->input('sid');
$id = $request->input('id');
$code = $request->input('code');
$email = $request->input('email');
$cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
$ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
if(!hash_equals($ac->secret_code, $code)) {
return redirect()->back()->withErrors(['code' => 'Invalid code']);
}
if(!hash_equals($cr->email, $email)) {
return redirect()->back()->withErrors(['email' => 'Invalid email']);
}
$request->session()->put('cur-reg-con.email-confirmed', true);
$request->session()->put('cur-reg-con.cr-id', $cr->id);
$request->session()->put('cur-reg-con.ac-id', $ac->id);
$emailConfirmed = true;
return redirect('/auth/sign_up/concierge/form');
}
public function confirmEmail(Request $request)
{
if($request->user()) {
return redirect(route('help.email-confirmation-issues'));
}
return view('auth.curated-register.confirm_email');
}
public function emailConfirmed(Request $request)
{
if($request->user()) {
return redirect(route('help.email-confirmation-issues'));
}
return view('auth.curated-register.email_confirmed');
}
public function resendConfirmation(Request $request)
{
return view('auth.curated-register.resend-confirmation');
}
public function resendConfirmationProcess(Request $request)
{
$rules = [
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'exists:curated_registers',
]
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
if(!$cur) {
return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
}
$totalCount = CuratedRegisterActivity::whereRegisterId($cur->id)
->whereType('user_resend_email_confirmation')
->count();
if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
}
$count = CuratedRegisterActivity::whereRegisterId($cur->id)
->whereType('user_resend_email_confirmation')
->where('created_at', '>', now()->subHours(12))
->count();
if($count) {
return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
}
CuratedRegisterActivity::create([
'register_id' => $cur->id,
'type' => 'user_resend_email_confirmation',
'admin_only_view' => true,
'from_admin' => false,
'from_user' => false,
'action_required' => false,
]);
Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
return view('auth.curated-register.resent-confirmation');
return $request->all();
}
public function confirmEmailHandle(Request $request)
{
$rules = [
'sid' => 'required',
'code' => 'required'
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$cr = CuratedRegister::whereNull('email_verified_at')
->where('created_at', '>', now()->subHours(24))
->find($request->input('sid'));
if(!$cr) {
return redirect(route('help.email-confirmation-issues'));
}
if(!hash_equals($cr->verify_code, $request->input('code'))) {
return redirect(route('help.email-confirmation-issues'));
}
$cr->email_verified_at = now();
$cr->save();
if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
}
return view('auth.curated-register.email_confirmed');
}
public function proceed(Request $request)
{
$this->validate($request, [
'step' => 'required|integer|in:1,2,3,4'
]);
$step = $request->input('step');
switch($step) {
case 1:
$step = 2;
$request->session()->put('cur-step', 1);
return view('auth.curated-register.index', compact('step'));
break;
case 2:
$this->stepTwo($request);
$step = 3;
$request->session()->put('cur-step', 2);
return view('auth.curated-register.index', compact('step'));
break;
case 3:
$this->stepThree($request);
$step = 3;
$request->session()->put('cur-step', 3);
$verifiedEmail = true;
$request->session()->pull('cur-reg');
return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
break;
}
}
protected function stepTwo($request)
{
if($request->filled('reason')) {
$request->session()->put('cur-reg.form-reason', $request->input('reason'));
}
if($request->filled('username')) {
$request->session()->put('cur-reg.form-username', $request->input('username'));
}
if($request->filled('email')) {
$request->session()->put('cur-reg.form-email', $request->input('email'));
}
$this->validate($request, [
'username' => [
'required',
'min:2',
'max:15',
'unique:curated_registers',
'unique:users',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
if(ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
if(($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if (!ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.');
}
if (!ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
$val = str_replace(['_', '.', '-'], '', $value);
if(!ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
}
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
],
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'max:255',
'unique:users',
'unique:curated_registers',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
],
'password' => 'required|min:8',
'password_confirmation' => 'required|same:password',
'reason' => 'required|min:20|max:1000',
'agree' => 'required|accepted'
]);
$request->session()->put('cur-reg.form-email', $request->input('email'));
$request->session()->put('cur-reg.form-password', $request->input('password'));
}
protected function stepThree($request)
{
$this->validate($request, [
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'max:255',
'unique:users',
'unique:curated_registers',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
]
]);
$cr = new CuratedRegister;
$cr->email = $request->email;
$cr->username = $request->session()->get('cur-reg.form-username');
$cr->password = bcrypt($request->session()->get('cur-reg.form-password'));
$cr->ip_address = $request->ip();
$cr->reason_to_join = $request->session()->get('cur-reg.form-reason');
$cr->verify_code = Str::random(40);
$cr->save();
Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr));
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -2,265 +2,269 @@
namespace App\Http\Controllers;
use App\Jobs\InboxPipeline\{
DeleteWorker,
InboxWorker,
InboxValidator
};
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
use App\{
AccountLog,
Like,
Profile,
Status,
User
};
use App\Util\Lexer\Nickname;
use App\Util\Webfinger\Webfinger;
use Auth;
use Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use League\Fractal;
use App\Util\Site\Nodeinfo;
use App\Util\ActivityPub\{
Helpers,
HttpSignature,
Outbox
};
use Zttp\Zttp;
use App\Jobs\InboxPipeline\DeleteWorker;
use App\Jobs\InboxPipeline\InboxValidator;
use App\Jobs\InboxPipeline\InboxWorker;
use App\Profile;
use App\Services\AccountService;
use App\Services\InstanceService;
use App\Status;
use App\Util\Lexer\Nickname;
use App\Util\Site\Nodeinfo;
use App\Util\Webfinger\Webfinger;
use Cache;
use Illuminate\Http\Request;
class FederationController extends Controller
{
public function nodeinfoWellKnown()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
public function nodeinfoWellKnown()
{
abort_if(! config('federation.nodeinfo.enabled'), 404);
public function nodeinfo()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin', '*');
}
public function webfinger(Request $request)
{
if (!config('federation.webfinger.enabled') ||
!$request->has('resource') ||
!$request->filled('resource')
) {
return response('', 400);
}
public function nodeinfo()
{
abort_if(! config('federation.nodeinfo.enabled'), 404);
$resource = $request->input('resource');
$domain = config('pixelfed.domain.app');
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin', '*');
}
if(config('federation.activitypub.sharedInbox') &&
$resource == 'acct:' . $domain . '@' . $domain) {
$res = [
'subject' => 'acct:' . $domain . '@' . $domain,
'aliases' => [
'https://' . $domain . '/i/actor'
],
'links' => [
[
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => 'https://' . $domain . '/site/kb/instance-actor'
],
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://' . $domain . '/i/actor'
]
]
];
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
}
$hash = hash('sha256', $resource);
$key = 'federation:webfinger:sha256:' . $hash;
if($cached = Cache::get($key)) {
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
}
if(strpos($resource, $domain) == false) {
return response('', 400);
}
$parsed = Nickname::normalizeProfileUrl($resource);
if(empty($parsed) || $parsed['domain'] !== $domain) {
return response('', 400);
}
$username = $parsed['username'];
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
if(!$profile || $profile->status !== null) {
return response('', 400);
}
$webfinger = (new Webfinger($profile))->generate();
Cache::put($key, $webfinger, 1209600);
public function webfinger(Request $request)
{
if (! config('federation.webfinger.enabled') ||
! $request->has('resource') ||
! $request->filled('resource')
) {
return response('', 400);
}
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
$resource = $request->input('resource');
$domain = config('pixelfed.domain.app');
public function hostMeta(Request $request)
{
abort_if(!config('federation.webfinger.enabled'), 404);
if (config('federation.activitypub.sharedInbox') &&
$resource == 'acct:'.$domain.'@'.$domain) {
$res = [
'subject' => 'acct:'.$domain.'@'.$domain,
'aliases' => [
'https://'.$domain.'/i/actor',
],
'links' => [
[
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => 'https://'.$domain.'/site/kb/instance-actor',
],
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://'.$domain.'/i/actor',
],
],
];
$path = route('well-known.webfinger');
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
}
$hash = hash('sha256', $resource);
$key = 'federation:webfinger:sha256:'.$hash;
if ($cached = Cache::get($key)) {
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
}
if (strpos($resource, $domain) == false) {
return response('', 400);
}
$parsed = Nickname::normalizeProfileUrl($resource);
if (empty($parsed) || $parsed['domain'] !== $domain) {
return response('', 400);
}
$username = $parsed['username'];
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
if (! $profile || $profile->status !== null) {
return response('', 400);
}
$webfinger = (new Webfinger($profile))->generate();
Cache::put($key, $webfinger, 1209600);
return response($xml)->header('Content-Type', 'application/xrd+xml');
}
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin', '*');
}
public function userOutbox(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
public function hostMeta(Request $request)
{
abort_if(! config('federation.webfinger.enabled'), 404);
if(!$request->wantsJson()) {
return redirect('/' . $username);
}
$path = route('well-known.webfinger');
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
$res = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox',
'type' => 'OrderedCollection',
'totalItems' => 0,
'orderedItems' => []
];
return response($xml)->header('Content-Type', 'application/xrd+xml');
}
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
}
public function userOutbox(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
public function userInbox(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.inbox'), 404);
if (! $request->wantsJson()) {
return redirect('/'.$username);
}
$headers = $request->headers->all();
$payload = $request->getContent();
if(!$payload || empty($payload)) {
return;
}
$obj = json_decode($payload, true, 8);
if(!isset($obj['id'])) {
return;
}
$domain = parse_url($obj['id'], PHP_URL_HOST);
if(in_array($domain, InstanceService::getBannedDomains())) {
return;
}
$id = AccountService::usernameToId($username);
abort_if(! $id, 404);
$account = AccountService::get($id);
abort_if(! $account || ! isset($account['statuses_count']), 404);
$res = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox',
'type' => 'OrderedCollection',
'totalItems' => $account['statuses_count'] ?? 0,
];
if(isset($obj['type']) && $obj['type'] === 'Delete') {
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
if($obj['object']['type'] === 'Person') {
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
return;
}
}
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
}
if($obj['object']['type'] === 'Tombstone') {
if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
return;
}
}
public function userInbox(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(! config('federation.activitypub.inbox'), 404);
if($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
return;
}
}
return;
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
} else {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
}
return;
}
$headers = $request->headers->all();
$payload = $request->getContent();
if (! $payload || empty($payload)) {
return;
}
$obj = json_decode($payload, true, 8);
if (! isset($obj['id'])) {
return;
}
$domain = parse_url($obj['id'], PHP_URL_HOST);
if (in_array($domain, InstanceService::getBannedDomains())) {
return;
}
public function sharedInbox(Request $request)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.sharedInbox'), 404);
if (isset($obj['type']) && $obj['type'] === 'Delete') {
if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
if ($obj['object']['type'] === 'Person') {
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
$headers = $request->headers->all();
$payload = $request->getContent();
return;
}
}
if(!$payload || empty($payload)) {
return;
}
if ($obj['object']['type'] === 'Tombstone') {
if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
$obj = json_decode($payload, true, 8);
if(!isset($obj['id'])) {
return;
}
return;
}
}
$domain = parse_url($obj['id'], PHP_URL_HOST);
if(in_array($domain, InstanceService::getBannedDomains())) {
return;
}
if ($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
if(isset($obj['type']) && $obj['type'] === 'Delete') {
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
if($obj['object']['type'] === 'Person') {
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
return;
}
}
return;
}
}
if($obj['object']['type'] === 'Tombstone') {
if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
return;
}
}
return;
} elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
} else {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
}
if($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
return;
}
}
return;
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
} else {
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
}
return;
}
}
public function userFollowing(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
public function sharedInbox(Request $request)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(! config('federation.activitypub.sharedInbox'), 404);
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
return response()->json($obj);
}
$headers = $request->headers->all();
$payload = $request->getContent();
public function userFollowers(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
if (! $payload || empty($payload)) {
return;
}
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
$obj = json_decode($payload, true, 8);
if (! isset($obj['id'])) {
return;
}
return response()->json($obj);
}
$domain = parse_url($obj['id'], PHP_URL_HOST);
if (in_array($domain, InstanceService::getBannedDomains())) {
return;
}
if (isset($obj['type']) && $obj['type'] === 'Delete') {
if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
if ($obj['object']['type'] === 'Person') {
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
return;
}
}
if ($obj['object']['type'] === 'Tombstone') {
if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
return;
}
}
if ($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
return;
}
}
return;
} elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
} else {
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
}
}
public function userFollowing(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
$id = AccountService::usernameToId($username);
abort_if(! $id, 404);
$account = AccountService::get($id);
abort_if(! $account || ! isset($account['following_count']), 404);
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollection',
'totalItems' => $account['following_count'] ?? 0,
];
return response()->json($obj)->header('Content-Type', 'application/activity+json');
}
public function userFollowers(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
$id = AccountService::usernameToId($username);
abort_if(! $id, 404);
$account = AccountService::get($id);
abort_if(! $account || ! isset($account['followers_count']), 404);
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollection',
'totalItems' => $account['followers_count'] ?? 0,
];
return response()->json($obj)->header('Content-Type', 'application/activity+json');
}
}

Wyświetl plik

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class HealthCheckController extends Controller
{
public function get(Request $request)
{
return response('OK')->withHeaders([
'Content-Type' => 'text/plain',
'Cache-Control' => 'max-age=0, must-revalidate, no-cache, no-store'
]);
}
}

Wyświetl plik

@ -17,7 +17,7 @@ trait Instagram
{
public function instagram()
{
if(config_cache('pixelfed.import.instagram.enabled') != true) {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
return view('settings.import.instagram.home');
@ -25,6 +25,9 @@ trait Instagram
public function instagramStart(Request $request)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$completed = ImportJob::whereProfileId(Auth::user()->profile->id)
->whereService('instagram')
->whereNotNull('completed_at')
@ -38,6 +41,9 @@ trait Instagram
protected function instagramRedirectOrNew()
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$exists = ImportJob::whereProfileId($profile->id)
->whereService('instagram')
@ -61,6 +67,9 @@ trait Instagram
public function instagramStepOne(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id)
->whereNull('completed_at')
@ -72,6 +81,9 @@ trait Instagram
public function instagramStepOneStore(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$max = 'max:' . config('pixelfed.import.instagram.limits.size');
$this->validate($request, [
'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max,
@ -114,6 +126,9 @@ trait Instagram
public function instagramStepTwo(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id)
->whereNull('completed_at')
@ -125,6 +140,9 @@ trait Instagram
public function instagramStepTwoStore(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$this->validate($request, [
'media' => 'required|file|max:1000'
]);
@ -150,6 +168,9 @@ trait Instagram
public function instagramStepThree(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id)
->whereService('instagram')
@ -162,6 +183,9 @@ trait Instagram
public function instagramStepThreeStore(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
try {

Wyświetl plik

@ -0,0 +1,309 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ImportPost;
use App\Services\ImportService;
use App\Services\StatusService;
use App\Http\Resources\ImportStatus;
use App\Follower;
use App\User;
class ImportPostController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function getConfig(Request $request)
{
return [
'enabled' => config('import.instagram.enabled'),
'limits' => [
'max_posts' => config('import.instagram.limits.max_posts'),
'max_attempts' => config('import.instagram.limits.max_attempts'),
],
'allow_video_posts' => config('import.instagram.allow_video_posts'),
'permissions' => [
'admins_only' => config('import.instagram.permissions.admins_only'),
'admin_follows_only' => config('import.instagram.permissions.admin_follows_only'),
'min_account_age' => config('import.instagram.permissions.min_account_age'),
'min_follower_count' => config('import.instagram.permissions.min_follower_count'),
],
'allowed' => $this->checkPermissions($request, false)
];
}
public function getProcessingCount(Request $request)
{
abort_unless(config('import.instagram.enabled'), 404);
$processing = ImportPost::whereProfileId($request->user()->profile_id)
->whereNull('status_id')
->whereSkipMissingMedia(false)
->count();
$finished = ImportPost::whereProfileId($request->user()->profile_id)
->whereNotNull('status_id')
->whereSkipMissingMedia(false)
->count();
return response()->json([
'processing_count' => $processing,
'finished_count' => $finished,
]);
}
public function getImportedFiles(Request $request)
{
abort_unless(config('import.instagram.enabled'), 404);
return response()->json(
ImportService::getImportedFiles($request->user()->profile_id),
200,
[],
JSON_UNESCAPED_SLASHES
);
}
public function getImportedPosts(Request $request)
{
abort_unless(config('import.instagram.enabled'), 404);
return ImportStatus::collection(
ImportPost::whereProfileId($request->user()->profile_id)
->has('status')
->cursorPaginate(9)
);
}
public function formatHashtags($val = false)
{
if(!$val || !strlen($val)) {
return null;
}
$groupedHashtagRegex = '/#\w+(?=#)/';
return preg_replace($groupedHashtagRegex, '$0 ', $val);
}
public function store(Request $request)
{
abort_unless(config('import.instagram.enabled'), 404);
$this->checkPermissions($request);
$uid = $request->user()->id;
$pid = $request->user()->profile_id;
foreach($request->input('files') as $file) {
$media = $file['media'];
$c = collect($media);
$postHash = hash('sha256', $c->toJson());
$exts = $c->map(function($m) {
$fn = last(explode('/', $m['uri']));
return last(explode('.', $fn));
});
$postType = 'photo';
if($exts->count() > 1) {
if($exts->contains('mp4')) {
if($exts->contains('jpg', 'png')) {
$postType = 'photo:video:album';
} else {
$postType = 'video:album';
}
} else {
$postType = 'photo:album';
}
} else {
if(in_array($exts[0], ['jpg', 'png'])) {
$postType = 'photo';
} else if(in_array($exts[0], ['mp4'])) {
$postType = 'video';
}
}
$ip = new ImportPost;
$ip->user_id = $uid;
$ip->profile_id = $pid;
$ip->post_hash = $postHash;
$ip->service = 'instagram';
$ip->post_type = $postType;
$ip->media_count = $c->count();
$ip->media = $c->map(function($m) {
return [
'uri' => $m['uri'],
'title' => $this->formatHashtags($m['title']),
'creation_timestamp' => $m['creation_timestamp']
];
})->toArray();
$ip->caption = $c->count() > 1 ? $this->formatHashtags($file['title']) : $this->formatHashtags($ip->media[0]['title']);
$ip->filename = last(explode('/', $ip->media[0]['uri']));
$ip->metadata = $c->map(function($m) {
return [
'uri' => $m['uri'],
'media_metadata' => isset($m['media_metadata']) ? $m['media_metadata'] : null
];
})->toArray();
$ip->creation_date = $c->count() > 1 ? now()->parse($file['creation_timestamp']) : now()->parse($media[0]['creation_timestamp']);
$ip->creation_year = now()->parse($ip->creation_date)->format('y');
$ip->creation_month = now()->parse($ip->creation_date)->format('m');
$ip->creation_day = now()->parse($ip->creation_date)->format('d');
$ip->save();
ImportService::getImportedFiles($pid, true);
ImportService::getPostCount($pid, true);
}
return [
'msg' => 'Success'
];
}
public function storeMedia(Request $request)
{
abort_unless(config('import.instagram.enabled'), 404);
$this->checkPermissions($request);
$mimes = config('import.instagram.allow_video_posts') ? 'mimetypes:image/png,image/jpeg,video/mp4' : 'mimetypes:image/png,image/jpeg';
$this->validate($request, [
'file' => 'required|array|max:10',
'file.*' => [
'required',
'file',
$mimes,
'max:' . config_cache('pixelfed.max_photo_size')
]
]);
foreach($request->file('file') as $file) {
$fileName = $file->getClientOriginalName();
$file->storeAs('imports/' . $request->user()->id . '/', $fileName);
}
ImportService::getImportedFiles($request->user()->profile_id, true);
return [
'msg' => 'Success'
];
}
protected function checkPermissions($request, $abortOnFail = true)
{
$user = $request->user();
if($abortOnFail) {
abort_unless(config('import.instagram.enabled'), 404);
}
if($user->is_admin) {
if(!$abortOnFail) {
return true;
} else {
return;
}
}
$admin = User::whereIsAdmin(true)->first();
if(config('import.instagram.permissions.admins_only')) {
if($abortOnFail) {
abort_unless($user->is_admin, 404, 'Only admins can use this feature.');
} else {
if(!$user->is_admin) {
return false;
}
}
}
if(config('import.instagram.permissions.admin_follows_only')) {
$exists = Follower::whereProfileId($admin->profile_id)
->whereFollowingId($user->profile_id)
->exists();
if($abortOnFail) {
abort_unless(
$exists,
404,
'Only admins, and accounts they follow can use this feature'
);
} else {
if(!$exists) {
return false;
}
}
}
if(config('import.instagram.permissions.min_account_age')) {
$res = $user->created_at->lt(
now()->subDays(config('import.instagram.permissions.min_account_age'))
);
if($abortOnFail) {
abort_unless(
$res,
404,
'Your account is too new to use this feature'
);
} else {
if(!$res) {
return false;
}
}
}
if(config('import.instagram.permissions.min_follower_count')) {
$res = Follower::whereFollowingId($user->profile_id)->count() >= config('import.instagram.permissions.min_follower_count');
if($abortOnFail) {
abort_unless(
$res,
404,
'You don\'t have enough followers to use this feature'
);
} else {
if(!$res) {
return false;
}
}
}
if(intval(config('import.instagram.limits.max_posts')) > 0) {
$res = ImportService::getPostCount($user->profile_id) >= intval(config('import.instagram.limits.max_posts'));
if($abortOnFail) {
abort_if(
$res,
404,
'You have reached the limit of post imports and cannot import any more posts'
);
} else {
if($res) {
return false;
}
}
}
if(intval(config('import.instagram.limits.max_attempts')) > 0) {
$res = ImportService::getAttempts($user->profile_id) >= intval(config('import.instagram.limits.max_attempts'));
if($abortOnFail) {
abort_if(
$res,
404,
'You have reached the limit of post import attempts and cannot import any more posts'
);
} else {
if($res) {
return false;
}
}
}
if(!$abortOnFail) {
return true;
}
}
}

Wyświetl plik

@ -25,7 +25,61 @@ class InstanceActorController extends Controller
public function outbox()
{
$res = json_encode([
'@context' => 'https://www.w3.org/ns/activitystreams',
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
[
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"toot" => "http://joinmastodon.org/ns#",
"featured" => [
"@id" => "toot:featured",
"@type" => "@id"
],
"featuredTags" => [
"@id" => "toot:featuredTags",
"@type" => "@id"
],
"alsoKnownAs" => [
"@id" => "as:alsoKnownAs",
"@type" => "@id"
],
"movedTo" => [
"@id" => "as:movedTo",
"@type" => "@id"
],
"schema" => "http://schema.org#",
"PropertyValue" => "schema:PropertyValue",
"value" => "schema:value",
"discoverable" => "toot:discoverable",
"Device" => "toot:Device",
"Ed25519Signature" => "toot:Ed25519Signature",
"Ed25519Key" => "toot:Ed25519Key",
"Curve25519Key" => "toot:Curve25519Key",
"EncryptedMessage" => "toot:EncryptedMessage",
"publicKeyBase64" => "toot:publicKeyBase64",
"deviceId" => "toot:deviceId",
"claim" => [
"@type" => "@id",
"@id" => "toot:claim"
],
"fingerprintKey" => [
"@type" => "@id",
"@id" => "toot:fingerprintKey"
],
"identityKey" => [
"@type" => "@id",
"@id" => "toot:identityKey"
],
"devices" => [
"@type" => "@id",
"@id" => "toot:devices"
],
"messageFranking" => "toot:messageFranking",
"messageType" => "toot:messageType",
"cipherText" => "toot:cipherText",
"suspended" => "toot:suspended"
]
],
'id' => config('app.url') . '/i/actor/outbox',
'type' => 'OrderedCollection',
'totalItems' => 0,

Wyświetl plik

@ -2,44 +2,43 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Profile;
use App\Services\AccountService;
use App\Http\Resources\DirectoryProfile;
use App\Profile;
use Illuminate\Http\Request;
class LandingController extends Controller
{
public function directoryRedirect(Request $request)
{
if($request->user()) {
return redirect('/');
}
if ($request->user()) {
return redirect('/');
}
abort_if(config_cache('instance.landing.show_directory') == false, 404);
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
return view('site.index');
return view('site.index');
}
public function exploreRedirect(Request $request)
{
if($request->user()) {
return redirect('/');
}
if ($request->user()) {
return redirect('/');
}
abort_if(config_cache('instance.landing.show_explore') == false, 404);
abort_if((bool) config_cache('instance.landing.show_explore') == false, 404);
return view('site.index');
return view('site.index');
}
public function getDirectoryApi(Request $request)
{
abort_if(config_cache('instance.landing.show_directory') == false, 404);
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
return DirectoryProfile::collection(
Profile::whereNull('domain')
->whereIsSuggestable(true)
->orderByDesc('updated_at')
->cursorPaginate(20)
);
return DirectoryProfile::collection(
Profile::whereNull('domain')
->whereIsSuggestable(true)
->orderByDesc('updated_at')
->cursorPaginate(20)
);
}
}

Wyświetl plik

@ -25,8 +25,7 @@ class LikeController extends Controller
'item' => 'required|integer|min:1',
]);
// API deprecated
return;
abort(422, 'Deprecated API Endpoint');
$user = Auth::user();
$profile = $user->profile;
@ -34,7 +33,7 @@ class LikeController extends Controller
if (Like::whereStatusId($status->id)->whereProfileId($profile->id)->exists()) {
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
UnlikePipeline::dispatch($like);
UnlikePipeline::dispatch($like)->onQueue('feed');
} else {
abort_if(
Like::whereProfileId($user->profile_id)
@ -60,7 +59,7 @@ class LikeController extends Controller
]) == false;
$like->save();
$status->save();
LikePipeline::dispatch($like);
LikePipeline::dispatch($like)->onQueue('feed');
}
}

Wyświetl plik

@ -2,30 +2,31 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Media;
use Illuminate\Http\Request;
class MediaController extends Controller
{
public function index(Request $request)
{
//return view('settings.drive.index');
}
public function index(Request $request)
{
//return view('settings.drive.index');
abort(404);
}
public function composeUpdate(Request $request, $id)
{
public function composeUpdate(Request $request, $id)
{
abort(400, 'Endpoint deprecated');
}
}
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
{
abort_if(!config_cache('pixelfed.cloud_storage'), 404);
$path = 'public/m/_v2/' . $pid . '/' . $mhash . '/' . $uhash . '/' . $f;
$media = Media::whereProfileId($pid)
->whereMediaPath($path)
->whereNotNull('cdn_url')
->firstOrFail();
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
{
abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404);
$path = 'public/m/_v2/'.$pid.'/'.$mhash.'/'.$uhash.'/'.$f;
$media = Media::whereProfileId($pid)
->whereMediaPath($path)
->whereNotNull('cdn_url')
->firstOrFail();
return redirect()->away($media->cdn_url);
}
return redirect()->away($media->cdn_url);
}
}

Wyświetl plik

@ -0,0 +1,231 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ParentalControls;
use App\Models\UserRoles;
use App\Profile;
use App\User;
use App\Http\Controllers\Auth\RegisterController;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Auth;
use App\Services\UserRoleService;
use App\Jobs\ParentalControlsPipeline\DispatchChildInvitePipeline;
class ParentalControlsController extends Controller
{
public function authPreflight($request, $maxUserCheck = false, $authCheck = true)
{
if($authCheck) {
abort_unless($request->user(), 404);
abort_unless($request->user()->has_roles === 0, 404);
}
abort_unless(config('instance.parental_controls.enabled'), 404);
if(config_cache('pixelfed.open_registration') == false) {
abort_if(config('instance.parental_controls.limits.respect_open_registration'), 404);
}
if($maxUserCheck == true) {
$hasLimit = config('pixelfed.enforce_max_users');
if($hasLimit) {
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
$limit = (int) config('pixelfed.max_users');
abort_if($limit && $limit <= $count, 404);
}
}
}
public function index(Request $request)
{
$this->authPreflight($request);
$children = ParentalControls::whereParentId($request->user()->id)->latest()->paginate(5);
return view('settings.parental-controls.index', compact('children'));
}
public function add(Request $request)
{
$this->authPreflight($request, true);
return view('settings.parental-controls.add');
}
public function view(Request $request, $id)
{
$this->authPreflight($request);
$uid = $request->user()->id;
$pc = ParentalControls::whereParentId($uid)->findOrFail($id);
return view('settings.parental-controls.manage', compact('pc'));
}
public function update(Request $request, $id)
{
$this->authPreflight($request);
$uid = $request->user()->id;
$ff = $this->requestFormFields($request);
$pc = ParentalControls::whereParentId($uid)->findOrFail($id);
$pc->permissions = $ff;
$pc->save();
$roles = UserRoleService::mapActions($pc->child_id, $ff);
if(isset($roles['account-force-private'])) {
$c = Profile::whereUserId($pc->child_id)->first();
$c->is_private = $roles['account-force-private'];
$c->save();
}
UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]);
return redirect($pc->manageUrl() . '?permissions');
}
public function store(Request $request)
{
$this->authPreflight($request, true);
$this->validate($request, [
'email' => 'required|email|unique:parental_controls,email|unique:users,email',
]);
$state = $this->requestFormFields($request);
$pc = new ParentalControls;
$pc->parent_id = $request->user()->id;
$pc->email = $request->input('email');
$pc->verify_code = str_random(32);
$pc->permissions = $state;
$pc->save();
DispatchChildInvitePipeline::dispatch($pc);
return redirect($pc->manageUrl());
}
public function inviteRegister(Request $request, $id, $code)
{
if($request->user()) {
$title = 'You cannot complete this action on this device.';
$body = 'Please log out or use a different device or browser to complete the invitation registration.';
return view('errors.custom', compact('title', 'body'));
}
$this->authPreflight($request, true, false);
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id);
abort_unless(User::whereId($pc->parent_id)->exists(), 404);
return view('settings.parental-controls.invite-register-form', compact('pc'));
}
public function inviteRegisterStore(Request $request, $id, $code)
{
if($request->user()) {
$title = 'You cannot complete this action on this device.';
$body = 'Please log out or use a different device or browser to complete the invitation registration.';
return view('errors.custom', compact('title', 'body'));
}
$this->authPreflight($request, true, false);
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id);
$fields = $request->all();
$fields['email'] = $pc->email;
$defaults = UserRoleService::defaultRoles();
$validator = (new RegisterController)->validator($fields);
$valid = $validator->validate();
abort_if(!$valid, 404);
event(new Registered($user = (new RegisterController)->create($fields)));
sleep(5);
$user->has_roles = true;
$user->parent_id = $pc->parent_id;
if(config('instance.parental_controls.limits.auto_verify_email')) {
$user->email_verified_at = now();
$user->save();
sleep(3);
} else {
$user->save();
sleep(3);
}
$ur = UserRoles::updateOrCreate([
'user_id' => $user->id,
],[
'roles' => UserRoleService::mapInvite($user->id, $pc->permissions)
]);
$pc->email_verified_at = now();
$pc->child_id = $user->id;
$pc->save();
sleep(2);
Auth::guard()->login($user);
return redirect('/i/web');
}
public function cancelInvite(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNull(['email_verified_at', 'child_id'])
->findOrFail($id);
return view('settings.parental-controls.delete-invite', compact('pc'));
}
public function cancelInviteHandle(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNull(['email_verified_at', 'child_id'])
->findOrFail($id);
$pc->delete();
return redirect('/settings/parental-controls');
}
public function stopManaging(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNotNull(['email_verified_at', 'child_id'])
->findOrFail($id);
return view('settings.parental-controls.stop-managing', compact('pc'));
}
public function stopManagingHandle(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNotNull(['email_verified_at', 'child_id'])
->findOrFail($id);
$pc->child()->update([
'has_roles' => false,
'parent_id' => null,
]);
$pc->delete();
return redirect('/settings/parental-controls');
}
protected function requestFormFields($request)
{
$state = [];
$fields = [
'post',
'comment',
'like',
'share',
'follow',
'bookmark',
'story',
'collection',
'discovery_feeds',
'dms',
'federation',
'hide_network',
'private',
'hide_cw'
];
foreach ($fields as $field) {
$state[$field] = $request->input($field) == 'on';
}
return $state;
}
}

Wyświetl plik

@ -2,37 +2,41 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ConfigCache;
use Storage;
use App\Services\AccountService;
use App\Services\StatusService;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Cache;
use Storage;
use App\Status;
use App\User;
class PixelfedDirectoryController extends Controller
{
public function get(Request $request)
{
if(!$request->filled('sk')) {
if (! $request->filled('sk')) {
abort(404);
}
if(!config_cache('pixelfed.directory.submission-key')) {
if (! config_cache('pixelfed.directory.submission-key')) {
abort(404);
}
if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
if (! hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
abort(403);
}
$res = $this->buildListing();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
public function buildListing()
{
$res = config_cache('pixelfed.directory');
if($res) {
if ($res) {
$res = is_string($res) ? json_decode($res, true) : $res;
}
@ -41,70 +45,71 @@ class PixelfedDirectoryController extends Controller
$res['_ts'] = config_cache('pixelfed.directory.submission-ts');
$res['version'] = config_cache('pixelfed.version');
if(empty($res['summary'])) {
if (empty($res['summary'])) {
$summary = ConfigCache::whereK('app.short_description')->pluck('v');
$res['summary'] = $summary ? $summary[0] : null;
}
if(isset($res['admin'])) {
if (isset($res['admin'])) {
$res['admin'] = AccountService::get($res['admin'], true);
}
if(isset($res['banner_image']) && !empty($res['banner_image'])) {
if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
$res['banner_image'] = url(Storage::url($res['banner_image']));
}
if(isset($res['favourite_posts'])) {
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
if (isset($res['favourite_posts'])) {
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
return StatusService::get($id);
})
->filter(function($post) {
return $post && isset($post['account']);
})
->map(function($post) {
return [
'avatar' => $post['account']['avatar'],
'display_name' => $post['account']['display_name'],
'username' => $post['account']['username'],
'media' => $post['media_attachments'][0]['url'],
'url' => $post['url']
];
})
->values();
->filter(function ($post) {
return $post && isset($post['account']);
})
->map(function ($post) {
return [
'avatar' => $post['account']['avatar'],
'display_name' => $post['account']['display_name'],
'username' => $post['account']['username'],
'media' => $post['media_attachments'][0]['url'],
'url' => $post['url'],
];
})
->values();
}
$guidelines = ConfigCache::whereK('app.rules')->first();
if($guidelines) {
if ($guidelines) {
$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) {
if ($oauthEnabled) {
$keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
$res['oauth_enabled'] = (bool) $oauthEnabled && $keys;
}
$activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first();
if($activityPubEnabled) {
if ($activityPubEnabled) {
$res['activitypub_enabled'] = (bool) $activityPubEnabled;
}
$res['feature_config'] = [
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => config_cache('pixelfed.optimize_image'),
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
'max_account_size' => config_cache('pixelfed.max_account_size'),
'max_album_length' => config_cache('pixelfed.max_album_length'),
'account_deletion' => config_cache('pixelfed.account_deletion'),
'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
];
$res['is_eligible'] = $this->validVal($res, 'admin') &&
@ -114,29 +119,36 @@ class PixelfedDirectoryController extends Controller
$this->validVal($res, 'privacy_pledge') &&
$this->validVal($res, 'location');
if(config_cache('pixelfed.directory.testimonials')) {
if (config_cache('pixelfed.directory.testimonials')) {
$res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
->map(function($testimonial) {
->map(function ($testimonial) {
$profile = AccountService::get($testimonial['profile_id']);
return [
'profile' => [
'username' => $profile['username'],
'display_name' => $profile['display_name'],
'avatar' => $profile['avatar'],
'created_at' => $profile['created_at']
'created_at' => $profile['created_at'],
],
'body' => $testimonial['body']
'body' => $testimonial['body'],
];
});
}
$res['features_enabled'] = [
'stories' => (bool) config_cache('instance.stories.enabled')
'stories' => (bool) config_cache('instance.stories.enabled'),
];
$statusesCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
return Status::whereLocal(true)->count();
});
$usersCount = Cache::remember('api:nodeinfo:users', 43200, function() {
return User::count();
});
$res['stats'] = [
'user_count' => \App\User::count(),
'post_count' => \App\Status::whereNull('uri')->count(),
'user_count' => (int) $usersCount,
'post_count' => (int) $statusesCount,
];
$res['primary_locale'] = config('app.locale');
@ -149,19 +161,18 @@ class PixelfedDirectoryController extends Controller
protected function validVal($res, $val, $count = false, $minLen = false)
{
if(!isset($res[$val])) {
if (! isset($res[$val])) {
return false;
}
if($count) {
if ($count) {
return count($res[$val]) >= $count;
}
if($minLen) {
if ($minLen) {
return strlen($res[$val]) >= $minLen;
}
return $res[$val];
}
}

Wyświetl plik

@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers;
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
{
public function __construct()
{
$this->middleware('auth');
}
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 = $request->input('acct');
$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);
$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 = $webfingerUrl;
$alias->save();
Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id);
return back()->with('status', 'Successfully added alias!');
}
public function delete(Request $request)
{
$this->validate($request, [
'acct' => 'required',
'id' => 'required|exists:profile_aliases',
]);
$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!');
}
}

Wyświetl plik

@ -2,356 +2,387 @@
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\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);
abort_if(! $user, 404);
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');
});
}
return redirect($user->url());
}
public function showAtomFeed(Request $request, $user)
{
abort_if(!config('federation.atom.enabled'), 404);
protected function privateProfileCheck(Profile $profile, $loggedIn)
{
if (! Auth::check()) {
return true;
}
$pid = AccountService::usernameToId($user);
$user = Auth::user()->profile;
if ($user->id == $profile->id || ! $profile->is_private) {
return false;
}
abort_if(!$pid, 404);
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
if ($follows == false) {
return true;
}
$profile = AccountService::get($pid, true);
return false;
}
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
public static function accountCheck(Profile $profile)
{
switch ($profile->status) {
case 'disabled':
case 'suspended':
case 'delete':
return view('profile.disabled');
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;
}
default:
break;
}
return false;
});
return abort(404);
}
abort_if($aiCheck, 404);
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;
}
$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;
}
return false;
}
return $settings->show_atom;
});
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);
abort_if(!$enabled, 404);
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();
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) {
$items = DB::table('statuses')
->whereProfileId($pid)
->whereVisibility('public')
->whereType('photo')
->orderByDesc('id')
->take(10)
->get()
->map(function($status) {
return StatusService::get($status->id);
})
->filter(function($status) {
return $status &&
isset($status['account']) &&
isset($status['media_attachments']) &&
count($status['media_attachments']);
})
->values();
$permalink = config('app.url') . "/users/{$profile['username']}.atom";
$headers = ['Content-Type' => 'application/atom+xml'];
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
});
}
if($items && $items->count()) {
$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
}
public function showAtomFeed(Request $request, $user)
{
abort_if(! config('federation.atom.enabled'), 404);
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']);
}
$pid = AccountService::usernameToId($user);
public function meRedirect()
{
abort_if(!Auth::check(), 404);
return redirect(Auth::user()->url());
}
abort_if(! $pid, 404);
public function embed(Request $request, $username)
{
$res = view('profile.embed-removed');
$profile = AccountService::get($pid, true);
if(!config('instance.embed.profile')) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404);
if(strlen($username) > 15 || strlen($username) < 2) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, 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;
}
$profile = Profile::whereUsername($username)
->whereIsPrivate(false)
->whereNull('status')
->whereNull('domain')
->first();
return false;
});
if(!$profile) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
abort_if($aiCheck, 404);
$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;
}
$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;
}
return false;
});
return $settings->show_atom;
});
if($aiCheck) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
abort_if(! $enabled, 404);
if(AccountService::canEmbed($profile->user_id) == false) {
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'];
$profile = AccountService::get($profile->id);
$res = view('profile.embed', compact('profile'));
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
if ($items && $items->count()) {
$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
}
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 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']);
}
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, 3600, 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(! (bool) 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'));
}
}

Wyświetl plik

@ -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!']);
}
}

Wyświetl plik

@ -42,6 +42,7 @@ use App\Services\{
use App\Jobs\StatusPipeline\NewStatusPipeline;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\InstanceService;
class PublicApiController extends Controller
{
@ -661,6 +662,10 @@ class PublicApiController extends Controller
public function account(Request $request, $id)
{
$res = AccountService::get($id);
if($res && isset($res['local'], $res['url']) && !$res['local']) {
$domain = parse_url($res['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
return response()->json($res);
}
@ -680,6 +685,11 @@ class PublicApiController extends Controller
$profile = AccountService::get($id);
abort_if(!$profile, 404);
if($profile && isset($profile['local'], $profile['url']) && !$profile['local']) {
$domain = parse_url($profile['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$limit = $request->limit ?? 9;
$max_id = $request->max_id;
$min_id = $request->min_id;

Wyświetl plik

@ -0,0 +1,728 @@
<?php
namespace App\Http\Controllers;
use App\Models\RemoteAuth;
use App\Services\Account\RemoteAuthService;
use App\Services\EmailService;
use App\Services\MediaStorageService;
use App\User;
use App\Util\ActivityPub\Helpers;
use App\Util\Lexer\RestrictedNames;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Purify;
class RemoteAuthController extends Controller
{
public function start(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
if ($request->user()) {
return redirect('/');
}
return view('auth.remote.start');
}
public function startRedirect(Request $request)
{
return redirect('/login');
}
public function getAuthDomains(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
if (config('remote-auth.mastodon.domains.only_custom')) {
$res = config('remote-auth.mastodon.domains.custom');
if (! $res || ! strlen($res)) {
return [];
}
$res = explode(',', $res);
return response()->json($res);
}
if (config('remote-auth.mastodon.domains.custom') &&
! config('remote-auth.mastodon.domains.only_default') &&
strlen(config('remote-auth.mastodon.domains.custom')) > 3 &&
strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1
) {
$res = config('remote-auth.mastodon.domains.custom');
if (! $res || ! strlen($res)) {
return [];
}
$res = explode(',', $res);
return response()->json($res);
}
$res = config('remote-auth.mastodon.domains.default');
$res = explode(',', $res);
return response()->json($res);
}
public function redirect(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
$this->validate($request, ['domain' => 'required']);
$domain = $request->input('domain');
if (str_starts_with(strtolower($domain), 'http')) {
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
];
return response()->json($res);
}
$validateInstance = Helpers::validateUrl('https://'.$domain.'/?block-check='.time());
if (! $validateInstance) {
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'blocked_domain',
];
return response()->json($res);
}
$compatible = RemoteAuthService::isDomainCompatible($domain);
if (! $compatible) {
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
];
return response()->json($res);
}
if (config('remote-auth.mastodon.domains.only_default')) {
$defaultDomains = explode(',', config('remote-auth.mastodon.domains.default'));
if (! in_array($domain, $defaultDomains)) {
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
];
return response()->json($res);
}
}
if (config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
$customDomains = explode(',', config('remote-auth.mastodon.domains.custom'));
if (! in_array($domain, $customDomains)) {
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
];
return response()->json($res);
}
}
$client = RemoteAuthService::getMastodonClient($domain);
abort_unless($client, 422, 'Invalid mastodon client');
$request->session()->put('state', $state = Str::random(40));
$request->session()->put('oauth_domain', $domain);
$query = http_build_query([
'client_id' => $client->client_id,
'redirect_uri' => $client->redirect_uri,
'response_type' => 'code',
'scope' => 'read',
'state' => $state,
]);
$request->session()->put('oauth_redirect_to', 'https://'.$domain.'/oauth/authorize?'.$query);
$dsh = Str::random(17);
$res = [
'domain' => $domain,
'ready' => true,
'dsh' => $dsh,
];
return response()->json($res);
}
public function preflight(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
if (! $request->filled('d') || ! $request->filled('dsh') || ! $request->session()->exists('oauth_redirect_to')) {
return redirect('/login');
}
return redirect()->away($request->session()->pull('oauth_redirect_to'));
}
public function handleCallback(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
$domain = $request->session()->get('oauth_domain');
if ($request->filled('code')) {
$code = $request->input('code');
$state = $request->session()->pull('state');
throw_unless(
strlen($state) > 0 && $state === $request->state,
InvalidArgumentException::class,
'Invalid state value.'
);
$res = RemoteAuthService::getToken($domain, $code);
if (! $res || ! isset($res['access_token'])) {
$request->session()->regenerate();
return redirect('/login');
}
$request->session()->put('oauth_remote_session_token', $res['access_token']);
return redirect('/auth/mastodon/getting-started');
}
return redirect('/login');
}
public function onboarding(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
if ($request->user()) {
return redirect('/');
}
return view('auth.remote.onboarding');
}
public function sessionCheck(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
$domain = $request->session()->get('oauth_domain');
$token = $request->session()->get('oauth_remote_session_token');
$res = RemoteAuthService::getVerifyCredentials($domain, $token);
abort_if(! $res || ! isset($res['acct']), 403, 'Invalid credentials');
$webfinger = strtolower('@'.$res['acct'].'@'.$domain);
$request->session()->put('oauth_masto_webfinger', $webfinger);
if (config('remote-auth.mastodon.max_uses.enabled')) {
$limit = config('remote-auth.mastodon.max_uses.limit');
$uses = RemoteAuthService::lookupWebfingerUses($webfinger);
if ($uses >= $limit) {
return response()->json([
'code' => 200,
'msg' => 'Success!',
'action' => 'max_uses_reached',
]);
}
}
$exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first();
if ($exists && $exists->user_id) {
return response()->json([
'code' => 200,
'msg' => 'Success!',
'action' => 'redirect_existing_user',
]);
}
return response()->json([
'code' => 200,
'msg' => 'Success!',
'action' => 'onboard',
]);
}
public function sessionGetMastodonData(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
$domain = $request->session()->get('oauth_domain');
$token = $request->session()->get('oauth_remote_session_token');
$res = RemoteAuthService::getVerifyCredentials($domain, $token);
$res['_webfinger'] = strtolower('@'.$res['acct'].'@'.$domain);
$res['_domain'] = strtolower($domain);
$request->session()->put('oauth_remasto_id', $res['id']);
$ra = RemoteAuth::updateOrCreate([
'domain' => $domain,
'webfinger' => $res['_webfinger'],
], [
'software' => 'mastodon',
'ip_address' => $request->ip(),
'bearer_token' => $token,
'verify_credentials' => $res,
'last_verify_credentials_at' => now(),
'last_successful_login_at' => now(),
]);
$request->session()->put('oauth_masto_raid', $ra->id);
return response()->json($res);
}
public function sessionValidateUsername(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
$this->validate($request, [
'username' => [
'required',
'min:2',
'max:15',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
if (ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
if (($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if (! ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.');
}
if (! ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
$val = str_replace(['_', '.', '-'], '', $value);
if (! ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
}
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
],
]);
$username = strtolower($request->input('username'));
$exists = User::where('username', $username)->exists();
return response()->json([
'code' => 200,
'username' => $username,
'exists' => $exists,
]);
}
public function sessionValidateEmail(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
$this->validate($request, [
'email' => [
'required',
'email:strict,filter_unicode,dns,spoof',
],
]);
$email = $request->input('email');
$banned = EmailService::isBanned($email);
$exists = User::where('email', $email)->exists();
return response()->json([
'code' => 200,
'email' => $email,
'exists' => $exists,
'banned' => $banned,
]);
}
public function sessionGetMastodonFollowers(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
$domain = $request->session()->get('oauth_domain');
$token = $request->session()->get('oauth_remote_session_token');
$id = $request->session()->get('oauth_remasto_id');
$res = RemoteAuthService::getFollowing($domain, $token, $id);
if (! $res) {
return response()->json([
'code' => 200,
'following' => [],
]);
}
$res = collect($res)->filter(fn ($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();
return response()->json([
'code' => 200,
'following' => $res,
]);
}
public function handleSubmit(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
abort_unless($request->session()->exists('oauth_masto_raid'), 403);
$this->validate($request, [
'email' => 'required|email:strict,filter_unicode,dns,spoof',
'username' => [
'required',
'min:2',
'max:15',
'unique:users,username',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
if (ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
if (($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if (! ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.');
}
if (! ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
$val = str_replace(['_', '.', '-'], '', $value);
if (! ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
}
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
],
'password' => 'required|string|min:8|confirmed',
'name' => 'nullable|max:30',
]);
$email = $request->input('email');
$username = $request->input('username');
$password = $request->input('password');
$name = $request->input('name');
$user = $this->createUser([
'name' => $name,
'username' => $username,
'password' => $password,
'email' => $email,
]);
$raid = $request->session()->pull('oauth_masto_raid');
$webfinger = $request->session()->pull('oauth_masto_webfinger');
$token = $user->createToken('Onboarding')->accessToken;
$ra = RemoteAuth::where('id', $raid)->where('webfinger', $webfinger)->firstOrFail();
$ra->user_id = $user->id;
$ra->save();
return [
'code' => 200,
'msg' => 'Success',
'token' => $token,
];
}
public function storeBio(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless($request->user(), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
$this->validate($request, [
'bio' => 'required|nullable|max:500',
]);
$profile = $request->user()->profile;
$profile->bio = Purify::clean($request->input('bio'));
$profile->save();
return [200];
}
public function accountToId(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
$this->validate($request, [
'account' => 'required|url',
]);
$account = $request->input('account');
abort_unless(substr(strtolower($account), 0, 8) === 'https://', 404);
$host = strtolower(config('pixelfed.domain.app'));
$domain = strtolower(parse_url($account, PHP_URL_HOST));
if ($domain == $host) {
$username = Str::of($account)->explode('/')->last();
$user = User::where('username', $username)->first();
if ($user) {
return ['id' => (string) $user->profile_id];
} else {
return [];
}
} else {
try {
$profile = Helpers::profileFetch($account);
if ($profile) {
return ['id' => (string) $profile->id];
} else {
return [];
}
} catch (\GuzzleHttp\Exception\RequestException $e) {
return;
} catch (Exception $e) {
return [];
}
}
}
public function storeAvatar(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless($request->user(), 404);
$this->validate($request, [
'avatar_url' => 'required|active_url',
]);
$user = $request->user();
$profile = $user->profile;
abort_if(! $profile->avatar, 404, 'Missing avatar');
$avatar = $profile->avatar;
$avatar->remote_url = $request->input('avatar_url');
$avatar->save();
MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false);
return [200];
}
public function finishUp(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless($request->user(), 404);
$currentWebfinger = '@'.$request->user()->username.'@'.config('pixelfed.domain.app');
$ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail();
RemoteAuthService::submitToBeagle(
$ra->webfinger,
$ra->verify_credentials['url'],
$currentWebfinger,
$request->user()->url()
);
return [200];
}
public function handleLogin(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
$domain = $request->session()->get('oauth_domain');
$wf = $request->session()->get('oauth_masto_webfinger');
$ra = RemoteAuth::where('webfinger', $wf)->where('domain', $domain)->whereNotNull('user_id')->firstOrFail();
$user = User::findOrFail($ra->user_id);
abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
Auth::loginUsingId($ra->user_id);
return [200];
}
protected function createUser($data)
{
event(new Registered($user = User::create([
'name' => Purify::clean($data['name']),
'username' => $data['username'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
'app_register_ip' => request()->ip(),
'register_source' => 'mastodon',
])));
$this->guarder()->login($user);
return $user;
}
protected function guarder()
{
return Auth::guard();
}
}

Wyświetl plik

@ -2,368 +2,367 @@
namespace App\Http\Controllers;
use Auth;
use App\Hashtag;
use App\Place;
use App\Profile;
use App\Services\WebfingerService;
use App\Status;
use Illuminate\Http\Request;
use App\Util\ActivityPub\Helpers;
use Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use App\Transformer\Api\{
AccountTransformer,
HashtagTransformer,
StatusTransformer,
};
use App\Services\WebfingerService;
class SearchController extends Controller
{
public $tokens = [];
public $term = '';
public $hash = '';
public $cacheKey = 'api:search:tag:';
public $tokens = [];
public function __construct()
{
$this->middleware('auth');
}
public $term = '';
public function searchAPI(Request $request)
{
$this->validate($request, [
'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro',
'v' => 'required|integer|in:2',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
]);
public $hash = '';
$scope = $request->input('scope') ?? 'all';
$this->term = e(urldecode($request->input('q')));
$this->hash = hash('sha256', $this->term);
public $cacheKey = 'api:search:tag:';
switch ($scope) {
case 'all':
$this->getHashtags();
$this->getPosts();
$this->getProfiles();
// $this->getPlaces();
break;
public function __construct()
{
$this->middleware('auth');
}
case 'hashtag':
$this->getHashtags();
break;
public function searchAPI(Request $request)
{
$this->validate($request, [
'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro',
'v' => 'required|integer|in:2',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger',
]);
case 'profile':
$this->getProfiles();
break;
$scope = $request->input('scope') ?? 'all';
$this->term = e(urldecode($request->input('q')));
$this->hash = hash('sha256', $this->term);
case 'webfinger':
$this->webfingerSearch();
break;
switch ($scope) {
case 'all':
$this->getHashtags();
$this->getPosts();
$this->getProfiles();
// $this->getPlaces();
break;
case 'remote':
$this->remoteLookupSearch();
break;
case 'hashtag':
$this->getHashtags();
break;
case 'place':
$this->getPlaces();
break;
case 'profile':
$this->getProfiles();
break;
default:
break;
}
case 'webfinger':
$this->webfingerSearch();
break;
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
}
case 'remote':
$this->remoteLookupSearch();
break;
protected function getPosts()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) &&
$remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$this->tokens['posts'] = [[
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
]];
}
} else {
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile_id)
->where('caption', 'like', '%'.$tag.'%')
->latest()
->limit(10)
->get();
case 'place':
$this->getPlaces();
break;
if($posts->count() > 0) {
$posts = $posts->map(function($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class
];
});
$this->tokens['posts'] = $posts;
}
}
}
default:
break;
}
protected function getHashtags()
{
$tag = $this->term;
$key = $this->cacheKey . 'hashtags:' . $this->hash;
$ttl = now()->addMinutes(1);
$tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
$hashtags = Hashtag::select('id', 'name', 'slug')
->where('slug', 'like', '%'.$htag.'%')
->whereHas('posts')
->limit(20)
->get();
if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
'count' => $item->posts()->count(),
'url' => $item->url(),
'type' => 'hashtag',
'value' => $item->name,
'tokens' => '',
'name' => null,
];
});
return $tags;
}
});
$this->tokens['hashtags'] = $tokens;
}
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
}
protected function getPlaces()
{
$tag = $this->term;
// $key = $this->cacheKey . 'places:' . $this->hash;
// $ttl = now()->addHours(12);
// $tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
$hashtags = Place::select('id', 'name', 'slug', 'country')
->where('name', 'like', '%'.$htag[0].'%')
->paginate(20);
$tags = [];
if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
'count' => null,
'url' => $item->url(),
'type' => 'place',
'value' => $item->name . ', ' . $item->country,
'tokens' => '',
'name' => null,
'city' => $item->name,
'country' => $item->country
];
});
// return $tags;
}
// });
$this->tokens['places'] = $tags;
$this->tokens['placesPagination'] = [
'total' => $hashtags->total(),
'current_page' => $hashtags->currentPage(),
'last_page' => $hashtags->lastPage()
];
}
protected function getPosts()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
if (Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
(bool) config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if (isset($remote['type']) &&
in_array($remote['type'], ['Note', 'Question'])
) {
$item = Helpers::statusFetch($tag);
$this->tokens['posts'] = [[
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
]];
}
} else {
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile_id)
->where('caption', 'like', '%'.$tag.'%')
->latest()
->limit(10)
->get();
protected function getProfiles()
{
$tag = $this->term;
$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
$key = $this->cacheKey . 'profiles:' . $this->hash;
$remoteTtl = now()->addMinutes(15);
$ttl = now()->addHours(2);
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) &&
$remote['type'] == 'Person'
) {
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
$item = Helpers::profileFirstOrNew($tag);
$tokens = [[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count()
]
]];
return $tokens;
});
}
}
if ($posts->count() > 0) {
$posts = $posts->map(function ($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class,
];
});
$this->tokens['posts'] = $posts;
}
}
}
else {
$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
if(Str::startsWith($tag, '@')) {
$tag = substr($tag, 1);
}
$users = Profile::select('status', 'domain', 'username', 'name', 'id')
->whereNull('status')
->where('username', 'like', '%'.$tag.'%')
->limit(20)
->orderBy('domain')
->get();
protected function getHashtags()
{
$tag = $this->term;
$key = $this->cacheKey.'hashtags:'.$this->hash;
$ttl = now()->addMinutes(1);
$tokens = Cache::remember($key, $ttl, function () use ($tag) {
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
$hashtags = Hashtag::select('id', 'name', 'slug')
->where('slug', 'like', '%'.$htag.'%')
->whereHas('posts')
->limit(20)
->get();
if ($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
'count' => $item->posts()->count(),
'url' => $item->url(),
'type' => 'hashtag',
'value' => $item->name,
'tokens' => '',
'name' => null,
];
});
if($users->count() > 0) {
return $users->map(function ($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'avatar' => $item->avatarUrl(),
'id' => (string) $item->id,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count()
]
];
});
}
});
}
}
return $tags;
}
});
$this->tokens['hashtags'] = $tokens;
}
public function results(Request $request)
{
$this->validate($request, [
'q' => 'required|string|min:1',
]);
protected function getPlaces()
{
$tag = $this->term;
// $key = $this->cacheKey . 'places:' . $this->hash;
// $ttl = now()->addHours(12);
// $tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
$hashtags = Place::select('id', 'name', 'slug', 'country')
->where('name', 'like', '%'.$htag[0].'%')
->paginate(20);
$tags = [];
if ($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
'count' => null,
'url' => $item->url(),
'type' => 'place',
'value' => $item->name.', '.$item->country,
'tokens' => '',
'name' => null,
'city' => $item->name,
'country' => $item->country,
];
});
// return $tags;
}
// });
$this->tokens['places'] = $tags;
$this->tokens['placesPagination'] = [
'total' => $hashtags->total(),
'current_page' => $hashtags->currentPage(),
'last_page' => $hashtags->lastPage(),
];
}
return view('search.results');
}
protected function getProfiles()
{
$tag = $this->term;
$remoteKey = $this->cacheKey.'profiles:remote:'.$this->hash;
$key = $this->cacheKey.'profiles:'.$this->hash;
$remoteTtl = now()->addMinutes(15);
$ttl = now()->addHours(2);
if (Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
(bool) config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if (isset($remote['type']) &&
$remote['type'] == 'Person'
) {
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function () use ($tag) {
$item = Helpers::profileFirstOrNew($tag);
$tokens = [[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) ! $item->domain,
'post_count' => $item->statuses()->count(),
],
]];
protected function webfingerSearch()
{
$wfs = WebfingerService::lookup($this->term);
return $tokens;
});
}
} else {
$this->tokens['profiles'] = Cache::remember($key, $ttl, function () use ($tag) {
if (Str::startsWith($tag, '@')) {
$tag = substr($tag, 1);
}
$users = Profile::select('status', 'domain', 'username', 'name', 'id')
->whereNull('status')
->where('username', 'like', '%'.$tag.'%')
->limit(20)
->orderBy('domain')
->get();
if(empty($wfs)) {
return;
}
if ($users->count() > 0) {
return $users->map(function ($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'avatar' => $item->avatarUrl(),
'id' => (string) $item->id,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) ! $item->domain,
'post_count' => $item->statuses()->count(),
],
];
});
}
});
}
}
$this->tokens['profiles'] = [
[
'count' => 1,
'url' => $wfs['url'],
'type' => 'profile',
'value' => $wfs['username'],
'tokens' => [$wfs['username']],
'name' => $wfs['display_name'],
'entity' => [
'id' => (string) $wfs['id'],
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local']
]
]
];
return;
}
public function results(Request $request)
{
$this->validate($request, [
'q' => 'required|string|min:1',
]);
protected function remotePostLookup()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
$local = Helpers::validateLocalUrl($tag);
$valid = Helpers::validateUrl($tag);
return view('search.results');
}
if($valid == false || $local == true) {
return;
}
protected function webfingerSearch()
{
$wfs = WebfingerService::lookup($this->term);
if(Status::whereUri($tag)->whereLocal(false)->exists()) {
$item = Status::whereUri($tag)->first();
$media = $item->firstMedia();
$url = null;
if($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
if (empty($wfs)) {
return;
}
$remote = Helpers::fetchFromUrl($tag);
$this->tokens['profiles'] = [
[
'count' => 1,
'url' => $wfs['url'],
'type' => 'profile',
'value' => $wfs['username'],
'tokens' => [$wfs['username']],
'name' => $wfs['display_name'],
'entity' => [
'id' => (string) $wfs['id'],
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local'],
],
],
];
if(isset($remote['type']) && $remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$media = $item->firstMedia();
$url = null;
if($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
}
}
protected function remoteLookupSearch()
{
if(!Helpers::validateUrl($this->term)) {
return;
}
$this->getProfiles();
$this->remotePostLookup();
}
protected function remotePostLookup()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
$local = Helpers::validateLocalUrl($tag);
$valid = Helpers::validateUrl($tag);
if ($valid == false || $local == true) {
return;
}
if (Status::whereUri($tag)->whereLocal(false)->exists()) {
$item = Status::whereUri($tag)->first();
$media = $item->firstMedia();
$url = null;
if ($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(),
]];
}
$remote = Helpers::fetchFromUrl($tag);
if (isset($remote['type']) && $remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$media = $item->firstMedia();
$url = null;
if ($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(),
]];
}
}
protected function remoteLookupSearch()
{
if (! Helpers::validateUrl($this->term)) {
return;
}
$this->getProfiles();
$this->remotePostLookup();
}
}

Wyświetl plik

@ -22,189 +22,189 @@ use App\Services\PronounService;
trait HomeSettings
{
public function home()
{
$id = Auth::user()->profile->id;
$storage = [];
$used = Media::whereProfileId($id)->sum('size');
$storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
$storage['used'] = $used;
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
$pronouns = PronounService::get($id);
public function home()
{
$id = Auth::user()->profile->id;
$storage = [];
$used = Media::whereProfileId($id)->sum('size');
$storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
$storage['used'] = $used;
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
$pronouns = PronounService::get($id);
return view('settings.home', compact('storage', 'pronouns'));
}
return view('settings.home', compact('storage', 'pronouns'));
}
public function homeUpdate(Request $request)
{
$this->validate($request, [
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
'website' => 'nullable|url',
'language' => 'nullable|string|min:2|max:5',
'pronouns' => 'nullable|array|max:4'
]);
public function homeUpdate(Request $request)
{
$this->validate($request, [
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
'website' => 'nullable|url',
'language' => 'nullable|string|min:2|max:5',
'pronouns' => 'nullable|array|max:4'
]);
$changes = false;
$name = strip_tags(Purify::clean($request->input('name')));
$bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null;
$website = $request->input('website');
$language = $request->input('language');
$user = Auth::user();
$profile = $user->profile;
$pronouns = $request->input('pronouns');
$existingPronouns = PronounService::get($profile->id);
$layout = $request->input('profile_layout');
if($layout) {
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
}
$changes = false;
$name = strip_tags(Purify::clean($request->input('name')));
$bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null;
$website = $request->input('website');
$language = $request->input('language');
$user = Auth::user();
$profile = $user->profile;
$pronouns = $request->input('pronouns');
$existingPronouns = PronounService::get($profile->id);
$layout = $request->input('profile_layout');
if($layout) {
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
}
$enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
$enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
// Only allow email to be updated if not yet verified
if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
if ($profile->name != $name) {
$changes = true;
$user->name = $name;
$profile->name = $name;
}
// Only allow email to be updated if not yet verified
if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
if ($profile->name != $name) {
$changes = true;
$user->name = $name;
$profile->name = $name;
}
if ($profile->website != $website) {
$changes = true;
$profile->website = $website;
}
if ($profile->website != $website) {
$changes = true;
$profile->website = $website;
}
if (strip_tags($profile->bio) != $bio) {
$changes = true;
$profile->bio = Autolink::create()->autolink($bio);
}
if (strip_tags($profile->bio) != $bio) {
$changes = true;
$profile->bio = Autolink::create()->autolink($bio);
}
if($user->language != $language &&
in_array($language, \App\Util\Localization\Localization::languages())
) {
$changes = true;
$user->language = $language;
session()->put('locale', $language);
}
if($user->language != $language &&
in_array($language, \App\Util\Localization\Localization::languages())
) {
$changes = true;
$user->language = $language;
session()->put('locale', $language);
}
if($existingPronouns != $pronouns) {
if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
PronounService::clear($profile->id);
} else {
PronounService::put($profile->id, $pronouns);
}
}
}
if($existingPronouns != $pronouns) {
if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
PronounService::clear($profile->id);
} else {
PronounService::put($profile->id, $pronouns);
}
}
}
if ($changes === true) {
$user->save();
$profile->save();
Cache::forget('user:account:id:'.$user->id);
AccountService::del($profile->id);
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
}
if ($changes === true) {
$user->save();
$profile->save();
Cache::forget('user:account:id:'.$user->id);
AccountService::del($profile->id);
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
}
return redirect('/settings/home');
}
return redirect('/settings/home');
}
public function password()
{
return view('settings.password');
}
public function password()
{
return view('settings.password');
}
public function passwordUpdate(Request $request)
{
$this->validate($request, [
'current' => 'required|string',
'password' => 'required|string',
'password_confirmation' => 'required|string',
]);
public function passwordUpdate(Request $request)
{
$this->validate($request, [
'current' => 'required|string',
'password' => 'required|string',
'password_confirmation' => 'required|string',
]);
$current = $request->input('current');
$new = $request->input('password');
$confirm = $request->input('password_confirmation');
$current = $request->input('current');
$new = $request->input('password');
$confirm = $request->input('password_confirmation');
$user = Auth::user();
$user = Auth::user();
if (password_verify($current, $user->password) && $new === $confirm) {
$user->password = bcrypt($new);
$user->save();
if (password_verify($current, $user->password) && $new === $confirm) {
$user->password = bcrypt($new);
$user->save();
$log = new AccountLog();
$log->user_id = $user->id;
$log->item_id = $user->id;
$log->item_type = 'App\User';
$log->action = 'account.edit.password';
$log->message = 'Password changed';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->save();
$log = new AccountLog();
$log->user_id = $user->id;
$log->item_id = $user->id;
$log->item_type = 'App\User';
$log->action = 'account.edit.password';
$log->message = 'Password changed';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->save();
Mail::to($request->user())->send(new PasswordChange($user));
return redirect('/settings/home')->with('status', 'Password successfully updated!');
} else {
return redirect()->back()->with('error', 'There was an error with your request! Please try again.');
}
Mail::to($request->user())->send(new PasswordChange($user));
return redirect('/settings/home')->with('status', 'Password successfully updated!');
} else {
return redirect()->back()->with('error', 'There was an error with your request! Please try again.');
}
}
}
public function email()
{
return view('settings.email');
}
public function email()
{
return view('settings.email');
}
public function emailUpdate(Request $request)
{
$this->validate($request, [
'email' => 'required|email|unique:users,email',
]);
$changes = false;
$email = $request->input('email');
$user = Auth::user();
$profile = $user->profile;
public function emailUpdate(Request $request)
{
$this->validate($request, [
'email' => 'required|email|unique:users,email',
]);
$changes = false;
$email = $request->input('email');
$user = Auth::user();
$profile = $user->profile;
$validate = config_cache('pixelfed.enforce_email_verification');
$validate = config_cache('pixelfed.enforce_email_verification');
if ($user->email != $email) {
$changes = true;
$user->email = $email;
if ($user->email != $email) {
$changes = true;
$user->email = $email;
if ($validate) {
$user->email_verified_at = null;
// Prevent old verifications from working
EmailVerification::whereUserId($user->id)->delete();
}
if ($validate) {
// auto verify admin email addresses
$user->email_verified_at = $user->is_admin == true ? now() : null;
// Prevent old verifications from working
EmailVerification::whereUserId($user->id)->delete();
}
$log = new AccountLog();
$log->user_id = $user->id;
$log->item_id = $user->id;
$log->item_type = 'App\User';
$log->action = 'account.edit.email';
$log->message = 'Email changed';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->save();
}
$log = new AccountLog();
$log->user_id = $user->id;
$log->item_id = $user->id;
$log->item_type = 'App\User';
$log->action = 'account.edit.email';
$log->message = 'Email changed';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->save();
}
if ($changes === true) {
Cache::forget('user:account:id:'.$user->id);
$user->save();
$profile->save();
if ($changes === true) {
Cache::forget('user:account:id:'.$user->id);
$user->save();
$profile->save();
return redirect('/settings/home')->with('status', 'Email successfully updated!');
} else {
return redirect('/settings/email');
}
return redirect('/settings/email')->with('status', 'Email successfully updated!');
} else {
return redirect('/settings/email');
}
}
public function avatar()
{
return view('settings.avatar');
}
}
public function avatar()
{
return view('settings.avatar');
}
}

Wyświetl plik

@ -2,31 +2,26 @@
namespace App\Http\Controllers\Settings;
use App\AccountLog;
use App\EmailVerification;
use App\Instance;
use App\Follower;
use App\Media;
use App\Profile;
use App\User;
use App\Services\RelationshipService;
use App\UserFilter;
use App\Util\Lexer\PrettyNumber;
use App\Util\ActivityPub\Helpers;
use Auth, Cache, DB;
use Auth;
use Cache;
use DB;
use Illuminate\Http\Request;
trait PrivacySettings
{
public function privacy()
{
$user = Auth::user();
$settings = $user->settings;
$profile = $user->profile;
$is_private = $profile->is_private;
$settings['is_private'] = (bool) $is_private;
$user = Auth::user();
$settings = $user->settings;
$profile = $user->profile;
$is_private = $profile->is_private;
$settings['is_private'] = (bool) $is_private;
return view('settings.privacy', compact('settings', 'profile'));
return view('settings.privacy', compact('settings', 'profile'));
}
public function privacyStore(Request $request)
@ -34,16 +29,18 @@ trait PrivacySettings
$settings = $request->user()->settings;
$profile = $request->user()->profile;
$fields = [
'is_private',
'crawlable',
'public_dm',
'show_profile_follower_count',
'show_profile_following_count',
'show_atom',
'is_private',
'crawlable',
'public_dm',
'show_profile_follower_count',
'show_profile_following_count',
'indexable',
'show_atom',
];
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
$profile->save();
$profile->indexable = $request->input('indexable') == 'on';
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
$profile->save();
foreach ($fields as $field) {
$form = $request->input($field);
@ -64,12 +61,14 @@ trait PrivacySettings
} else {
$settings->{$field} = true;
}
} elseif ($field == 'public_dm') {
} elseif ($field == 'public_dm') {
if ($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
} elseif ($field == 'indexable') {
} else {
if ($form == 'on') {
$settings->{$field} = true;
@ -79,29 +78,36 @@ trait PrivacySettings
}
$settings->save();
}
Cache::forget('profile:settings:' . $profile->id);
Cache::forget('user:account:id:' . $profile->user_id);
Cache::forget('profile:follower_count:' . $profile->id);
Cache::forget('profile:following_count:' . $profile->id);
Cache::forget('profile: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);
$pid = $profile->id;
Cache::forget('profile:settings:'.$pid);
Cache::forget('user:account:id:'.$profile->user_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!');
}
public function mutedUsers()
{
{
$pid = Auth::user()->profile->id;
$ids = (new UserFilter())->mutedUserIds($pid);
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
return view('settings.privacy.muted', compact('users'));
}
public function mutedUsersUpdate(Request $request)
{
{
$this->validate($request, [
'profile_id' => 'required|integer|min:1'
'profile_id' => 'required|integer|min:1',
]);
$fid = $request->input('profile_id');
$pid = Auth::user()->profile->id;
@ -113,6 +119,8 @@ trait PrivacySettings
->firstOrFail();
$filter->delete();
});
RelationshipService::refresh($pid, $fid);
return redirect()->back();
}
@ -121,14 +129,14 @@ trait PrivacySettings
$pid = Auth::user()->profile->id;
$ids = (new UserFilter())->blockedUserIds($pid);
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
return view('settings.privacy.blocked', compact('users'));
}
public function blockedUsersUpdate(Request $request)
{
{
$this->validate($request, [
'profile_id' => 'required|integer|min:1'
'profile_id' => 'required|integer|min:1',
]);
$fid = $request->input('profile_id');
$pid = Auth::user()->profile->id;
@ -140,52 +148,32 @@ trait PrivacySettings
->firstOrFail();
$filter->delete();
});
RelationshipService::refresh($pid, $fid);
return redirect()->back();
}
public function blockedInstances()
{
$pid = Auth::user()->profile->id;
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Instance')
->whereFilterType('block')
->orderByDesc('id')
->paginate(10);
return view('settings.privacy.blocked-instances', compact('filters'));
// deprecated
abort(404);
}
public function domainBlocks()
{
return view('settings.privacy.domain-blocks');
}
public function blockedInstanceStore(Request $request)
{
$this->validate($request, [
'domain' => 'required|url|min:1|max:120'
]);
$domain = $request->input('domain');
if(Helpers::validateUrl($domain) == false) {
return abort(400, 'Invalid domain');
}
$domain = parse_url($domain, PHP_URL_HOST);
$instance = Instance::firstOrCreate(['domain' => $domain]);
$filter = new UserFilter;
$filter->user_id = Auth::user()->profile->id;
$filter->filterable_id = $instance->id;
$filter->filterable_type = 'App\Instance';
$filter->filter_type = 'block';
$filter->save();
return response()->json(['msg' => 200]);
// deprecated
abort(404);
}
public function blockedInstanceUnblock(Request $request)
{
$this->validate($request, [
'id' => 'required|integer|min:1'
]);
$pid = Auth::user()->profile->id;
$filter = UserFilter::whereFilterableType('App\Instance')
->whereUserId($pid)
->findOrFail($request->input('id'));
$filter->delete();
return redirect(route('settings.privacy.blocked-instances'));
// deprecated
abort(404);
}
public function blockedKeywords()
@ -206,7 +194,7 @@ trait PrivacySettings
$profile = Auth::user()->profile;
$settings = Auth::user()->settings;
if($mode !== 'keep-all') {
if ($mode !== 'keep-all') {
switch ($mode) {
case 'mutual-only':
$following = $profile->following()->pluck('profiles.id');
@ -221,9 +209,9 @@ trait PrivacySettings
case 'remove-all':
Follower::whereFollowingId($profile->id)->delete();
break;
default:
# code...
// code...
break;
}
}
@ -233,6 +221,7 @@ trait PrivacySettings
$settings->save();
$profile->save();
Cache::forget('profiles:private');
return [200];
}
}

Wyświetl plik

@ -81,14 +81,12 @@ class SettingsController extends Controller
public function dataImport()
{
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.home');
}
public function dataImportInstagram()
{
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.instagram.home');
abort(404);
}
public function developers()
@ -232,29 +230,51 @@ class SettingsController extends Controller
public function timelineSettings(Request $request)
{
$uid = $request->user()->id;
$pid = $request->user()->profile_id;
$top = Redis::zscore('pf:tl:top', $pid) != false;
$replies = Redis::zscore('pf:tl:replies', $pid) != false;
return view('settings.timeline', compact('top', 'replies'));
$userSettings = UserSetting::firstOrCreate([
'user_id' => $uid
]);
if(!$userSettings || !$userSettings->other) {
$userSettings = [
'enable_reblogs' => false,
'photo_reblogs_only' => false
];
} else {
$userSettings = array_merge([
'enable_reblogs' => false,
'photo_reblogs_only' => false
],
$userSettings->other);
}
return view('settings.timeline', compact('top', 'replies', 'userSettings'));
}
public function updateTimelineSettings(Request $request)
{
$pid = $request->user()->profile_id;
$top = $request->has('top') && $request->input('top') === 'on';
$replies = $request->has('replies') && $request->input('replies') === 'on';
if($top) {
Redis::zadd('pf:tl:top', $pid, $pid);
} else {
Redis::zrem('pf:tl:top', $pid);
}
if($replies) {
Redis::zadd('pf:tl:replies', $pid, $pid);
} else {
Redis::zrem('pf:tl:replies', $pid);
}
$pid = $request->user()->profile_id;
$uid = $request->user()->id;
$this->validate($request, [
'enable_reblogs' => 'sometimes',
'photo_reblogs_only' => 'sometimes'
]);
Redis::zrem('pf:tl:top', $pid);
Redis::zrem('pf:tl:replies', $pid);
$userSettings = UserSetting::firstOrCreate([
'user_id' => $uid
]);
if($userSettings->other) {
$other = $userSettings->other;
$other['enable_reblogs'] = $request->has('enable_reblogs');
$other['photo_reblogs_only'] = $request->has('photo_reblogs_only');
} else {
$other['enable_reblogs'] = $request->has('enable_reblogs');
$other['photo_reblogs_only'] = $request->has('photo_reblogs_only');
}
$userSettings->other = $other;
$userSettings->save();
return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!');
}

Wyświetl plik

@ -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]);
}
}

Wyświetl plik

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\Internal\SoftwareUpdateService;
class SoftwareUpdateController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('admin');
}
public function getSoftwareUpdateCheck(Request $request)
{
$res = SoftwareUpdateService::get();
return $res;
}
}

Wyświetl plik

@ -2,457 +2,486 @@
namespace App\Http\Controllers;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
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\AccountService;
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);
StatusDelete::dispatch($status);
}
} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) {
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
Cache::forget('profile:status_count:' . $status->profile_id);
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
StatusDelete::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() && (bool) 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']);
}
$status = StatusService::get($id);
if (
! $status ||
! isset($status['account'], $status['account']['id'], $status['local']) ||
! $status['local'] ||
strtolower($status['account']['username']) !== strtolower($username)
) {
$content = view('status.embed-removed');
return response($content, 404)->header('X-Frame-Options', 'ALLOWALL');
}
$profile = AccountService::get($status['account']['id'], true);
if (! $profile || $profile['locked'] || ! $profile['local']) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) {
$user = Profile::find($profile['id']);
if (! $user) {
return true;
}
$exists = AccountInterstitial::whereUserId($user->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 = StatusService::get($id);
if (
! $status ||
! isset($status['account'], $status['account']['id']) ||
intval($status['account']['id']) !== intval($profile['id']) ||
$status['sensitive'] ||
$status['visibility'] !== 'public' ||
$status['pf_type'] !== 'photo'
) {
$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);
}
}

Wyświetl plik

@ -2,357 +2,514 @@
namespace App\Http\Controllers\Stories;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use App\Models\Conversation;
use App\DirectMessage;
use App\Notification;
use App\Story;
use App\Status;
use App\StoryView;
use App\Http\Controllers\Controller;
use App\Http\Resources\StoryView as StoryViewResource;
use App\Jobs\StoryPipeline\StoryDelete;
use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryReplyDeliver;
use App\Jobs\StoryPipeline\StoryViewDeliver;
use App\Models\Conversation;
use App\Notification;
use App\Services\AccountService;
use App\Services\MediaPathService;
use App\Services\StoryService;
use App\Status;
use App\Story;
use App\StoryView;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class StoryApiV1Controller extends Controller
{
public function carousel(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$pid = $request->user()->profile_id;
const RECENT_KEY = 'pf:stories:recent-by-id:';
if(config('database.default') == 'pgsql') {
$s = Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->get();
} else {
$s = Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->orderBy('id')
->get();
}
const RECENT_TTL = 300;
$nodes = $s->map(function($s) use($pid) {
$profile = AccountService::get($s->profile_id, true);
if(!$profile || !isset($profile['id'])) {
return false;
}
public function carousel(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$pid = $request->user()->profile_id;
return [
'id' => (string) $s->id,
'pid' => (string) $s->profile_id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration ?? 3,
'seen' => StoryService::hasSeen($pid, $s->id),
'created_at' => $s->created_at->format('c')
];
})
->filter()
->groupBy('pid')
->map(function($item) use($pid) {
$profile = AccountService::get($item[0]['pid'], true);
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}");
return [
'id' => 'pfs:' . $profile['id'],
'user' => [
'id' => (string) $profile['id'],
'username' => $profile['username'],
'username_acct' => $profile['acct'],
'avatar' => $profile['avatar'],
'local' => $profile['local'],
'is_author' => $profile['id'] == $pid
],
'nodes' => $item,
'url' => $url,
'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
];
})
->sortBy('seen')
->values();
if (config('database.default') == 'pgsql') {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->map(function ($s) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $s->profile_id;
$r->type = $s->type;
$r->path = $s->path;
$res = [
'self' => [],
'nodes' => $nodes,
];
return $r;
})
->unique('profile_id');
});
} else {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->orderBy('id')
->get();
});
}
if(Story::whereProfileId($pid)->whereActive(true)->exists()) {
$selfStories = Story::whereProfileId($pid)
->whereActive(true)
->get()
->map(function($s) use($pid) {
return [
'id' => (string) $s->id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration,
'seen' => true,
'created_at' => $s->created_at->format('c')
];
})
->sortBy('id')
->values();
$selfProfile = AccountService::get($pid, true);
$res['self'] = [
'user' => [
'id' => (string) $selfProfile['id'],
'username' => $selfProfile['acct'],
'avatar' => $selfProfile['avatar'],
'local' => $selfProfile['local'],
'is_author' => true
],
$nodes = $s->map(function ($s) use ($pid) {
$profile = AccountService::get($s->profile_id, true);
if (! $profile || ! isset($profile['id'])) {
return false;
}
'nodes' => $selfStories,
];
}
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
return [
'id' => (string) $s->id,
'pid' => (string) $s->profile_id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration ?? 3,
'seen' => StoryService::hasSeen($pid, $s->id),
'created_at' => $s->created_at->format('c'),
];
})
->filter()
->groupBy('pid')
->map(function ($item) use ($pid) {
$profile = AccountService::get($item[0]['pid'], true);
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}");
public function add(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
return [
'id' => 'pfs:'.$profile['id'],
'user' => [
'id' => (string) $profile['id'],
'username' => $profile['username'],
'username_acct' => $profile['acct'],
'avatar' => $profile['avatar'],
'local' => $profile['local'],
'is_author' => $profile['id'] == $pid,
],
'nodes' => $item,
'url' => $url,
'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
];
})
->sortBy('seen')
->values();
$this->validate($request, [
'file' => function() {
return [
'required',
'mimetypes:image/jpeg,image/png,video/mp4',
'max:' . config_cache('pixelfed.max_photo_size'),
];
},
'duration' => 'sometimes|integer|min:0|max:30'
]);
$res = [
'self' => [],
'nodes' => $nodes,
];
$user = $request->user();
if (Story::whereProfileId($pid)->whereActive(true)->exists()) {
$selfStories = Story::whereProfileId($pid)
->whereActive(true)
->get()
->map(function ($s) {
return [
'id' => (string) $s->id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration,
'seen' => true,
'created_at' => $s->created_at->format('c'),
];
})
->sortBy('id')
->values();
$selfProfile = AccountService::get($pid, true);
$res['self'] = [
'user' => [
'id' => (string) $selfProfile['id'],
'username' => $selfProfile['acct'],
'avatar' => $selfProfile['avatar'],
'local' => $selfProfile['local'],
'is_author' => true,
],
$count = Story::whereProfileId($user->profile_id)
->whereActive(true)
->where('expires_at', '>', now())
->count();
'nodes' => $selfStories,
];
}
if($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.');
}
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
$photo = $request->file('file');
$path = $this->storeMedia($photo, $user);
public function selfCarousel(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$pid = $request->user()->profile_id;
$story = new Story();
$story->duration = $request->input('duration', 3);
$story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
$story->mime = $photo->getMimeType();
$story->path = $path;
$story->local = true;
$story->size = $photo->getSize();
$story->bearcap_token = str_random(64);
$story->expires_at = now()->addMinutes(1440);
$story->save();
if (config('database.default') == 'pgsql') {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->map(function ($s) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $s->profile_id;
$r->type = $s->type;
$r->path = $s->path;
$url = $story->path;
return $r;
})
->unique('profile_id');
});
} else {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->orderBy('id')
->get();
});
}
$res = [
'code' => 200,
'msg' => 'Successfully added',
'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)) . '?v=' . time(),
'media_type' => $story->type
];
$nodes = $s->map(function ($s) use ($pid) {
$profile = AccountService::get($s->profile_id, true);
if (! $profile || ! isset($profile['id'])) {
return false;
}
return $res;
}
return [
'id' => (string) $s->id,
'pid' => (string) $s->profile_id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration ?? 3,
'seen' => StoryService::hasSeen($pid, $s->id),
'created_at' => $s->created_at->format('c'),
];
})
->filter()
->groupBy('pid')
->map(function ($item) use ($pid) {
$profile = AccountService::get($item[0]['pid'], true);
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}");
public function publish(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
return [
'id' => 'pfs:'.$profile['id'],
'user' => [
'id' => (string) $profile['id'],
'username' => $profile['username'],
'username_acct' => $profile['acct'],
'avatar' => $profile['avatar'],
'local' => $profile['local'],
'is_author' => $profile['id'] == $pid,
],
'nodes' => $item,
'url' => $url,
'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
];
})
->sortBy('seen')
->values();
$this->validate($request, [
'media_id' => 'required',
'duration' => 'required|integer|min:0|max:30',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean'
]);
$selfProfile = AccountService::get($pid, true);
$res = [
'self' => [
'user' => [
'id' => (string) $selfProfile['id'],
'username' => $selfProfile['acct'],
'avatar' => $selfProfile['avatar'],
'local' => $selfProfile['local'],
'is_author' => true,
],
$id = $request->input('media_id');
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
'nodes' => [],
],
'nodes' => $nodes,
];
$story->active = true;
$story->duration = $request->input('duration', 10);
$story->can_reply = $request->input('can_reply');
$story->can_react = $request->input('can_react');
$story->save();
if (Story::whereProfileId($pid)->whereActive(true)->exists()) {
$selfStories = Story::whereProfileId($pid)
->whereActive(true)
->get()
->map(function ($s) {
return [
'id' => (string) $s->id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration,
'seen' => true,
'created_at' => $s->created_at->format('c'),
];
})
->sortBy('id')
->values();
$res['self']['nodes'] = $selfStories;
}
StoryService::delLatest($story->profile_id);
StoryFanout::dispatch($story)->onQueue('story');
StoryService::addRotateQueue($story->id);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
return [
'code' => 200,
'msg' => 'Successfully published',
];
}
public function add(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
public function delete(Request $request, $id)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'file' => function () {
return [
'required',
'mimetypes:image/jpeg,image/png,video/mp4',
'max:'.config_cache('pixelfed.max_photo_size'),
];
},
'duration' => 'sometimes|integer|min:0|max:30',
]);
$user = $request->user();
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$story->active = false;
$story->save();
$count = Story::whereProfileId($user->profile_id)
->whereActive(true)
->where('expires_at', '>', now())
->count();
StoryDelete::dispatch($story)->onQueue('story');
if ($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.');
}
return [
'code' => 200,
'msg' => 'Successfully deleted'
];
}
$photo = $request->file('file');
$path = $this->storeMedia($photo, $user);
public function viewed(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$story = new Story();
$story->duration = $request->input('duration', 3);
$story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
$story->mime = $photo->getMimeType();
$story->path = $path;
$story->local = true;
$story->size = $photo->getSize();
$story->bearcap_token = str_random(64);
$story->expires_at = now()->addMinutes(1440);
$story->save();
$this->validate($request, [
'id' => 'required|min:1',
]);
$id = $request->input('id');
$url = $story->path;
$authed = $request->user()->profile;
$res = [
'code' => 200,
'msg' => 'Successfully added',
'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)).'?v='.time(),
'media_type' => $story->type,
];
$story = Story::with('profile')
->findOrFail($id);
$exp = $story->expires_at;
return $res;
}
$profile = $story->profile;
public function publish(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
if($story->profile_id == $authed->id) {
return [];
}
$this->validate($request, [
'media_id' => 'required',
'duration' => 'required|integer|min:0|max:30',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean',
]);
$publicOnly = (bool) $profile->followedBy($authed);
abort_if(!$publicOnly, 403);
$id = $request->input('media_id');
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$v = StoryView::firstOrCreate([
'story_id' => $id,
'profile_id' => $authed->id
]);
$story->active = true;
$story->duration = $request->input('duration', 10);
$story->can_reply = $request->input('can_reply');
$story->can_react = $request->input('can_react');
$story->save();
if($v->wasRecentlyCreated) {
Story::findOrFail($story->id)->increment('view_count');
StoryService::delLatest($story->profile_id);
StoryFanout::dispatch($story)->onQueue('story');
StoryService::addRotateQueue($story->id);
if($story->local == false) {
StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
}
}
return [
'code' => 200,
'msg' => 'Successfully published',
];
}
Cache::forget('stories:recent:by_id:' . $authed->id);
StoryService::addSeen($authed->id, $story->id);
return ['code' => 200];
}
public function delete(Request $request, $id)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
public function comment(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'caption' => 'required|string'
]);
$pid = $request->user()->profile_id;
$text = $request->input('caption');
$user = $request->user();
$story = Story::findOrFail($request->input('sid'));
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$story->active = false;
$story->save();
abort_if(!$story->can_reply, 422);
StoryDelete::dispatch($story)->onQueue('story');
$status = new Status;
$status->type = 'story:reply';
$status->profile_id = $pid;
$status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct';
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id
]);
$status->save();
return [
'code' => 200,
'msg' => 'Successfully deleted',
];
}
$dm = new DirectMessage;
$dm->to_id = $story->profile_id;
$dm->from_id = $pid;
$dm->type = 'story:comment';
$dm->status_id = $status->id;
$dm->meta = json_encode([
'story_username' => $story->profile->username,
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'caption' => $text
]);
$dm->save();
public function viewed(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid
],
[
'type' => 'story:comment',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false
]
);
$this->validate($request, [
'id' => 'required|min:1',
]);
$id = $request->input('id');
if($story->local) {
$n = new Notification;
$n->profile_id = $dm->to_id;
$n->actor_id = $dm->from_id;
$n->item_id = $dm->id;
$n->item_type = 'App\DirectMessage';
$n->action = 'story:comment';
$n->save();
} else {
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
}
$authed = $request->user()->profile;
return [
'code' => 200,
'msg' => 'Sent!'
];
}
$story = Story::with('profile')
->findOrFail($id);
$exp = $story->expires_at;
protected function storeMedia($photo, $user)
{
$mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [
'image/jpeg',
'image/png',
'video/mp4'
]) == false) {
abort(400, 'Invalid media type');
return;
}
$profile = $story->profile;
$storagePath = MediaPathService::story($user->profile);
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
return $path;
}
if ($story->profile_id == $authed->id) {
return [];
}
$publicOnly = (bool) $profile->followedBy($authed);
abort_if(! $publicOnly, 403);
$v = StoryView::firstOrCreate([
'story_id' => $id,
'profile_id' => $authed->id,
]);
if ($v->wasRecentlyCreated) {
Story::findOrFail($story->id)->increment('view_count');
if ($story->local == false) {
StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
}
}
Cache::forget('stories:recent:by_id:'.$authed->id);
StoryService::addSeen($authed->id, $story->id);
return ['code' => 200];
}
public function comment(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'caption' => 'required|string',
]);
$pid = $request->user()->profile_id;
$text = $request->input('caption');
$story = Story::findOrFail($request->input('sid'));
abort_if(! $story->can_reply, 422);
$status = new Status;
$status->type = 'story:reply';
$status->profile_id = $pid;
$status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct';
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id,
]);
$status->save();
$dm = new DirectMessage;
$dm->to_id = $story->profile_id;
$dm->from_id = $pid;
$dm->type = 'story:comment';
$dm->status_id = $status->id;
$dm->meta = json_encode([
'story_username' => $story->profile->username,
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'caption' => $text,
]);
$dm->save();
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid,
],
[
'type' => 'story:comment',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false,
]
);
if ($story->local) {
$n = new Notification;
$n->profile_id = $dm->to_id;
$n->actor_id = $dm->from_id;
$n->item_id = $dm->id;
$n->item_type = 'App\DirectMessage';
$n->action = 'story:comment';
$n->save();
} else {
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
}
return [
'code' => 200,
'msg' => 'Sent!',
];
}
protected function storeMedia($photo, $user)
{
$mimes = explode(',', config_cache('pixelfed.media_types'));
if (in_array($photo->getMimeType(), [
'image/jpeg',
'image/png',
'video/mp4',
]) == false) {
abort(400, 'Invalid media type');
return;
}
$storagePath = MediaPathService::story($user->profile);
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)).'_'.Str::random(random_int(32, 35)).'_'.Str::random(random_int(1, 14)).'.'.$photo->extension());
return $path;
}
public function viewers(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'sid' => 'required|string|min:1|max:50',
]);
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
$viewers = StoryView::whereStoryId($story->id)
->orderByDesc('id')
->cursorPaginate(10);
return StoryViewResource::collection($viewers);
}
}

Wyświetl plik

@ -2,333 +2,338 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Media;
use App\Profile;
use App\Report;
use App\DirectMessage;
use App\Notification;
use App\Status;
use App\Story;
use App\StoryView;
use App\Models\Poll;
use App\Models\PollVote;
use App\Services\ProfileService;
use App\Services\StoryService;
use Cache, Storage;
use Image as Intervention;
use App\Services\FollowerService;
use App\Services\MediaPathService;
use FFMpeg;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\Format\Video\X264;
use App\Jobs\StoryPipeline\StoryDelete;
use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryReactionDeliver;
use App\Jobs\StoryPipeline\StoryReplyDeliver;
use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryDelete;
use ImageOptimizer;
use App\Models\Conversation;
use App\Models\Poll;
use App\Models\PollVote;
use App\Notification;
use App\Report;
use App\Services\FollowerService;
use App\Services\MediaPathService;
use App\Services\StoryService;
use App\Services\UserRoleService;
use App\Status;
use App\Story;
use FFMpeg;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Image as Intervention;
use Storage;
class StoryComposeController extends Controller
{
public function apiV1Add(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'file' => function() {
return [
'required',
'mimetypes:image/jpeg,image/png,video/mp4',
'max:' . config_cache('pixelfed.max_photo_size'),
];
},
]);
$user = $request->user();
$count = Story::whereProfileId($user->profile_id)
->whereActive(true)
->where('expires_at', '>', now())
->count();
if($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.');
}
$photo = $request->file('file');
$path = $this->storePhoto($photo, $user);
$story = new Story();
$story->duration = 3;
$story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
$story->mime = $photo->getMimeType();
$story->path = $path;
$story->local = true;
$story->size = $photo->getSize();
$story->bearcap_token = str_random(64);
$story->expires_at = now()->addMinutes(1440);
$story->save();
$url = $story->path;
$res = [
'code' => 200,
'msg' => 'Successfully added',
'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)) . '?v=' . time(),
'media_type' => $story->type
];
if($story->type === 'video') {
$video = FFMpeg::open($path);
$duration = $video->getDurationInSeconds();
$res['media_duration'] = $duration;
if($duration > 500) {
Storage::delete($story->path);
$story->delete();
return response()->json([
'message' => 'Video duration cannot exceed 60 seconds'
], 422);
}
}
return $res;
}
protected function storePhoto($photo, $user)
{
$mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [
'image/jpeg',
'image/png',
'video/mp4'
]) == false) {
abort(400, 'Invalid media type');
return;
}
$storagePath = MediaPathService::story($user->profile);
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) {
$fpath = storage_path('app/' . $path);
$img = Intervention::make($fpath);
$img->orientate();
$img->save($fpath, config_cache('pixelfed.image_quality'));
$img->destroy();
}
return $path;
}
public function cropPhoto(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'media_id' => 'required|integer|min:1',
'width' => 'required',
'height' => 'required',
'x' => 'required',
'y' => 'required'
]);
$user = $request->user();
$id = $request->input('media_id');
$width = round($request->input('width'));
$height = round($request->input('height'));
$x = round($request->input('x'));
$y = round($request->input('y'));
$story = Story::whereProfileId($user->profile_id)->findOrFail($id);
$path = storage_path('app/' . $story->path);
if(!is_file($path)) {
abort(400, 'Invalid or missing media.');
}
if($story->type === 'photo') {
$img = Intervention::make($path);
$img->crop($width, $height, $x, $y);
$img->resize(1080, 1920, function ($constraint) {
$constraint->aspectRatio();
});
$img->save($path, config_cache('pixelfed.image_quality'));
}
return [
'code' => 200,
'msg' => 'Successfully cropped',
];
}
public function publishStory(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'media_id' => 'required',
'duration' => 'required|integer|min:3|max:120',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean'
]);
$id = $request->input('media_id');
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$story->active = true;
$story->duration = $request->input('duration', 10);
$story->can_reply = $request->input('can_reply');
$story->can_react = $request->input('can_react');
$story->save();
StoryService::delLatest($story->profile_id);
StoryFanout::dispatch($story)->onQueue('story');
StoryService::addRotateQueue($story->id);
return [
'code' => 200,
'msg' => 'Successfully published',
];
}
public function apiV1Delete(Request $request, $id)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$story->active = false;
$story->save();
StoryDelete::dispatch($story)->onQueue('story');
return [
'code' => 200,
'msg' => 'Successfully deleted'
];
}
public function compose(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
return view('stories.compose');
}
public function createPoll(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.polls.enabled'), 404);
return $request->all();
}
public function publishStoryPoll(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'question' => 'required|string|min:6|max:140',
'options' => 'required|array|min:2|max:4',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean'
]);
$pid = $request->user()->profile_id;
$count = Story::whereProfileId($pid)
->whereActive(true)
->where('expires_at', '>', now())
->count();
if($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.');
}
$story = new Story;
$story->type = 'poll';
$story->story = json_encode([
'question' => $request->input('question'),
'options' => $request->input('options')
]);
$story->public = false;
$story->local = true;
$story->profile_id = $pid;
$story->expires_at = now()->addMinutes(1440);
$story->duration = 30;
$story->can_reply = false;
$story->can_react = false;
$story->save();
$poll = new Poll;
$poll->story_id = $story->id;
$poll->profile_id = $pid;
$poll->poll_options = $request->input('options');
$poll->expires_at = $story->expires_at;
$poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
return 0;
})->toArray();
$poll->save();
$story->active = true;
$story->save();
StoryService::delLatest($story->profile_id);
return [
'code' => 200,
'msg' => 'Successfully published',
];
}
public function storyPollVote(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'ci' => 'required|integer|min:0|max:3'
]);
$pid = $request->user()->profile_id;
$ci = $request->input('ci');
$story = Story::findOrFail($request->input('sid'));
abort_if(!FollowerService::follows($pid, $story->profile_id), 403);
$poll = Poll::whereStoryId($story->id)->firstOrFail();
$vote = new PollVote;
$vote->profile_id = $pid;
$vote->poll_id = $poll->id;
$vote->story_id = $story->id;
$vote->status_id = null;
$vote->choice = $ci;
$vote->save();
$poll->votes_count = $poll->votes_count + 1;
$poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) {
return $ci == $key ? $tally + 1 : $tally;
})->toArray();
$poll->save();
return 200;
}
public function storeReport(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
$this->validate($request, [
'file' => function () {
return [
'required',
'mimetypes:image/jpeg,image/png,video/mp4',
'max:'.config_cache('pixelfed.max_photo_size'),
];
},
]);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$count = Story::whereProfileId($user->profile_id)
->whereActive(true)
->where('expires_at', '>', now())
->count();
if ($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.');
}
$photo = $request->file('file');
$path = $this->storePhoto($photo, $user);
$story = new Story();
$story->duration = 3;
$story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
$story->mime = $photo->getMimeType();
$story->path = $path;
$story->local = true;
$story->size = $photo->getSize();
$story->bearcap_token = str_random(64);
$story->expires_at = now()->addMinutes(1440);
$story->save();
$url = $story->path;
$res = [
'code' => 200,
'msg' => 'Successfully added',
'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)).'?v='.time(),
'media_type' => $story->type,
];
if ($story->type === 'video') {
$video = FFMpeg::open($path);
$duration = $video->getDurationInSeconds();
$res['media_duration'] = $duration;
if ($duration > 500) {
Storage::delete($story->path);
$story->delete();
return response()->json([
'message' => 'Video duration cannot exceed 60 seconds',
], 422);
}
}
return $res;
}
protected function storePhoto($photo, $user)
{
$mimes = explode(',', config_cache('pixelfed.media_types'));
if (in_array($photo->getMimeType(), [
'image/jpeg',
'image/png',
'video/mp4',
]) == false) {
abort(400, 'Invalid media type');
return;
}
$storagePath = MediaPathService::story($user->profile);
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)).'_'.Str::random(random_int(32, 35)).'_'.Str::random(random_int(1, 14)).'.'.$photo->extension());
if (in_array($photo->getMimeType(), ['image/jpeg', 'image/png'])) {
$fpath = storage_path('app/'.$path);
$img = Intervention::make($fpath);
$img->orientate();
$img->save($fpath, config_cache('pixelfed.image_quality'));
$img->destroy();
}
return $path;
}
public function cropPhoto(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'media_id' => 'required|integer|min:1',
'width' => 'required',
'height' => 'required',
'x' => 'required',
'y' => 'required',
]);
$user = $request->user();
$id = $request->input('media_id');
$width = round($request->input('width'));
$height = round($request->input('height'));
$x = round($request->input('x'));
$y = round($request->input('y'));
$story = Story::whereProfileId($user->profile_id)->findOrFail($id);
$path = storage_path('app/'.$story->path);
if (! is_file($path)) {
abort(400, 'Invalid or missing media.');
}
if ($story->type === 'photo') {
$img = Intervention::make($path);
$img->crop($width, $height, $x, $y);
$img->resize(1080, 1920, function ($constraint) {
$constraint->aspectRatio();
});
$img->save($path, config_cache('pixelfed.image_quality'));
}
return [
'code' => 200,
'msg' => 'Successfully cropped',
];
}
public function publishStory(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'media_id' => 'required',
'duration' => 'required|integer|min:3|max:120',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean',
]);
$id = $request->input('media_id');
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$story->active = true;
$story->duration = $request->input('duration', 10);
$story->can_reply = $request->input('can_reply');
$story->can_react = $request->input('can_react');
$story->save();
StoryService::delLatest($story->profile_id);
StoryFanout::dispatch($story)->onQueue('story');
StoryService::addRotateQueue($story->id);
return [
'code' => 200,
'msg' => 'Successfully published',
];
}
public function apiV1Delete(Request $request, $id)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$story->active = false;
$story->save();
StoryDelete::dispatch($story)->onQueue('story');
return [
'code' => 200,
'msg' => 'Successfully deleted',
];
}
public function compose(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
return view('stories.compose');
}
public function createPoll(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(! config_cache('instance.polls.enabled'), 404);
return $request->all();
}
public function publishStoryPoll(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'question' => 'required|string|min:6|max:140',
'options' => 'required|array|min:2|max:4',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean',
]);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$pid = $request->user()->profile_id;
$count = Story::whereProfileId($pid)
->whereActive(true)
->where('expires_at', '>', now())
->count();
if ($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.');
}
$story = new Story;
$story->type = 'poll';
$story->story = json_encode([
'question' => $request->input('question'),
'options' => $request->input('options'),
]);
$story->public = false;
$story->local = true;
$story->profile_id = $pid;
$story->expires_at = now()->addMinutes(1440);
$story->duration = 30;
$story->can_reply = false;
$story->can_react = false;
$story->save();
$poll = new Poll;
$poll->story_id = $story->id;
$poll->profile_id = $pid;
$poll->poll_options = $request->input('options');
$poll->expires_at = $story->expires_at;
$poll->cached_tallies = collect($poll->poll_options)->map(function ($o) {
return 0;
})->toArray();
$poll->save();
$story->active = true;
$story->save();
StoryService::delLatest($story->profile_id);
return [
'code' => 200,
'msg' => 'Successfully published',
];
}
public function storyPollVote(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'ci' => 'required|integer|min:0|max:3',
]);
$pid = $request->user()->profile_id;
$ci = $request->input('ci');
$story = Story::findOrFail($request->input('sid'));
abort_if(! FollowerService::follows($pid, $story->profile_id), 403);
$poll = Poll::whereStoryId($story->id)->firstOrFail();
$vote = new PollVote;
$vote->profile_id = $pid;
$vote->poll_id = $poll->id;
$vote->story_id = $story->id;
$vote->status_id = null;
$vote->choice = $ci;
$vote->save();
$poll->votes_count = $poll->votes_count + 1;
$poll->cached_tallies = collect($poll->getTallies())->map(function ($tally, $key) use ($ci) {
return $ci == $key ? $tally + 1 : $tally;
})->toArray();
$poll->save();
return 200;
}
public function storeReport(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
]);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$pid = $request->user()->profile_id;
$sid = $request->input('id');
$type = $request->input('type');
@ -344,28 +349,28 @@ class StoryComposeController extends Controller
'copyright',
'impersonation',
'scam',
'terrorism'
'terrorism',
];
abort_if(!in_array($type, $types), 422, 'Invalid story report type');
abort_if(! in_array($type, $types), 422, 'Invalid story report type');
$story = Story::findOrFail($sid);
abort_if($story->profile_id == $pid, 422, 'Cannot report your own story');
abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow');
abort_if(! FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow');
if( Report::whereProfileId($pid)
->whereObjectType('App\Story')
->whereObjectId($story->id)
->exists()
if (Report::whereProfileId($pid)
->whereObjectType('App\Story')
->whereObjectId($story->id)
->exists()
) {
return response()->json(['error' => [
'code' => 409,
'message' => 'Cannot report the same story again'
]], 409);
return response()->json(['error' => [
'code' => 409,
'message' => 'Cannot report the same story again',
]], 409);
}
$report = new Report;
$report = new Report;
$report->profile_id = $pid;
$report->user_id = $request->user()->id;
$report->object_id = $story->id;
@ -376,149 +381,151 @@ class StoryComposeController extends Controller
$report->save();
return [200];
}
}
public function react(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'reaction' => 'required|string'
]);
$pid = $request->user()->profile_id;
$text = $request->input('reaction');
public function react(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'reaction' => 'required|string',
]);
$pid = $request->user()->profile_id;
$text = $request->input('reaction');
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$story = Story::findOrFail($request->input('sid'));
$story = Story::findOrFail($request->input('sid'));
abort_if(! $story->can_react, 422);
abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story');
abort_if(!$story->can_react, 422);
abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story');
$status = new Status;
$status->profile_id = $pid;
$status->type = 'story:reaction';
$status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct';
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id,
'reaction' => $text,
]);
$status->save();
$status = new Status;
$status->profile_id = $pid;
$status->type = 'story:reaction';
$status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct';
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id,
'reaction' => $text
]);
$status->save();
$dm = new DirectMessage;
$dm->to_id = $story->profile_id;
$dm->from_id = $pid;
$dm->type = 'story:react';
$dm->status_id = $status->id;
$dm->meta = json_encode([
'story_username' => $story->profile->username,
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'reaction' => $text,
]);
$dm->save();
$dm = new DirectMessage;
$dm->to_id = $story->profile_id;
$dm->from_id = $pid;
$dm->type = 'story:react';
$dm->status_id = $status->id;
$dm->meta = json_encode([
'story_username' => $story->profile->username,
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'reaction' => $text
]);
$dm->save();
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid,
],
[
'type' => 'story:react',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false,
]
);
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid
],
[
'type' => 'story:react',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false
]
);
if ($story->local) {
// generate notification
$n = new Notification;
$n->profile_id = $dm->to_id;
$n->actor_id = $dm->from_id;
$n->item_id = $dm->id;
$n->item_type = 'App\DirectMessage';
$n->action = 'story:react';
$n->save();
} else {
StoryReactionDeliver::dispatch($story, $status)->onQueue('story');
}
if($story->local) {
// generate notification
$n = new Notification;
$n->profile_id = $dm->to_id;
$n->actor_id = $dm->from_id;
$n->item_id = $dm->id;
$n->item_type = 'App\DirectMessage';
$n->action = 'story:react';
$n->save();
} else {
StoryReactionDeliver::dispatch($story, $status)->onQueue('story');
}
StoryService::reactIncrement($story->id, $pid);
StoryService::reactIncrement($story->id, $pid);
return 200;
}
return 200;
}
public function comment(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'caption' => 'required|string',
]);
$pid = $request->user()->profile_id;
$text = $request->input('caption');
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$story = Story::findOrFail($request->input('sid'));
public function comment(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'caption' => 'required|string'
]);
$pid = $request->user()->profile_id;
$text = $request->input('caption');
abort_if(! $story->can_reply, 422);
$story = Story::findOrFail($request->input('sid'));
$status = new Status;
$status->type = 'story:reply';
$status->profile_id = $pid;
$status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct';
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id,
]);
$status->save();
abort_if(!$story->can_reply, 422);
$dm = new DirectMessage;
$dm->to_id = $story->profile_id;
$dm->from_id = $pid;
$dm->type = 'story:comment';
$dm->status_id = $status->id;
$dm->meta = json_encode([
'story_username' => $story->profile->username,
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'caption' => $text,
]);
$dm->save();
$status = new Status;
$status->type = 'story:reply';
$status->profile_id = $pid;
$status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct';
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id
]);
$status->save();
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid,
],
[
'type' => 'story:comment',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false,
]
);
$dm = new DirectMessage;
$dm->to_id = $story->profile_id;
$dm->from_id = $pid;
$dm->type = 'story:comment';
$dm->status_id = $status->id;
$dm->meta = json_encode([
'story_username' => $story->profile->username,
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'caption' => $text
]);
$dm->save();
if ($story->local) {
// generate notification
$n = new Notification;
$n->profile_id = $dm->to_id;
$n->actor_id = $dm->from_id;
$n->item_id = $dm->id;
$n->item_type = 'App\DirectMessage';
$n->action = 'story:comment';
$n->save();
} else {
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
}
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid
],
[
'type' => 'story:comment',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false
]
);
if($story->local) {
// generate notification
$n = new Notification;
$n->profile_id = $dm->to_id;
$n->actor_id = $dm->from_id;
$n->item_id = $dm->id;
$n->item_type = 'App\DirectMessage';
$n->action = 'story:comment';
$n->save();
} else {
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
}
return 200;
}
return 200;
}
}

Wyświetl plik

@ -28,288 +28,308 @@ use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Resource\Item;
use App\Transformer\ActivityPub\Verb\StoryVerb;
use App\Jobs\StoryPipeline\StoryViewDeliver;
use App\Services\UserRoleService;
class StoryController extends StoryComposeController
{
public function recent(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$pid = $request->user()->profile_id;
public function recent(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return [];
}
$pid = $user->profile_id;
if(config('database.default') == 'pgsql') {
$s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->get()
->map(function($s) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $s->profile_id;
$r->type = $s->type;
$r->path = $s->path;
return $r;
})
->unique('profile_id');
});
if(config('database.default') == 'pgsql') {
$s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->get()
->map(function($s) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $s->profile_id;
$r->type = $s->type;
$r->path = $s->path;
return $r;
})
->unique('profile_id');
});
} else {
$s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->groupBy('followers.following_id')
->orderByDesc('id')
->get();
});
}
} else {
$s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->groupBy('followers.following_id')
->orderByDesc('id')
->get();
});
}
$self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) {
return Story::whereProfileId($pid)
->whereActive(true)
->orderByDesc('id')
->limit(1)
->get()
->map(function($s) use($pid) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $pid;
$r->type = $s->type;
$r->path = $s->path;
return $r;
});
});
$self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) {
return Story::whereProfileId($pid)
->whereActive(true)
->orderByDesc('id')
->limit(1)
->get()
->map(function($s) use($pid) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $pid;
$r->type = $s->type;
$r->path = $s->path;
return $r;
});
});
if($self->count()) {
$s->prepend($self->first());
}
if($self->count()) {
$s->prepend($self->first());
}
$res = $s->map(function($s) use($pid) {
$profile = AccountService::get($s->profile_id);
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}");
return [
'pid' => $profile['id'],
'avatar' => $profile['avatar'],
'local' => $profile['local'],
'username' => $profile['acct'],
'latest' => [
'id' => $s->id,
'type' => $s->type,
'preview_url' => url(Storage::url($s->path))
],
'url' => $url,
'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)),
'sid' => $s->id
];
})
->sortBy('seen')
->values();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
$res = $s->map(function($s) use($pid) {
$profile = AccountService::get($s->profile_id);
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}");
return [
'pid' => $profile['id'],
'avatar' => $profile['avatar'],
'local' => $profile['local'],
'username' => $profile['acct'],
'latest' => [
'id' => $s->id,
'type' => $s->type,
'preview_url' => url(Storage::url($s->path))
],
'url' => $url,
'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)),
'sid' => $s->id
];
})
->sortBy('seen')
->values();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function profile(Request $request, $id)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
public function profile(Request $request, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile_id;
$profile = Profile::findOrFail($id);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return [];
}
$authed = $user->profile_id;
$profile = Profile::findOrFail($id);
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
return abort([], 403);
}
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
return abort([], 403);
}
$stories = Story::whereProfileId($profile->id)
->whereActive(true)
->orderBy('expires_at')
->get()
->map(function($s, $k) use($authed) {
$seen = StoryService::hasSeen($authed, $s->id);
$res = [
'id' => (string) $s->id,
'type' => $s->type,
'duration' => $s->duration,
'src' => url(Storage::url($s->path)),
'created_at' => $s->created_at->toAtomString(),
'expires_at' => $s->expires_at->toAtomString(),
'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null,
'seen' => $seen,
'progress' => $seen ? 100 : 0,
'can_reply' => (bool) $s->can_reply,
'can_react' => (bool) $s->can_react
];
$stories = Story::whereProfileId($profile->id)
->whereActive(true)
->orderBy('expires_at')
->get()
->map(function($s, $k) use($authed) {
$seen = StoryService::hasSeen($authed, $s->id);
$res = [
'id' => (string) $s->id,
'type' => $s->type,
'duration' => $s->duration,
'src' => url(Storage::url($s->path)),
'created_at' => $s->created_at->toAtomString(),
'expires_at' => $s->expires_at->toAtomString(),
'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null,
'seen' => $seen,
'progress' => $seen ? 100 : 0,
'can_reply' => (bool) $s->can_reply,
'can_react' => (bool) $s->can_react
];
if($s->type == 'poll') {
$res['question'] = json_decode($s->story, true)['question'];
$res['options'] = json_decode($s->story, true)['options'];
$res['voted'] = PollService::votedStory($s->id, $authed);
if($res['voted']) {
$res['voted_index'] = PollService::storyChoice($s->id, $authed);
}
}
if($s->type == 'poll') {
$res['question'] = json_decode($s->story, true)['question'];
$res['options'] = json_decode($s->story, true)['options'];
$res['voted'] = PollService::votedStory($s->id, $authed);
if($res['voted']) {
$res['voted_index'] = PollService::storyChoice($s->id, $authed);
}
}
return $res;
})->toArray();
if(count($stories) == 0) {
return [];
}
$cursor = count($stories) - 1;
$stories = [[
'id' => (string) $stories[$cursor]['id'],
'nodes' => $stories,
'account' => AccountService::get($profile->id),
'pid' => (string) $profile->id
]];
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
return $res;
})->toArray();
if(count($stories) == 0) {
return [];
}
$cursor = count($stories) - 1;
$stories = [[
'id' => (string) $stories[$cursor]['id'],
'nodes' => $stories,
'account' => AccountService::get($profile->id),
'pid' => (string) $profile->id
]];
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function viewed(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
public function viewed(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'id' => 'required|min:1',
]);
$id = $request->input('id');
$this->validate($request, [
'id' => 'required|min:1',
]);
$id = $request->input('id');
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return [];
}
$authed = $user->profile;
$authed = $request->user()->profile;
$story = Story::with('profile')
->findOrFail($id);
$exp = $story->expires_at;
$story = Story::with('profile')
->findOrFail($id);
$exp = $story->expires_at;
$profile = $story->profile;
$profile = $story->profile;
if($story->profile_id == $authed->id) {
return [];
}
if($story->profile_id == $authed->id) {
return [];
}
$publicOnly = (bool) $profile->followedBy($authed);
abort_if(!$publicOnly, 403);
$publicOnly = (bool) $profile->followedBy($authed);
abort_if(!$publicOnly, 403);
$v = StoryView::firstOrCreate([
'story_id' => $id,
'profile_id' => $authed->id
]);
$v = StoryView::firstOrCreate([
'story_id' => $id,
'profile_id' => $authed->id
]);
if($v->wasRecentlyCreated) {
Story::findOrFail($story->id)->increment('view_count');
if($v->wasRecentlyCreated) {
Story::findOrFail($story->id)->increment('view_count');
if($story->local == false) {
StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
}
}
if($story->local == false) {
StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
}
}
Cache::forget('stories:recent:by_id:' . $authed->id);
StoryService::addSeen($authed->id, $story->id);
return ['code' => 200];
}
Cache::forget('stories:recent:by_id:' . $authed->id);
StoryService::addSeen($authed->id, $story->id);
return ['code' => 200];
}
public function exists(Request $request, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return response()->json(false);
}
return response()->json(Story::whereProfileId($id)
->whereActive(true)
->exists());
}
public function exists(Request $request, $id)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
public function iRedirect(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
return response()->json(Story::whereProfileId($id)
->whereActive(true)
->exists());
}
$user = $request->user();
abort_if(!$user, 404);
$username = $user->username;
return redirect("/stories/{$username}");
}
public function iRedirect(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
public function viewers(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
abort_if(!$user, 404);
$username = $user->username;
return redirect("/stories/{$username}");
}
$this->validate($request, [
'sid' => 'required|string'
]);
public function viewers(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return response()->json([]);
}
$this->validate($request, [
'sid' => 'required|string'
]);
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
$viewers = StoryView::whereStoryId($story->id)
->latest()
->simplePaginate(10)
->map(function($view) {
return AccountService::get($view->profile_id);
})
->values();
$viewers = StoryView::whereStoryId($story->id)
->latest()
->simplePaginate(10)
->map(function($view) {
return AccountService::get($view->profile_id);
})
->values();
return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function remoteStory(Request $request, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
public function remoteStory(Request $request, $id)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::findOrFail($id);
if($profile->user_id != null || $profile->domain == null) {
return redirect('/stories/' . $profile->username);
}
$pid = $profile->id;
return view('stories.show_remote', compact('pid'));
}
$profile = Profile::findOrFail($id);
if($profile->user_id != null || $profile->domain == null) {
return redirect('/stories/' . $profile->username);
}
$pid = $profile->id;
return view('stories.show_remote', compact('pid'));
}
public function pollResults(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
public function pollResults(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required|string'
]);
$this->validate($request, [
'sid' => 'required|string'
]);
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
return PollService::storyResults($sid);
}
return PollService::storyResults($sid);
}
public function getActivityObject(Request $request, $username, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled'), 404);
public function getActivityObject(Request $request, $username, $id)
{
abort_if(!config_cache('instance.stories.enabled'), 404);
if(!$request->wantsJson()) {
return redirect('/stories/' . $username);
}
if(!$request->wantsJson()) {
return redirect('/stories/' . $username);
}
abort_if(!$request->hasHeader('Authorization'), 404);
abort_if(!$request->hasHeader('Authorization'), 404);
$profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail();
$story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id);
$profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail();
$story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id);
abort_if($story->bearcap_token == null, 404);
abort_if(now()->gt($story->expires_at), 404);
$token = substr($request->header('Authorization'), 7);
abort_if(hash_equals($story->bearcap_token, $token) === false, 404);
abort_if($story->created_at->lt(now()->subMinutes(20)), 404);
abort_if($story->bearcap_token == null, 404);
abort_if(now()->gt($story->expires_at), 404);
$token = substr($request->header('Authorization'), 7);
abort_if(hash_equals($story->bearcap_token, $token) === false, 404);
abort_if($story->created_at->lt(now()->subMinutes(20)), 404);
$fractal = new Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Item($story, new StoryVerb());
$res = $fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
$fractal = new Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Item($story, new StoryVerb());
$res = $fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function showSystemStory()
{
// return view('stories.system');
}
public function showSystemStory()
{
// return view('stories.system');
}
}

Wyświetl plik

@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use App\Models\UserEmailForgot;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use App\Mail\UserEmailForgotReminder;
use Illuminate\Support\Facades\RateLimiter;
class UserEmailForgotController extends Controller
{
public function __construct()
{
$this->middleware('guest');
abort_unless(config('security.forgot-email.enabled'), 404);
}
public function index(Request $request)
{
abort_if($request->user(), 404);
return view('auth.email.forgot');
}
public function store(Request $request)
{
$rules = [
'username' => 'required|min:2|max:15|exists:users'
];
$messages = [
'username.exists' => 'This username is no longer active or does not exist!'
];
if((bool) config_cache('captcha.enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'You need to complete the captcha!';
}
$randomDelay = random_int(500000, 2000000);
usleep($randomDelay);
$this->validate($request, $rules, $messages);
$check = self::checkLimits();
if(!$check) {
return redirect()->back()->withErrors([
'username' => 'Please try again later, we\'ve reached our quota and cannot process any more requests at this time.'
]);
}
$user = User::whereUsername($request->input('username'))
->whereNotNull('email_verified_at')
->whereNull('status')
->whereIsAdmin(false)
->first();
if(!$user) {
return redirect()->back()->withErrors([
'username' => 'Invalid username or account. It may not exist, or does not have a verified email, is an admin account or is disabled.'
]);
}
$exists = UserEmailForgot::whereUserId($user->id)
->where('email_sent_at', '>', now()->subHours(24))
->count();
if($exists) {
return redirect()->back()->withErrors([
'username' => 'An email reminder was recently sent to this account, please try again after 24 hours!'
]);
}
return $this->storeHandle($request, $user);
}
protected function storeHandle($request, $user)
{
UserEmailForgot::create([
'user_id' => $user->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'email_sent_at' => now()
]);
Mail::to($user->email)->send(new UserEmailForgotReminder($user));
self::getLimits(true);
return redirect()->back()->with(['status' => 'Successfully sent an email reminder!']);
}
public static function checkLimits()
{
$limits = self::getLimits();
if(
$limits['current']['hourly'] >= $limits['max']['hourly'] ||
$limits['current']['daily'] >= $limits['max']['daily'] ||
$limits['current']['weekly'] >= $limits['max']['weekly'] ||
$limits['current']['monthly'] >= $limits['max']['monthly']
) {
return false;
}
return true;
}
public static function getLimits($forget = false)
{
return [
'max' => config('security.forgot-email.limits.max'),
'current' => [
'hourly' => self::activeCount(60, $forget),
'daily' => self::activeCount(1440, $forget),
'weekly' => self::activeCount(10080, $forget),
'monthly' => self::activeCount(43800, $forget)
]
];
}
public static function activeCount($mins, $forget = false)
{
if($forget) {
Cache::forget('pf:auth:forgot-email:active-count:dur-' . $mins);
}
return Cache::remember('pf:auth:forgot-email:active-count:dur-' . $mins, 14200, function() use($mins) {
return UserEmailForgot::where('email_sent_at', '>', now()->subMinutes($mins))->count();
});
}
}

Wyświetl plik

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\UserRoleService;
class UserRolesController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function getRoles(Request $request)
{
$this->validate($request, [
'id' => 'required'
]);
return UserRoleService::getRoles($request->user()->id);
}
}

Wyświetl plik

@ -14,12 +14,12 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
];
/**

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