Compare commits

...
Sign in to create a new pull request.

1681 commits
fix-tos ... dev

Author SHA1 Message Date
daniel
c909e9757a
Merge pull request #3195 from pixelfed/staging
Staging
2022-01-28 23:23:43 -07:00
Daniel Supernault
5caf160633
Update compiled assets 2022-01-28 23:22:52 -07:00
Daniel Supernault
f8bc0d9c03
Update InternalApiController 2022-01-28 22:56:52 -07:00
daniel
a2c56c4e71
Merge pull request #3194 from pixelfed/staging
Staging
2022-01-28 22:20:38 -07:00
Daniel Supernault
d30a5eb311
Update compiled assets 2022-01-28 22:19:32 -07:00
Daniel Supernault
86b93efdfd
Update changelog 2022-01-28 22:18:39 -07:00
Daniel Supernault
6cfd6be523
Add ReblogService, improve reblogged state for api entities 2022-01-28 22:06:43 -07:00
Daniel Supernault
a7d715517d
Update StatusService, use BookmarkService for bookmarked state 2022-01-28 20:57:25 -07:00
Daniel Supernault
ff4519bfaf
Update i18n strings 2022-01-28 20:33:24 -07:00
Daniel Supernault
0db82ff2dd
Update PublicApiController 2022-01-28 20:29:46 -07:00
Daniel Supernault
73226360fc
Update ApiV1Controller, fix private status replies returning 404 2022-01-28 20:04:02 -07:00
Daniel Supernault
a11772bcfe
Update BookmarkService, use sorted set 2022-01-28 18:27:53 -07:00
Daniel Supernault
0157566c25
Add BookmarkService 2022-01-27 05:17:55 -07:00
Daniel Supernault
c0b1e0427e
Update PublicApiController, add bookmark state to timeline endpoints 2022-01-27 05:11:34 -07:00
Daniel Supernault
34a3d65f30
Update InternalApiController 2022-01-27 05:11:06 -07:00
Daniel Supernault
19867ce97a
Add fallback custom emoji image 2022-01-27 02:59:16 -07:00
Daniel Supernault
3c261bbfec
Update ApiV1Controller, invalidate status reply cache on new reply 2022-01-27 00:39:44 -07:00
Daniel Supernault
3d86d21be6
Update BaseApiController, improve favourites endpoint 2022-01-26 23:48:32 -07:00
Daniel Supernault
f063cb0185
Update BaseApiController, improve favourites endpoint 2022-01-26 23:32:31 -07:00
Daniel Supernault
3c35158ebc
Update InternalApiController, redirect remote post and profiles to Metro 2.0 2022-01-25 02:18:50 -07:00
Daniel Supernault
16e725183e
Update ApiV1Controller, add custom_emoji endpoint 2022-01-25 02:08:50 -07:00
Daniel Supernault
d1ff86ee34
Update nav 2022-01-25 01:58:20 -07:00
daniel
80ac325fb6
Merge pull request #3186 from pixelfed/staging
Update atom feeds, include media alt text. Fixes #3184
2022-01-25 00:50:39 -07:00
Daniel Supernault
fea2faaba8
Update compiled assets 2022-01-25 00:50:10 -07:00
Daniel Supernault
c6d2db68a0
Update changelog 2022-01-25 00:40:59 -07:00
Daniel Supernault
5d9b6863b6
Update atom feeds, include media alt text. Fixes #3184 2022-01-25 00:39:46 -07:00
Daniel Supernault
8c54ab57ac
Update web routes, fix atom feeds for account usernames containing a dot 2022-01-25 00:05:00 -07:00
daniel
daeccef756
Merge pull request #3185 from pixelfed/staging
Staging
2022-01-24 23:27:28 -07:00
Daniel Supernault
80c4126d48
Update changelog 2022-01-24 23:26:48 -07:00
Daniel Supernault
042c5b6c33
Improve onboarding 2022-01-24 23:26:38 -07:00
Daniel Supernault
b3d9b9e5cd
Update i18n 2022-01-24 23:14:22 -07:00
Daniel Supernault
4b6effb9c8
Update ApiV1Controller, improve follow count cache invalidation 2022-01-24 22:46:31 -07:00
daniel
86b4fd77eb
Merge pull request #3182 from pixelfed/staging
Update compiled assets
2022-01-23 03:34:53 -07:00
Daniel Supernault
0ea26415a9
Update compiled assets 2022-01-23 03:33:39 -07:00
daniel
ce29d9208b
Merge pull request #3181 from pixelfed/staging
Update ProfileController
2022-01-23 03:13:33 -07:00
Daniel Supernault
940209018a
Update ProfileController 2022-01-23 02:44:34 -07:00
daniel
ff6e2f2c12
Merge pull request #3180 from pixelfed/staging
Staging
2022-01-23 01:48:38 -07:00
Daniel Supernault
97a75e0fde
Update changelog 2022-01-23 01:48:03 -07:00
Daniel Supernault
3bd211d7f5
Update profile embeds, fix NaN bug and improve performance 2022-01-23 01:47:31 -07:00
daniel
ddd9b7ef4e
Merge pull request #3179 from pixelfed/staging
Update compiled assets
2022-01-23 00:06:18 -07:00
Daniel Supernault
803bbac0ea
Update compiled assets 2022-01-23 00:04:34 -07:00
daniel
d61c91d1a5
Merge pull request #3178 from pixelfed/staging
Staging
2022-01-22 21:19:17 -07:00
Daniel Supernault
f582041a20
Update changelog 2022-01-22 21:18:01 -07:00
Daniel Supernault
792bfaa599
Update compiled assets 2022-01-22 21:17:40 -07:00
Daniel Supernault
879281cccb
Update ComposeModal, add max file size and allowed mime types. Fixes #3162 2022-01-22 21:11:34 -07:00
Daniel Supernault
33f863e8d3
Update compiled assets 2022-01-22 20:38:16 -07:00
daniel
9f9a52b683
Merge pull request #3177 from pixelfed/staging
Staging
2022-01-22 19:44:27 -07:00
Daniel Supernault
792212b655
Cleanup 2022-01-22 19:42:37 -07:00
Daniel Supernault
127777261a
Update changelog 2022-01-22 19:38:25 -07:00
Daniel Supernault
aff7456639
Update WebfingerService. Fixes #3167 2022-01-22 19:37:50 -07:00
daniel
c7dec86ccd
Merge pull request #3176 from pixelfed/staging
Staging
2022-01-22 19:13:32 -07:00
Daniel Supernault
58f90e577b
Update compiled assets 2022-01-22 19:12:43 -07:00
Daniel Supernault
0a4505ed3a
Update PostComponent, add custom emoji support 2022-01-22 19:05:53 -07:00
Daniel Supernault
42602351c7
Update StatusTransformer, add emoji entities 2022-01-22 18:54:12 -07:00
daniel
18c49dd50c
Merge pull request #3173 from pixelfed/staging
Fix custom emoji admin dashboard bug
2022-01-21 03:20:00 -07:00
Daniel Supernault
1e00c431a8
Fix custom emoji admin dashboard bug 2022-01-21 03:19:09 -07:00
daniel
e9f3bbe8ad
Merge pull request #3172 from pixelfed/staging
Update custom emoji json-ld
2022-01-21 01:56:54 -07:00
Daniel Supernault
46d5f12590
Update custom emoji json-ld 2022-01-21 01:55:58 -07:00
daniel
b0006478ee
Merge pull request #3171 from pixelfed/staging
Update compiled assets
2022-01-21 01:50:01 -07:00
Daniel Supernault
6ec1c8468a
Update compiled assets 2022-01-21 01:32:26 -07:00
daniel
a9da420b43
Merge pull request #3166 from pixelfed/staging
Add custom emoji
2022-01-21 01:31:43 -07:00
Daniel Supernault
c4df9c4547
Update changelog 2022-01-21 01:30:09 -07:00
Daniel Supernault
dbb1638fd6
Invalidate status cache after importing custom emoji 2022-01-21 01:28:36 -07:00
Daniel Supernault
dc7962d898
Add shortcode & domain search 2022-01-21 01:12:56 -07:00
Daniel Supernault
efeaf427e1
Add CustomEmoji admin dashboard 2022-01-21 00:27:30 -07:00
Daniel Supernault
b2016e6c21
Update note transformers, add custom emoji support 2022-01-19 02:21:26 -07:00
Daniel Supernault
0d78a9aaf8
Update compiled assets 2022-01-19 01:04:25 -07:00
Daniel Supernault
489fdbb248
Update StatusTagPipeline 2022-01-19 00:58:13 -07:00
Daniel Supernault
bf2828c2ee
Add emoji directory and update .gitignore 2022-01-19 00:53:06 -07:00
Daniel Supernault
dc17c9fc27
Improve emoji import 2022-01-19 00:46:30 -07:00
Daniel Supernault
01798daf56
Update AP helpers, import Emoji tags 2022-01-18 23:26:44 -07:00
Daniel Supernault
ca79e26d3a
Add custom emoji 2022-01-18 23:03:21 -07:00
daniel
d659dfdc9a
Merge pull request #3165 from pixelfed/staging
Update compiled assets
2022-01-18 19:00:54 -07:00
Daniel Supernault
16ced7b463
Update compiled assets 2022-01-18 18:58:36 -07:00
daniel
b95e416c1e
Merge pull request #3164 from pixelfed/staging
Staging
2022-01-17 17:13:45 -07:00
Daniel Supernault
207742479c
Update changelog 2022-01-17 17:12:15 -07:00
Daniel Supernault
1c20d6960a
Update MediaStorageService, fix reremote avatar bug 2022-01-17 17:11:16 -07:00
daniel
ea17d763e8
Merge pull request #3161 from pixelfed/staging
Fix typo in ApiV1Controller
2022-01-14 17:27:48 -07:00
Daniel Supernault
350585d316
Fix typo in ApiV1Controller 2022-01-14 17:27:21 -07:00
daniel
c1ed560394
Merge pull request #3147 from pixelfed/staging
Staging
2022-01-09 23:54:53 -07:00
Daniel Supernault
1be5925263
Update changelog 2022-01-09 23:54:22 -07:00
Daniel Supernault
8077ded2dd
Bump version to 0.11.2 2022-01-09 23:54:05 -07:00
daniel
89e39ff699
Merge pull request #3146 from pixelfed/staging
Staging
2022-01-09 23:30:05 -07:00
Daniel Supernault
c68e18aecf
Update changelog 2022-01-09 23:27:57 -07:00
Daniel Supernault
e61dc66a36
Update compiled assets 2022-01-09 23:27:13 -07:00
Daniel Supernault
c8a667f20e
Update SearchApiV2Service, resolve remote queries 2022-01-09 23:15:45 -07:00
daniel
a62d1a1e23
Merge pull request #3145 from pixelfed/staging
Staging
2022-01-09 20:50:51 -07:00
Daniel Supernault
9030c77800
Update changelog 2022-01-09 20:50:21 -07:00
Daniel Supernault
de42d84c11
Update ApiV1Controller, fix reblogs endpoints 2022-01-09 20:49:34 -07:00
Daniel Supernault
d6d99385db
Update ApiV1Controller, fix favourites endpoints 2022-01-09 20:33:43 -07:00
daniel
f543073778
Merge pull request #3144 from pixelfed/staging
Staging
2022-01-09 20:21:46 -07:00
Daniel Supernault
5de6d8accb
Update changelog 2022-01-09 20:20:38 -07:00
Daniel Supernault
6fc0dcb34d
Update ApiV1Controller, fix home timeline entities 2022-01-09 20:18:51 -07:00
daniel
535a0e63ab
Merge pull request #3143 from pixelfed/staging
Staging
2022-01-09 19:33:41 -07:00
Daniel Supernault
80e389b6e6
Update changelog 2022-01-09 19:33:12 -07:00
Daniel Supernault
dcb7ba9c7a
Update PublicApiController, fix public timeline endpoint 2022-01-09 19:32:41 -07:00
Daniel Supernault
80c7def3df
Update ApiV1Controller, fix public timeline endpoint 2022-01-09 19:30:53 -07:00
daniel
b11c77ccb5
Merge pull request #3142 from pixelfed/staging
Staging
2022-01-09 18:20:31 -07:00
Daniel Supernault
7aea7e80e6
Update changelog 2022-01-09 18:19:53 -07:00
Daniel Supernault
dc743aebd0
Update compiled assets 2022-01-09 18:19:44 -07:00
Daniel Supernault
e724633e0f
Update components, fix api endpoints. Fixes #3138 2022-01-09 18:18:25 -07:00
daniel
aa4daa2cf5
Merge pull request #3141 from pixelfed/staging
Staging
2022-01-09 15:42:26 -07:00
Daniel Supernault
f4150df593
Update changelog 2022-01-09 15:41:49 -07:00
Daniel Supernault
a6261221ad
Update ApiV1Controller, fix version on instance endpoint 2022-01-09 15:40:28 -07:00
Daniel Supernault
4ab99d66d1
Update FederationController 2022-01-09 15:09:37 -07:00
daniel
40a96a33f0
Merge pull request #3137 from pixelfed/staging
Update compiled assets
2022-01-08 05:47:04 -07:00
Daniel Supernault
93196e57a0
Update compiled assets 2022-01-08 05:46:36 -07:00
daniel
94df59ed1f
Merge pull request #3136 from pixelfed/staging
Update CommentPipeline
2022-01-08 05:02:26 -07:00
Daniel Supernault
4fe421a166
Update ApiV1Controller 2022-01-08 05:01:26 -07:00
Daniel Supernault
1515a9f111
Update CommentPipeline 2022-01-08 04:59:28 -07:00
daniel
a061e04bbb
Merge pull request #3135 from pixelfed/staging
Update ApiV1Controller, fix account settings bug
2022-01-08 04:06:57 -07:00
Daniel Supernault
a578035bbc
Update AccountService 2022-01-08 04:03:45 -07:00
Daniel Supernault
08246f2482
Update ApiV1Controller, fix account settings bug 2022-01-08 04:02:14 -07:00
daniel
f772925d70
Merge pull request #3134 from pixelfed/staging
Staging
2022-01-08 01:59:01 -07:00
Daniel Supernault
75a44fdc2f
Update compiled assets 2022-01-08 01:57:51 -07:00
Daniel Supernault
f5402de74d
Update i18n strings 2022-01-08 01:52:56 -07:00
Daniel Supernault
699cc3faad
Add i18n strings 2022-01-08 01:52:28 -07:00
Daniel Supernault
da6312c2b0
Update ApiV1Controller 2022-01-08 01:51:24 -07:00
daniel
4efd0b3d91
Merge pull request #3133 from pixelfed/staging
Staging
2022-01-07 22:10:29 -07:00
Daniel Supernault
32bbcc12c3
Update web routes 2022-01-07 22:09:15 -07:00
Daniel Supernault
eeaf0f9311
Update StatusService 2022-01-07 19:59:29 -07:00
Daniel Supernault
0c1a0fa55b
Update PublicApiController 2022-01-07 19:47:21 -07:00
daniel
639c7117f4
Merge pull request #3129 from pixelfed/staging
Staging
2022-01-06 01:17:09 -07:00
Daniel Supernault
24fbdf0afb
Update compiled assets 2022-01-06 01:16:06 -07:00
Daniel Supernault
6d01466239
Update changelog 2022-01-06 01:13:43 -07:00
Daniel Supernault
6f1b02456f
Update AccountController, refresh RelationshipService on mute/blocks 2022-01-06 01:05:11 -07:00
Daniel Supernault
cdf78e23a4
Update ApiV1Controller, improve instance endpoint 2022-01-05 23:37:27 -07:00
Daniel Supernault
ff211dbec5
Update RestrictedNames 2022-01-05 22:28:39 -07:00
Daniel Supernault
e825b07d88
Update ApiV1Controller 2022-01-05 21:40:07 -07:00
Daniel Supernault
31d9e5fb9e
Update changelog 2022-01-05 21:36:26 -07:00
Daniel Supernault
46485426ea
Update ApiV1Controller, add mastoapi strict mode 2022-01-05 21:36:02 -07:00
daniel
2f83d953c2
Merge pull request #3128 from pixelfed/staging
Update compiled assets
2022-01-05 21:09:46 -07:00
Daniel Supernault
1f70e248ff
Update compiled assets 2022-01-05 21:08:37 -07:00
daniel
b34d97d6b7
Merge pull request #3127 from pixelfed/staging
Staging
2022-01-05 19:56:35 -07:00
Daniel Supernault
04c5b79029
Update changelog 2022-01-05 19:47:00 -07:00
Daniel Supernault
861a2d36df
Update PublicApiController, enforce only_media on accountStatuses method. Fixes #3105 2022-01-05 19:31:49 -07:00
daniel
e23d24d3cf
Merge pull request #3126 from pixelfed/staging
Staging
2022-01-05 19:18:05 -07:00
Daniel Supernault
9169d40ce0
Update compiled assets 2022-01-05 19:07:20 -07:00
Daniel Supernault
5bb6ebaec0
Update i18n strings 2022-01-05 19:02:31 -07:00
daniel
21dabfb85b
Merge pull request #3114 from pixelfed/l10n_staging
New Crowdin updates
2022-01-05 18:51:55 -07:00
daniel
3ee699ba3f
Merge pull request #3122 from idanoo/hashtag_unique_constraint_fix
Fix for firstOrCreate failing hashtags with case differences on name column
2022-01-05 18:44:32 -07:00
daniel
854f858543
Merge pull request #3121 from inthreedee/patch-1
Avoid upscaling small images
2022-01-05 18:37:13 -07:00
daniel
c43f9ab7a4
Update app/Util/Media/Image.php
Co-authored-by: Daniel Mason <daniel@m2.nz>
2022-01-05 18:36:02 -07:00
daniel
e2e1a1b2c3
Merge pull request #3124 from pixelfed/staging
Staging
2022-01-05 18:34:50 -07:00
Daniel Supernault
fac8f2c660
Update changelog 2022-01-05 18:32:57 -07:00
Daniel Supernault
fcabc9be02
Fix Direct Message conversations endpoint on postgres instances 2022-01-05 18:30:58 -07:00
Daniel Supernault
ddf41dc347
Update StoryController, add postgres bug fix 2022-01-05 18:17:05 -07:00
Daniel Mason
9cc18eb82a Fix for firstOrCreate failing hashtags with case differences on name 2022-01-05 19:45:19 +13:00
daniel
db058390c3 New translations web.php (Montenegrin (Latin))
[ci skip]
2022-01-04 19:50:03 -07:00
daniel
63e7248ccc New translations web.php (Bosnian)
[ci skip]
2022-01-04 19:50:02 -07:00
daniel
9943e4f1cf New translations web.php (Hindi)
[ci skip]
2022-01-04 19:50:01 -07:00
daniel
5184aca09a New translations web.php (Croatian)
[ci skip]
2022-01-04 19:49:59 -07:00
daniel
d064a5de0d New translations web.php (Macedonian)
[ci skip]
2022-01-04 19:49:58 -07:00
daniel
b1d2e9cf59 New translations web.php (Basque)
[ci skip]
2022-01-04 19:49:57 -07:00
daniel
461956014d New translations web.php (Basque)
[ci skip]
2022-01-03 19:37:53 -07:00
Jonathan
27b715cb35
Avoid upscaling small images 2022-01-03 17:46:10 -05:00
Daniel Supernault
454b4e21dd
Update Services, fix mastoapi compat 2022-01-03 02:31:03 -07:00
Daniel Supernault
85fc9dd0a8
Update PublicApiController, fix accountStatuses pagination operator 2022-01-03 00:59:39 -07:00
Daniel Supernault
c32fde22e7
Add default header 2022-01-03 00:55:13 -07:00
Daniel Supernault
85e4be8172
Update AccountService, add getMastodon method for mastoapi compatibility 2022-01-03 00:53:15 -07:00
daniel
5fc83beb2c
Merge pull request #3117 from pixelfed/staging
Staging
2022-01-03 00:00:14 -07:00
Daniel Supernault
6667f4f1d7
Update changelog 2022-01-02 23:59:20 -07:00
Daniel Supernault
846ce395d5
Update compiled assets 2022-01-02 23:59:08 -07:00
Daniel Supernault
36a129fe89
Update StatusService, add getMastodon method for mastoapi compatibility 2022-01-02 23:47:08 -07:00
Daniel Supernault
772352903b
Update web routes 2022-01-02 23:46:17 -07:00
Daniel Supernault
a64aef6726
Update Profile model, cast last_status_at as timestamp 2022-01-02 23:27:47 -07:00
Daniel Supernault
bdc3ab17dc
Add migration 2022-01-02 23:27:05 -07:00
Daniel Supernault
89206d6e1f
Update mastoapi transformers 2022-01-02 23:26:18 -07:00
daniel
a3c4d29b44
Merge pull request #3116 from pixelfed/staging
Store remote avatars locally if S3 not enabled
2022-01-02 21:39:46 -07:00
Daniel Supernault
41cc741be6
Add avatar:sync command 2022-01-02 21:34:15 -07:00
Daniel Supernault
63e44d7c50
Add changelog 2022-01-02 21:32:24 -07:00
Daniel Supernault
b4bd0400c2
Store remote avatars locally if S3 not enabled 2022-01-02 21:30:02 -07:00
daniel
026ab3f29a New translations web.php (Persian)
[ci skip]
2022-01-02 19:42:07 -07:00
daniel
27599e0da0 New translations web.php (Slovak)
[ci skip]
2022-01-02 19:42:06 -07:00
daniel
028b67e0fd New translations web.php (Italian)
[ci skip]
2022-01-02 19:42:05 -07:00
daniel
8ab69dbe4c New translations web.php (Vietnamese)
[ci skip]
2022-01-01 19:29:22 -07:00
daniel
4d04f03d34 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-31 19:32:06 -07:00
daniel
a411ec813f New translations web.php (Slovak)
[ci skip]
2021-12-31 19:32:06 -07:00
daniel
e1d2d91b76 New translations web.php (German)
[ci skip]
2021-12-31 19:32:05 -07:00
daniel
91b2d3c2db New translations web.php (Czech)
[ci skip]
2021-12-31 19:32:04 -07:00
daniel
2072c44d8b New translations web.php (Arabic)
[ci skip]
2021-12-31 19:32:03 -07:00
daniel
039328a32e
Merge pull request #3113 from pixelfed/staging
Update exception handler
2021-12-31 01:15:44 -07:00
Daniel Supernault
1a979bee5c
Update exception handler 2021-12-31 01:14:50 -07:00
Daniel Supernault
f4e12c42b3
Update exception handler 2021-12-31 01:12:33 -07:00
daniel
4e27fee344
Merge pull request #3112 from pixelfed/staging
Staging
2021-12-31 00:58:44 -07:00
Daniel Supernault
c481e473d8
Update Exception handler 2021-12-31 00:58:16 -07:00
Daniel Supernault
d820669bd1
Update StatusController, change param signature 2021-12-31 00:58:00 -07:00
daniel
9d8168cf20
Merge pull request #3110 from pixelfed/staging
Staging
2021-12-30 23:20:40 -07:00
Daniel Supernault
0acd79742c
Update changelog 2021-12-30 23:20:12 -07:00
Daniel Supernault
415826f253
Update ApiV1Controller, fix illegal operator bug by setting default min_id 2021-12-30 23:19:38 -07:00
daniel
93c32b0771
Merge pull request #3109 from pixelfed/staging
Staging
2021-12-30 23:13:04 -07:00
Daniel Supernault
35e2cf1f6b
Update changelog 2021-12-30 23:12:16 -07:00
Daniel Supernault
e5f8f3441c
Update AccountService, fix json casting 2021-12-30 23:11:46 -07:00
daniel
8957e6e3c0
Merge pull request #3108 from pixelfed/staging
Update RemoteAvatarFetch job
2021-12-30 21:06:24 -07:00
Daniel Supernault
92bc28456d
Update RemoteAvatarFetch job 2021-12-30 21:05:50 -07:00
daniel
dc603062fa
Merge pull request #3106 from pixelfed/staging
Staging
2021-12-30 20:31:24 -07:00
Daniel Supernault
59ffa22e08
Update compiled assets 2021-12-30 20:31:12 -07:00
daniel
20e11e175a
Merge pull request #3107 from pixelfed/l10n_staging
New Crowdin updates
2021-12-30 20:24:58 -07:00
daniel
cd3f8f1cce New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-30 19:33:25 -07:00
daniel
faabe3e708 New translations web.php (Portuguese)
[ci skip]
2021-12-30 19:33:23 -07:00
daniel
3baa275292 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-30 19:33:22 -07:00
daniel
86cacf7b52 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-30 19:33:18 -07:00
Daniel Supernault
bc91909ca6
Update i18n strings 2021-12-30 18:45:30 -07:00
daniel
fab76db993
Merge pull request #3098 from pixelfed/l10n_staging
New Crowdin updates
2021-12-30 18:43:08 -07:00
daniel
03b4b8a81e New translations web.php (Italian)
[ci skip]
2021-12-30 17:31:39 -07:00
daniel
318062cc34 New translations web.php (Hungarian)
[ci skip]
2021-12-30 17:31:38 -07:00
daniel
11ffe5973b New translations web.php (Finnish)
[ci skip]
2021-12-30 17:31:37 -07:00
daniel
ac9e7eac6d New translations web.php (Basque)
[ci skip]
2021-12-30 17:31:35 -07:00
daniel
1a33a91eff New translations web.php (German)
[ci skip]
2021-12-30 17:31:35 -07:00
daniel
d157a7608f New translations web.php (Czech)
[ci skip]
2021-12-30 17:31:33 -07:00
daniel
efb60698c9 New translations web.php (Spanish)
[ci skip]
2021-12-30 17:31:30 -07:00
daniel
1efd420f33 New translations web.php (French)
[ci skip]
2021-12-30 17:31:29 -07:00
daniel
e1fa9d9a87 New translations web.php (Polish)
[ci skip]
2021-12-30 17:31:28 -07:00
daniel
73c7e0d446 New translations web.php (Russian)
[ci skip]
2021-12-30 17:31:27 -07:00
daniel
1df0669c09 New translations web.php (Welsh)
[ci skip]
2021-12-30 17:31:25 -07:00
daniel
38784a344f New translations web.php (Indonesian)
[ci skip]
2021-12-30 17:31:23 -07:00
daniel
6b76e85ff5 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-30 17:31:22 -07:00
daniel
9ab11f49b6 New translations web.php (Galician)
[ci skip]
2021-12-30 17:31:21 -07:00
daniel
9eca5de3c7 New translations web.php (Portuguese)
[ci skip]
2021-12-30 17:31:20 -07:00
daniel
37555b4da5 New translations web.php (Vietnamese)
[ci skip]
2021-12-30 17:31:19 -07:00
daniel
c2eddbd4a1 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-30 17:31:18 -07:00
daniel
fb02f1e8c4 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-30 17:31:14 -07:00
daniel
c70a4c786e New translations web.php (Romanian)
[ci skip]
2021-12-30 17:31:13 -07:00
daniel
9b55e48dca New translations web.php (Romanian)
[ci skip]
2021-12-30 12:48:14 -07:00
daniel
ff7c7915f9 New translations web.php (Romanian)
[ci skip]
2021-12-30 11:40:36 -07:00
daniel
37a57e5a46 New translations web.php (Basque)
[ci skip]
2021-12-30 10:39:06 -07:00
daniel
4b982a1423 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-30 02:07:25 -07:00
daniel
6657fe6513
Merge pull request #3104 from pixelfed/staging
Update compiled assets
2021-12-29 20:54:11 -07:00
Daniel Supernault
ed190bc7ab
Update changelog 2021-12-29 20:53:53 -07:00
daniel
69bb55e9d7 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-29 19:32:31 -07:00
Daniel Supernault
4dd6caf02d
Update compiled assets 2021-12-29 18:29:28 -07:00
daniel
fa1cf25c70 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-29 18:27:09 -07:00
daniel
4c0626f428 New translations web.php (Polish)
[ci skip]
2021-12-29 13:24:57 -07:00
daniel
68e82c0aea New translations web.php (Welsh)
[ci skip]
2021-12-29 12:15:50 -07:00
daniel
c0f04a0dfb New translations web.php (Indonesian)
[ci skip]
2021-12-29 12:15:49 -07:00
daniel
ed183ade4b New translations web.php (Galician)
[ci skip]
2021-12-29 12:15:48 -07:00
daniel
4a34671261 New translations web.php (Finnish)
[ci skip]
2021-12-29 10:58:40 -07:00
daniel
88e48df000 New translations web.php (Finnish)
[ci skip]
2021-12-29 10:00:38 -07:00
daniel
853d64e91c New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-29 05:54:45 -07:00
daniel
970fa7db62 New translations web.php (Spanish)
[ci skip]
2021-12-29 04:52:58 -07:00
daniel
18b5070206 New translations web.php (Italian)
[ci skip]
2021-12-29 03:31:45 -07:00
daniel
fd8a0a603c New translations web.php (Hungarian)
[ci skip]
2021-12-29 03:31:44 -07:00
daniel
012d81b67a New translations web.php (Czech)
[ci skip]
2021-12-29 03:31:43 -07:00
daniel
50f59c82ee New translations web.php (French)
[ci skip]
2021-12-29 03:31:42 -07:00
daniel
c047237f33 New translations web.php (German)
[ci skip]
2021-12-29 02:17:05 -07:00
daniel
5646fbf878 New translations web.php (Czech)
[ci skip]
2021-12-29 02:17:04 -07:00
daniel
8c917e8996
Merge pull request #3101 from pixelfed/staging
Staging
2021-12-29 02:10:13 -07:00
Daniel Supernault
cc522ccf3f
Update changelog 2021-12-29 02:09:30 -07:00
Daniel Supernault
8596451066
Update compiled assets 2021-12-29 02:07:47 -07:00
Daniel Supernault
b95aec1208
Enable network timeline by default 2021-12-29 02:04:39 -07:00
daniel
c74b45da7a
Merge pull request #3100 from pixelfed/staging
Staging
2021-12-29 01:48:02 -07:00
Daniel Supernault
6913366063
Update changelog 2021-12-29 01:47:31 -07:00
Daniel Supernault
50e845d606
Update compiled assets 2021-12-29 01:47:15 -07:00
Daniel Supernault
10b6058cc0
Update DiscoverController, add yearly option and increase limit from 15 to 30 posts 2021-12-29 01:46:29 -07:00
daniel
2df960ac7a
Merge pull request #3099 from pixelfed/staging
Update RegisterController
2021-12-29 00:51:40 -07:00
Daniel Supernault
af68872a81
Update compiled assets 2021-12-29 00:51:22 -07:00
Daniel Supernault
e503a8da85
Update RegisterController 2021-12-29 00:38:08 -07:00
daniel
f6426cb9bd
Merge pull request #3097 from pixelfed/staging
Staging
2021-12-29 00:17:20 -07:00
Daniel Supernault
a259c6919f
Update changelog 2021-12-29 00:17:08 -07:00
Daniel Supernault
ae1b50d84b
Update compiled assets 2021-12-29 00:17:03 -07:00
Daniel Supernault
b37c805172
Update AdminReportController 2021-12-29 00:15:01 -07:00
daniel
44d877d670 New translations web.php (Vietnamese)
[ci skip]
2021-12-29 00:07:52 -07:00
daniel
bc69463492 New translations web.php (Russian)
[ci skip]
2021-12-29 00:07:51 -07:00
Daniel Supernault
30c1de9796
Update i18n strings 2021-12-29 00:07:19 -07:00
Daniel Supernault
745c35807c
Update FederationController, increase webfinger cache ttl from 12 hours to 14 days 2021-12-28 23:49:56 -07:00
daniel
8db6a79de9
Merge pull request #3095 from pixelfed/l10n_staging
New Crowdin updates
2021-12-28 23:43:33 -07:00
daniel
41758e37fb
Merge pull request #3096 from pixelfed/staging
Staging
2021-12-28 23:04:17 -07:00
Daniel Supernault
1d74070bf0
Update changelog 2021-12-28 23:04:02 -07:00
daniel
3c0fbcac69 New translations web.php (Hebrew)
[ci skip]
2021-12-28 23:02:38 -07:00
Daniel Supernault
f2467e71e9
Update compiled assets 2021-12-28 23:01:25 -07:00
Daniel Supernault
5b5d1b2bf2
Update timeline copy 2021-12-28 22:56:06 -07:00
daniel
f2a505b71d New translations web.php (Greek)
[ci skip]
2021-12-28 22:04:38 -07:00
daniel
c12d866be6 New translations web.php (Japanese)
[ci skip]
2021-12-28 20:17:17 -07:00
daniel
4f572af75c New translations web.php (Indonesian)
[ci skip]
2021-12-28 20:17:05 -07:00
daniel
037db34e67 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-28 20:17:04 -07:00
daniel
849c0fea2b New translations web.php (Portuguese)
[ci skip]
2021-12-28 20:17:03 -07:00
daniel
bc3864d38d New translations web.php (Chinese Simplified)
[ci skip]
2021-12-28 20:17:02 -07:00
daniel
3bdfc94244 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-28 20:16:58 -07:00
daniel
711c571a3b
Merge pull request #3094 from pixelfed/staging
Update compiled assets
2021-12-28 19:45:36 -07:00
Daniel Supernault
c9d35f1003
Update compiled assets 2021-12-28 19:45:16 -07:00
daniel
d2bc455194
Merge pull request #3093 from pixelfed/staging
Staging
2021-12-28 19:38:51 -07:00
Daniel Supernault
2b11205a2c
Update compiled assets 2021-12-28 19:37:57 -07:00
Daniel Supernault
f3a66fff61
Update experiment config, force enabled single page app 2021-12-28 19:37:30 -07:00
Daniel Supernault
e13959ae45
Update Timeline, add new ui promo in timelines that can be hidden using localstorage 2021-12-28 19:35:37 -07:00
daniel
cc63d3d1f7
Merge pull request #3092 from pixelfed/staging
Staging
2021-12-28 19:12:44 -07:00
Daniel Supernault
07a20e578c
Update compiled assets 2021-12-28 19:12:30 -07:00
Daniel Supernault
1344de4cf2
Update i18n strings 2021-12-28 19:09:56 -07:00
daniel
e567dfd9a0
Merge pull request #3090 from pixelfed/l10n_staging
New Crowdin updates
2021-12-28 19:09:03 -07:00
daniel
a95c5ce8e1 New translations web.php (Occitan)
[ci skip]
2021-12-28 19:08:29 -07:00
daniel
d623059052 New translations web.php (Dutch)
[ci skip]
2021-12-28 19:08:28 -07:00
daniel
8d5b584f10 New translations web.php (Korean)
[ci skip]
2021-12-28 19:08:27 -07:00
daniel
bbb82569f0 New translations web.php (Japanese)
[ci skip]
2021-12-28 19:08:26 -07:00
daniel
8fa6d65102 New translations web.php (Italian)
[ci skip]
2021-12-28 19:08:25 -07:00
daniel
5c84fcdfa4 New translations web.php (Hungarian)
[ci skip]
2021-12-28 19:08:24 -07:00
daniel
9dea452844 New translations web.php (Hebrew)
[ci skip]
2021-12-28 19:08:24 -07:00
daniel
b7fcfe51bd New translations web.php (Finnish)
[ci skip]
2021-12-28 19:08:23 -07:00
daniel
e0a0a5f949 New translations web.php (Norwegian)
[ci skip]
2021-12-28 19:08:21 -07:00
daniel
cdb9e344ba New translations web.php (Basque)
[ci skip]
2021-12-28 19:08:21 -07:00
daniel
f9ea5eb227 New translations web.php (German)
[ci skip]
2021-12-28 19:08:19 -07:00
daniel
a7fd7d2834 New translations web.php (Danish)
[ci skip]
2021-12-28 19:08:19 -07:00
daniel
8ada9f12aa New translations web.php (Czech)
[ci skip]
2021-12-28 19:08:18 -07:00
daniel
5413ad8145 New translations web.php (Catalan)
[ci skip]
2021-12-28 19:08:17 -07:00
daniel
fd015096d8 New translations web.php (Arabic)
[ci skip]
2021-12-28 19:08:16 -07:00
daniel
aeb4555078 New translations web.php (Afrikaans)
[ci skip]
2021-12-28 19:08:15 -07:00
daniel
ecd73b77d7 New translations web.php (Spanish)
[ci skip]
2021-12-28 19:08:14 -07:00
daniel
6f374f80da New translations web.php (Greek)
[ci skip]
2021-12-28 19:08:13 -07:00
daniel
1d21922de3 New translations web.php (French)
[ci skip]
2021-12-28 19:08:12 -07:00
daniel
9b6630c86e New translations web.php (Polish)
[ci skip]
2021-12-28 19:08:11 -07:00
daniel
780d07c32f New translations web.php (Russian)
[ci skip]
2021-12-28 19:08:10 -07:00
daniel
68d76e9ac8 New translations web.php (Scottish Gaelic)
[ci skip]
2021-12-28 19:08:09 -07:00
daniel
f46c5e033a New translations web.php (Welsh)
[ci skip]
2021-12-28 19:08:08 -07:00
daniel
08c2a842cd New translations web.php (Bengali)
[ci skip]
2021-12-28 19:08:07 -07:00
daniel
f79905ca40 New translations web.php (Persian)
[ci skip]
2021-12-28 19:08:06 -07:00
daniel
bb27ae4794 New translations web.php (Indonesian)
[ci skip]
2021-12-28 19:08:05 -07:00
daniel
b43fb88a81 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-28 19:08:04 -07:00
daniel
64aacd5e9d New translations web.php (Galician)
[ci skip]
2021-12-28 19:08:03 -07:00
daniel
8e0f9ddb93 New translations web.php (Portuguese)
[ci skip]
2021-12-28 19:08:02 -07:00
daniel
454fbd8a24 New translations web.php (Vietnamese)
[ci skip]
2021-12-28 19:08:01 -07:00
daniel
d00dc1c0a7 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-28 19:08:00 -07:00
daniel
fdd889dcff New translations web.php (Ukrainian)
[ci skip]
2021-12-28 19:07:59 -07:00
daniel
c660ff9fbe New translations web.php (Turkish)
[ci skip]
2021-12-28 19:07:58 -07:00
daniel
047d4bc07c New translations web.php (Swedish)
[ci skip]
2021-12-28 19:07:57 -07:00
daniel
64fb4d1713 New translations web.php (Serbian (Cyrillic))
[ci skip]
2021-12-28 19:07:56 -07:00
daniel
cea7dd291b New translations web.php (Slovak)
[ci skip]
2021-12-28 19:07:55 -07:00
daniel
23f1f81c21 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-28 19:07:54 -07:00
daniel
3a6c852724 New translations web.php (Romanian)
[ci skip]
2021-12-28 19:07:53 -07:00
Daniel Supernault
802d9026c6
Update changelog 2021-12-28 19:06:22 -07:00
Daniel Supernault
b042661df2
Update compiled assets 2021-12-28 19:06:08 -07:00
Daniel Supernault
e95718fe6f
Update i18n strings 2021-12-28 19:04:52 -07:00
Daniel Supernault
e46b9b2798
Add i18n string 2021-12-28 19:02:29 -07:00
Daniel Supernault
b00a453b99
Update status api, autolink caption before returning response 2021-12-28 18:26:01 -07:00
daniel
e8dc131042 New translations web.php (French)
[ci skip]
2021-12-28 12:22:28 -07:00
daniel
e0bd18dc69 New translations web.php (Welsh)
[ci skip]
2021-12-28 11:07:27 -07:00
daniel
142c964557 New translations web.php (Basque)
[ci skip]
2021-12-28 11:07:26 -07:00
daniel
914ba6f41f New translations web.php (Welsh)
[ci skip]
2021-12-28 10:10:25 -07:00
daniel
a3341b166f New translations web.php (Basque)
[ci skip]
2021-12-28 10:10:24 -07:00
daniel
57949fa68b New translations web.php (Czech)
[ci skip]
2021-12-28 10:10:22 -07:00
daniel
23f625669d New translations web.php (Czech)
[ci skip]
2021-12-28 08:37:59 -07:00
daniel
4bb3d84fcb New translations web.php (Vietnamese)
[ci skip]
2021-12-27 20:52:57 -07:00
daniel
4890528510 New translations web.php (Vietnamese)
[ci skip]
2021-12-27 19:56:48 -07:00
daniel
936d0b8f95 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-27 16:59:10 -07:00
daniel
244a6c3fae New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-27 16:59:08 -07:00
daniel
926e6b15b1 New translations web.php (Portuguese)
[ci skip]
2021-12-27 16:58:52 -07:00
daniel
af573f2d99 New translations web.php (Hungarian)
[ci skip]
2021-12-27 14:22:40 -07:00
daniel
f7438ef022 New translations web.php (Hungarian)
[ci skip]
2021-12-27 13:23:41 -07:00
daniel
c979154b97 New translations web.php (Scottish Gaelic)
[ci skip]
2021-12-26 14:50:21 -07:00
daniel
63d7e7fccd New translations web.php (Arabic)
[ci skip]
2021-12-26 13:51:45 -07:00
daniel
b56813360d New translations web.php (Polish)
[ci skip]
2021-12-26 12:25:56 -07:00
daniel
b4030c2d82 New translations web.php (Hebrew)
[ci skip]
2021-12-26 11:25:37 -07:00
daniel
cdfd53ec27 New translations web.php (Russian)
[ci skip]
2021-12-26 10:11:59 -07:00
daniel
866ba80f23 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-26 09:06:58 -07:00
daniel
d3da149b17 New translations web.php (Vietnamese)
[ci skip]
2021-12-26 09:06:57 -07:00
daniel
088ec7e266 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-26 07:46:29 -07:00
daniel
7a0ffddcd7 New translations web.php (Basque)
[ci skip]
2021-12-26 06:32:26 -07:00
daniel
9cad926a51 New translations web.php (Occitan)
[ci skip]
2021-12-26 05:28:27 -07:00
daniel
471474b3fd New translations web.php (French)
[ci skip]
2021-12-26 05:28:26 -07:00
daniel
59c226bd73
Merge pull request #3091 from pixelfed/staging
Staging
2021-12-26 05:01:13 -07:00
Daniel Supernault
a994ec7be7
Update changelog 2021-12-26 04:59:07 -07:00
Daniel Supernault
ff7ee3bd62
Update Webfinger, fixes #3050 2021-12-26 04:58:19 -07:00
daniel
e8b2aa06c6 New translations web.php (Slovak)
[ci skip]
2021-12-26 04:26:31 -07:00
daniel
37e081cc80 New translations web.php (Japanese)
[ci skip]
2021-12-26 04:26:30 -07:00
daniel
4ece158c22 New translations web.php (German)
[ci skip]
2021-12-26 04:26:27 -07:00
daniel
735c820cdc New translations web.php (Portuguese)
[ci skip]
2021-12-26 04:26:22 -07:00
daniel
a8443729f2 New translations web.php (Indonesian)
[ci skip]
2021-12-26 04:26:19 -07:00
daniel
a9f9b02108 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-26 04:26:16 -07:00
daniel
06e9c0f88e New translations web.php (Chinese Simplified)
[ci skip]
2021-12-26 04:26:16 -07:00
daniel
fe0e0388cb New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-26 04:26:14 -07:00
Daniel Supernault
469d49d832
Update site config, increase ttl and enable SPA by default 2021-12-26 03:46:15 -07:00
daniel
803cc058b2
Merge pull request #3089 from pixelfed/staging
Staging
2021-12-26 03:30:48 -07:00
Daniel Supernault
d9551fc3b7
Update compiled assets 2021-12-26 03:30:15 -07:00
Daniel Supernault
3c5bf61861
Update i18n strings 2021-12-26 03:27:10 -07:00
daniel
2b1809e478
Merge pull request #3088 from pixelfed/l10n_staging
New Crowdin updates
2021-12-26 03:25:47 -07:00
daniel
30896e0380 New translations web.php (Slovak)
[ci skip]
2021-12-26 03:25:10 -07:00
daniel
64448bb0b0 New translations web.php (Norwegian)
[ci skip]
2021-12-26 03:25:09 -07:00
daniel
d0d85b342a New translations web.php (Dutch)
[ci skip]
2021-12-26 03:25:08 -07:00
daniel
cb51d304ef New translations web.php (Korean)
[ci skip]
2021-12-26 03:25:03 -07:00
daniel
22719874aa New translations web.php (Japanese)
[ci skip]
2021-12-26 03:25:02 -07:00
daniel
7c4107d134 New translations web.php (Italian)
[ci skip]
2021-12-26 03:25:01 -07:00
daniel
7ab7881f60 New translations web.php (Hungarian)
[ci skip]
2021-12-26 03:25:01 -07:00
daniel
ee682a946c New translations web.php (Hebrew)
[ci skip]
2021-12-26 03:24:59 -07:00
daniel
7dc3c41cbd New translations web.php (Polish)
[ci skip]
2021-12-26 03:24:58 -07:00
daniel
9e9436f58e New translations web.php (Finnish)
[ci skip]
2021-12-26 03:24:57 -07:00
daniel
46f273fdaa New translations web.php (German)
[ci skip]
2021-12-26 03:24:56 -07:00
daniel
0437008a03 New translations web.php (Danish)
[ci skip]
2021-12-26 03:24:55 -07:00
daniel
1fd96bd20b New translations web.php (Czech)
[ci skip]
2021-12-26 03:24:55 -07:00
daniel
1a24810148 New translations web.php (Catalan)
[ci skip]
2021-12-26 03:24:54 -07:00
daniel
661a422985 New translations web.php (Arabic)
[ci skip]
2021-12-26 03:24:53 -07:00
daniel
3fd6605e9b New translations web.php (Afrikaans)
[ci skip]
2021-12-26 03:24:52 -07:00
daniel
70f8cde66e New translations web.php (Spanish)
[ci skip]
2021-12-26 03:24:51 -07:00
daniel
eefb981d82 New translations web.php (Greek)
[ci skip]
2021-12-26 03:24:50 -07:00
daniel
4c85838c97 New translations web.php (French)
[ci skip]
2021-12-26 03:24:49 -07:00
daniel
e10605c921 New translations web.php (Portuguese)
[ci skip]
2021-12-26 03:24:48 -07:00
daniel
79d3042015 New translations web.php (Serbian (Cyrillic))
[ci skip]
2021-12-26 03:24:48 -07:00
daniel
30d3ba3e35 New translations web.php (Occitan)
[ci skip]
2021-12-26 03:24:47 -07:00
daniel
0df994ea56 New translations web.php (Persian)
[ci skip]
2021-12-26 03:24:46 -07:00
daniel
8f6511094c New translations web.php (Bengali)
[ci skip]
2021-12-26 03:24:45 -07:00
daniel
55953fac2c New translations web.php (Scottish Gaelic)
[ci skip]
2021-12-26 03:24:44 -07:00
daniel
bb48d25977 New translations web.php (Basque)
[ci skip]
2021-12-26 03:24:43 -07:00
daniel
bc9e312a02 New translations web.php (Welsh)
[ci skip]
2021-12-26 03:24:42 -07:00
daniel
88234b8b14 New translations web.php (Indonesian)
[ci skip]
2021-12-26 03:24:41 -07:00
daniel
224c4f568c New translations web.php (Russian)
[ci skip]
2021-12-26 03:24:40 -07:00
daniel
9a48c77ce8 New translations web.php (Galician)
[ci skip]
2021-12-26 03:24:40 -07:00
daniel
d81c9a15ad New translations web.php (Vietnamese)
[ci skip]
2021-12-26 03:24:39 -07:00
daniel
11cf631506 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-26 03:24:38 -07:00
daniel
c01ef6b345 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-26 03:24:37 -07:00
daniel
dc7f3f6132 New translations web.php (Ukrainian)
[ci skip]
2021-12-26 03:24:36 -07:00
daniel
44ee6f1801 New translations web.php (Turkish)
[ci skip]
2021-12-26 03:24:35 -07:00
daniel
1e44d4041f New translations web.php (Swedish)
[ci skip]
2021-12-26 03:24:34 -07:00
daniel
e18866fc8d New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-26 03:24:34 -07:00
daniel
d0acc69461 New translations web.php (Romanian)
[ci skip]
2021-12-26 03:24:33 -07:00
Daniel Supernault
5dd7b48b29
Add i18n string 2021-12-26 03:23:59 -07:00
daniel
eeae69b8cb
Merge pull request #3082 from pixelfed/l10n_staging
New Crowdin updates
2021-12-26 03:21:01 -07:00
daniel
d2d6226677 New translations web.php (Slovak)
[ci skip]
2021-12-26 03:01:40 -07:00
daniel
3dc45ee331 New translations web.php (Vietnamese)
[ci skip]
2021-12-26 03:01:39 -07:00
daniel
dadb01176c New translations web.php (Slovak)
[ci skip]
2021-12-26 02:01:00 -07:00
daniel
c96ca13d0a
Merge pull request #3087 from pixelfed/staging
Update compiled assets
2021-12-26 01:02:05 -07:00
Daniel Supernault
182902a553
Update compiled assets 2021-12-26 01:01:36 -07:00
daniel
d518f0e96f New translations web.php (Chinese Simplified)
[ci skip]
2021-12-26 00:45:05 -07:00
daniel
f914d14d97
Merge pull request #3086 from pixelfed/staging
Update compiled assets
2021-12-26 00:26:24 -07:00
Daniel Supernault
e1a071109e
Update compiled assets 2021-12-26 00:26:01 -07:00
daniel
4e7d0c153c
Merge pull request #3085 from pixelfed/staging
Staging
2021-12-26 00:17:16 -07:00
Daniel Supernault
47ebd1e4b1
Update changelog 2021-12-26 00:16:36 -07:00
Daniel Supernault
0837b410b1
Update SPA, rewrite autolink urls to SPA when applicable 2021-12-26 00:16:04 -07:00
Daniel Supernault
5c65e2b761
Update SpaController, add username redirect method 2021-12-26 00:08:18 -07:00
daniel
b4ecee9d7c New translations web.php (Japanese)
[ci skip]
2021-12-25 23:45:56 -07:00
daniel
cd6c916512
Merge pull request #3084 from pixelfed/staging
Update LoginController, bump decayMinutes from 1 to 60
2021-12-25 21:48:15 -07:00
Daniel Supernault
8c2c69c3aa
Update changelog 2021-12-25 21:48:05 -07:00
Daniel Supernault
6bf92bed27
Update LoginController, bump decayMinutes from 1 to 60 2021-12-25 21:47:16 -07:00
daniel
cde17e77a3
Merge pull request #3083 from pixelfed/staging
Update compiled assets
2021-12-25 21:13:34 -07:00
Daniel Supernault
3f511e3a39
Update compiled assets 2021-12-25 21:13:03 -07:00
daniel
ec9d52f8bb New translations web.php (Slovak)
[ci skip]
2021-12-25 21:11:54 -07:00
daniel
5b56b5ebae New translations web.php (Norwegian)
[ci skip]
2021-12-25 21:11:54 -07:00
daniel
9a5619c2dd New translations web.php (Dutch)
[ci skip]
2021-12-25 21:11:53 -07:00
daniel
c66c473f55 New translations web.php (Korean)
[ci skip]
2021-12-25 21:11:52 -07:00
daniel
6d5cb3e166 New translations web.php (Japanese)
[ci skip]
2021-12-25 21:11:51 -07:00
daniel
1e4cb9886f New translations web.php (Italian)
[ci skip]
2021-12-25 21:11:50 -07:00
daniel
f218706d8b New translations web.php (Hungarian)
[ci skip]
2021-12-25 21:11:49 -07:00
daniel
57a9df0a08 New translations web.php (Hebrew)
[ci skip]
2021-12-25 21:11:48 -07:00
daniel
f69c521a3d New translations web.php (Polish)
[ci skip]
2021-12-25 21:11:47 -07:00
daniel
e58e4a50fc New translations web.php (Finnish)
[ci skip]
2021-12-25 21:11:46 -07:00
daniel
264dd55103 New translations web.php (German)
[ci skip]
2021-12-25 21:11:45 -07:00
daniel
20044c2bd4 New translations web.php (Danish)
[ci skip]
2021-12-25 21:11:44 -07:00
daniel
d8ab23cce0 New translations web.php (Czech)
[ci skip]
2021-12-25 21:11:43 -07:00
daniel
64308584cd New translations web.php (Catalan)
[ci skip]
2021-12-25 21:11:42 -07:00
daniel
4a3cc65caa New translations web.php (Arabic)
[ci skip]
2021-12-25 21:11:41 -07:00
daniel
623e4881fb New translations web.php (Afrikaans)
[ci skip]
2021-12-25 21:11:40 -07:00
daniel
88fe848d3a New translations web.php (Spanish)
[ci skip]
2021-12-25 21:11:40 -07:00
daniel
13ffd1b7f6 New translations web.php (Greek)
[ci skip]
2021-12-25 21:11:39 -07:00
daniel
ffb352d2e9 New translations web.php (French)
[ci skip]
2021-12-25 21:11:38 -07:00
daniel
1e84895532 New translations web.php (Portuguese)
[ci skip]
2021-12-25 21:11:37 -07:00
daniel
f004757756 New translations web.php (Serbian (Cyrillic))
[ci skip]
2021-12-25 21:11:36 -07:00
daniel
24db88cd62 New translations web.php (Occitan)
[ci skip]
2021-12-25 21:11:35 -07:00
daniel
fdff3baa37 New translations web.php (Persian)
[ci skip]
2021-12-25 21:11:34 -07:00
daniel
c18063fef2 New translations web.php (Bengali)
[ci skip]
2021-12-25 21:11:33 -07:00
daniel
24d1f059a4 New translations web.php (Scottish Gaelic)
[ci skip]
2021-12-25 21:11:32 -07:00
daniel
9d29193a85 New translations web.php (Basque)
[ci skip]
2021-12-25 21:11:32 -07:00
daniel
f6531ade09 New translations web.php (Welsh)
[ci skip]
2021-12-25 21:11:31 -07:00
daniel
eac09c34e1 New translations web.php (Indonesian)
[ci skip]
2021-12-25 21:11:30 -07:00
daniel
d7bc2914a4 New translations web.php (Russian)
[ci skip]
2021-12-25 21:11:29 -07:00
daniel
9e333d87b9 New translations web.php (Galician)
[ci skip]
2021-12-25 21:11:28 -07:00
daniel
ede6759b54 New translations web.php (Vietnamese)
[ci skip]
2021-12-25 21:11:27 -07:00
daniel
53e187b1d1 New translations web.php (Chinese Simplified)
[ci skip]
2021-12-25 21:11:26 -07:00
daniel
b9139f3a2f New translations web.php (Ukrainian)
[ci skip]
2021-12-25 21:11:25 -07:00
daniel
fe8c50812b New translations web.php (Turkish)
[ci skip]
2021-12-25 21:11:24 -07:00
daniel
fa83d8f955 New translations web.php (Swedish)
[ci skip]
2021-12-25 21:11:23 -07:00
daniel
1ebefcba93 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-25 21:11:22 -07:00
daniel
36c2983a1e New translations web.php (Romanian)
[ci skip]
2021-12-25 21:11:21 -07:00
daniel
9140a19741
Merge pull request #3081 from pixelfed/staging
Staging
2021-12-25 20:13:51 -07:00
Daniel Supernault
aa89075317
Update compiled assets 2021-12-25 20:13:01 -07:00
Daniel Supernault
8be29d295d
Update i18n strings 2021-12-25 20:09:39 -07:00
Daniel Supernault
07c508021e
Add i18n string 2021-12-25 20:08:52 -07:00
daniel
3cccb8e237
Merge pull request #3079 from pixelfed/l10n_staging
New Crowdin updates
2021-12-25 19:55:12 -07:00
daniel
f2511f2d15 New translations web.php (Slovak)
[ci skip]
2021-12-25 19:54:51 -07:00
daniel
ee62f967bd New translations web.php (Portuguese)
[ci skip]
2021-12-25 19:54:40 -07:00
daniel
9f37f574b5 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-25 19:54:29 -07:00
daniel
703d92d9f4 New translations web.php (Occitan)
[ci skip]
2021-12-25 15:37:38 -07:00
daniel
cf9eb563c1 New translations web.php (Persian)
[ci skip]
2021-12-25 11:03:19 -07:00
daniel
6bbc2f78e6 New translations web.php (Persian)
[ci skip]
2021-12-25 09:54:55 -07:00
daniel
55322bb298 New translations web.php (Basque)
[ci skip]
2021-12-25 01:49:42 -07:00
daniel
e33f7ec91e New translations web.php (Hungarian)
[ci skip]
2021-12-24 13:52:23 -07:00
daniel
b70142f8c9 New translations web.php (Turkish)
[ci skip]
2021-12-24 07:49:42 -07:00
daniel
3b9154e5a7 New translations web.php (Persian)
[ci skip]
2021-12-24 06:46:30 -07:00
daniel
2f1c1d53c2 New translations web.php (Welsh)
[ci skip]
2021-12-24 06:46:29 -07:00
daniel
c2d0c2dc42 New translations web.php (Turkish)
[ci skip]
2021-12-24 06:46:28 -07:00
daniel
79089aa0ea New translations web.php (Persian)
[ci skip]
2021-12-24 05:17:44 -07:00
daniel
e522553592 New translations web.php (Persian)
[ci skip]
2021-12-24 04:02:06 -07:00
daniel
5a3bf4ba10 New translations web.php (Turkish)
[ci skip]
2021-12-24 04:02:05 -07:00
daniel
ab3e4386dc New translations web.php (Hungarian)
[ci skip]
2021-12-24 04:02:04 -07:00
daniel
a902edc336 New translations web.php (Hungarian)
[ci skip]
2021-12-24 02:56:20 -07:00
daniel
ffc8529ad3 New translations web.php (Russian)
[ci skip]
2021-12-23 22:26:50 -07:00
daniel
757615572d
Merge pull request #3078 from pixelfed/staging
Add illustrations
2021-12-23 18:54:29 -07:00
Daniel Supernault
b87fc212f0
Add illustrations 2021-12-23 18:53:54 -07:00
daniel
db2a241ce6
Merge pull request #3077 from pixelfed/staging
Staging
2021-12-23 17:41:26 -07:00
Daniel Supernault
0068b0cdc4
Update compiled assets 2021-12-23 17:40:05 -07:00
Daniel Supernault
7bc684e5d1
Update SpaController, persist web language changes 2021-12-23 17:37:08 -07:00
Daniel Supernault
aed570b3f6
Update lang strings 2021-12-23 17:35:10 -07:00
daniel
46b185351f
Merge pull request #3076 from pixelfed/l10n_staging
New Crowdin updates
2021-12-23 17:26:59 -07:00
daniel
689ea50ff5 New translations web.php (Portuguese)
[ci skip]
2021-12-23 17:26:30 -07:00
daniel
bddc271cf5 New translations web.php (German)
[ci skip]
2021-12-23 17:26:14 -07:00
daniel
8cf345a3a0
Merge pull request #3072 from pixelfed/l10n_staging
New Crowdin updates
2021-12-23 17:11:31 -07:00
daniel
8d697f8de9 New translations web.php (Occitan)
[ci skip]
2021-12-23 16:39:24 -07:00
daniel
b5c864a56f New translations web.php (French)
[ci skip]
2021-12-23 15:42:25 -07:00
daniel
5fa27d4ec9 New translations web.php (Basque)
[ci skip]
2021-12-23 14:40:55 -07:00
daniel
1c22c3b680 New translations web.php (Basque)
[ci skip]
2021-12-23 12:19:17 -07:00
daniel
a36b8cfccf New translations web.php (Persian)
[ci skip]
2021-12-23 11:03:33 -07:00
daniel
fbb1be99ce New translations web.php (Basque)
[ci skip]
2021-12-23 10:04:40 -07:00
daniel
132541cae8 New translations web.php (Basque)
[ci skip]
2021-12-23 08:59:47 -07:00
daniel
65fdd5f13c New translations web.php (Basque)
[ci skip]
2021-12-23 06:33:53 -07:00
daniel
da547344a6 New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-23 06:33:52 -07:00
daniel
ff2be7c49a New translations web.php (Basque)
[ci skip]
2021-12-23 04:19:18 -07:00
daniel
d8ed5739e8 New translations web.php (Turkish)
[ci skip]
2021-12-23 04:19:17 -07:00
daniel
c4c0f6daaf New translations web.php (Basque)
[ci skip]
2021-12-23 03:02:48 -07:00
daniel
c5ec38301c New translations web.php (Turkish)
[ci skip]
2021-12-23 03:02:47 -07:00
daniel
7fd5f5ef1c New translations web.php (Japanese)
[ci skip]
2021-12-23 03:02:46 -07:00
daniel
7b95d730f2 New translations web.php (Basque)
[ci skip]
2021-12-23 02:02:22 -07:00
daniel
0d142dc00b New translations web.php (Turkish)
[ci skip]
2021-12-23 02:02:21 -07:00
daniel
452dfecade New translations web.php (Japanese)
[ci skip]
2021-12-23 02:02:21 -07:00
daniel
466a65294d New translations web.php (German)
[ci skip]
2021-12-23 02:02:19 -07:00
daniel
ba56c8c187 New translations web.php (Japanese)
[ci skip]
2021-12-23 00:44:31 -07:00
daniel
4a0cb8aaac New translations web.php (Czech)
[ci skip]
2021-12-23 00:44:30 -07:00
daniel
be4b971ace New translations web.php (Czech)
[ci skip]
2021-12-22 23:46:45 -07:00
daniel
8e2a1e8240 New translations web.php (Arabic)
[ci skip]
2021-12-22 23:46:44 -07:00
daniel
1edb6fe224 New translations web.php (Greek)
[ci skip]
2021-12-22 22:48:23 -07:00
daniel
09c28046ef New translations web.php (Arabic)
[ci skip]
2021-12-22 22:48:22 -07:00
daniel
1a6ac406d6 New translations web.php (Arabic)
[ci skip]
2021-12-22 21:49:20 -07:00
daniel
6008a5d186 New translations web.php (Polish)
[ci skip]
2021-12-22 19:24:19 -07:00
daniel
c988f07895 New translations web.php (Dutch)
[ci skip]
2021-12-22 19:24:18 -07:00
daniel
a28d06a2ab New translations web.php (German)
[ci skip]
2021-12-22 19:24:16 -07:00
daniel
5945a4f4a0 New translations web.php (Spanish)
[ci skip]
2021-12-22 19:24:15 -07:00
daniel
3e3f56a934 New translations web.php (French)
[ci skip]
2021-12-22 19:24:14 -07:00
daniel
ca4206055c New translations web.php (Bengali)
[ci skip]
2021-12-22 18:08:40 -07:00
daniel
882d1b37f2 New translations web.php (Korean)
[ci skip]
2021-12-22 18:08:39 -07:00
daniel
6f11eb4885 New translations web.php (Arabic)
[ci skip]
2021-12-22 17:11:56 -07:00
daniel
f75273973b New translations web.php (Scottish Gaelic)
[ci skip]
2021-12-22 16:09:24 -07:00
daniel
95537611c9 New translations web.php (Basque)
[ci skip]
2021-12-22 16:09:23 -07:00
daniel
5685716446 New translations web.php (Indonesian)
[ci skip]
2021-12-22 16:09:23 -07:00
daniel
a0281168d6 New translations web.php (Portuguese)
[ci skip]
2021-12-22 16:09:22 -07:00
daniel
8c9fbdd547 New translations web.php (Dutch)
[ci skip]
2021-12-22 16:09:21 -07:00
daniel
97b04372c0 New translations web.php (German)
[ci skip]
2021-12-22 14:39:14 -07:00
daniel
2e557e6bba New translations web.php (Arabic)
[ci skip]
2021-12-22 14:39:13 -07:00
daniel
7857fd7091 New translations web.php (French)
[ci skip]
2021-12-22 14:39:12 -07:00
daniel
481dc1aaa6 New translations web.php (Scottish Gaelic)
[ci skip]
2021-12-22 13:23:07 -07:00
daniel
150070f8b1 New translations web.php (Basque)
[ci skip]
2021-12-22 13:23:06 -07:00
daniel
a4922de49e New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-22 13:23:05 -07:00
daniel
f4401184ce New translations web.php (Basque)
[ci skip]
2021-12-22 12:19:14 -07:00
daniel
3534d6c11a New translations web.php (Basque)
[ci skip]
2021-12-22 11:18:07 -07:00
daniel
1964408d66 New translations web.php (Arabic)
[ci skip]
2021-12-22 11:18:06 -07:00
daniel
1ecadfd9bd New translations web.php (French)
[ci skip]
2021-12-22 11:18:04 -07:00
daniel
d530e22878 New translations web.php (Welsh)
[ci skip]
2021-12-22 09:58:03 -07:00
daniel
b95d879b9e New translations web.php (Portuguese, Brazilian)
[ci skip]
2021-12-22 09:58:02 -07:00
daniel
36e9f47e69 New translations web.php (Russian)
[ci skip]
2021-12-22 09:58:01 -07:00
daniel
5d4ac0b161 New translations web.php (Arabic)
[ci skip]
2021-12-22 09:57:59 -07:00
daniel
1f21cc7543 New translations web.php (French)
[ci skip]
2021-12-22 09:57:58 -07:00
daniel
eb9914e8da New translations web.php (Welsh)
[ci skip]
2021-12-22 04:51:53 -07:00
daniel
a29c748961 New translations web.php (Russian)
[ci skip]
2021-12-22 04:51:52 -07:00
daniel
5be9671fd5 New translations web.php (Hebrew)
[ci skip]
2021-12-22 04:51:51 -07:00
daniel
434260d18f New translations web.php (German)
[ci skip]
2021-12-22 04:51:50 -07:00
daniel
215076ea6b New translations web.php (Spanish)
[ci skip]
2021-12-22 04:51:49 -07:00
daniel
29cde0eecc New translations web.php (French)
[ci skip]
2021-12-22 04:51:48 -07:00
daniel
bc5dc64d9d New translations web.php (Indonesian)
[ci skip]
2021-12-22 03:47:37 -07:00
daniel
7ece0b4764 New translations web.php (Galician)
[ci skip]
2021-12-22 03:47:36 -07:00
daniel
b70b57c625 New translations web.php (Polish)
[ci skip]
2021-12-22 03:47:35 -07:00
daniel
e4703a85fd New translations web.php (Hebrew)
[ci skip]
2021-12-22 03:47:34 -07:00
daniel
83580d0510 New translations web.php (German)
[ci skip]
2021-12-22 03:47:33 -07:00
daniel
82b97653d9 New translations web.php (Catalan)
[ci skip]
2021-12-22 03:47:32 -07:00
daniel
2c83df8a0f New translations web.php (Spanish)
[ci skip]
2021-12-22 03:47:31 -07:00
daniel
f72a3e2a78 New translations web.php (French)
[ci skip]
2021-12-22 03:47:30 -07:00
daniel
b3652a1e36 New translations web.php (Indonesian)
[ci skip]
2021-12-22 02:23:25 -07:00
daniel
73b7aa078b New translations web.php (Galician)
[ci skip]
2021-12-22 02:23:24 -07:00
daniel
004a647f9c New translations web.php (Italian)
[ci skip]
2021-12-22 02:23:23 -07:00
daniel
4cd77c6ca9 New translations web.php (German)
[ci skip]
2021-12-22 02:23:22 -07:00
daniel
9ee6cbd4b1 New translations web.php (Catalan)
[ci skip]
2021-12-22 02:23:21 -07:00
daniel
14a1679492 New translations web.php (French)
[ci skip]
2021-12-22 02:23:20 -07:00
daniel
4f2c8b39b3
Merge pull request #3071 from pixelfed/staging
Staging
2021-12-22 01:38:06 -07:00
Daniel Supernault
599ed20ec0
Update compiled assets 2021-12-22 01:35:49 -07:00
Daniel Supernault
08584b6dd3
Update i18n strings 2021-12-22 01:33:57 -07:00
daniel
781788bdcc
Merge pull request #3070 from pixelfed/l10n_staging
New Crowdin updates
2021-12-22 01:28:35 -07:00
daniel
4c7f4fd7cb New translations web.php (Indonesian)
[ci skip]
2021-12-22 01:27:48 -07:00
daniel
ce066baf34 New translations web.php (German)
[ci skip]
2021-12-22 01:27:42 -07:00
daniel
299283fe75 New translations web.php (Greek)
[ci skip]
2021-12-22 01:27:41 -07:00
daniel
bdd925551c New translations web.php (French)
[ci skip]
2021-12-22 01:27:37 -07:00
daniel
cf934ad994 New translations web.php (Polish)
[ci skip]
2021-12-22 01:27:34 -07:00
daniel
8719516be1 New translations web.php (Russian)
[ci skip]
2021-12-22 01:27:32 -07:00
daniel
4e8fb2f849
Merge pull request #3068 from pixelfed/l10n_staging
New Crowdin updates
2021-12-22 01:21:33 -07:00
daniel
bcba16f928
Merge pull request #3069 from pixelfed/staging
Staging
2021-12-22 01:00:50 -07:00
Daniel Supernault
4661dbadd1
Update compiled assets 2021-12-22 00:59:53 -07:00
Daniel Supernault
37dcc4ae13
Update PublicApiController 2021-12-22 00:59:03 -07:00
Daniel Supernault
7d3214d37e
Update web routes 2021-12-22 00:54:53 -07:00
Daniel Supernault
65064a8832
Update DiscoverComponent 2021-12-22 00:51:24 -07:00
Daniel Supernault
2befff8fd1
Update ComposeModal 2021-12-22 00:47:33 -07:00
daniel
75b8553ddc New translations web.php (Indonesian)
[ci skip]
2021-12-22 00:24:51 -07:00
daniel
25deee375f New translations web.php (Dutch)
[ci skip]
2021-12-22 00:24:50 -07:00
daniel
bded1737b5 New translations web.php (Hebrew)
[ci skip]
2021-12-22 00:24:49 -07:00
daniel
8f941d52bd New translations web.php (Greek)
[ci skip]
2021-12-22 00:24:47 -07:00
daniel
b2e221d2a3 New translations web.php (German)
[ci skip]
2021-12-22 00:24:46 -07:00
daniel
e6d029733c New translations web.php (Galician)
[ci skip]
2021-12-21 23:29:18 -07:00
daniel
7d57bb8ced New translations web.php (Russian)
[ci skip]
2021-12-21 23:29:17 -07:00
daniel
1d13ea98d9 New translations web.php (Dutch)
[ci skip]
2021-12-21 23:29:16 -07:00
daniel
4d124dbe41 New translations web.php (Hebrew)
[ci skip]
2021-12-21 23:29:15 -07:00
daniel
be706b273d
Merge pull request #3067 from pixelfed/staging
Staging
2021-12-21 22:50:01 -07:00
Daniel Supernault
fb742cf4db
Update changelog 2021-12-21 22:49:20 -07:00
Daniel Supernault
327073724f
Update StatusEntityLexer, prevent boosts and replies from being added to PublicTimelineService 2021-12-21 22:48:47 -07:00
daniel
657175366f
Merge pull request #3066 from pixelfed/staging
Staging
2021-12-21 22:38:02 -07:00
Daniel Supernault
5e1e207329
Update langs 2021-12-21 22:33:54 -07:00
Daniel Supernault
7bef159119
Add i18n dev command 2021-12-21 22:32:33 -07:00
daniel
adbc7dec0b
Merge pull request #3065 from pixelfed/l10n_staging
New Crowdin updates
2021-12-21 22:26:29 -07:00
daniel
91d85fd751 New translations web.php (German)
[ci skip]
2021-12-21 22:11:03 -07:00
daniel
3160f719b6
Merge pull request #3064 from pixelfed/staging
Staging
2021-12-21 21:57:34 -07:00
Daniel Supernault
5828a863cb
Update web routes 2021-12-21 21:54:48 -07:00
Daniel Supernault
67b37be2a7
Update compiled assets 2021-12-21 21:45:43 -07:00
Daniel Supernault
7e35ab80ad
Update web routes 2021-12-21 21:44:52 -07:00
Daniel Supernault
937cdfb7f9
Update StatusService 2021-12-21 21:40:10 -07:00
daniel
2a651b72d2
Merge pull request #3063 from pixelfed/staging
Staging
2021-12-21 21:33:34 -07:00
Daniel Supernault
e12227aee1
Update i18n 2021-12-21 21:26:52 -07:00
daniel
978d336c01
Merge pull request #3062 from pixelfed/l10n_staging
New Crowdin updates
2021-12-21 21:23:43 -07:00
daniel
fa0636d325 New translations web.php (Spanish)
[ci skip]
2021-12-21 21:12:07 -07:00
daniel
7c5da78cb0 New translations web.php (Afrikaans)
[ci skip]
2021-12-21 21:12:07 -07:00
daniel
a2d69257cf New translations web.php (Arabic)
[ci skip]
2021-12-21 21:12:05 -07:00
daniel
3bf2ddb1fc New translations web.php (Catalan)
[ci skip]
2021-12-21 21:12:04 -07:00
daniel
8ed2f843df New translations web.php (Czech)
[ci skip]
2021-12-21 21:12:03 -07:00
daniel
5a036b239f New translations web.php (Danish)
[ci skip]
2021-12-21 21:12:02 -07:00
daniel
b0056081f6 New translations web.php (German)
[ci skip]
2021-12-21 21:12:01 -07:00
daniel
9c0315cf5f New translations web.php (Greek)
[ci skip]
2021-12-21 21:12:00 -07:00
daniel
c085c9288a New translations web.php (Finnish)
[ci skip]
2021-12-21 21:11:59 -07:00
daniel
a543616502 New translations web.php (Hebrew)
[ci skip]
2021-12-21 21:11:58 -07:00
daniel
b5b08620e9 New translations web.php (Hungarian)
[ci skip]
2021-12-21 21:11:57 -07:00
daniel
3681811515 New translations web.php (Italian)
[ci skip]
2021-12-21 21:11:56 -07:00
daniel
dfa47b4673 New translations web.php (Japanese)
[ci skip]
2021-12-21 21:11:55 -07:00
daniel
2008fad133 New translations web.php (French)
[ci skip]
2021-12-21 21:11:54 -07:00
daniel
8b1f02b253 New translations web.php (Korean)
[ci skip]
2021-12-21 21:11:53 -07:00
daniel
1d81cc5431 New translations web.php (Norwegian)
[ci skip]
2021-12-21 21:11:52 -07:00
daniel
01466f5355 New translations web.php (Polish)
[ci skip]
2021-12-21 21:11:51 -07:00
daniel
c6ab146a0f New translations web.php (Portuguese)
[ci skip]
2021-12-21 21:11:50 -07:00
daniel
0ee0b13da8 New translations web.php (Russian)
[ci skip]
2021-12-21 21:11:49 -07:00
daniel
b388b9fde7 New translations web.php (Serbian (Cyrillic))
[ci skip]
2021-12-21 21:11:48 -07:00
daniel
82e804feb5 New translations web.php (Swedish)
[ci skip]
2021-12-21 21:11:47 -07:00
daniel
1d6cc9cfcc New translations web.php (Turkish)
[ci skip]
2021-12-21 21:11:46 -07:00
daniel
2c813e32f5 New translations web.php (Ukrainian)
[ci skip]
2021-12-21 21:11:45 -07:00
daniel
ad44111bb0 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-21 21:11:44 -07:00
daniel
193d517c81 New translations web.php (Vietnamese)
[ci skip]
2021-12-21 21:11:42 -07:00
daniel
ff9aaadd9e New translations web.php (Dutch)
[ci skip]
2021-12-21 21:11:41 -07:00
daniel
8be8b93fbb New translations web.php (Romanian)
[ci skip]
2021-12-21 21:11:41 -07:00
Daniel Supernault
b1a6ce029c
Add lang strings 2021-12-21 20:54:33 -07:00
daniel
37212de5cd
Merge pull request #3061 from pixelfed/l10n_staging
New Crowdin updates
2021-12-21 20:54:14 -07:00
daniel
af3e204e6e New translations web.php (German)
[ci skip]
2021-12-21 20:12:53 -07:00
daniel
76c61cab3a
Merge pull request #3060 from pixelfed/l10n_staging
New Crowdin updates
2021-12-21 18:02:56 -07:00
daniel
85db47200f New translations web.php (Afrikaans)
[ci skip]
2021-12-21 18:01:03 -07:00
daniel
2c0545c552 New translations web.php (German)
[ci skip]
2021-12-21 18:00:59 -07:00
daniel
9630c34eb1 New translations web.php (Hungarian)
[ci skip]
2021-12-21 18:00:57 -07:00
daniel
c1bf2de10c New translations web.php (Korean)
[ci skip]
2021-12-21 18:00:55 -07:00
daniel
763d3bb4a6 New translations web.php (Norwegian)
[ci skip]
2021-12-21 18:00:54 -07:00
daniel
6f6a167d8c New translations web.php (Serbian (Cyrillic))
[ci skip]
2021-12-21 18:00:51 -07:00
daniel
c8b488b24a New translations web.php (Ukrainian)
[ci skip]
2021-12-21 18:00:50 -07:00
daniel
967e495363 New translations web.php (Chinese Traditional)
[ci skip]
2021-12-21 18:00:49 -07:00
daniel
e95909b093 New translations web.php (Vietnamese)
[ci skip]
2021-12-21 18:00:48 -07:00
daniel
d01a32b35f New translations web.php (Romanian)
[ci skip]
2021-12-21 18:00:46 -07:00
Daniel Supernault
edcf9e2b4f
Update crowdin config 2021-12-21 18:00:05 -07:00
Daniel Supernault
b1dde37adc
Update l10n 2021-12-21 17:49:44 -07:00
Daniel Supernault
132c8b8b71
Add web lang files 2021-12-21 17:43:37 -07:00
daniel
acd607a867 Update Crowdin configuration file 2021-12-21 17:23:30 -07:00
daniel
ed3a2f90f2
Merge pull request #3057 from pixelfed/staging
Staging
2021-12-21 17:20:05 -07:00
Daniel Supernault
7ff120c943
Add crowdin configuration 2021-12-21 17:18:24 -07:00
Daniel Supernault
f7d9b40b03
Add frontend translation strings 2021-12-21 17:16:47 -07:00
daniel
5442e232dc
Merge pull request #3056 from pixelfed/staging
Update ComposeController, refactor compose_settings
2021-12-19 04:25:49 -07:00
Daniel Supernault
a114c7d12a
Update changelog 2021-12-19 04:20:53 -07:00
Daniel Supernault
edc2958bf2
Update ComposeController, refactor compose_settings 2021-12-19 04:19:26 -07:00
daniel
ff1a12cdd7
Merge pull request #3055 from pixelfed/staging
Improve DiscoverService
2021-12-19 04:08:15 -07:00
Daniel Supernault
aee9b994e3
Improve DiscoverService 2021-12-19 04:07:40 -07:00
daniel
8d002415e7
Merge pull request #3054 from pixelfed/staging
Update console kernel, add db session garbage collector that runs twi…
2021-12-19 01:29:20 -07:00
Daniel Supernault
fd2d37432b
Update changelog 2021-12-19 01:28:55 -07:00
Daniel Supernault
03b0a62a22
Update console kernel, add db session garbage collector that runs twice daily 2021-12-19 01:25:49 -07:00
daniel
935fa47c59
Merge pull request #3053 from pixelfed/staging
Staging
2021-12-19 01:03:43 -07:00
Daniel Supernault
0aad8d5934
Fix typo in api route 2021-12-19 00:51:57 -07:00
Daniel Supernault
367d74fbfb
Update ApiV1Controller, use DiscoverService for discoverPosts method 2021-12-19 00:48:59 -07:00
Daniel Supernault
f36da7816f
Update InternalApiController, use DiscoverService for discoverPosts method 2021-12-19 00:48:05 -07:00
Daniel Supernault
493c5ca0ce
Add DiscoverService 2021-12-19 00:43:09 -07:00
daniel
23e82e28b4
Merge pull request #3052 from pixelfed/staging
Staging
2021-12-18 19:38:23 -07:00
Daniel Supernault
2f0cd86ce3
Add webfonts 2021-12-18 19:37:53 -07:00
daniel
f5387e84e8
Merge pull request #3047 from Wv5twkFEKh54vo4tta9yu7dHa3/patch-2
Add discover to routes
2021-12-18 19:35:07 -07:00
daniel
d33531568f
Merge pull request #2992 from xplosionmind/patch-1
Added “Run on YunoHost”
2021-12-18 19:33:52 -07:00
daniel
c8a8928da4
Merge pull request #3030 from lisbonjoker/patch-1
Patch 1 - Portuguese Translation
2021-12-18 19:31:54 -07:00
daniel
a65c6e2183
Merge pull request #3029 from Strubbl/docker-apache-update-debian-and-composer
docker-apache: update Debian to Bullseye and composer to 2.1.14
2021-12-18 19:30:44 -07:00
daniel
f3814c1987
Merge pull request #3048 from pixelfed/staging
Staging
2021-12-17 00:28:18 -07:00
daniel
7860499e67
Merge pull request #3009 from pixelfed/pwa-manifest
Update manifest with new icons, shortcuts, and categories
2021-12-17 00:27:49 -07:00
Wv5twkFEKh54vo4tta9yu7dHa3
82d1ca14bb Add discover to routes 2021-12-15 12:31:54 +01:00
daniel
c580c3dc5e
Merge pull request #3042 from pixelfed/staging
Update compiled assets
2021-12-15 00:08:04 -07:00
Daniel Supernault
0b27e988e6
Update compiled assets 2021-12-15 00:07:36 -07:00
daniel
9a57002b13
Merge pull request #3041 from pixelfed/staging
Drop PHP 7.3 support, make 7.4 the minimum and fix 8.0 support
2021-12-14 23:50:52 -07:00
Daniel Supernault
bf4bd00bbc
Update composer.lock 2021-12-14 23:49:19 -07:00
Daniel Supernault
9ad91a65d6
Update changelog 2021-12-14 23:47:28 -07:00
Daniel Supernault
33b69499dd
Update circleci config 2021-12-14 23:27:09 -07:00
Daniel Supernault
57268a827e
Update circleci config 2021-12-14 23:07:17 -07:00
Daniel Supernault
9aa883d621
Drop PHP 7.3 support, make PHP 7.4 min version 2021-12-14 23:06:59 -07:00
Daniel Supernault
b1ce49191b
Fix PHP 8 support 2021-12-14 22:59:31 -07:00
daniel
05041b95f3
Merge pull request #3040 from pixelfed/staging
Staging
2021-12-14 22:37:55 -07:00
Daniel Supernault
a065e17cc2
Update pixelfed config, remove env_editor 2021-12-14 22:37:07 -07:00
Daniel Supernault
f411c4e004
Update changelog 2021-12-14 22:36:18 -07:00
Daniel Supernault
dc3168506c
Update compiled assets 2021-12-14 22:34:14 -07:00
Daniel Supernault
91e078e031
Update Activity component 2021-12-14 22:26:10 -07:00
Daniel Supernault
e975372631
Update ComposeModal 2021-12-14 22:25:50 -07:00
Daniel Supernault
b78a51890d
Update Timeline component, change api endpoint 2021-12-14 22:24:15 -07:00
Daniel Supernault
0cc1365f98
Update PhotoPresenter component, add lightbox toggle 2021-12-14 22:18:56 -07:00
Daniel Supernault
0c8fffbd73
Update PollCard component, add showBorder prop 2021-12-14 22:18:01 -07:00
Daniel Supernault
01ca1edd84
Update ContextMenu component, fix account url paths 2021-12-14 22:17:08 -07:00
Daniel Supernault
e09a14d892
Update NotificationCard, update api endpoint and add group notifications 2021-12-14 22:16:13 -07:00
Daniel Supernault
fefbc44a5f
Update Hashtag component, fix spinner 2021-12-14 22:13:59 -07:00
Daniel Supernault
c5145b3db1
Remove ace 2021-12-14 21:49:48 -07:00
Daniel Supernault
f3f35f590d
Remove admin .env editor 2021-12-14 21:46:24 -07:00
Daniel Supernault
9093549dc2
Remove quill editor 2021-12-14 21:37:19 -07:00
daniel
6b055724a5
Merge pull request #3039 from pixelfed/staging
Staging
2021-12-14 21:20:29 -07:00
Daniel Supernault
642b802022
Update Http Kernel 2021-12-14 21:18:29 -07:00
Daniel Supernault
b8ebc00212
Update SharePipeline, fix variable bug 2021-12-14 21:13:30 -07:00
daniel
bb243222e2
Merge pull request #3038 from pixelfed/staging
Update CommentPipeline, fix variable bug
2021-12-14 21:09:54 -07:00
Daniel Supernault
0616a859d5
Update CommentPipeline, fix variable bug 2021-12-14 21:09:23 -07:00
daniel
c19cd49523
Merge pull request #3037 from pixelfed/staging
Add Cloud Backups
2021-12-14 20:32:22 -07:00
Daniel Supernault
7ca63f2eb6
Update changelog 2021-12-14 20:27:17 -07:00
Daniel Supernault
3515a98e55
Add Cloud Backups, a command to store backups on S3 or compatible filesystems 2021-12-14 20:08:44 -07:00
daniel
3d848fd5b0
Merge pull request #3035 from pixelfed/staging
Staging
2021-12-14 20:04:13 -07:00
Daniel Supernault
ce8edb0998
Update changelog 2021-12-14 19:58:17 -07:00
Daniel Supernault
83b48b5681
Update StatusService, improve cache invalidation 2021-12-12 22:30:55 -07:00
Daniel Supernault
c2910e5d42
Update MediaService, return empty array if cantt find status 2021-12-12 19:33:20 -07:00
Daniel Supernault
44b32d8bb6
Update SharePipeline 2021-12-12 19:32:11 -07:00
Daniel Supernault
466286af92
Update ApiV1Controller, improve statusesById perf and dispatch CommentPipeline job when applicable 2021-12-10 22:03:33 -07:00
Daniel Supernault
b6b0837f49
Update CommentPipeline, move reply_count calculation to comment pipeline job and improve count calculation 2021-12-10 21:55:42 -07:00
Artur Mancha
7a448da89b
Merge branch 'pixelfed:dev' into patch-1 2021-12-05 17:44:46 +00:00
Daniel Supernault
945a7e49f5
Update InternalApiController, prevent moderation actions against admin accounts 2021-12-05 01:42:55 -07:00
Daniel Supernault
2923453e13
Update PublicApiController 2021-12-04 20:41:46 -07:00
Daniel Supernault
fe64e18712
Update LikePipeline jobs, fix likes_count calculation 2021-12-04 20:31:36 -07:00
Daniel Supernault
a641d3a32c
Update changelog 2021-12-04 17:55:56 -07:00
Daniel Supernault
079804e65b
Update ApiV1Controller, improve settings and add discoverPosts endpoint 2021-12-04 17:55:38 -07:00
Daniel Supernault
cdaa153003
Update StoryController 2021-12-04 17:46:23 -07:00
Daniel Supernault
19d140b020
Update FollowerService 2021-12-04 17:37:44 -07:00
Daniel Supernault
0342027867
Update UserSetting model, cast compose_settings and other as json 2021-12-04 17:35:50 -07:00
Daniel Supernault
98f76abbe8
Update AccountTransformer, add note_text and location fields 2021-12-04 17:34:32 -07:00
Daniel Supernault
f88b3a2fb9
Update StatusService, add getDirectMessage method 2021-12-04 17:33:32 -07:00
Daniel Supernault
fbaed93eda
Update SearchApiV2Service, improve performance and include hashtag post counts when applicable 2021-12-04 17:32:43 -07:00
Daniel Supernault
a37971dd28
Add HashtagService 2021-12-04 17:31:53 -07:00
Daniel Supernault
9d9e9ce7fa
Update MediaStorageService, improve header parsing 2021-12-04 17:30:05 -07:00
Daniel Supernault
2aa73c1ffa
Update AccountService, add dynamic user settings methods 2021-12-04 17:27:58 -07:00
daniel
cd1a263f6e
Merge pull request #3034 from pixelfed/staging
Staging
2021-12-04 15:31:40 -07:00
Daniel Supernault
c4d3851afe
Update changelog 2021-12-04 15:31:09 -07:00
Daniel Supernault
b8e9056ee3
Update UserInviteController, fixes #3017 2021-12-04 15:30:08 -07:00
daniel
59f6a2ead7
Merge pull request #3032 from pixelfed/staging
Staging
2021-12-03 20:49:28 -07:00
Daniel Supernault
78b2650f3a
Update changelog 2021-12-03 20:48:45 -07:00
Daniel Supernault
e5387d6742
Add StatusMentionService, fixes #3026 2021-12-03 20:47:00 -07:00
daniel
89d62176cb
Merge pull request #3031 from samuelroland/remote-redirect-remove
Remove remoteRedirect() for profile website fix #2996
2021-12-03 18:24:59 -07:00
Samuel Roland
3880ab2230
Remove remoteRedirect() for profile website 2021-12-04 02:15:29 +01:00
Artur Mancha
f7f62b3df8
Create helpcenter.php
Added missing translation
2021-12-02 11:14:52 +01:00
Artur Mancha
821eb77f41
Create exception.php
Added missing translation
2021-12-02 11:11:48 +01:00
Artur Mancha
9d92608d2c
Update auth.php
Minor change
2021-12-02 11:07:47 +01:00
Artur Mancha
3904849e36
Update validation.php
Minor change
2021-12-02 11:07:26 +01:00
Artur Mancha
9fa1f5b634
Update timeline.php
Improved translation
2021-12-02 11:04:38 +01:00
Artur Mancha
5143a4b877
Update site.php
Improved translation
2021-12-02 11:04:11 +01:00
Artur Mancha
acc26b34a7
Update profile.php
Improved translation
2021-12-02 11:02:42 +01:00
Artur Mancha
6d3618d2cf
Update passwords.php
Improved translation
2021-12-02 10:57:58 +01:00
Artur Mancha
7c4ac2dbcc
Update notification.php
Improved translation
2021-12-02 10:54:01 +01:00
Artur Mancha
93392b85b2
Update navmenu.php
Pequena adição
2021-12-02 10:51:35 +01:00
Artur Mancha
d2518994b9
Update auth.php
Improved translation
2021-12-02 10:50:09 +01:00
Sven Fischer
b57e397d5e docker-apache: update Debian to Bullseye and composer to 2.1.14 2021-11-30 20:17:45 +01:00
a
ed2d3b1388 Update manifest with new icons, shortcuts, and categories 2021-11-16 07:18:56 -06:00
daniel
0c3691efc7
Merge pull request #3008 from pixelfed/staging
Update npm deps
2021-11-15 19:54:09 -07:00
Daniel Supernault
8dee519904
Update npm deps 2021-11-15 19:53:37 -07:00
daniel
36b7a63d14
Merge pull request #3006 from pixelfed/staging
Staging
2021-11-15 19:50:01 -07:00
Daniel Supernault
f4af875a1c
Update composer deps 2021-11-15 19:47:47 -07:00
Daniel Supernault
ed62a09f9a
Update lexer utils 2021-11-15 19:35:10 -07:00
Daniel Supernault
639e9859ed
Add AutolinkService 2021-11-15 19:33:52 -07:00
daniel
ec4482b02d
Merge pull request #3005 from pixelfed/staging
Staging
2021-11-10 22:39:35 -07:00
Daniel Supernault
d02a8f005f
Update login view 2021-11-10 22:12:46 -07:00
Daniel Supernault
9811dda7a7
Update login view 2021-11-10 22:09:06 -07:00
daniel
8a0ba77867
Merge pull request #3001 from scottaohara/patch-1
fix broken accessibility for search field
2021-11-10 22:05:21 -07:00
Daniel Supernault
4cb956aea2
Update AdminStatsService, add storage() method 2021-11-10 21:54:50 -07:00
daniel
59d76bb9b3
Merge pull request #2956 from h3xx/fix-psql-grouping
Fix grouping error in PostgreSQL
2021-11-10 21:53:30 -07:00
daniel
ceba6b90a1
Merge pull request #3004 from pixelfed/staging
Update Autospam service
2021-11-10 21:51:20 -07:00
Daniel Supernault
98bfa420c6
Update changelog 2021-11-10 21:46:59 -07:00
Daniel Supernault
ae8c751796
Update Autospam service, add mark all as read and mark all as not spam options and filter active, spam and not spamreports 2021-11-10 21:46:31 -07:00
Daniel Supernault
dff3dad1c8
Update AdminController, move report methods to AdminReports trait 2021-11-09 00:06:56 -07:00
daniel
d4d92187db
Merge pull request #3002 from pixelfed/staging
Staging
2021-11-08 23:26:52 -07:00
Daniel Supernault
a956b50e8e
Add account.email blade view 2021-11-08 23:22:55 -07:00
Daniel Supernault
e6d9437846
Update middleware 2021-11-08 23:21:53 -07:00
Daniel Supernault
0e892bb3fd
Update AccountController 2021-11-08 23:13:10 -07:00
Daniel Supernault
82a84a733b
Update changelog 2021-11-08 23:03:35 -07:00
Daniel Supernault
bc65938757
Add manual email verification requests 2021-11-08 23:02:34 -07:00
Scott O'Hara
345d7eed2c
fix broken accessibility for search field
This PR fixes the incorrect use of `role=search` on the `<input>` element.  The `search` role is not a type of text input, and its use here was negating the semantics of the text field's ability to announce itself as a text field to screen readers.  The proper place for a `role=search` is on the `<form>` element that contains this input, so I have placed it there in this fix.

There are likely other accessibility improvements that could be made in this file, but this was an easy fix that I figured I could make a quick PR for.  

Thanks
2021-11-06 10:25:46 -04:00
Daniel Supernault
9bd53524c7
Update setting layouts 2021-11-05 21:51:52 -06:00
Daniel Supernault
3d814de9a6
Update email settings view 2021-11-05 18:37:33 -06:00
daniel
7899242ee8
Merge pull request #2999 from pixelfed/staging
Update api routes
2021-11-03 23:47:28 -06:00
Daniel Supernault
cbd755082e
Update api routes 2021-11-03 23:47:00 -06:00
daniel
71706d1e45
Merge pull request #2998 from pixelfed/staging
Staging
2021-11-03 23:40:24 -06:00
Daniel Supernault
16f2758808
Update changelog 2021-11-03 23:37:59 -06:00
Daniel Supernault
2ae527c0f3
Update Status model, use AccountService to generate urls instead of loading profile relation 2021-11-03 23:29:12 -06:00
daniel
64112e41df
Merge pull request #2997 from pixelfed/staging
Staging
2021-11-03 19:41:37 -06:00
Daniel Supernault
694ce6ff3d
Update changelog 2021-11-03 19:41:12 -06:00
Daniel Supernault
aa2dd26c1b
Update PublicApiController, fix private account statuses api. Closes #2995 2021-11-03 19:40:46 -06:00
Tommi
74d2401cf5
Added “Run on YunoHost”
Pixelfed is super simple to self-host even for people with limited technical skills, thanks to YunoHost. Such a great advantage (which very few Fediverse social media have) should be made more visible, as in the [Wallabag repository](https://github.com/wallabag/wallabag#run-on-yunohost '“Run on YunoHost“ on Wallabag README on GitHub')
2021-10-31 00:04:43 +02:00
Daniel Supernault
0107e8fd68
Update Localization util, filter out .DS_Store 2021-10-30 00:56:48 -06:00
daniel
3d2ac8bce9
Merge pull request #2991 from pixelfed/staging
Staging
2021-10-30 00:13:30 -06:00
Daniel Supernault
4dda3b3763
Update changelog 2021-10-30 00:12:08 -06:00
Daniel Supernault
7df3540b08
Update verify_credentials api endpoint to improve performance 2021-10-30 00:11:35 -06:00
daniel
2a91b5321e
Merge pull request #2984 from pixelfed/staging
Staging
2021-10-22 19:03:07 -06:00
Daniel Supernault
a982d1900f
Update changelog 2021-10-22 19:01:45 -06:00
Daniel Supernault
2b254c8f94
Update compiled assets 2021-10-22 19:01:22 -06:00
Daniel Supernault
3169f68e65
Update dark mode styles, fix black box on stories. Closes #2982 2021-10-22 18:56:29 -06:00
daniel
14ee32adf0
Merge pull request #2983 from pixelfed/staging
Staging
2021-10-21 19:07:31 -06:00
Daniel Supernault
1e44a8128a
Update nav, fix link 2021-10-21 19:04:46 -06:00
Daniel Supernault
304cab98a7
Update changelog 2021-10-21 19:03:07 -06:00
Daniel Supernault
192553ff77
Update public timeline api, add experimental cache 2021-10-21 19:02:15 -06:00
Dan Church
b167873632
Fix grouping error in PostgreSQL 2021-10-20 14:11:33 -05:00
Daniel Supernault
37abcf3898
Update public timeline api, use cached sorted set and client side block/mute filtering 2021-10-20 04:31:07 -06:00
Daniel Supernault
be194b8a3f
Update Timeline component, apply block/mute filters client side for local and network timelines 2021-10-20 04:22:19 -06:00
Daniel Supernault
5167c68d6b
Update Activity component, fix missing types 2021-10-20 04:19:20 -06:00
Daniel Supernault
02237845b5
Update Profile component, improve error messages when block/mute limit reached 2021-10-20 04:17:46 -06:00
Daniel Supernault
bc16b936d6
Update StatusCard component 2021-10-20 04:16:39 -06:00
daniel
708a363c60
Merge pull request #2981 from pixelfed/staging
Staging
2021-10-19 23:40:27 -06:00
Daniel Supernault
4225f32af0
Update changelog 2021-10-19 23:38:34 -06:00
Daniel Supernault
eac6b76779
Update horizon config 2021-10-19 23:15:03 -06:00
Daniel Supernault
f0d4c17236
Update Autospam service, use silent classification for better user experience 2021-10-19 23:13:12 -06:00
Daniel Supernault
64c75bdaec
Update story views 2021-10-19 22:07:57 -06:00
daniel
2ecba8e144
Merge pull request #2979 from pixelfed/staging
Staging
2021-10-19 21:03:16 -06:00
daniel
1badbf83cd
Merge pull request #2973 from okpierre/patch-11
Improve tappable area
2021-10-19 20:58:46 -06:00
daniel
1d57085ba8
Merge pull request #2971 from HDVinnie/patch-1
Create SECURITY.md
2021-10-19 20:57:39 -06:00
daniel
ce489fac41
Merge pull request #2969 from nogafam-es/staging
Fix PWA not working on firefox because of wrong icon sizes
2021-10-19 20:57:09 -06:00
daniel
a53692f6bf
Merge pull request #2957 from h3xx/fix-memory-limit
Fix global memory limit set in constructor
2021-10-19 20:49:15 -06:00
daniel
78aee2d23c
Merge pull request #2948 from criadoperez/docker-compose-patch
docker-compose file fix
2021-10-19 20:46:52 -06:00
daniel
3d2c98c033
Merge pull request #2941 from rfonseca/rfonseca-patch-1
Fix starting check of username to allow numbers.
2021-10-19 20:45:55 -06:00
daniel
2aab326bc4
Merge pull request #2934 from vszakats/patch-1
fix 'powered by pixelfed' link to be HTTPS
2021-10-19 20:44:20 -06:00
daniel
2617ba93ed
Merge pull request #2928 from brabitom/dev
Improve Czech localization
2021-10-19 20:42:21 -06:00
daniel
f79486c448
Merge branch 'staging' into dev 2021-10-19 20:41:49 -06:00
daniel
853d62e83f
Merge pull request #2978 from pixelfed/staging
Staging
2021-10-19 20:30:30 -06:00
Daniel Supernault
bb1e3267a4
Update composer 2021-10-19 20:23:33 -06:00
Daniel Supernault
b8a49d457b
Update composer deps 2021-10-19 20:06:30 -06:00
Daniel Supernault
66f04bec25
Update InstanceService 2021-10-19 19:51:14 -06:00
Daniel Supernault
7a9a06e0fd
Update NotificationTransformer 2021-10-19 19:49:44 -06:00
Daniel Supernault
601992ed48
Update changelog 2021-10-19 19:47:23 -06:00
Daniel Supernault
ba5a01f8b8
Update compiled assets 2021-10-19 19:47:12 -06:00
Daniel Supernault
dbc3c0fbe3
Update timeago helper, add long form option 2021-10-19 19:44:41 -06:00
Daniel Supernault
7886fd5998
Update Activity component, only show context button for actionable activities 2021-10-19 19:43:40 -06:00
Daniel Supernault
f4bd5672b1
Update Timeline component, cascade relationship state change 2021-10-19 19:41:51 -06:00
Daniel Supernault
0436b124e4
Update StatusCard component, add relationship state button 2021-10-19 19:40:31 -06:00
Daniel Supernault
2609c86ad6
Update auth config, add throttle limit to password resets 2021-10-14 20:37:46 -06:00
okpierre
d500626295
Update index.blade.php 2021-10-14 19:14:46 -04:00
HDVinnie
530bac2ffa
Create SECURITY.md 2021-10-13 01:07:01 -04:00
nogafam.es Admin
2d493f5d5f Fix PWA not working on firefox because of wrong icon sizes 2021-10-10 15:56:22 +02:00
Daniel Supernault
e5454620e6
Update HomeTimeline api 2021-10-07 04:49:33 -06:00
Daniel Supernault
bef959f451
Update PublicApiController, use AccountService in accountStatuses method 2021-10-07 03:30:23 -06:00
Daniel Supernault
80d9b9399a
Refactor following & relationship logic. Replace FollowerObserver with FollowerService and added RelationshipService to cache results. Removed NotificationTransformer includes and replaced with cached services to improve performance and reduce database queries. 2021-10-07 03:27:13 -06:00
Daniel Supernault
0a8eb81bf0
Update InstanceActorController, improve json seralization by not escaping slashes 2021-10-07 00:46:20 -06:00
Daniel Supernault
4505d1f0f9
Update FederationController, move well-known to api middleware and cache webfinger lookups 2021-10-07 00:34:37 -06:00
Dan Church
c685e3e9f4
Fix global memory limit set in constructor
Instead, set it when running.

Having the set in the constructor was causing memory_limit to be changed
for "artisan queue:work" processes.
2021-09-27 15:09:00 -05:00
Daniel Supernault
754151dc84
Update RemotePost, RemoteProfile components, add fallback avatars 2021-09-22 20:17:34 -06:00
Daniel Supernault
0299aa5b63
Update video presenters, add playsinline attribute to video tags 2021-09-22 20:12:28 -06:00
Daniel Supernault
7b3e672d89
Update NoteTransformer, fix tag array 2021-09-22 20:04:23 -06:00
criadoperez
bbccfdeff2
docker-compose file fix 2021-09-21 17:08:19 +02:00
daniel
dbf314151e
Merge pull request #2946 from pixelfed/staging
Staging
2021-09-20 00:23:19 -06:00
daniel
8c6ef9bae2
Merge branch 'dev' into staging 2021-09-20 00:23:10 -06:00
Daniel Supernault
9b4b78ef31
Update changelog 2021-09-20 00:22:09 -06:00
Daniel Supernault
a900de2121
Update HttpSignatures, update instance actor headers. Fixes #2935 2021-09-20 00:21:16 -06:00
daniel
68437e614d
Staging (#2927)
* Created localized exception.php (DE)

* Update German translation

* Update German translation

* Update de localization, Create New Post -> New

* Formatting

* Update site.php (#2889)

Co-authored-by: daniel <danielsupernault@gmail.com>

* Formatting

* Update controllers, fixes #2906

* Update NotificationService, fix 500 bug

* Update changelog

Co-authored-by: forenta <ueblesurmeli-github@yahoo.de>
Co-authored-by: Tomas Brabenec <tomas@brabenec.net>
2021-09-19 23:10:18 -06:00
daniel
213bed7cd1
Merge branch 'dev' into staging 2021-09-19 23:10:00 -06:00
Daniel Supernault
3c7ac155f5
Update changelog 2021-09-19 23:07:01 -06:00
Daniel Supernault
4a609dc377
Update NotificationService, fix 500 bug 2021-09-19 23:05:56 -06:00
Rodrigo Fonseca
31330e0aed
Fix starting check of username to allow numbers.
The check for the first letter of username used to be !ctype_alpha, but the error message says "Must start with a letter or number." Updated check to be !ctype_alnum, to be coherent with the error message.
2021-09-16 22:36:30 -07:00
Viktor Szakats
275e711624
fix 'powered by pixelfed' link to be HTTPS 2021-09-15 05:00:22 +02:00
brabitom
09edaa87b7 Improve Czech localization 2021-09-11 10:17:40 +02:00
Daniel Supernault
ac6b3f07de
Update controllers, fixes #2906 2021-09-10 21:44:29 -06:00
Daniel Supernault
a1d7586be4
Formatting 2021-09-10 21:29:45 -06:00
Tomas Brabenec
9f0f5a3d46
Update site.php (#2889)
Co-authored-by: daniel <danielsupernault@gmail.com>
2021-09-10 21:28:59 -06:00
Daniel Supernault
65655eaa89
Formatting 2021-09-10 21:25:30 -06:00
daniel
42816bd329
Merge pull request #2916 from forenta/patch-3
Update German translation
2021-09-10 21:25:02 -06:00
Daniel Supernault
44dfeb8c9a
Update de localization, Create New Post -> New 2021-09-10 21:23:47 -06:00
daniel
121e8f99a7
Merge pull request #2915 from forenta/patch-2
Update German translation
2021-09-10 21:22:46 -06:00
daniel
df972ea157
Merge pull request #2914 from forenta/patch-1
Created localized exception.php (DE)
2021-09-10 21:20:22 -06:00
daniel
cdfb6ac25f
Merge pull request #2926 from pixelfed/staging
Staging
2021-09-10 21:08:20 -06:00
Daniel Supernault
0fc3a1d6a5
Formatting 2021-09-10 21:08:04 -06:00
daniel
14f9033715
Merge pull request #2868 from nogafam-es/staging
Fix PWA implementation to support Gecko-based browsers, using html link tag
2021-09-10 21:02:36 -06:00
daniel
931cafc9bc
Merge pull request #2858 from shynome/i18n_zh_cn
add zh-cn lang
2021-09-10 20:58:54 -06:00
daniel
907945a7a0
Merge pull request #2923 from Wv5twkFEKh54vo4tta9yu7dHa3/patch-1
Fix media upload
2021-09-10 20:55:55 -06:00
Wv5twkFEKh54vo4tta9yu7dHa3
13f65629e1 Fix error 500 on image upload
Without this missing import, the server threw "Class 'App\Http\Controllers\Api\UserSetting' not found" when trying to upload images.
2021-09-09 15:03:00 +02:00
daniel
1f1c5da1f5
Merge pull request #2919 from pixelfed/staging
v0.11.1
2021-09-07 21:21:55 -06:00
Daniel Supernault
18f3e08851
Update changelog 2021-09-07 21:21:20 -06:00
Daniel Supernault
fe42c0b245
Bump version 2021-09-07 21:17:39 -06:00
Daniel Supernault
d4df221afa
Update changelog 2021-09-07 21:17:07 -06:00
Daniel Supernault
48eb75baab
Update compiled assets 2021-09-07 21:16:17 -06:00
Daniel Supernault
0584f9ee95
Update ApiV1Controller, fix empty public timeline bug 2021-09-07 21:07:20 -06:00
Daniel Supernault
00c32360ae
Update StoryController, fix postgres bug. Fixes #2904 2021-09-06 01:41:24 -06:00
Daniel Supernault
32995064ec
Update Profile model 2021-09-06 00:29:06 -06:00
forenta
85847f7a9f
Update German translation 2021-09-05 21:23:24 +02:00
forenta
f0bcab4b41
Update German translation 2021-09-05 21:21:49 +02:00
forenta
236559a4d9
Created localized exception.php (DE) 2021-09-05 21:19:35 +02:00
daniel
687030090f
Merge pull request #2909 from pixelfed/staging
Update AccountService, fix status bug
2021-09-04 21:13:10 -06:00
Daniel Supernault
9e630851a3
Update AccountService, fix status bug 2021-09-04 21:12:44 -06:00
daniel
82c5e74feb
Merge pull request #2907 from pixelfed/staging
Update StoryService, fix division by zero bug
2021-09-04 18:07:12 -06:00
Daniel Supernault
c3ffd5cbd6
Update changelog 2021-09-04 18:05:43 -06:00
Daniel Supernault
6ae1ba0a64
Update StoryService, fix division by zero bug 2021-09-04 17:55:58 -06:00
daniel
35de09dcf3
Merge pull request #2902 from pixelfed/staging
Staging
2021-09-04 03:23:11 -06:00
Daniel Supernault
97ca139c76
Update changelog 2021-09-04 03:22:33 -06:00
Daniel Supernault
0f00be4d98
Update DirectMessageController, fix autocomplete bug 2021-09-04 03:22:05 -06:00
daniel
945568da6a
Merge pull request #2901 from pixelfed/staging
Update Profile, fix following count bug
2021-09-04 03:19:06 -06:00
Daniel Supernault
29d7514a55
Update changelog 2021-09-04 03:18:19 -06:00
Daniel Supernault
ee9f079551
Update Profile, fix following count bug 2021-09-04 03:17:54 -06:00
daniel
78a277d125
Merge pull request #2900 from pixelfed/staging
Staging
2021-09-04 02:00:02 -06:00
Daniel Supernault
d32d05eed0
Update story gc 2021-09-04 01:59:50 -06:00
Daniel Supernault
84382c8aa9
Update changelog 2021-09-04 01:56:08 -06:00
Daniel Supernault
7dee8f58fe
Update StoryComposeController, fix expiry bug 2021-09-04 01:55:29 -06:00
daniel
4ec357bfd0
Merge pull request #2899 from pixelfed/staging
Add remote story view
2021-09-04 00:15:37 -06:00
Daniel Supernault
8c86953f42
Add remote story view 2021-09-04 00:15:06 -06:00
daniel
81073f4269
Merge pull request #2898 from pixelfed/staging
Update AP Inbox
2021-09-04 00:12:29 -06:00
Daniel Supernault
1f45580f18
Update AP Inbox 2021-09-04 00:11:20 -06:00
daniel
373c5ec691
Merge pull request #2897 from pixelfed/staging
Fix story object route
2021-09-03 23:57:49 -06:00
Daniel Supernault
4c662ff55c
Fix story object route 2021-09-03 23:57:07 -06:00
daniel
cbeab42f54
Merge pull request #2896 from pixelfed/staging
Staging
2021-09-03 23:01:55 -06:00
Daniel Supernault
6b9197cca6
Update changelog 2021-09-03 23:00:58 -06:00
Daniel Supernault
e73cf531cb
Update InstanceCrawlPipeline, remove unused variable 2021-09-03 23:00:30 -06:00
daniel
f593e2b709
Merge pull request #2895 from pixelfed/staging
Archives, Polls and Stories
2021-09-03 22:52:17 -06:00
Daniel Supernault
588384543b
Update Story model, use immutable datetime 2021-09-03 22:46:06 -06:00
Daniel Supernault
4c5b48810f
Update compiled assets 2021-09-03 22:36:52 -06:00
Daniel Supernault
2d99434e2b
Update web routes 2021-09-03 22:26:39 -06:00
Daniel Supernault
6ca140e5cc
Update nav view 2021-09-03 22:20:55 -06:00
Daniel Supernault
a08c8a29d5
Update image optimizer 2021-09-03 22:09:58 -06:00
Daniel Supernault
2ab032599c
Update db config 2021-09-03 22:09:25 -06:00
Daniel Supernault
dd7262d841
Update StoryController, add StoryComposeController 2021-09-03 22:08:15 -06:00
Daniel Supernault
d0bfefe8d0
Update Media model 2021-09-03 21:25:19 -06:00
Daniel Supernault
d7b6edc018
Update NotificationTransformer 2021-09-03 21:23:43 -06:00
Daniel Supernault
3c8c23a143
Update AP Inbox 2021-09-03 21:21:17 -06:00
Daniel Supernault
c7a5715a60
Add StoryPipeline jobs 2021-09-03 21:18:33 -06:00
Daniel Supernault
0d8d6bc71e
Update FollowerService 2021-09-03 20:51:56 -06:00
Daniel Supernault
6b0b2cfaa5
Update StoryService 2021-09-03 20:51:26 -06:00
Daniel Supernault
b32f4d91c4
Update Snowflake service 2021-09-03 20:51:05 -06:00
Daniel Supernault
f808b7b19d
Story transformers 2021-09-03 20:50:38 -06:00
Daniel Supernault
37054e8393
migrations 2021-09-03 20:47:14 -06:00
Daniel Supernault
da6943daed
Add InstancePipeline and NodeinfoService 2021-09-03 20:45:56 -06:00
Daniel Supernault
7641b73158
Update Timeline, remove recent posts 2021-09-03 20:37:36 -06:00
Daniel Supernault
e5aea490b1
Refactor snowflake id generation to improve randomness 2021-09-01 22:46:57 -06:00
Daniel Supernault
e95b702e23
Add activitypub story validator 2021-09-01 03:34:41 -06:00
Daniel Supernault
e90637098a
Add Bearcap util 2021-09-01 01:21:47 -06:00
Daniel Supernault
0e13ab074c
Update SnowflakeService 2021-09-01 01:17:37 -06:00
Daniel Supernault
942fdf5486
Update components 2021-08-31 01:30:21 -06:00
Daniel Supernault
e5e7839736
Update js resources 2021-08-31 01:16:25 -06:00
Daniel Supernault
29e84fa08c
Update sass 2021-08-31 01:15:10 -06:00
Daniel Supernault
f575fd4d76
Update blade views 2021-08-31 01:12:16 -06:00
Daniel Supernault
bc2d664fdb
Update blade views 2021-08-31 01:09:09 -06:00
Daniel Supernault
ca5d964f5f
Update Profile model 2021-08-31 00:42:14 -06:00
Daniel Supernault
e1a3e26644
Update Story model 2021-08-31 00:41:43 -06:00
Daniel Supernault
2593cdeed9
Update Status model, add poll relation and allow up to 2 urls to autolink 2021-08-31 00:40:41 -06:00
Daniel Supernault
ef8e38298f
Update StatusService 2021-08-31 00:40:07 -06:00
Daniel Supernault
f9194df332
Update PollService 2021-08-31 00:39:32 -06:00
Daniel Supernault
2d93bc8b51
Update NotificationService 2021-08-31 00:39:03 -06:00
Daniel Supernault
07bc5d5c83
Update MediaStorageService 2021-08-31 00:38:43 -06:00
Daniel Supernault
450154e5af
Update MediaService 2021-08-31 00:38:27 -06:00
Daniel Supernault
427f9da33e
Update MediaPathService, change story paths 2021-08-31 00:38:07 -06:00
Daniel Supernault
168c19c5b6
Update LikeService 2021-08-31 00:37:40 -06:00
Daniel Supernault
2fb916c22b
Update InstanceService 2021-08-31 00:37:28 -06:00
Daniel Supernault
ad2db4aea7
Update FollowerService 2021-08-31 00:37:02 -06:00
Daniel Supernault
1c59933c0a
Update ProfileController 2021-08-31 00:35:29 -06:00
Daniel Supernault
a4a1270e70
Add Stories to admin dashboard 2021-08-31 00:33:04 -06:00
Daniel Supernault
0573213093
Add StoryService 2021-08-31 00:29:17 -06:00
Daniel Supernault
9a81a69fbb
Update compiled assets 2021-08-31 00:25:13 -06:00
Daniel Supernault
a0da80bc70
Update media gc command 2021-08-31 00:24:20 -06:00
Daniel Supernault
fee2857deb
Update ComposeController 2021-08-31 00:22:08 -06:00
Daniel Supernault
bdc5abbfbc
Update horizon config 2021-08-31 00:18:19 -06:00
Daniel Supernault
07e8ddf8eb
Add new horizon queue 2021-08-30 00:38:12 -06:00
Daniel Supernault
e1277d4081
Update StatusStatelessTransformer, cast snowflake ids as strings 2021-08-27 20:37:00 -06:00
Daniel Supernault
6832836c55
Update changelog 2021-08-27 20:36:01 -06:00
Daniel Supernault
d5e5644dbc
Migrations 2021-08-27 20:34:47 -06:00
Daniel Supernault
fb652805dd
Migrations 2021-08-27 20:33:30 -06:00
Daniel Supernault
bc53ac2af8
Update polls migration, add group support 2021-08-10 22:25:42 -06:00
Daniel Supernault
7709220074
Add Polls 2021-08-04 20:29:21 -06:00
Daniel Supernault
5916f8c76a
Update Profile model, fix getAudienceInbox method 2021-08-04 00:00:50 -06:00
Daniel Supernault
03199e2f68
Update follow intent, fix follower count leak 2021-08-01 15:09:52 -06:00
nogafam.es Admin
e56da35826 Fix PWA implementation to support standard using html link tag 2021-07-27 14:09:17 +02:00
Daniel Supernault
c5281dcdb3
Update PostComponents, re-add time to timestamp 2021-07-27 00:49:59 -06:00
Daniel Supernault
4fb3d1fa70
Update status.reply view, fix archived post leakage 2021-07-27 00:27:36 -06:00
Daniel Supernault
acaf630dee
Update StatusService, invalidate profile embed cache on deletion 2021-07-27 00:13:03 -06:00
Daniel Supernault
3f8acb1266
Update AccountService, add syncPostCount method 2021-07-26 23:59:38 -06:00
Daniel Supernault
b34814e9ff
Update compiled assets 2021-07-26 22:52:46 -06:00
Daniel Supernault
d4921209c5
Update changelog 2021-07-26 22:50:28 -06:00
Daniel Supernault
e9ef0c887a
Add Archive Posts 2021-07-26 22:49:46 -06:00
Daniel Supernault
6e45021fc2
Update StatusTransformer, prioritize scope over deprecated visibility attribute 2021-07-26 22:21:03 -06:00
Daniel Supernault
bc3add0525
Update ContactAdmin mail, set New Support Message subject 2021-07-26 19:23:55 -06:00
Daniel Supernault
15c4fdd90c
Update StatusService, add non-public option and improve cache invalidation 2021-07-26 19:02:11 -06:00
Daniel Supernault
ee0028bc57
Update PublicApiController, use account service 2021-07-26 18:47:40 -06:00
Daniel Supernault
2376580eb7
Update NotificationCard component 2021-07-25 06:15:10 -06:00
Daniel Supernault
ecbc464587
Update changelog 2021-07-25 06:02:28 -06:00
Daniel Supernault
22257cc2a7
Update FollowerService, cache audience 2021-07-25 05:56:35 -06:00
Daniel Supernault
38e5fc43eb
Add FollowObserver 2021-07-25 05:46:42 -06:00
Daniel Supernault
85d5639a52
Update Hashtag component 2021-07-25 05:32:42 -06:00
Daniel Supernault
c4146a3040
Update RemoteProfile component, add follower modals 2021-07-25 05:06:38 -06:00
Daniel Supernault
0e178a3371
Update NotifcationCard.vue component, add refresh button for cold notification cache 2021-07-25 03:33:47 -06:00
Daniel Supernault
7274574c68
Update RemoteProfile, add warning about potentially out of date information 2021-07-25 03:31:47 -06:00
Daniel Supernault
f6131ed764
Update License util, add nameToId method 2021-07-25 03:29:22 -06:00
Daniel Supernault
f3d6023ef8
Update LikeController, improve query perf 2021-07-25 03:19:48 -06:00
Daniel Supernault
14a1367a8f
Federate Media Licenses 2021-07-25 03:17:49 -06:00
Daniel Supernault
b6e226aef9
Update Notification components, fix old notifications with missing attributes 2021-07-25 02:14:41 -06:00
Daniel Supernault
f9516ac316
Update ApiControllers, use NotificationService 2021-07-25 02:12:30 -06:00
Daniel Supernault
bce8edd994
Update PublicApiController, improve accountStatuses api perf 2021-07-25 01:39:03 -06:00
Daniel Supernault
c1f14f89f6
Update FollowPipeline, fix cache invalidation bug 2021-07-25 01:36:57 -06:00
Daniel Supernault
1060dd23d5
Update RemotePost component, update likes reaction bar 2021-07-24 23:37:44 -06:00
Daniel Supernault
7c6cff3103
Update StatusTransformer 2021-07-24 23:24:56 -06:00
Daniel Supernault
1054b025b1
Update StatusTransformer 2021-07-24 23:16:01 -06:00
Daniel Supernault
09d5198c55
Update StatusTransformers, remove includes and use cached services 2021-07-24 23:10:44 -06:00
Daniel Supernault
8001ad998c
Update compiled assets 2021-07-24 22:21:27 -06:00
Daniel Supernault
7314065a2a
Update changelog 2021-07-24 22:20:44 -06:00
Daniel Supernault
2a791f1991
Update ApiV1Controller, add default license support 2021-07-24 22:20:05 -06:00
Daniel Supernault
833d110c9e
Update changelog 2021-07-24 22:14:58 -06:00
Daniel Supernault
ea0fc90c92
Add default licenses and license sync 2021-07-24 22:13:14 -06:00
Daniel Supernault
072d55d1a8
Update Compose Apis, make media descriptions/alt text length limit configurable. Default length: 1000 2021-07-24 21:15:15 -06:00
Daniel Supernault
67e3f6048f
Update Settings, add default license and enforced media descriptions 2021-07-23 09:47:14 -06:00
daniel
eb84b6547d
Merge pull request #2865 from pixelfed/staging
Update LikeController, add UndoLikePipeline and federate Undo Like ac…
2021-07-21 03:45:10 -06:00
Daniel Supernault
27778e00c9
Update changelog 2021-07-21 03:44:57 -06:00
Daniel Supernault
8ac8fcad3f
Update LikeController, add UndoLikePipeline and federate Undo Like activities 2021-07-21 03:41:28 -06:00
daniel
87a6f2cd6f
Merge pull request #2864 from pixelfed/staging
Update ApiController, fix notification bug
2021-07-21 02:17:05 -06:00
Daniel Supernault
f39f32c866
Update ApiController, fix notification bug 2021-07-21 02:16:20 -06:00
daniel
a55d9bf6fd
Merge pull request #2863 from pixelfed/staging
Update Timeline, make text-only posts opt-in by default
2021-07-21 02:03:50 -06:00
Daniel Supernault
aa5b090af8
Update changelog 2021-07-21 02:03:16 -06:00
Daniel Supernault
0153ed6d64
Update Timeline, make text-only posts opt-in by default 2021-07-21 02:00:57 -06:00
daniel
d6d6e6dc75
Merge pull request #2862 from pixelfed/staging
Update PublicApiController, remove text only posts from Public timeline
2021-07-21 01:20:24 -06:00
Daniel Supernault
08f492bd3d
Update PublicApiController, remove text only posts 2021-07-21 01:19:53 -06:00
daniel
b8864b9ae2
Merge pull request #2861 from pixelfed/staging
Staging
2021-07-21 01:18:33 -06:00
Daniel Supernault
68509e4f4f
Update compiled assets 2021-07-21 01:17:40 -06:00
Daniel Supernault
53a80255d0
Update changelog 2021-07-21 01:17:04 -06:00
Daniel Supernault
b0257be20e
Update RemotePost.vue, improve text only post UI 2021-07-21 01:16:37 -06:00
Daniel Supernault
86219b57fc
Update PublicApiController, filter out text replies on home timeline 2021-07-21 01:03:43 -06:00
daniel
a4b697741b
Merge pull request #2860 from pixelfed/staging
Staging
2021-07-20 23:49:37 -06:00
Daniel Supernault
5fb187a062
Update changelog 2021-07-20 23:48:44 -06:00
Daniel Supernault
2fea6ed9ec
Update compiled assets 2021-07-20 23:48:33 -06:00
Daniel Supernault
ed14ee48e1
Update StatusCard, add text support 2021-07-20 23:47:19 -06:00
daniel
e86b4d8007
Merge pull request #2859 from pixelfed/staging
Staging
2021-07-20 23:17:26 -06:00
Daniel Supernault
9f4f983f27
Update ap helpers, set text type when appropriate 2021-07-20 22:17:31 -06:00
Daniel Supernault
f392fa4ed4
Update nav 2021-07-20 21:52:49 -06:00
Daniel Supernault
32268efbc9
Update changelog 2021-07-20 18:15:36 -06:00
Daniel Supernault
4778053f88
Update compiled assets 2021-07-20 18:15:30 -06:00
Daniel Supernault
11eb6acdaf
Update Timeline, fix empty timeline card 2021-07-20 18:13:17 -06:00
Daniel Supernault
7ed65fc9fe
Update PostComponent, use profileUrl method for comments 2021-07-20 18:06:57 -06:00
shynome
e825d73b9a add zh-cn lang 2021-07-18 18:19:11 +08:00
daniel
dd5d767879
Merge pull request #2857 from pixelfed/staging
Update Timeline.vue, improve followed hashtags
2021-07-16 01:26:01 -06:00
Daniel Supernault
c571bd90ef
Update compiled assets 2021-07-16 01:24:45 -06:00
Daniel Supernault
d32ef1dc9d
Update changelog 2021-07-16 01:24:31 -06:00
Daniel Supernault
728f10d778
Update Timeline.vue, improve followed hashtags 2021-07-16 01:20:21 -06:00
daniel
26ca28b4fd
Merge pull request #2856 from pixelfed/staging
Staging
2021-07-15 23:43:35 -06:00
Daniel Supernault
a2494da95a
Update compiled assets 2021-07-15 23:39:45 -06:00
Daniel Supernault
91682473a5
Update changelog 2021-07-15 23:37:55 -06:00
Daniel Supernault
49d27caae8
Update remote profile components 2021-07-15 23:37:23 -06:00
Daniel Supernault
916e8f7111
Update Timeline.vue, increase pagination limit from 3 to 12 and add empty feed placeholder 2021-07-15 23:36:44 -06:00
Daniel Supernault
86422c81b7
Update presenters, improve content warnings 2021-07-15 23:35:34 -06:00
Daniel Supernault
8544bcbda6
Update ContextMenu, add View Profile link 2021-07-15 23:18:40 -06:00
Daniel Supernault
ca6e491c83
Update PublicApiController, use fUserFilterService in public timeline endpoint 2021-07-15 22:39:40 -06:00
daniel
f33422e758
Merge pull request #2855 from pixelfed/staging
Staging
2021-07-15 20:53:02 -06:00
Daniel Supernault
2fe676aa55
Update changelog 2021-07-15 20:50:27 -06:00
Daniel Supernault
135474ae11
Update DiscoverController, use UserFilterService on trendingApi 2021-07-15 20:48:39 -06:00
daniel
e4b47d001c
Merge pull request #2853 from pixelfed/staging
Staging
2021-07-13 23:31:27 -06:00
Daniel Supernault
51bde8ff33
Update changelog 2021-07-13 23:10:25 -06:00
Daniel Supernault
7f4213924f
Update job queue, separate deletes into their own queue 2021-07-13 23:09:50 -06:00
Daniel Supernault
3b071e56ac
Update Timeline, fix padding 2021-07-12 10:47:24 -06:00
Daniel Supernault
c8824d1b51
Update FollowerService 2021-07-11 07:43:29 -06:00
Daniel Supernault
ff48400a38
Update components, add fallback default avatar 2021-07-11 06:07:05 -06:00
Daniel Supernault
f9ba7ed57a
Update changelog 2021-07-11 03:08:34 -06:00
Daniel Supernault
fc93cba96e
Update compiled assets 2021-07-11 03:07:57 -06:00
Daniel Supernault
726553f552
Update components, add fallback default avatar 2021-07-11 02:57:14 -06:00
Daniel Supernault
57e0a741ab
Add FanoutDeletePipeline 2021-07-09 02:07:14 -06:00
Daniel Supernault
5fb33772c3
Update reply blade view, fix missing avatar and media images 2021-07-08 23:23:56 -06:00
daniel
ae995bf4d3
Merge pull request #2850 from pixelfed/staging
Staging
2021-07-08 22:29:56 -06:00
Daniel Supernault
5634ba179d
Update changelog 2021-07-08 22:28:52 -06:00
Daniel Supernault
f33247c30a
Update compiled assets 2021-07-08 22:25:24 -06:00
Daniel Supernault
fc56acb835
Update presenters, fix content warning layout 2021-07-08 22:23:55 -06:00
Daniel Supernault
9607243ff7
Update StatusCard.vue, add togglecw events to other presenters 2021-07-08 22:21:47 -06:00
Daniel Supernault
0f542065bd
Update changelog 2021-07-08 22:20:59 -06:00
Daniel Supernault
b37bb426a2
Update NotificationCard, fix typo in mention, share and comments. Fixes #2848 2021-07-08 22:20:28 -06:00
Daniel Supernault
dd8661a09a
Update changelog 2021-07-08 21:31:40 -06:00
Daniel Supernault
26b9c1401c
Update ActivityPub helpers, fix comment threading in statusFetch() method 2021-07-08 21:30:59 -06:00
daniel
52b1a1ca39
Merge pull request #2849 from pixelfed/staging
Update PublicApiController, fix public timeline filtering
2021-07-08 17:35:05 -06:00
Daniel Supernault
9650b668e9
Update PublicApiController, fix public timeline filtering 2021-07-08 17:33:51 -06:00
daniel
567a4b6b04
Merge pull request #2847 from pixelfed/staging
Update PublicTimelineService
2021-07-07 01:59:05 -06:00
Daniel Supernault
08467d52d5
Update PublicTimelineService 2021-07-07 01:58:39 -06:00
daniel
a35759cef1
Merge pull request #2846 from pixelfed/staging
Update StatusEntityLexer, only add specific status types to PublicTimelineService
2021-07-07 01:55:13 -06:00
Daniel Supernault
7b758135a3
Update changelog 2021-07-07 01:54:23 -06:00
Daniel Supernault
1fdcbe5bf9
Update StatusEntityLexer, only add specific status types to PublicTimelineService 2021-07-07 01:53:28 -06:00
daniel
5378031f8b
Merge pull request #2845 from pixelfed/staging
Update Timeline.vue, fix comment button
2021-07-07 01:49:07 -06:00
Daniel Supernault
f09845bc0b
Update changelog 2021-07-07 01:48:42 -06:00
Daniel Supernault
5a01d5f5bd
Update compiled assets 2021-07-07 01:48:31 -06:00
Daniel Supernault
b6b5ce7c76
Update Timeline.vue, fix comment button 2021-07-07 01:47:16 -06:00
daniel
941a5d0d2d
Merge pull request #2844 from pixelfed/staging
Staging
2021-07-07 01:12:10 -06:00
Daniel Supernault
8a683a9285
Update changelog 2021-07-07 01:07:30 -06:00
Daniel Supernault
a9565ad558
Update compiled assets 2021-07-07 01:07:02 -06:00
Daniel Supernault
4fe42e5b57
Update PublicApiController, improve home timeline perf 2021-07-07 01:06:20 -06:00
Daniel Supernault
9017f7c4e1
Update Profile, fix unauthenticated private profiles 2021-07-07 00:40:01 -06:00
Daniel Supernault
1a2e41b1e3
Update status views, remove like counts from status embed 2021-07-07 00:23:43 -06:00
Daniel Supernault
0e7b2617d4
Update compiled assets 2021-07-06 23:49:31 -06:00
Daniel Supernault
2d6c335f1f
Update changelog 2021-07-06 23:40:35 -06:00
Daniel Supernault
42c6121ab9
Update PostComponent, fix MomentUI like counter 2021-07-06 23:40:08 -06:00
Daniel Supernault
2e54643760
Update changelog 2021-07-06 23:36:56 -06:00
Daniel Supernault
f2686cacd2
Update AdminMediaController, improve perf and use simple pagination 2021-07-06 23:36:25 -06:00
Daniel Supernault
4a30ff118d
Update api routes 2021-07-06 21:21:16 -06:00
daniel
83c0275832
Merge pull request #2797 from ahangarha/improve-persian-translation
Improve Persian Translation
2021-07-06 21:20:14 -06:00
daniel
25fb62e43f
Merge pull request #2843 from pixelfed/staging
Update RemotePost.vue, fix content warning button.
2021-07-06 21:15:09 -06:00
Daniel Supernault
7dbf6690f5
Update changelog 2021-07-06 21:14:29 -06:00
Daniel Supernault
fe5a0d2697
Update compiled assets 2021-07-06 21:14:15 -06:00
Daniel Supernault
7647e724bc
Update RemotePost.vue, fix content warning button 2021-07-06 21:12:37 -06:00
daniel
d2545b2850
Merge pull request #2842 from pixelfed/staging
API perf improvements
2021-07-06 20:45:55 -06:00
Daniel Supernault
3a0d88e2b4
Update changelog 2021-07-06 20:43:53 -06:00
Daniel Supernault
352aa57346
Update ApiV1Controller, use ProfileService for verify_credentials 2021-07-06 20:43:24 -06:00
Daniel Supernault
f67c67bce1
Update ApiV1Controller, use PublicTimelineService 2021-07-06 20:29:52 -06:00
Daniel Supernault
d43e6d8d07
Update NotificationService, use zrevrangebyscore for api 2021-07-06 20:10:42 -06:00
Daniel Supernault
76b6ec2aa6
Update admin instance view 2021-07-06 19:36:43 -06:00
Daniel Supernault
918016a5ad
Update PublicTimelineService 2021-07-06 02:01:24 -06:00
daniel
2ee4b5f853
Merge pull request #2838 from pixelfed/staging
Update PublicApiController
2021-07-05 23:46:11 -06:00
Daniel Supernault
562726bc17
Update changelog 2021-07-05 23:44:15 -06:00
Daniel Supernault
38726d699f
Update compiled assets 2021-07-05 23:43:33 -06:00
Daniel Supernault
950fc27773
Update components 2021-07-05 23:42:46 -06:00
Daniel Supernault
51a277e1ae
Update StatusHashtagService, fix null status bug 2021-07-05 23:40:54 -06:00
Daniel Supernault
f215ee26b3
Update moderator api, expire cached status 2021-07-05 23:00:59 -06:00
Daniel Supernault
78529cb1f8
Update PublicApiController 2021-07-02 03:05:33 -06:00
daniel
2158b2ffdf
Merge pull request #2837 from pixelfed/staging
Staging
2021-07-02 01:45:34 -06:00
Daniel Supernault
af19dd7bb0
Update compiled assets 2021-07-02 01:36:16 -06:00
Daniel Supernault
2bdb638c4f
Update Timeline component, add reaction bar back to network timeline 2021-07-02 01:36:01 -06:00
Daniel Supernault
645f0eef08
Update changelog 2021-07-02 01:35:26 -06:00
Daniel Supernault
82895591c3
Update PublicApiController, add LikeService to Network timeline 2021-07-02 01:34:35 -06:00
Daniel Supernault
d3157f2a2d
Add LikeObserver 2021-07-02 01:33:49 -06:00
Daniel Supernault
447e44e5ac
Update LikeService 2021-07-02 01:31:45 -06:00
Daniel Supernault
117b8410eb
Update PublicTimelineService, skip adds for now 2021-07-02 01:21:21 -06:00
daniel
3c1c7a3b26
Merge pull request #2836 from pixelfed/staging
Add diagnostics to error page and admin dashboard
2021-07-01 21:49:58 -06:00
Daniel Supernault
52ed8bf4ea
Add changelog 2021-07-01 21:47:48 -06:00
Daniel Supernault
64725ecce7
Add diagnostics to error page and admin dashboard 2021-07-01 21:46:45 -06:00
daniel
28f9fe588a
Merge pull request #2835 from pixelfed/staging
Staging
2021-07-01 17:31:29 -06:00
Daniel Supernault
2a38201acb
Update compiled assets 2021-07-01 17:27:07 -06:00
Daniel Supernault
53ae9ca0d9
Update changelog 2021-07-01 17:26:50 -06:00
Daniel Supernault
3cffdb116f
Update ContextMenu, add missing statusUrl method 2021-07-01 17:26:14 -06:00
daniel
a8507dd813
Merge pull request #2833 from pixelfed/staging
Update ApiController, fix hashtag endpoint
2021-06-30 17:11:07 -06:00
Daniel Supernault
9ac8bbde91
Update ApiController, fix hashtag endpoint 2021-06-30 17:10:33 -06:00
daniel
80a3bc1461
Merge pull request #2830 from pixelfed/staging
Staging
2021-06-29 03:07:29 -06:00
Daniel Supernault
076ec333a9
Update compiled assets 2021-06-29 03:06:21 -06:00
Daniel Supernault
8ca685e6ab
Update changelog 2021-06-29 03:06:06 -06:00
Daniel Supernault
e9c46bab75
Update PostComponent, show like count to owner using MomentUI 2021-06-29 03:04:56 -06:00
daniel
8513b6d1e5
Merge pull request #2829 from pixelfed/staging
Staging
2021-06-29 02:17:06 -06:00
Daniel Supernault
c0a2f88b49
Update compiled assets 2021-06-29 02:15:30 -06:00
Daniel Supernault
bae8075ef1
Update changelog 2021-06-29 02:15:15 -06:00
Daniel Supernault
6d956a86f4
Add mark as spammer mod tool, unlists and applies content warning to existing and future posts 2021-06-29 02:14:22 -06:00
Daniel Supernault
7b9e0eefd3
Update InternalApiController formatting 2021-06-29 01:46:04 -06:00
daniel
48dd65144d
Merge pull request #2828 from pixelfed/staging
Staging
2021-06-29 01:30:28 -06:00
Daniel Supernault
a90dc247e9
Update changelog 2021-06-29 01:29:04 -06:00
Daniel Supernault
5be8a36ab2
Update compiled assets 2021-06-29 01:28:24 -06:00
Daniel Supernault
0deaafc0dc
Update Network Timeline, use existing Timeline component 2021-06-29 01:27:35 -06:00
Daniel Supernault
e5f683fda4
Update PublicApiController, improve network timeline perf 2021-06-29 01:23:24 -06:00
Daniel Supernault
1e3d3a69d2
Update Timeline, disable new post update checker and hide reaction bar on network timeline 2021-06-29 01:03:47 -06:00
daniel
66605c0a3d
Merge pull request #2827 from pixelfed/staging
Add UndoAnnounce transformer
2021-06-28 23:26:14 -06:00
Daniel Supernault
3ba31bdc7c
Add UndoAnnounce transformer 2021-06-28 23:25:49 -06:00
daniel
b79e395284
Merge pull request #2826 from pixelfed/staging
Update Timeline, fix suggested posts
2021-06-28 23:20:12 -06:00
Daniel Supernault
c9d1c8e937
Update compiled assets 2021-06-28 23:19:31 -06:00
Daniel Supernault
b530a3bd08
Update changelog 2021-06-28 23:18:56 -06:00
Daniel Supernault
3ba5c88c35
Update Timeline, fix suggested posts 2021-06-28 23:18:28 -06:00
daniel
d9578fa56d
Merge pull request #2825 from pixelfed/staging
Staging
2021-06-28 22:58:52 -06:00
Daniel Supernault
c1a6f7c4d2
Update changelog 2021-06-28 22:53:57 -06:00
Daniel Supernault
dbe730236a
Update compiled assets 2021-06-28 22:53:47 -06:00
Daniel Supernault
858f3f9e0c
Update Timeline component, abstracted reusable partials 2021-06-28 22:51:51 -06:00
Daniel Supernault
a0e83468e9
Update PostComponent, fix formatting 2021-06-28 22:50:40 -06:00
Daniel Supernault
308acc91d3
Update NetworkTimeline, fix remote comment urls 2021-06-28 22:49:22 -06:00
Daniel Supernault
f2fb64ecd4
Add component partials 2021-06-28 22:48:23 -06:00
Daniel Supernault
c8e40e0fd3
Update SharePipeline, add Undo->Announce support 2021-06-28 22:37:38 -06:00
Daniel Supernault
f1208de0ef
Update ApiController, fix nulls in hashtag endpoint 2021-06-28 21:01:45 -06:00
Daniel Supernault
d48ebb829c
Update StatusController, improve share api perf (11s to 72ms) 2021-06-28 20:19:57 -06:00
daniel
d2ff41db05
Merge pull request #2817 from pixelfed/staging
Staging
2021-06-24 20:35:52 -06:00
Daniel Supernault
3741c76da3
Update LikeService, skip self likes 2021-06-23 21:26:45 -06:00
Daniel Supernault
cc47243733
Update Inbox 2021-06-23 21:21:33 -06:00
daniel
5ee61f7c80
Merge pull request #2815 from pixelfed/staging
Staging
2021-06-23 20:02:57 -06:00
Daniel Supernault
c8eb326ef4
Update changelog 2021-06-23 20:01:24 -06:00
Daniel Supernault
8f7f1956fb
Update composer deps 2021-06-23 19:57:52 -06:00
Daniel Supernault
929ff5eb01
Update Inbox, fix tombstone bug 2021-06-23 19:57:39 -06:00
daniel
a6ea480f84
Merge pull request #2811 from pixelfed/staging
Staging
2021-06-18 06:21:40 -06:00
Daniel Supernault
5a3384d0e4
wip 2021-06-18 06:19:45 -06:00
Daniel Supernault
49d68cdd96
Update compiled assets 2021-06-18 06:19:36 -06:00
Daniel Supernault
9a2db8ebc0
Update Activity component, fix comment bug 2021-06-18 06:18:35 -06:00
daniel
4e86f76ab9
Merge pull request #2810 from pixelfed/staging
Staging
2021-06-18 05:31:33 -06:00
Daniel Supernault
04b6eb94ca
Update compiled assets 2021-06-18 05:30:53 -06:00
Daniel Supernault
34bbb2d1a2
Update changelog 2021-06-18 05:30:03 -06:00
Daniel Supernault
a3a86d46a1
Update NotificationCard, fix missing status bug 2021-06-18 05:29:37 -06:00
Daniel Supernault
ef63124d88
Update Inbox, delete notifications on tombstone 2021-06-18 05:24:26 -06:00
daniel
e24c11c5ff
Merge pull request #2809 from pixelfed/staging
Staging
2021-06-18 05:08:21 -06:00
Daniel Supernault
cfccb6f251
Update changelog 2021-06-18 05:06:39 -06:00
Daniel Supernault
2cc577a6be
Update compiled assets 2021-06-18 05:06:24 -06:00
Daniel Supernault
1fa08644b4
Update Remote Post + Profile hashtag to redirect to local urls 2021-06-18 05:03:40 -06:00
daniel
24905614ff
Merge pull request #2808 from pixelfed/staging
Staging
2021-06-18 02:58:36 -06:00
Daniel Supernault
27fbe88438
Update changelog 2021-06-18 02:58:03 -06:00
Daniel Supernault
c8c6b98380
Update like api, store status_profile_id and is_comment 2021-06-18 02:57:08 -06:00
Daniel Supernault
8edd829436
Update AP Helpers, generate notification for remote replies 2021-06-18 02:55:42 -06:00
daniel
4f2eb712ad
Merge pull request #2807 from pixelfed/staging
Staging
2021-06-17 22:44:39 -06:00
Daniel Supernault
79324679f1
Update compiled assets 2021-06-17 22:42:42 -06:00
Daniel Supernault
78ee44662c
Add PWA + ServiceWorker with offline support 2021-06-17 22:42:08 -06:00
daniel
1ede2ef8c1
Merge pull request #2805 from pixelfed/staging
Staging
2021-06-13 02:16:52 -06:00
Daniel Supernault
22c321a998
Update changelog 2021-06-13 02:16:25 -06:00
Daniel Supernault
f0bb79e249
Update compiled assets 2021-06-13 02:16:14 -06:00
Daniel Supernault
02b04a4b4d
Update RemoteProfile component, implement pagination 2021-06-13 01:52:05 -06:00
daniel
963bea595b
Merge pull request #2804 from pixelfed/staging
Staging
2021-06-13 00:49:12 -06:00
Daniel Supernault
ab985167f9
Update readme 2021-06-13 00:48:51 -06:00
Daniel Supernault
4abb2b1e92
Update compiled assets 2021-06-13 00:42:49 -06:00
Daniel Supernault
0e0d50aa8f
Update changelog 2021-06-13 00:39:07 -06:00
Daniel Supernault
f37952d6cf
Update verify email screen, add contact admin link 2021-06-13 00:37:54 -06:00
Daniel Supernault
d0ddb4e78e
Update StoryCompose component 2021-06-13 00:19:59 -06:00
Daniel Supernault
6e56dbed1a
Update Profile component, fix remote urls 2021-06-13 00:19:01 -06:00
daniel
79d792dacc
Merge pull request #2803 from pixelfed/staging
Update StoryController, fix expiration time bug
2021-06-12 20:02:00 -06:00
Daniel Supernault
1e424f023a
Update changelog 2021-06-12 20:01:31 -06:00
Daniel Supernault
39e57f9506
Update StoryController, fix expiration time bug 2021-06-12 20:00:59 -06:00
daniel
95037d4d88
Merge pull request #2800 from pixelfed/staging
Update routes, add legacy webfinger profile redirect
2021-06-11 00:06:04 -06:00
Daniel Supernault
174260787e
Update changelog 2021-06-11 00:05:15 -06:00
Daniel Supernault
93c7af7464
Update routes, add legacy webfinger profile redirect 2021-06-11 00:03:41 -06:00
daniel
ed3ba496dc
Merge pull request #2799 from pixelfed/staging
Update Profile, add linkified bio, joined date, follows you label and improved website handling
2021-06-10 22:59:20 -06:00
Daniel Supernault
07321b021c
Update changelog 2021-06-10 22:57:28 -06:00
Daniel Supernault
afe901fffa
Update compiled assets 2021-06-10 22:57:15 -06:00
Daniel Supernault
8ee104363a
Update Profile, add linkified bio, joined date, follows you label and improved website handling 2021-06-10 22:54:31 -06:00
daniel
8dc6d17bfd
Merge pull request #2798 from pixelfed/staging
Auto Following support for admins
2021-06-10 21:11:33 -06:00
Daniel Supernault
fbd77f4410
Update changelog 2021-06-10 21:08:28 -06:00
Daniel Supernault
68aa25400b
Add Auto Following support for admins 2021-06-10 21:07:35 -06:00
Mostafa Ahangarha
2e2a946c64 improve persian translation 2021-06-10 13:29:26 +04:30
daniel
7a234ccd4c
Merge pull request #2791 from pixelfed/staging
Add WebP support
2021-06-08 03:28:48 -06:00
Daniel Supernault
9928bfc273
Update changelog 2021-06-08 03:22:46 -06:00
Daniel Supernault
069a0e4ae1
Add WebP support 2021-06-08 03:22:01 -06:00
daniel
b46035df87
Merge pull request #2790 from pixelfed/staging
Update Activity component, add at (@) symbol for remote profiles and local urls for remote posts and profile
2021-06-08 01:50:20 -06:00
Daniel Supernault
1baf378497
Update changelog 2021-06-08 01:49:26 -06:00
Daniel Supernault
b230834d25
Update compiled assets 2021-06-08 01:49:18 -06:00
Daniel Supernault
a2211815d4
Update Activity component, add at (@) symbol for remote profiles and local urls for remote posts and profile 2021-06-08 01:48:33 -06:00
daniel
23e46bcfd1
Merge pull request #2789 from pixelfed/staging
Update Notification component, add at (@) symbol for remote profiles and local urls for remote posts and profiles
2021-06-08 01:32:41 -06:00
Daniel Supernault
a20931eeb4
Update changelog 2021-06-08 01:31:07 -06:00
Daniel Supernault
06e7ef16c6
Update compiled assets 2021-06-08 01:30:53 -06:00
Daniel Supernault
aafd6a21a5
Update Notification component, add at (@) symbol for remote profiles and local urls for remote posts and profiles 2021-06-08 01:29:44 -06:00
daniel
004eff1625
Merge pull request #2788 from pixelfed/staging
Staging
2021-06-07 23:34:53 -06:00
Daniel Supernault
c27da498db
Update changelog 2021-06-07 23:33:36 -06:00
Daniel Supernault
65259e1135
Update compiled assets 2021-06-07 23:33:20 -06:00
Daniel Supernault
66750d347a
Update Timeline, implement suggested post opt out 2021-06-07 23:32:35 -06:00
Daniel Supernault
549202948d
Update landing page, use config_cache 2021-06-07 19:28:26 -06:00
daniel
658411f8b4
Merge pull request #2787 from pixelfed/staging
Staging
2021-06-07 17:36:56 -06:00
Daniel Supernault
2d5a259e6f
Update changelog 2021-06-07 17:18:56 -06:00
Daniel Supernault
20ec870bf9
Update PrettyNumber, fix deprecated warning 2021-06-07 17:18:06 -06:00
daniel
a34fe00114
Merge pull request #2776 from pixelfed/staging
v0.11.0
2021-06-01 23:54:19 -06:00
Daniel Supernault
91cc9adf52
Update changelog 2021-06-01 23:50:32 -06:00
Daniel Supernault
a4121cb8e4
Bump version 2021-06-01 23:50:21 -06:00
Daniel Supernault
b8f4385d40
Update compiled assets 2021-06-01 23:47:01 -06:00
Daniel Supernault
56215be73a
Update Timeline, add recent home feed 2021-06-01 23:40:34 -06:00
Daniel Supernault
2d0a253e07
Update DirectMessageController, disable exception logging for invalid urls. Fixes #2752 2021-06-01 21:18:02 -06:00
Daniel Supernault
69567e19df
Update NotificationCard, fix loading bug 2021-06-01 21:01:00 -06:00
Daniel Supernault
4662afd248
Update pixelfed config formatting 2021-06-01 20:09:25 -06:00
Daniel Supernault
7d24560dd0
Update app config, change default descriptions 2021-06-01 20:07:52 -06:00
Daniel Supernault
84520fe103
Update PrettyNumber, add decimal option 2021-06-01 20:07:07 -06:00
Daniel Supernault
28df9f7e80
Update Inbox, fix reply/comment bug by moving attachment validation to Note with attachments 2021-06-01 20:06:18 -06:00
Daniel Supernault
1e230e80fb
Update PublicApiController, add recent feed support to home timeline 2021-06-01 20:03:59 -06:00
Daniel Supernault
f3bf2fd41e
Update StatusController, add cache invalidation for timeline cursor 2021-06-01 19:51:22 -06:00
Daniel Supernault
7cbd6bc36d
Update StatusController, use transactions for status views 2021-05-31 23:54:20 -06:00
Daniel Supernault
e3899d3684
Fix migration sqlite support 2021-05-31 23:26:40 -06:00
daniel
014d6144f8
Merge pull request #2775 from pixelfed/staging
Update liked by, fix remote username urls
2021-05-31 22:46:15 -06:00
Daniel Supernault
cbdfb69436
Update compiled assets 2021-05-31 22:45:16 -06:00
Daniel Supernault
0affcb0a68
Update changelog 2021-05-31 22:44:43 -06:00
Daniel Supernault
f767d99ad5
Update liked by, fix remote username urls 2021-05-31 22:43:53 -06:00
daniel
d230ab9aa4
Merge pull request #2774 from pixelfed/staging
Staging
2021-05-31 21:52:15 -06:00
Daniel Supernault
d496a48574
Add admin.js, fixes #2772 2021-05-31 21:51:23 -06:00
Daniel Supernault
821b58bbb3
Update composer.lock deps 2021-05-31 21:50:18 -06:00
Daniel Supernault
3c1e1f7e87
Update PublicApiController, fix likes bug 2021-05-31 21:49:44 -06:00
daniel
bbb1ffe821
Merge pull request #2771 from pixelfed/staging
Staging
2021-05-27 23:09:31 -06:00
Daniel Supernault
525dce1c59
Update changelog 2021-05-27 23:07:30 -06:00
Daniel Supernault
7e0be15404
Update exceptions handler 2021-05-27 23:06:47 -06:00
Daniel Supernault
178ed63d0a
Update AuthServiceProvider, increase default token + refresh token lifetime 2021-05-27 23:04:24 -06:00
Daniel Supernault
0788bffa37
Update Timeline component, show counts and make sidebar footer lighter 2021-05-26 20:31:11 -06:00
Daniel Supernault
78ad4e77d9
Update NotificationCard component, fix default value 2021-05-26 20:29:33 -06:00
Daniel Supernault
224bda1760
Update changelog 2021-05-26 20:12:24 -06:00
Daniel Supernault
c8e43c6094
Update api, remove auth requirement for hashtag timeline 2021-05-26 20:11:25 -06:00
daniel
bfe6969dae
Merge pull request #2770 from pixelfed/staging
Update AdminStatsService, fix postgres bug
2021-05-25 01:37:13 -06:00
Daniel Supernault
55c3eaf26d
Update changelog 2021-05-25 01:36:45 -06:00
Daniel Supernault
af71913597
Update AdminStatsService, fix postgres bug 2021-05-25 01:36:13 -06:00
daniel
f68b93a37f
Merge pull request #2769 from pixelfed/staging
New admin dashboard layout
2021-05-24 23:36:21 -06:00
Daniel Supernault
b971f07e8c
Update changelog 2021-05-24 23:26:17 -06:00
Daniel Supernault
117ac9a286
Update compiled assets 2021-05-24 23:25:59 -06:00
Daniel Supernault
eb7d5a4e36
New admin dashboard layout 2021-05-24 23:24:42 -06:00
daniel
d8a169870e
Merge pull request #2768 from pixelfed/staging
Staging
2021-05-24 19:42:35 -06:00
Daniel Supernault
53730e493f
Update package-lock 2021-05-24 18:21:59 -06:00
Daniel Supernault
3de44f3392
Update Timeline.vue, batch api views 2021-05-24 00:37:52 -06:00
daniel
b654d68833
Merge pull request #2767 from pixelfed/staging
Staging
2021-05-23 23:13:41 -06:00
Daniel Supernault
444c6d5163
Update changelog 2021-05-23 23:09:59 -06:00
Daniel Supernault
92dc7af69b
Update landing and about page 2021-05-23 23:09:26 -06:00
Daniel Supernault
061b145b54
Update ComposeController, bail on empty attachments 2021-05-20 20:26:26 -06:00
daniel
446ccc6a58
Merge pull request #2762 from pixelfed/staging
Staging
2021-05-19 02:20:28 -06:00
Daniel Supernault
29947e157c
Update admin settings 2021-05-19 02:15:37 -06:00
Daniel Supernault
24e77f647d
Update admin settings 2021-05-19 02:01:33 -06:00
Daniel Supernault
41792eea56
Update config() to config_cache() 2021-05-18 22:45:04 -06:00
daniel
50fad248cb
Merge pull request #2761 from pixelfed/staging
Staging
2021-05-18 19:49:14 -06:00
Daniel Supernault
e8f15b6fa1
Update changelog 2021-05-18 19:47:47 -06:00
Daniel Supernault
241ae0368f
Add hashtag timeline to v1 api 2021-05-18 19:46:26 -06:00
Daniel Supernault
dac326e949
Update StatusTransformer, fix missing tags attribute 2021-05-18 19:39:33 -06:00
Daniel Supernault
955696b8a0
Update site config, fix boolean casting 2021-05-18 17:53:59 -06:00
daniel
08dd3bf66a
Merge pull request #2759 from pixelfed/staging
Staging
2021-05-14 17:16:34 -06:00
Daniel Supernault
74261999d3
Update changelog 2021-05-14 17:15:40 -06:00
Daniel Supernault
c9abd70e8a
Update LikeService, fix authentication bug 2021-05-14 17:15:08 -06:00
Daniel Supernault
3b4add7b8e
Update changelog 2021-05-13 23:46:38 -06:00
Daniel Supernault
a4efbb75d8
Update admin settings, add rules 2021-05-13 23:45:36 -06:00
daniel
f960f63b6f
Merge pull request #2757 from pixelfed/staging
Staging
2021-05-12 22:21:52 -06:00
Daniel Supernault
73d9799a38
Update changelog 2021-05-12 22:21:22 -06:00
Daniel Supernault
de117050c3
Update compiled assets 2021-05-12 22:21:07 -06:00
Daniel Supernault
04094baaef
Update PostComponent, show like count to status owner 2021-05-12 22:20:46 -06:00
Daniel Supernault
4408e2ef8c
Update LikeService, show like count to status owner 2021-05-12 22:18:00 -06:00
Daniel Supernault
f47161fcd9
Update StatusDeletePipeline 2021-05-12 22:16:35 -06:00
daniel
c50a640368
Merge pull request #2756 from pixelfed/staging
Update composer.json
2021-05-11 23:57:00 -06:00
Daniel Supernault
f2be7ea551
Update composer.json 2021-05-11 23:56:33 -06:00
daniel
bb545db5b2
Merge pull request #2755 from pixelfed/staging
Update admin settings
2021-05-11 23:48:06 -06:00
Daniel Supernault
68a21de57c
Update admin settings 2021-05-11 23:47:29 -06:00
daniel
9a88d645ab
Merge pull request #2754 from pixelfed/staging
Update AuthServiceProvider
2021-05-11 23:45:33 -06:00
Daniel Supernault
868cea96ca
Update AuthServiceProvider 2021-05-11 23:45:01 -06:00
daniel
70e3fed0f2
Merge pull request #2753 from pixelfed/staging
Staging
2021-05-11 23:40:57 -06:00
Daniel Supernault
e379ae2cbd
Update admin settings, add warning about enabling config cache 2021-05-11 23:39:55 -06:00
Daniel Supernault
d2cb7d4770
Update ConfigCacheService, fix db issue 2021-05-11 23:33:08 -06:00
Daniel Supernault
b9054f6fd2
Update changelog 2021-05-11 23:26:30 -06:00
Daniel Supernault
377f9d65a8
Update compiled assets 2021-05-11 23:26:22 -06:00
Daniel Supernault
fabb57a9d5
Add profile pronouns 2021-05-11 23:25:10 -06:00
Daniel Supernault
8a73643277
Update PublicApiController, increase public timeline to 6 months from 3 2021-05-11 22:12:10 -06:00
Daniel Supernault
813648cfbb
Update admin settings views 2021-05-11 22:11:10 -06:00
Daniel Supernault
a3a761c348
Update app layout 2021-05-11 22:09:36 -06:00
Daniel Supernault
d60f5c314a
Update changelog 2021-05-11 22:08:47 -06:00
Daniel Supernault
f2066b7401
Add admin config settings 2021-05-11 22:07:55 -06:00
Daniel Supernault
7895097fc1
Update config() to config_cache() 2021-05-11 20:14:51 -06:00
Daniel Supernault
3a9203e039
Update config() to config_cache() 2021-05-11 20:03:29 -06:00
Daniel Supernault
f4fc8347c9
Update config() to config_cache() 2021-05-11 19:59:24 -06:00
Daniel Supernault
7873b7ecc5
Update config() to config_cache() 2021-05-11 19:08:08 -06:00
Daniel Supernault
c65d03788b
Update config() to config_cache() 2021-05-11 19:07:03 -06:00
Daniel Supernault
1c2baa8f2c
Update config() to config_cache() 2021-05-11 18:30:32 -06:00
Daniel Supernault
3e52458889
Update config() to config_cache() 2021-05-11 18:26:52 -06:00
Daniel Supernault
53134208fe
Update config() to config_cache() 2021-05-11 18:17:03 -06:00
Daniel Supernault
27b722e7a7
Update config() to config_cache() 2021-05-10 23:23:09 -06:00
Daniel Supernault
c0e693cc73
Update config() to config_cache() 2021-05-10 21:11:43 -06:00
Daniel Supernault
a9f009305c
Update config() to config_cache() 2021-05-10 21:04:23 -06:00
Daniel Supernault
1d54204635
Update config() to config_cache() 2021-05-10 20:04:13 -06:00
Daniel Supernault
e7b33c4cc6
Update app config 2021-05-07 22:22:00 -06:00
Daniel Supernault
35b92fd7c9
Update admin settings 2021-05-07 22:19:58 -06:00
Daniel Supernault
61254b907f
Update site name config 2021-05-07 21:47:51 -06:00
Daniel Supernault
c6848e99a4
Add helpers 2021-05-07 21:37:10 -06:00
Daniel Supernault
3136f8e137
Add ConfigCacheService 2021-05-07 21:34:58 -06:00
Daniel Supernault
ba37a54a20
Add ConfigCache model and migration 2021-05-07 21:32:59 -06:00
daniel
4ad3de3b88
Merge pull request #2747 from pixelfed/staging
Update LikeService, fix likedBy method
2021-05-03 17:57:56 -06:00
Daniel Supernault
5d2f575984
Update changelog 2021-05-03 17:55:34 -06:00
Daniel Supernault
a5e64da69b
Update LikeService, fix likedBy method 2021-05-03 17:55:06 -06:00
daniel
67f7a75bd0
Merge pull request #2746 from pixelfed/staging
Staging
2021-05-01 16:17:20 -06:00
Daniel Supernault
c10a94649c
Update changelog 2021-05-01 16:12:33 -06:00
Daniel Supernault
b1cde9840c
Update compiled assets 2021-05-01 16:12:12 -06:00
Daniel Supernault
7bcbf96bee
Update Timeline component, change like logic 2021-05-01 16:10:03 -06:00
Daniel Supernault
0a35f5d636
Update PostComponent, change like logic 2021-05-01 16:08:42 -06:00
Daniel Supernault
372bacb01b
Update StatusTransformers, add liked_by attribute 2021-05-01 16:06:39 -06:00
Daniel Supernault
ea68724054
Update LikeController, hide like counts 2021-05-01 16:01:32 -06:00
Daniel Supernault
477db75896
Add LikeService 2021-05-01 15:51:02 -06:00
Daniel Supernault
391b1287ac
Update ProfileService, use account transformer 2021-05-01 13:35:47 -06:00
Daniel Supernault
9cf962fff5
Update Settings, remove reports page 2021-05-01 12:40:32 -06:00
daniel
2e2078a2fe
Merge pull request #2744 from pixelfed/staging
Staging
2021-04-30 23:50:40 -06:00
Daniel Supernault
f2055b4dcd
Update compiled assets 2021-04-30 23:49:39 -06:00
Daniel Supernault
9341f0b38f
Update components, remove like counts 2021-04-30 23:48:36 -06:00
Daniel Supernault
7694487aea
Update changelog 2021-04-30 21:42:23 -06:00
Daniel Supernault
c799a01aa9
Update StatusController, allow license edits without 24 hour limit 2021-04-30 21:41:42 -06:00
Daniel Supernault
4dac1a43dd
Update web routes 2021-04-30 21:38:14 -06:00
daniel
a96f6d96cb
Merge pull request #2743 from pixelfed/staging
Update StoryCompose crop logic
2021-04-30 20:08:34 -06:00
Daniel Supernault
b3e7052426
Update compiled assets 2021-04-30 20:07:15 -06:00
Daniel Supernault
bbad32f73d
Add changelog 2021-04-30 20:04:10 -06:00
Daniel Supernault
2ead622c41
Update StoryCompose crop logic 2021-04-30 20:03:36 -06:00
daniel
2cbb7b9a6b
Merge pull request #2742 from pixelfed/staging
Update story garbage collection
2021-04-30 20:00:17 -06:00
Daniel Supernault
e8e249ce31
Update story garbage collection 2021-04-30 19:59:56 -06:00
daniel
0869c10a37
Merge pull request #2741 from pixelfed/staging
Update StoryController, optimize photo size by resizing to 9:16 aspect
2021-04-30 00:21:05 -06:00
Daniel Supernault
86a1c5a603
Update changelog 2021-04-30 00:20:14 -06:00
Daniel Supernault
e66ed9a222
Update StoryController, optimize photo size by resizing to 9:16 aspect 2021-04-30 00:19:45 -06:00
daniel
f718346031
Merge pull request #2740 from pixelfed/staging
Staging
2021-04-30 00:12:30 -06:00
Daniel Supernault
ca00f2bb97
Update changelog 2021-04-30 00:11:54 -06:00
Daniel Supernault
c2f8faaeee
Update StoryController, fix cache crop bug 2021-04-30 00:10:50 -06:00
daniel
c250bdaaf9
Merge pull request #2739 from pixelfed/staging
Update StoryController
2021-04-29 23:58:29 -06:00
Daniel Supernault
ae46bad70a
Update StoryController 2021-04-29 23:58:06 -06:00
daniel
1b7ef61a4a
Merge pull request #2738 from pixelfed/staging
Staging
2021-04-29 23:55:10 -06:00
Daniel Supernault
40f9aa6055
Update import job 2021-04-29 23:52:59 -06:00
daniel
f25452ea1a
Merge pull request #2642 from stelzch/dev
Create directory in InstagramImport
2021-04-29 23:52:09 -06:00
daniel
bd27ecb68e
Merge pull request #2732 from Wv5twkFEKh54vo4tta9yu7dHa3/staging
Use same json error format as Mastodon
2021-04-29 23:43:42 -06:00
Daniel Supernault
391c841b04
Update changelog 2021-04-29 23:40:10 -06:00
Daniel Supernault
22dddaa044
Update Helpers, fix broken tests 2021-04-29 23:39:38 -06:00
Daniel Supernault
37969da936
Update compiled assets 2021-04-29 23:31:21 -06:00
Daniel Supernault
2814b55b1b
Update changelog 2021-04-29 23:29:30 -06:00
Daniel Supernault
87b0cba027
Update composer.lock 2021-04-29 23:29:13 -06:00
Daniel Supernault
39a7610314
Update StoryCompose component, improve full screen preview 2021-04-29 23:28:26 -06:00
Daniel Supernault
2119965659
Update ComposeModal 2021-04-29 23:27:25 -06:00
Daniel Supernault
668e936eb5
Update instance endpoint, add custom description 2021-04-28 19:26:12 -06:00
Daniel Supernault
c8edca696b
Update Stories, add crop and duration settings to composer 2021-04-27 20:15:31 -06:00
Daniel Supernault
9f380a5597
Add new migration 2021-04-27 20:04:15 -06:00
Daniel Supernault
c43f8bcce8
Update story garbage collection, handle non active stories and new ephemeral story media directory 2021-04-27 20:02:33 -06:00
daniel
a2b8b1d799
Merge pull request #2734 from pixelfed/staging
Update ApiV1Controller, add missing instance api attributes
2021-04-22 22:47:40 -06:00
Daniel Supernault
71aeb8a25d
Update changelog 2021-04-22 22:47:30 -06:00
Daniel Supernault
64b86546b9
Update ApiV1Controller, add missing instance api attributes 2021-04-22 22:46:05 -06:00
daniel
de8ee9f21f
Merge pull request #2733 from pixelfed/staging
Staging
2021-04-20 23:32:58 -06:00
Daniel Supernault
716247857c
Update changelog 2021-04-20 23:18:43 -06:00
Daniel Supernault
66b4f8c773
Update AP Helpers, use instance filtering 2021-04-20 23:18:07 -06:00
Daniel Supernault
c20a9dfefe
Add InstanceService 2021-04-20 23:11:43 -06:00
Daniel Supernault
35393edfef
Update AdminInstanceController, invalidate banned domain cache when updated 2021-04-20 22:31:11 -06:00
Daniel Supernault
e73b2f8373
Update ap helpers 2021-04-20 22:29:52 -06:00
Daniel Supernault
7066e19d0e
Update ap helpers 2021-04-20 22:09:59 -06:00
Daniel Supernault
533cabed47
Update changelog 2021-04-20 22:04:29 -06:00
Daniel Supernault
f582937300
Update admin instance page, add search and improve performance 2021-04-20 22:03:41 -06:00
Daniel Supernault
ab755811d1
Update composer deps 2021-04-20 21:29:06 -06:00
Daniel Supernault
8216c6da88
Update npm deps 2021-04-20 21:25:33 -06:00
Wv5twkFEKh54vo4tta9yu7dHa3
a19d4d5418
Use same json error format as Mastodon
As documented in the Mastodon API ( https://docs.joinmastodon.org/entities/error/ ),   error responses use "error" as the key for the value, instead of Laravel's default (which is "message")
2021-04-20 18:18:42 +02:00
daniel
f526090229
Merge pull request #2725 from pixelfed/staging
Staging
2021-04-07 19:01:00 -06:00
daniel
6a570d07ec
Merge pull request #2723 from pixelfed/dependabot/composer/phpseclib/phpseclib-2.0.31
Bump phpseclib/phpseclib from 2.0.30 to 2.0.31
2021-04-07 16:00:33 -06:00
dependabot[bot]
12282b9819
Bump phpseclib/phpseclib from 2.0.30 to 2.0.31
Bumps [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) from 2.0.30 to 2.0.31.
- [Release notes](https://github.com/phpseclib/phpseclib/releases)
- [Changelog](https://github.com/phpseclib/phpseclib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/phpseclib/phpseclib/compare/2.0.30...2.0.31)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 21:24:06 +00:00
daniel
089ca37457
Merge pull request #2720 from pixelfed/staging
Staging
2021-04-06 23:38:50 -06:00
Daniel Supernault
aa5bb3f7fa
Update changelog 2021-04-06 23:38:27 -06:00
Daniel Supernault
10119bbbea
Update PublicApiController, limit network pagination to 3 months 2021-04-06 23:37:53 -06:00
daniel
4f31d7f391
Merge pull request #2705 from wileyfoxyx/dev
Update and improve Russian translate
2021-04-06 22:01:11 -06:00
daniel
57818c2fe7
Merge pull request #2696 from ZER0-X/dev
Improve and update the Arabic language
2021-04-06 21:53:57 -06:00
Daniel Supernault
898e41cfe1
Fix cs 2021-04-06 21:53:39 -06:00
daniel
56458c1f40
Merge pull request #2588 from dunn/docker-compose-env
docker-compose: store default db passwords in .env.docker
2021-04-06 21:38:14 -06:00
daniel
95b6b78f73
Merge pull request #2677 from hellcp/patch-1
Fix label describing the wrong field in settings
2021-04-06 21:37:40 -06:00
daniel
d87eb0bfb8
Merge pull request #2719 from pixelfed/staging
Add Network Timeline
2021-04-06 21:29:53 -06:00
Daniel Supernault
e5b61be52d
Update network blade view 2021-04-06 21:25:07 -06:00
Daniel Supernault
5fb7ef491c
Update compiled assets 2021-04-06 21:23:44 -06:00
Daniel Supernault
5be02155c1
Update changelog 2021-04-06 21:19:50 -06:00
Daniel Supernault
af7face4da
Add Network Timeline 2021-04-06 21:17:42 -06:00
daniel
d07d236c14
Merge pull request #2711 from pixelfed/staging
New License formats
2021-03-31 22:29:27 -06:00
Daniel Supernault
b9221ffc27
Update changelog 2021-03-31 22:28:33 -06:00
Daniel Supernault
b22a59337f
Update compiled assets 2021-03-31 22:25:23 -06:00
Daniel Supernault
4ad573c8de
Update components 2021-03-31 22:24:59 -06:00
Daniel Supernault
552e950d7a
New License formats 2021-03-31 22:08:03 -06:00
daniel
a368c24d63
Merge pull request #2706 from pixelfed/staging
Staging
2021-03-19 21:24:49 -06:00
Daniel Supernault
22827bbc46
Update changelog 2021-03-19 21:23:32 -06:00
Daniel Supernault
886ea6175d
Update ApiV1Controller, add missing variable 2021-03-19 21:22:37 -06:00
Ilya
78d8799cdc
Fixed translate 2021-03-18 22:39:57 +03:00
Ilya
22fc1a8c64
Added 'profiles' 2021-03-18 22:38:44 +03:00
Ilya
cc5dec2c3e
Update profile.php
fixed translate
2021-03-18 22:37:40 +03:00
Ilya
edf9c565c7
Update passwords.php
Fixed translate
2021-03-18 22:36:36 +03:00
Ilya
53d0298ae1
Update navmenu.php
Fixed translate
2021-03-18 22:33:40 +03:00
Ilya
34aad3608c
Update helpcenter.php
Fixed and added 'taggingPeople'
2021-03-18 22:32:25 +03:00
Ilya
9b27533f49
Create exception.php 2021-03-18 22:30:43 +03:00
daniel
a3eacfde50
Merge pull request #2698 from pixelfed/staging
Staging
2021-03-03 20:22:01 -07:00
Daniel Supernault
42faa9ff7e
Update changelog 2021-03-03 20:19:09 -07:00
Daniel Supernault
e1c6297ee7
Update PublicApiController, show unlisted comments 2021-03-03 20:18:32 -07:00
zer0-x
9f204e4c23 Improve and update the Arabic language 2021-03-02 21:02:44 +03:00
daniel
e25600cdc6
Merge pull request #2695 from pixelfed/staging
Add Autocomplete (hashtags + mentions)
2021-03-01 23:00:11 -07:00
Daniel Supernault
64c86cffa4
Update changelog 2021-03-01 22:57:36 -07:00
Daniel Supernault
be8d36ee46
Update compiled assets 2021-03-01 22:57:21 -07:00
Daniel Supernault
d1d74707a7
Update autocomplete, add dark mode support 2021-03-01 22:52:12 -07:00
Daniel Supernault
de514f7d76
Add Autocomplete for hashtags + mentions 2021-03-01 22:04:46 -07:00
daniel
1a213ae9bc
Merge pull request #2694 from pixelfed/staging
Update Compose Apis, refactor rate limits
2021-03-01 21:15:57 -07:00
Daniel Supernault
77e1115740
Update changelog 2021-03-01 21:02:51 -07:00
Daniel Supernault
c2350f10d7
Update compiled assets 2021-03-01 21:02:45 -07:00
Daniel Supernault
42375b3d79
Update Compose Apis, refactor rate limits 2021-03-01 20:58:35 -07:00
daniel
d2ff146526
Merge pull request #2691 from pixelfed/staging
Staging
2021-03-01 02:21:02 -07:00
Daniel Supernault
f3a2b354db
Update bouncer 2021-03-01 02:20:19 -07:00
daniel
aad77239a9
Merge pull request #2690 from pixelfed/staging
Staging
2021-03-01 00:31:29 -07:00
Daniel Supernault
106964f8ea
Update changelog 2021-03-01 00:29:42 -07:00
Daniel Supernault
5cf8bee47a
Update compiled assets 2021-03-01 00:25:46 -07:00
Daniel Supernault
5f67a48442
Timeline refactor 2021-03-01 00:24:44 -07:00
Daniel Supernault
3f8202e29a
Update PhotoPresenter, add width and height to images 2021-03-01 00:08:10 -07:00
Daniel Supernault
d200c12cf2
Update Nodeinfo util, use last_active_at for monthly active user count 2021-02-28 23:25:37 -07:00
Daniel Supernault
b2501bfcc3
Update moderation api, invalidate profile embed 2021-02-28 23:14:27 -07:00
Daniel Supernault
9c8a87c331
Update StatusController, invalidate profile embed cache on status delete 2021-02-28 23:04:15 -07:00
Daniel Supernault
063558e3ac
Update comment apis 2021-02-28 22:41:07 -07:00
Daniel Supernault
f0e48a09a3
Update ComposeController, add autocomplete apis for hashtags and mentions 2021-02-28 22:38:07 -07:00
Daniel Supernault
001d410579
Update ComposeModal, limit visibility scope for private accounts 2021-02-28 22:35:43 -07:00
Daniel Supernault
77d4353a0a
Update font icons, use font-display:swap 2021-02-28 22:33:55 -07:00
daniel
0ac9fadc9b
Merge pull request #2685 from pixelfed/staging
Staging
2021-02-24 20:08:32 -07:00
Daniel Supernault
a1f1b6e48c
Update changelog 2021-02-24 20:07:30 -07:00
Daniel Supernault
f53bfa6fa6
Update Compose apis, prevent private accounts from posting public or unlisted scopes 2021-02-24 20:06:58 -07:00
daniel
390c083d12
Merge pull request #2682 from pixelfed/staging
Update user admin, fix pagination
2021-02-23 00:32:47 -07:00
Daniel Supernault
43201a70e6
Update user admin, fix pagination 2021-02-23 00:31:12 -07:00
daniel
46ca396360
Merge pull request #2680 from pixelfed/staging
Update user admin, remove expensive db query and add search
2021-02-23 00:06:01 -07:00
Daniel Supernault
53042d295e
Update changelog 2021-02-22 23:05:52 -07:00
Daniel Supernault
8feeadbf4e
Update user admin, remove expensive db query and add search 2021-02-22 23:03:49 -07:00
Sasi Olin
7e7d2e60dd
Fix label describing the wrong field in settings 2021-02-22 22:18:05 +01:00
daniel
2ff863fa00
Merge pull request #2666 from pixelfed/staging
Update MediaStorageService, improve head checks to fix failed jobs
2021-02-17 01:16:31 -07:00
Daniel Supernault
7b7e7cf697
Update changelog 2021-02-17 01:14:24 -07:00
Daniel Supernault
1769cdfd74
Update MediaStorageService, improve head checks to fix failed jobs 2021-02-17 01:09:39 -07:00
daniel
c2a674d2c5
Merge pull request #2665 from pixelfed/staging
Update federation pipeline, add locks
2021-02-17 00:01:03 -07:00
Daniel Supernault
850e4499f1
Update web routes 2021-02-16 23:59:10 -07:00
Daniel Supernault
acbca90c78
Update changelog 2021-02-16 23:56:54 -07:00
Daniel Supernault
ddc768871b
Update federation pipeline, add locks 2021-02-16 23:39:39 -07:00
daniel
68586d765b
Merge pull request #2664 from pixelfed/staging
Update SnowflakeTest, fixes #2661
2021-02-16 23:00:15 -07:00
Daniel Supernault
c55f14764f
Update SnowflakeTest, fixes #2661 2021-02-16 22:59:40 -07:00
daniel
32a9afc7ac
Merge pull request #2660 from pixelfed/staging
Staging
2021-02-15 21:07:51 -07:00
Daniel Supernault
2ab73e1e96
Update AdminController, update reports method 2021-02-15 21:07:18 -07:00
Daniel Supernault
a70ab1ba36
Update reports view 2021-02-15 20:38:33 -07:00
daniel
a0cfe87224
Merge pull request #2659 from pixelfed/staging
Staging
2021-02-15 20:35:16 -07:00
Daniel Supernault
0118f2c685
Update compiled assets 2021-02-15 20:33:21 -07:00
Daniel Supernault
ae23d0ad1e
Update changelog 2021-02-15 20:28:04 -07:00
Daniel Supernault
e64b4bd382
Update Timeline component, add inline reports modal 2021-02-15 20:27:20 -07:00
Daniel Supernault
3dfb8738a2
Update PostComponent, update menu 2021-02-15 20:26:33 -07:00
Daniel Supernault
4507d4520c
Update RestrictedNames 2021-02-15 20:23:44 -07:00
Daniel Supernault
5206b09627
Update ReportController, handle json reports 2021-02-15 20:23:06 -07:00
Daniel Supernault
2cf85394b3
Update components.js, update read more 2021-02-15 17:36:52 -07:00
Daniel Supernault
d4c647cb4d
Update ReportController, add new report types 2021-02-15 02:49:17 -07:00
Daniel Supernault
b1fd99644b
Update AdminController, show open reports by default 2021-02-15 02:46:58 -07:00
Daniel Supernault
52a120c0db
Update admin reports views 2021-02-15 02:44:51 -07:00
Daniel Supernault
85bd4847e4
Update api routes, remove throttle middleware 2021-02-15 02:28:35 -07:00
daniel
0d7aee0e21
Merge pull request #2658 from pixelfed/staging
Updated Status model, refactor liked and shared methods to fix cache invalidation bug
2021-02-14 22:38:48 -07:00
Daniel Supernault
1b92f9f455
Update changelog 2021-02-14 22:36:05 -07:00
Daniel Supernault
f05c3b66fc
Update Status model, refactor liked and shared methods to fix cache invalidation bug 2021-02-14 22:35:31 -07:00
daniel
2fddef80a8
Merge pull request #2651 from pixelfed/staging
Staging
2021-02-12 23:16:56 -07:00
Daniel Supernault
2598520bbe
Update tests 2021-02-12 22:50:46 -07:00
Daniel Supernault
f63f48beb6
Update changelog 2021-02-12 22:27:45 -07:00
Daniel Supernault
d1c5e9b867
Update InboxPipeline, fail earlier for invalid public keys. Fixes #2648 2021-02-12 22:25:34 -07:00
Daniel Supernault
40db9a1296
Update changelog 2021-02-12 21:44:46 -07:00
Daniel Supernault
deb6f1153f
Update FederationController, return 404 for invalid webfinger addresses. Fixes #2647 2021-02-12 21:44:06 -07:00
Christoph Stelz
f5aa89b2d2 Add fix for InstagramImport 2021-02-09 09:34:05 +01:00
daniel
4150a72b18
Merge pull request #2641 from pixelfed/staging
Fix Profile + Status embeds, remove following count and improve cache invalidation and hidden follower counts.
2021-02-09 00:45:04 -07:00
Daniel Supernault
723eb00039
Update changelog 2021-02-09 00:42:57 -07:00
Daniel Supernault
5ac9d0e8f2
Update Embeds. Fix Profile + Status embeds, remove following count and improve cache invalidation and hidden follower counts 2021-02-09 00:42:12 -07:00
daniel
12c5f74e6b
Merge pull request #2638 from pixelfed/staging
Staging
2021-02-07 00:21:08 -07:00
Daniel Supernault
d31a1dbcdf
Update compiled assets 2021-02-07 00:20:26 -07:00
Daniel Supernault
2962c389ad
Update changelog 2021-02-07 00:18:20 -07:00
Daniel Supernault
ae90eef965
Update filesystems config, add backup driver to store backups on other filesystems 2021-02-07 00:14:52 -07:00
Daniel Supernault
551365183e
Update Hashtag component, fix null infinite loading bug. Fixes #2637 2021-02-07 00:12:19 -07:00
Daniel Supernault
12ce76026f
Update ComposeModal, show filter warning for unsupported browsers 2021-02-07 00:09:28 -07:00
daniel
57ee2734ba
Merge pull request #2636 from pixelfed/staging
Staging
2021-02-06 21:24:18 -07:00
Daniel Supernault
88d801bdf0
Update compiled assets 2021-02-06 21:23:42 -07:00
Daniel Supernault
6c6115b2b0
Update changelog 2021-02-06 21:23:02 -07:00
Daniel Supernault
ceae664ce0
Update ComposeModal, prevent tagging empty users. Fixes #2633 2021-02-06 21:22:27 -07:00
daniel
2558648e24
Merge pull request #2635 from pixelfed/staging
Revert database change
2021-02-06 21:07:19 -07:00
Daniel Supernault
60554c24b0
Revert database change 2021-02-06 21:06:19 -07:00
daniel
4884875f15
Merge pull request #2634 from pixelfed/staging
Staging
2021-02-06 21:04:32 -07:00
Daniel Supernault
e462b2af13
Update compiled assets 2021-02-06 21:03:55 -07:00
Daniel Supernault
1f09b4f5c5
Revert ComposeModal 2021-02-06 21:00:42 -07:00
daniel
6c9e9b15d6
Merge pull request #2632 from pixelfed/trwnh-patch-1
change default client to phpredis
2021-02-06 20:14:20 -07:00
trwnh
aa99784fed
change default client to phpredis 2021-02-06 20:53:56 -06:00
daniel
2d28257054
Merge pull request #2630 from pixelfed/trwnh-patch-1
increase default upload limit from 1M to 15M
2021-02-06 18:33:08 -07:00
trwnh
02ad300443
increase default upload limit from 1M to 15M 2021-02-06 19:30:58 -06:00
daniel
e9dcc3c72a
Merge pull request #2623 from pixelfed/staging
Update web routes
2021-02-05 21:31:55 -07:00
Daniel Supernault
946c909783
Update web routes 2021-02-05 21:31:23 -07:00
daniel
9cd9b68664
Merge pull request #2622 from pixelfed/staging
Staging
2021-02-05 21:26:03 -07:00
Daniel Supernault
e6a897bf3f
Update compiled assets 2021-02-05 21:25:25 -07:00
Daniel Supernault
832dac45c5
Update changelog 2021-02-05 21:22:25 -07:00
Daniel Supernault
a9e98965ab
Update components, improve content warnings 2021-02-05 21:21:41 -07:00
Daniel Supernault
a1059a6e12
Update DiscoverComponent, allow unathenicated if enabled 2021-02-05 21:20:19 -07:00
Daniel Supernault
e6e76e809d
Update ComposeModal, add processing step disabled by default 2021-02-05 21:19:12 -07:00
Daniel Supernault
33b625f508
Update ComposeController, add mediaProcessingCheck method 2021-02-05 21:15:13 -07:00
Daniel Supernault
6bee5072d0
Update pixelfed config, add media_fast_process setting 2021-02-05 21:13:52 -07:00
daniel
ec5646d098
Merge pull request #2621 from pixelfed/staging
Staging
2021-02-05 21:12:12 -07:00
Daniel Supernault
18ee2def92
Update changelog 2021-02-05 21:10:25 -07:00
Daniel Supernault
1404ac6e6f
Update Discover, allow public discover access 2021-02-05 21:09:03 -07:00
Daniel Supernault
9fd90e174b
Update StatusDeletePipeline, use StorageMediaService for media deletes 2021-02-05 21:05:58 -07:00
Daniel Supernault
ab5469ff70
Update ComposeController, use MediaStorageService for media deletes 2021-02-05 21:04:34 -07:00
Daniel Supernault
37dbb3de29
Update MediaStorageService, dispatch deletes to MediaDeletePipeline 2021-02-05 21:00:29 -07:00
daniel
83aecc5595
Merge pull request #2614 from pixelfed/staging
Update webfinger util, fail on invalid webfinger url. Fixes #2613
2021-02-03 20:59:53 -07:00
Daniel Supernault
0cef5aec26
Update changelog 2021-02-03 20:57:31 -07:00
Daniel Supernault
2d11317ceb
Update webfinger util, fail on invalid webfinger url 2021-02-03 20:55:49 -07:00
Daniel Supernault
aad07e2c83
Update changelog 2021-02-01 00:48:36 -07:00
Daniel Supernault
b299da9311
Update AccountService, cache object and observe changes 2021-02-01 00:47:54 -07:00
daniel
7290445f78
Merge pull request #2604 from pixelfed/staging
Add new api endpoints
2021-01-31 13:50:00 -07:00
Daniel Supernault
61c71e8905
Update changelog 2021-01-31 13:45:58 -07:00
Daniel Supernault
1fb7e2b2c9
Update AccountController, add mutes and blocks endpoint to pixelfed api 2021-01-31 13:45:22 -07:00
daniel
59edf497bf
Merge pull request #2603 from pixelfed/staging
Staging
2021-01-31 01:01:41 -07:00
Daniel Supernault
fc0afcb2f8
Update compiled assets 2021-01-31 01:01:11 -07:00
Daniel Supernault
6af2bea5e0
Update DiscoverComponent 2021-01-31 00:58:24 -07:00
Daniel Supernault
2c661b815c
Update changelog 2021-01-31 00:51:09 -07:00
Daniel Supernault
c56a299cfb
Update compiled assets 2021-01-31 00:50:51 -07:00
Daniel Supernault
348692476c
Update DiscoverComponent, add spinner loaders and remove deprecated sections 2021-01-31 00:49:40 -07:00
Daniel Supernault
b16e9452bc
Update DiscoverController, deprecate unused endpoints 2021-01-30 23:13:54 -07:00
daniel
4c2661b8dd
Merge pull request #2602 from pixelfed/staging
Staging
2021-01-30 22:20:34 -07:00
Daniel Supernault
c34a8f4556
Update changelog 2021-01-30 22:16:51 -07:00
Daniel Supernault
eadf0c3b6c
Update nav partial 2021-01-30 22:15:54 -07:00
Daniel Supernault
1324605389
Update footer 2021-01-30 22:11:12 -07:00
Daniel Supernault
7075b7f83e
Update compiled assets 2021-01-30 21:58:37 -07:00
Daniel Supernault
a8ebdd2e39
Update DiscoverComponent, add blurhash and like/comment counts 2021-01-30 21:56:50 -07:00
Daniel Supernault
9862a85599
Update InternalApiController, update discoverPosts method to improve performance 2021-01-30 21:39:31 -07:00
Daniel Supernault
1ac60173af
Update AccountLog model, add fillable attribute 2021-01-30 19:56:31 -07:00
Daniel Supernault
090b6d0336
Update SeasonalController 2021-01-30 19:56:10 -07:00
Daniel Supernault
2281727ac0
Update nav partial 2021-01-30 19:45:02 -07:00
daniel
ee62997168
Merge pull request #2601 from pixelfed/staging
Update AP helpers, fix statusFetch 404s
2021-01-30 18:43:53 -07:00
Daniel Supernault
b5ecb2ebc8
Update changelog 2021-01-30 18:43:40 -07:00
Daniel Supernault
3419379aa8
Update AP helpers, fix statusFetch 404s 2021-01-30 18:41:51 -07:00
daniel
6840f81c04
Merge pull request #2600 from pixelfed/staging
Staging
2021-01-30 18:17:48 -07:00
Daniel Supernault
69d7f7e452
Update compiled assets 2021-01-30 18:16:40 -07:00
Daniel Supernault
55248ef092
Update changelog 2021-01-30 18:16:26 -07:00
Daniel Supernault
7768e84485
Update Timeline.vue, fix hashtag status previews 2021-01-30 18:15:34 -07:00
Daniel Supernault
240e36ed45
Update AP helpers 2021-01-30 18:14:04 -07:00
daniel
8757476aff
Merge pull request #2599 from pixelfed/staging
Staging
2021-01-30 17:39:04 -07:00
Daniel Supernault
d90b085906
Update changelog 2021-01-30 17:34:04 -07:00
Daniel Supernault
3f14a4c412
Update MediaTransformers, add default blurhash attribute 2021-01-30 17:31:07 -07:00
Daniel Supernault
896452c74c
Update VideoThumbnail job, generate blurhash for videos 2021-01-30 17:30:04 -07:00
Daniel Supernault
38a37c15af
Update Blurhash util, add default hash for invalid media 2021-01-30 17:28:35 -07:00
Daniel Supernault
8cfe7d79e1
Update compiled assets 2021-01-30 17:16:56 -07:00
Daniel Supernault
39e389dd4e
Update status square previews, add blurhash and improved content warnings 2021-01-30 17:15:49 -07:00
Daniel Supernault
9e0fd36ba7
Update Status tranformers, add version attribute 2021-01-30 16:39:42 -07:00
Daniel Supernault
899bbeba5f
Update StatusHashtagTransformer, add blurhash attribute 2021-01-30 16:38:43 -07:00
Daniel Supernault
3f772ff864
Update StatusDelete pipeline, call StatusService::del() to remove status from cache 2021-01-30 16:37:29 -07:00
Daniel Supernault
069f20ff77
Update LikePipeline, add StatusService del() method 2021-01-30 16:36:58 -07:00
Daniel Supernault
2eea04097a
Update DiscoverController, change api schema 2021-01-30 16:35:52 -07:00
Daniel Supernault
789ed4b498
Update ComposeController, use placeholder image for video media. Fixes #2595 2021-01-30 16:34:44 -07:00
Daniel Supernault
eab4370c84
Update Like, Status and Comment controllers to add StatusService del() method to update counts 2021-01-30 16:25:50 -07:00
Daniel Supernault
05b9445c8f
Update ApiV1Controller, add StatusService del calls to update likes_count, reblogs_count and reply_count 2021-01-30 16:15:56 -07:00
Daniel Supernault
aa4c718d79
Update StatusHashtagService, remove deprecated methods 2021-01-30 16:09:17 -07:00
Daniel Supernault
0355b567dd
Update StatusHashtagService, use StatusService for statuses 2021-01-30 16:07:12 -07:00
Daniel Supernault
6e44ae0b64
Update StatusService, add ttl of 7 days 2021-01-30 16:05:18 -07:00
daniel
c898dc5408
Merge pull request #2587 from dunn/docker-logging
.env.docker: log to stderr
2021-01-30 09:50:28 -07:00
daniel
25fd23a81b
Merge pull request #2597 from pixelfed/staging
Staging
2021-01-30 09:48:09 -07:00
Daniel Supernault
7d6e42f246
Update changelog 2021-01-30 09:47:34 -07:00
Daniel Supernault
4f40f6f5a1
Update RemotAvatarFetch, only dispatch jobs if cloud storage is enabled 2021-01-30 09:47:02 -07:00
Daniel Supernault
5a116c7ae0
Update changelog 2021-01-30 09:37:02 -07:00
Daniel Supernault
6edaf94099
Update AdminController, fix variable name in updateSpam method 2021-01-30 09:36:09 -07:00
daniel
aebd65c6a3
Merge pull request #2594 from pixelfed/staging
Staging
2021-01-29 19:48:24 -07:00
Daniel Supernault
b7fcf8a504
Update StatusLabelService 2021-01-29 19:46:55 -07:00
Daniel Supernault
cc93b3e3a5
Update package.json, fixes #2593 2021-01-29 19:46:24 -07:00
daniel
ed94041a19
Merge pull request #2592 from pixelfed/staging
Update StatusTransformer, add blurhash
2021-01-29 01:39:11 -07:00
Daniel Supernault
23db9de870
Update StatusTransformer, add blurhash 2021-01-29 01:38:40 -07:00
daniel
5d85a7977b
Merge pull request #2591 from pixelfed/staging
Staging
2021-01-28 23:09:19 -07:00
Daniel Supernault
30cf865154
Update compiled assets 2021-01-28 23:08:26 -07:00
Daniel Supernault
54abfef4f7
Update Timeline.vue, add border-top to status label 2021-01-28 23:07:58 -07:00
Daniel Supernault
b43bb1dd0d
Update web routes 2021-01-28 22:38:30 -07:00
daniel
4deec1f6c1
Merge pull request #2571 from pixelfed/staging
v0.10.10
2021-01-28 21:36:08 -07:00
Daniel Supernault
98cdd52e99
Bump version to v0.10.10 2021-01-28 21:34:42 -07:00
Daniel Supernault
d0e7f13ea2
Bump changelog 2021-01-28 21:33:56 -07:00
Daniel Supernault
2841a42d72
Update changelog 2021-01-28 21:32:16 -07:00
Daniel Supernault
fa0df4004c
Update AP Helpers 2021-01-28 21:26:45 -07:00
Daniel Supernault
7d376c64ec
Update SiteController, only allow redirects for logged in users 2021-01-28 21:25:43 -07:00
Daniel Supernault
10b178c8ee
Update SiteController, use url validator in redirect endpoint 2021-01-28 21:23:15 -07:00
Daniel Supernault
c45ea96eb3
Update compiled assets 2021-01-28 20:11:15 -07:00
Daniel Supernault
f38d2a09a4
Update components 2021-01-28 20:08:06 -07:00
Daniel Supernault
4abfe76a49
Update StatusLabelService, change config key 2021-01-28 18:42:15 -07:00
Daniel Supernault
abe9cb3db0
Update site config, add labels to config 2021-01-28 18:40:13 -07:00
Daniel Supernault
ab888b2e70
Add StatusLabelService 2021-01-27 22:25:14 -07:00
Alexandra Dunn
927ef3b057 docker-compose: store default db passwords in .env.docker
and have redis use a password as well
2021-01-27 11:58:24 -08:00
Alexandra Dunn
3bf2366d82 .env.docker: log to stderr 2021-01-27 11:57:20 -08:00
Daniel Supernault
10ca7ac052
Update static assets 2021-01-26 22:40:01 -07:00
Daniel Supernault
52bb90fd40
Update compiled assets 2021-01-26 22:02:37 -07:00
Daniel Supernault
674701c1df
Update web routes 2021-01-26 21:49:56 -07:00
Daniel Supernault
df980d0092
Update composer 2021-01-26 21:16:44 -07:00
Daniel Supernault
184854b0de
Update compiled assets 2021-01-26 21:11:14 -07:00
Daniel Supernault
cc84125ba2
Update Timeline, prevent nextTick() when reloading same comment modal. Fixes #2584 2021-01-26 20:33:09 -07:00
Daniel Supernault
de8828e88a
Update AP Helpers, add blurhash and RemoteAvatarFetch 2021-01-25 22:24:24 -07:00
Daniel Supernault
cc2d4bf8d8
Update ComposeController, update media version 2021-01-25 22:22:01 -07:00
Daniel Supernault
4c148055cf
Update AvatarPipeline, add remote avatar fetch 2021-01-25 22:03:27 -07:00
Daniel Supernault
94a9f685b5
Update MediaStorageService, add avatar method 2021-01-25 21:54:30 -07:00
Daniel Supernault
654b08d382
Update ActivityPubFetchService, add url validation 2021-01-25 21:44:07 -07:00
Daniel Supernault
ea8e426174
Update Profile model, use cdn_url for avatars 2021-01-25 21:39:10 -07:00
Daniel Supernault
9eafc31e6f
Update AvatarObserver, add logic to delete avatars stored in S3 2021-01-25 21:37:44 -07:00
Daniel Supernault
f7e72d7c62
Update avatar model 2021-01-25 21:30:37 -07:00
Daniel Supernault
9c5e6a7172
Update migration 2021-01-25 21:06:25 -07:00
Daniel Supernault
1031311f06
Update RestrictedNames, add additional static assets 2021-01-25 17:48:03 -07:00
Daniel Supernault
77f21b4b33
Update AP helpers, only run MediaStoragePipeline if using cloud storage 2021-01-24 21:13:31 -07:00
Daniel Supernault
258b2729d3
Update MediaTransformer, remove cache busting 2021-01-24 21:11:52 -07:00
Daniel Supernault
ce6ab80dba
Update MediaStorageService, clear transformer cache after storing media 2021-01-24 21:07:28 -07:00
Daniel Supernault
f930c4bda2
Update StatusDelete pipeline, fix object storage thumbnail deletion 2021-01-24 19:08:55 -07:00
Daniel Supernault
98c44f7bdb
Update VideoThumbnail, add MediaStoragePipeline 2021-01-24 18:58:38 -07:00
Daniel Supernault
9051c1f998
Add migration 2021-01-24 18:24:25 -07:00
Daniel Supernault
889c3d8758
Update AvatarController, remove deprecated thumb_path 2021-01-24 18:22:13 -07:00
Daniel Supernault
1e9958f521
Update changelog 2021-01-24 15:48:00 -07:00
Daniel Supernault
7736bfaa8a
Update compiled assets 2021-01-24 15:47:52 -07:00
Daniel Supernault
b2b8c9f99c
Update UserCreate command, closes #2581 2021-01-24 15:43:37 -07:00
Daniel Supernault
e8cc66dce7
Update components, fix url rewriter. Closes #2538 2021-01-24 15:32:14 -07:00
Daniel Supernault
80b911c4d2
Update changelog 2021-01-24 14:28:25 -07:00
Daniel Supernault
ed6877df7a
Update cache config, use phpredis by default 2021-01-24 14:13:56 -07:00
Daniel Supernault
febe9e1b6d
Add ComposeController 2021-01-24 14:11:58 -07:00
Daniel Supernault
2f639fb5c7
Update compiled assets 2021-01-24 14:11:10 -07:00
Daniel Supernault
2a890c6618
Update web routes 2021-01-24 14:05:49 -07:00
Daniel Supernault
9683e846e9
Update blade views 2021-01-24 14:03:55 -07:00
Daniel Supernault
05b7e1395f
Update Timeline component 2021-01-24 13:39:56 -07:00
Daniel Supernault
c1118956c3
Update RemoteProfile component, change thumbnail url 2021-01-24 13:36:26 -07:00
Daniel Supernault
1c608440a5
Update package lock 2021-01-24 13:35:28 -07:00
Daniel Supernault
01a1ffd64a
Update AP Helpers, use MediaStoragePipeline 2021-01-24 13:32:20 -07:00
Daniel Supernault
be6d12fcb6
Update MediaPipeline, handle cloud object storage 2021-01-24 13:30:31 -07:00
Daniel Supernault
2538673a7b
Update StatusController, unescape slashes in json response 2021-01-24 13:22:10 -07:00
Daniel Supernault
6d078377f1
Update AdminController 2021-01-23 23:12:05 -07:00
Daniel Supernault
9cd4bd74a5
Update backup config, prevents gateway timeouts for large databases using mysql 2021-01-22 17:53:06 -07:00
Daniel Supernault
0eabbfdd07
Update storage, add remote media cache directory 2021-01-19 23:57:30 -07:00
Daniel Supernault
2284d85f9c
Update VideoThumbnail job, remove outdated cloud storage logic 2021-01-19 19:42:31 -07:00
Daniel Supernault
4a70e18596
Update Media model, fix url methods 2021-01-19 19:40:41 -07:00
Daniel Supernault
6b411d23b5
Update changelog 2021-01-19 19:39:36 -07:00
Daniel Supernault
f8cbe1e42c
Update MediaTransformers, include meta attribute with focus and dimensions 2021-01-19 19:39:04 -07:00
Daniel Supernault
40bd64aae7
Update Image media util, store dimensions of media not thumbnail 2021-01-19 19:36:05 -07:00
Daniel Supernault
cf40526ef9
Update mobile apis, add blurhash 2021-01-18 18:25:03 -07:00
Daniel Supernault
4415af1bf0
Update api controllers, deprecate old endpoints 2021-01-17 23:43:23 -07:00
Daniel Supernault
8132db74e5
Update MediaController, remove deprecated endpoint 2021-01-17 23:37:10 -07:00
Daniel Supernault
57fa889d16
Update Media model, add cdn support to url and thumbnailUrl methods 2021-01-17 22:45:40 -07:00
Daniel Supernault
a98b65bf5c
Update DiscoverController 2021-01-17 22:02:27 -07:00
Daniel Supernault
234f72f3aa
Update ImageOptimizePipeline, add skip_optimize and MediaStorageService support 2021-01-17 21:59:34 -07:00
Daniel Supernault
4b1a0fd750
Update StatusDelete job, handle cloud storage media deletes 2021-01-17 21:51:25 -07:00
Daniel Supernault
aac4430970
Update MediaPathService, add story method 2021-01-17 21:46:40 -07:00
Daniel Supernault
0cfc12c5dd
Update StatusController, add view method 2021-01-17 21:38:34 -07:00
Daniel Supernault
5e13643246
Update MediaTagController 2021-01-17 21:27:55 -07:00
Daniel Supernault
8fc741c68d
Update web routes 2021-01-17 21:25:41 -07:00
Daniel Supernault
caafef580c
Update ComposeModal, use new routes 2021-01-17 20:25:19 -07:00
Daniel Supernault
a0a15730db
Update help template 2021-01-17 19:55:04 -07:00
Daniel Supernault
8474d32114
Add instance actor help page 2021-01-17 19:53:01 -07:00
Daniel Supernault
fcebf5960f
Update changelog 2021-01-17 19:51:21 -07:00
Daniel Supernault
f32072a396
Add Year in Review feature (mysql only) 2021-01-17 19:50:35 -07:00
Daniel Supernault
b00e2b0868
Update EmailService 2021-01-17 15:33:56 -07:00
Daniel Supernault
5b0b14fc42
Update StoryItemTransformer, increase story duration from 5 seconds to 10 seconds 2021-01-17 15:27:26 -07:00
Daniel Supernault
c91891b457
Update InstanceActor model 2021-01-17 14:03:32 -07:00
Daniel Supernault
1450f54f65
Update api routes 2021-01-17 14:00:26 -07:00
Daniel Supernault
07f419d5cb
Update changelog 2021-01-17 12:51:37 -07:00
Daniel Supernault
3ee1215a4a
Add signed GET for secure mode compatibility 2021-01-17 12:51:07 -07:00
Daniel Supernault
b29b845533
Update components 2021-01-17 03:07:08 -07:00
679 changed files with 142908 additions and 44723 deletions

View file

@ -7,7 +7,7 @@ jobs:
build:
docker:
# Specify the version you desire here
- image: circleci/php:7.3-cli-stretch-node
- image: cimg/php:7.4.26
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
@ -22,7 +22,6 @@ jobs:
- checkout
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
- run: sudo -E docker-php-ext-install bcmath pcntl zip
# Download and cache dependencies

View file

@ -56,11 +56,16 @@ MAIL_ENCRYPTION=null
## Databases (MySQL)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=pixelfed_prod
DB_HOST=db
DB_PASSWORD=pixelfed_db_pass
DB_PORT=3306
DB_DATABASE=pixelfed
DB_USERNAME=pixelfed
DB_PASSWORD=pixelfed
# pass the same values to the db itself
MYSQL_DATABASE=pixelfed_prod
MYSQL_PASSWORD=pixelfed_db_pass
MYSQL_RANDOM_ROOT_PASSWORD=true
MYSQL_USER=pixelfed
## Databases (Postgres)
#DB_CONNECTION=pgsql
@ -74,7 +79,7 @@ DB_PASSWORD=pixelfed
REDIS_CLIENT=phpredis
REDIS_SCHEME=tcp
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PASSWORD=redis_password
REDIS_PORT=6379
REDIS_DATABASE=0
@ -115,7 +120,7 @@ PF_COSTAR_ENABLED=false
MEDIA_EXIF_DATABASE=false
## Logging
LOG_CHANNEL=stack
LOG_CHANNEL=stderr
## Image
IMAGE_DRIVER=imagick

View file

@ -1,16 +1,365 @@
# Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.9...dev)
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.2...dev)
### Added
- Custom Emoji ([#3166](https://github.com/pixelfed/pixelfed/pull/3166))
### Metro 2.0 UI
- Added Hovercards ([16ced7b4](https://github.com/pixelfed/pixelfed/commit/16ced7b4))
- Fix word-break on statuses ([16ced7b4](https://github.com/pixelfed/pixelfed/commit/16ced7b4))
- Add pronouns to hovercards ([33f863e8](https://github.com/pixelfed/pixelfed/commit/33f863e8))
- Improved onboarding ([042c5b6c](https://github.com/pixelfed/pixelfed/commit/042c5b6c))
### Updated
- Updated MediaStorageService, fix remote avatar bug. ([1c20d696](https://github.com/pixelfed/pixelfed/commit/1c20d696))
- Updated WebfingerService. Fixes #3167. ([aff74566](https://github.com/pixelfed/pixelfed/commit/aff74566))
- Updated ComposeModal, add max file size and allowed mime types. Fixes #3162. ([879281cc](https://github.com/pixelfed/pixelfed/commit/879281cc))
- Updated profile embeds, fix NaN bug and improve performance. ([3bd211d7](https://github.com/pixelfed/pixelfed/commit/3bd211d7))
- Updated ApiV1Controller, improve follow count cache invalidation. ([4b6effb9](https://github.com/pixelfed/pixelfed/commit/4b6effb9))
- Updated web routes, fix atom feeds for account usernames containing a dot. ([8c54ab57](https://github.com/pixelfed/pixelfed/commit/8c54ab57))
- Updated atom feeds, include media alt text. Fixes #3184. ([5d9b6863](https://github.com/pixelfed/pixelfed/commit/5d9b6863))
- Updated ApiV1Controller, add custom_emoji endpoint. ([16e72518](https://github.com/pixelfed/pixelfed/commit/16e72518))
- Updated InternalApiController, redirect remote post and profiles to Metro 2.0. ([3c35158e](https://github.com/pixelfed/pixelfed/commit/3c35158e))
- Updated BaseApiController, improve favourites endpoint. ([f063cb01](https://github.com/pixelfed/pixelfed/commit/f063cb01))
- Updated ApiV1Controller, invalidate status reply cache on new reply. ([3c261bbf](https://github.com/pixelfed/pixelfed/commit/3c261bbf))
- Updated PublicApiController, add bookmark state to timeline endpoints. ([c0b1e042](https://github.com/pixelfed/pixelfed/commit/c0b1e042))
- Updated ApiV1Controller, fix private status replies returning 404. ([73226360](https://github.com/pixelfed/pixelfed/commit/73226360))
- Updated StatusService, use BookmarkService for bookmarked state. ([a7d71551](https://github.com/pixelfed/pixelfed/commit/a7d71551))
- Updated Apis, added ReblogService to improve reblogged state for api entities ([6cfd6be5](https://github.com/pixelfed/pixelfed/commit/6cfd6be5))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.2 (2022-01-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.1...v0.11.2)
### Breaking
- Dropped support for PHP 7.3 [#3041](https://github.com/pixelfed/pixelfed/pull/3041)
### Metro 2.0 UI
- Added UI Settings modal and fixed height media previews setting ([f2467e71](https://github.com/pixelfed/pixelfed/commit/f2467e71))
- Set max-width of 1440px for larger screens ([af68872a](https://github.com/pixelfed/pixelfed/commit/af68872a))
- Add link to sidebar profile card ([85964510](https://github.com/pixelfed/pixelfed/commit/85964510))
- Improved search bar, now resolves (and imports) remote accounts and posts, including webfinger addresses ([c8a667f2](https://github.com/pixelfed/pixelfed/commit/c8a667f2))
- Added user facing changelog at `/i/web/whats-new` ([e61dc66a](https://github.com/pixelfed/pixelfed/commit/e61dc66a))
### Configuration
- Enable network timeline by default ([b95aec12](https://github.com/pixelfed/pixelfed/commit/b95aec12))
### Postgres Compatibility
- Fix Story recent endpoint on postgres instances ([ddf41dc3](https://github.com/pixelfed/pixelfed/commit/ddf41dc3))
- Fix Direct Message conversations endpoint on postgres instances ([fcabc9be](https://github.com/pixelfed/pixelfed/commit/fcabc9be))
### Added
- Manual email verification requests. ([bc659387](https://github.com/pixelfed/pixelfed/commit/bc659387))
- Added StatusMentionService, fixes #3026. ([e5387d67](https://github.com/pixelfed/pixelfed/commit/e5387d67))
- Cloud Backups, a command to store backups on S3 or compatible filesystems. [#3037](https://github.com/pixelfed/pixelfed/pull/3037) ([3515a98e](https://github.com/pixelfed/pixelfed/commit/3515a98e))
- Web UI Localizations + Crowdin integration. ([f7d9b40b](https://github.com/pixelfed/pixelfed/commit/f7d9b40b)) ([7ff120c9](https://github.com/pixelfed/pixelfed/commit/7ff120c9))
- Store remote avatars locally if S3 not enabled. ([b4bd0400](https://github.com/pixelfed/pixelfed/commit/b4bd0400))
### Updated
- Updated NotificationService, fix 500 bug. ([4a609dc3](https://github.com/pixelfed/pixelfed/commit/4a609dc3))
- Updated HttpSignatures, update instance actor headers. Fixes #2935. ([a900de21](https://github.com/pixelfed/pixelfed/commit/a900de21))
- Updated NoteTransformer, fix tag array. ([7b3e672d](https://github.com/pixelfed/pixelfed/commit/7b3e672d))
- Updated video presenters, add playsinline attribute to video tags. ([0299aa5b](https://github.com/pixelfed/pixelfed/commit/0299aa5b))
- Updated RemotePost, RemoteProfile components, add fallback avatars. ([754151dc](https://github.com/pixelfed/pixelfed/commit/754151dc))
- Updated FederationController, move well-known to api middleware and cache webfinger lookups. ([4505d1f0](https://github.com/pixelfed/pixelfed/commit/4505d1f0))
- Updated InstanceActorController, improve json seralization by not escaping slashes. ([0a8eb81b](https://github.com/pixelfed/pixelfed/commit/0a8eb81b))
- Refactor following & relationship logic. Replace FollowerObserver with FollowerService and added RelationshipService to cache results. Removed NotificationTransformer includes and replaced with cached services to improve performance and reduce database queries. ([80d9b939](https://github.com/pixelfed/pixelfed/commit/80d9b939))
- Updated PublicApiController, use AccountService in accountStatuses method. ([bef959f4](https://github.com/pixelfed/pixelfed/commit/bef959f4))
- Updated auth config, add throttle limit to password resets. ([2609c86a](https://github.com/pixelfed/pixelfed/commit/2609c86a))
- Updated StatusCard component, add relationship state button. ([0436b124](https://github.com/pixelfed/pixelfed/commit/0436b124))
- Updated Timeline component, cascade relationship state change. ([f4bd5672](https://github.com/pixelfed/pixelfed/commit/f4bd5672))
- Updated Activity component, only show context button for actionable activities. ([7886fd59](https://github.com/pixelfed/pixelfed/commit/7886fd59))
- Updated Autospam service, use silent classification for better user experience. ([f0d4c172](https://github.com/pixelfed/pixelfed/commit/f0d4c172))
- Updated Profile component, improve error messages when block/mute limit reached. ([02237845](https://github.com/pixelfed/pixelfed/commit/02237845))
- Updated Activity component, fix missing types. ([5167c68d](https://github.com/pixelfed/pixelfed/commit/5167c68d))
- Updated Timeline component, apply block/mute filters client side for local and network timelines. ([be194b8a](https://github.com/pixelfed/pixelfed/commit/be194b8a))
- Updated public timeline api, use cached sorted set and client side block/mute filtering. ([37abcf38](https://github.com/pixelfed/pixelfed/commit/37abcf38))
- Updated public timeline api, add experimental cache. ([192553ff](https://github.com/pixelfed/pixelfed/commit/192553ff))
- Updated dark mode styles, fix black box on stories. Closes #2982. ([3169f68e](https://github.com/pixelfed/pixelfed/commit/3169f68e))
- Updated verify_credentials api endpoint to improve performance. ([7df3540b](https://github.com/pixelfed/pixelfed/commit/7df3540b))
- Updated Localization util, filter out .DS_Store. ([0107e8fd](https://github.com/pixelfed/pixelfed/commit/0107e8fd))
- Updated PublicApiController, fix private account statuses api. Closes #2995. ([aa2dd26c](https://github.com/pixelfed/pixelfed/commit/aa2dd26c))
- Updated Status model, use AccountService to generate urls instead of loading profile relation. ([2ae527c0](https://github.com/pixelfed/pixelfed/commit/2ae527c0))
- Updated Autospam service, add mark all as read and mark all as not spam options and filter active, spam and not spam reports. ([ae8c7517](https://github.com/pixelfed/pixelfed/commit/ae8c7517))
- Updated UserInviteController, fixes #3017. ([b8e9056e](https://github.com/pixelfed/pixelfed/commit/b8e9056e))
- Updated AccountService, add dynamic user settings methods. ([2aa73c1f](https://github.com/pixelfed/pixelfed/commit/2aa73c1f))
- Updated MediaStorageService, improve header parsing. ([9d9e9ce7](https://github.com/pixelfed/pixelfed/commit/9d9e9ce7))
- Updated SearchApiV2Service, improve performance and include hashtag post counts when applicable ([fbaed93e](https://github.com/pixelfed/pixelfed/commit/fbaed93e))
- Updated AccountTransformer, add note_text and location fields. ([98f76abb](https://github.com/pixelfed/pixelfed/commit/98f76abb))
- Updated UserSetting model, cast compose_settings and other as json. ([03420278](https://github.com/pixelfed/pixelfed/commit/03420278))
- Updated ApiV1Controller, improve settings and add discoverPosts endpoint. ([079804e6](https://github.com/pixelfed/pixelfed/commit/079804e6))
- Updated LikePipeline jobs, fix likes_count calculation. ([fe64e187](https://github.com/pixelfed/pixelfed/commit/fe64e187))
- Updated InternalApiController, prevent moderation actions against admin accounts. ([945a7e49](https://github.com/pixelfed/pixelfed/commit/945a7e49))
- Updated CommentPipeline, move reply_count calculation to comment pipeline job and improve count calculation. ([b6b0837f](https://github.com/pixelfed/pixelfed/commit/b6b0837f))
- Updated ApiV1Controller, improve statusesById perf and dispatch CommentPipeline job when applicable. ([466286af](https://github.com/pixelfed/pixelfed/commit/466286af))
- Updated MediaService, return empty array if cant find status. ([c2910e5d](https://github.com/pixelfed/pixelfed/commit/c2910e5d))
- Updated StatusService, improve cache invalidation. ([83b48b56](https://github.com/pixelfed/pixelfed/commit/83b48b56))
- Updated Hashtag component, fix spinner. ([fefbc44a](https://github.com/pixelfed/pixelfed/commit/fefbc44a))
- Updated NotificationCard, update api endpoint and add group notification types. ([e09a14d8](https://github.com/pixelfed/pixelfed/commit/e09a14d8))
- Updated ContextMenu component, fix account url paths. ([01ca1edd](https://github.com/pixelfed/pixelfed/commit/01ca1edd))
- Updated PollCard component, add showBorder prop. ([0c8fffbd](https://github.com/pixelfed/pixelfed/commit/0c8fffbd))
- Updated PhotoPresenter component, add lightbox toggle. ([0cc1365f](https://github.com/pixelfed/pixelfed/commit/0cc1365f))
- Updated console kernel, add db session garbage collector that runs twice daily. ([03b0a62a](https://github.com/pixelfed/pixelfed/commit/03b0a62a))
- Updated ComposeController, refactor compose_settings. ([edc2958b](https://github.com/pixelfed/pixelfed/commit/edc2958b))
- Updated StatusEntityLexer, prevent boosts and replies from being added to PublicTimelineService. ([32707372](https://github.com/pixelfed/pixelfed/commit/32707372))
- Updated SpaController, persist web language changes. ([7bc684e5](https://github.com/pixelfed/pixelfed/commit/7bc684e5))
- Updated LoginController, bump decayMinutes from 1 to 60. ([6bf92bed](https://github.com/pixelfed/pixelfed/commit/6bf92bed))
- Updated SPA, rewrite autolink urls to SPA when applicable. ([0837b410](https://github.com/pixelfed/pixelfed/commit/0837b410))
- Updated site config, increase ttl and enable SPA by default. ([469d49d8](https://github.com/pixelfed/pixelfed/commit/469d49d8))
- Updated Webfinger, fixes #3050. ([ff7ee3bd](https://github.com/pixelfed/pixelfed/commit/ff7ee3bd))
- Updated status api, autolink caption before returning response. ([b00a453b](https://github.com/pixelfed/pixelfed/commit/b00a453b))
- Updated Timeline, add new ui promo in timelines that can be hidden using localstorage. ([e13959ae](https://github.com/pixelfed/pixelfed/commit/e13959ae))
- Updated FederationController, increase webfinger cache ttl from 12 hours to 14 days. ([745c3580](https://github.com/pixelfed/pixelfed/commit/745c3580))
- Updated DiscoverController, add yearly option and increase limit from 15 to 30 posts. ([10b6058c](https://github.com/pixelfed/pixelfed/commit/10b6058c))
- Updated RemoteAvatarFetch job, fixed bug preventing new avatars from being stored. ([92bc2845](https://github.com/pixelfed/pixelfed/commit/92bc2845))
- Updated AccountService, fix json casting. ([e5f8f344](https://github.com/pixelfed/pixelfed/commit/e5f8f344))
- Updated ApiV1Controller, fix illegal operator bug by setting default min_id. ([415826f2](https://github.com/pixelfed/pixelfed/commit/415826f2))
- Updated StatusService, add getMastodon method for mastoapi compatibility. ([36a129fe](https://github.com/pixelfed/pixelfed/commit/36a129fe))
- Updated PublicApiController, fix accountStatuses pagination operator. ([85fc9dd0](https://github.com/pixelfed/pixelfed/commit/85fc9dd0))
- Updated PublicApiController, enforce only_media on accountStatuses method. Fixes #3105. ([861a2d36](https://github.com/pixelfed/pixelfed/commit/861a2d36))
- Updated ApiV1Controller, add mastoapi strict mode. ([46485426](https://github.com/pixelfed/pixelfed/commit/46485426))
- Updated AccountController, refresh RelationshipService on mute/block. ([6f1b0245](https://github.com/pixelfed/pixelfed/commit/6f1b0245))
- Updated ApiV1Controller, fix version on instance endpoint. ([a6261221](https://github.com/pixelfed/pixelfed/commit/a6261221))
- Updated components, fix api endpoints. Fixes #3138. ([e724633e](https://github.com/pixelfed/pixelfed/commit/e724633e))
- Updated ApiV1Controller, fix public timeline endpoint. ([80c7def3](https://github.com/pixelfed/pixelfed/commit/80c7def3))
- Updated PublicApiController, fix public timeline endpoint. ([dcb7ba9c](https://github.com/pixelfed/pixelfed/commit/dcb7ba9c))
- Updated ApiV1Controller, fix home timeline entities. ([6fc0dcb3](https://github.com/pixelfed/pixelfed/commit/6fc0dcb3))
- Updated ApiV1Controller, fix favourites endpoints ([d6d99385](https://github.com/pixelfed/pixelfed/commit/d6d99385))
- Updated ApiV1Controller, fix reblogs endpoints ([de42d84c](https://github.com/pixelfed/pixelfed/commit/de42d84c))
- Updated SearchApiV2Service, resolve remote queries. ([c8a667f2](https://github.com/pixelfed/pixelfed/commit/c8a667f2))
## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1)
### Added
- WebP Support ([069a0e4a](https://github.com/pixelfed/pixelfed/commit/069a0e4a))
- Auto Following support for admins ([68aa2540](https://github.com/pixelfed/pixelfed/commit/68aa2540))
- Mark as spammer mod tool, unlists and applies content warning to existing and future post ([6d956a86](https://github.com/pixelfed/pixelfed/commit/6d956a86))
- Diagnostics for error page and admin dashboard ([64725ecc](https://github.com/pixelfed/pixelfed/commit/64725ecc))
- Default media licenses and media license sync ([ea0fc90c](https://github.com/pixelfed/pixelfed/commit/ea0fc90c))
- Customize media description/alt-text length limit ([072d55d1](https://github.com/pixelfed/pixelfed/commit/072d55d1))
- Federate Media Licenses ([14a1367a](https://github.com/pixelfed/pixelfed/commit/14a1367a))
- Archive Posts ([e9ef0c88](https://github.com/pixelfed/pixelfed/commit/e9ef0c88))
- Polls ([77092200](https://github.com/pixelfed/pixelfed/commit/77092200))
- Federated Stories (#2895)
### Updated
- Updated PrettyNumber, fix deprecated warning. ([20ec870b](https://github.com/pixelfed/pixelfed/commit/20ec870b))
- Updated landing page, use config_cache. ([54920294](https://github.com/pixelfed/pixelfed/commit/54920294))
- Updated Timeline, implement suggested post opt out. ([66750d34](https://github.com/pixelfed/pixelfed/commit/66750d34))
- Updated Notification component, add at (@) symbol for remote profiles and local urls for remote posts and profile. ([aafd6a21](https://github.com/pixelfed/pixelfed/commit/aafd6a21))
- Updated Activity component, add at (@) symbol for remote profiles and local urls for remote posts and profile. ([a2211815](https://github.com/pixelfed/pixelfed/commit/a2211815))
- Updated Profile, add linkified bio, joined date, follows you label and improved website handling. ([8ee10436](https://github.com/pixelfed/pixelfed/commit/8ee10436))
- Updated routes, add legacy webfinger profile redirect. ([93c7af74](https://github.com/pixelfed/pixelfed/commit/93c7af74))
- Updated StoryController, fix expiration time bug. ([39e57f95](https://github.com/pixelfed/pixelfed/commit/39e57f95))
- Updated Profile component, fix remote urls. ([6e56dbed](https://github.com/pixelfed/pixelfed/commit/6e56dbed))
- Updated verify email screen, add contact admin link. ([f37952d6](https://github.com/pixelfed/pixelfed/commit/f37952d6))
- Updated RemoteProfile component, implement pagination. ([02b04a4b](https://github.com/pixelfed/pixelfed/commit/02b04a4b))
- Updated AP Helpers, generate notification for remote replies. ([8edd8294](https://github.com/pixelfed/pixelfed/commit/8edd8294))
- Updated like api, store status_profile_id and is_comment. ([c8c6b983](https://github.com/pixelfed/pixelfed/commit/c8c6b983))
- Updated Remote Post + Profile hashtag to redirect to local urls. ([1fa08644](https://github.com/pixelfed/pixelfed/commit/1fa08644))
- Updated Inbox, delete notifications on tombstone. ([ef63124d](https://github.com/pixelfed/pixelfed/commit/ef63124d))
- Updated NotificationCard, fix missing status bug. ([a3a86d46](https://github.com/pixelfed/pixelfed/commit/a3a86d46))
- Updated Activity component, fix comment bug. ([9a2db8eb](https://github.com/pixelfed/pixelfed/commit/9a2db8eb))
- Updated Inbox, fix tombstone bug. ([929ff5eb](https://github.com/pixelfed/pixelfed/commit/929ff5eb))
- Updated LikeService, skip self likes. ([3741c76d](https://github.com/pixelfed/pixelfed/commit/3741c76d))
- Updated StatusController, improve share api perf (11s to 72ms). ([d48ebb82](https://github.com/pixelfed/pixelfed/commit/d48ebb82))
- Updated ApiController, fix nulls in hashtag endpoint. ([f1208de0](https://github.com/pixelfed/pixelfed/commit/f1208de0))
- Updated SharePipeline, add Undo->Announce support. ([c8e40e0f](https://github.com/pixelfed/pixelfed/commit/c8e40e0f))
- Updated NetworkTimeline, fix remote comment urls. ([308acc91](https://github.com/pixelfed/pixelfed/commit/308acc91))
- Updated Timeline component, abstracted reusable partials. ([858f3f9e](https://github.com/pixelfed/pixelfed/commit/858f3f9e))
- Updated Timeline, fix suggested posts. ([3ba5c88c](https://github.com/pixelfed/pixelfed/commit/3ba5c88c))
- Updated Timeline, disable new post update checker and hide reaction bar on network timeline. ([1e3d3a69](https://github.com/pixelfed/pixelfed/commit/1e3d3a69))
- Updated PublicApiController, improve network timeline perf. ([e5f683fd](https://github.com/pixelfed/pixelfed/commit/e5f683fd))
- Updated Network Timeline, use existing Timeline component. ([0deaafc0](https://github.com/pixelfed/pixelfed/commit/0deaafc0))
- Updated PostComponent, show like count to owner using MomentUI. ([e9c46bab](https://github.com/pixelfed/pixelfed/commit/e9c46bab))
- Updated ContextMenu, add missing statusUrl method. ([3cffdb11](https://github.com/pixelfed/pixelfed/commit/3cffdb11))
- Updated PublicApiController, add LikeService to Network timeline. ([82895591](https://github.com/pixelfed/pixelfed/commit/82895591))
- Updated moderator api, expire cached status in StatusService. ([f215ee26](https://github.com/pixelfed/pixelfed/commit/f215ee26))
- Updated StatusHashtagService, fix null status bug. ([51a277e1](https://github.com/pixelfed/pixelfed/commit/51a277e1))
- Updated NotificationService, use zrevrangebyscore for api. ([d43e6d8d](https://github.com/pixelfed/pixelfed/commit/d43e6d8d))
- Updated ApiV1Controller, use PublicTimelineService. ([f67c67bc](https://github.com/pixelfed/pixelfed/commit/f67c67bc))
- Updated ApiV1Controller, use ProfileService for verify_credentials. ([352aa573](https://github.com/pixelfed/pixelfed/commit/352aa573))
- Updated RemotePost.vue, fix content warning button. ([7647e724](https://github.com/pixelfed/pixelfed/commit/7647e724))
- Updated AdminMediaController, improve perf and use simple pagination. ([f2686cac](https://github.com/pixelfed/pixelfed/commit/f2686cac))
- Updated PostComponent, fix MomentUI like counter. ([42c6121a](https://github.com/pixelfed/pixelfed/commit/42c6121a))
- Updated status views, remove like counts from status embed. ([1a2e41b1](https://github.com/pixelfed/pixelfed/commit/1a2e41b1))
- Updated Profile, fix unauthenticated private profiles. ([9017f7c4](https://github.com/pixelfed/pixelfed/commit/9017f7c4))
- Updated PublicApiController, impr home timeline perf. ([4fe42e5b](https://github.com/pixelfed/pixelfed/commit/4fe42e5b))
- Updated Timeline.vue, fix comment button. ([b6b5ce7c](https://github.com/pixelfed/pixelfed/commit/b6b5ce7c))
- Updated StatusEntityLexer, only add specific status types to PublicTimelineService. ([1fdcbe5b](https://github.com/pixelfed/pixelfed/commit/1fdcbe5b))
- Updated ActivityPub helpers, fix comment threading in statusFetch() method ([26b9c140](https://github.com/pixelfed/pixelfed/commit/26b9c140))
- Updated NotificationCard, fix typo in mention, share and comments. Fixes #2848. ([b37bb426](https://github.com/pixelfed/pixelfed/commit/b37bb426))
- Updated StatusCard.vue, add togglecw events to other presenters. ([9607243f](https://github.com/pixelfed/pixelfed/commit/9607243f))
- Updated presenters, fix content warning layout. ([fc56acb8](https://github.com/pixelfed/pixelfed/commit/fc56acb8))
- Updated reply blade view, fix missing avatar and media images. ([5fb33772](https://github.com/pixelfed/pixelfed/commit/5fb33772))
- Updated components, add fallback default avatar. ([726553f5](https://github.com/pixelfed/pixelfed/commit/726553f5))
- Updated job queue, separate deletes into their own queue. ([7f421392](https://github.com/pixelfed/pixelfed/commit/7f421392))
- Updated DiscoverController, use UserFilterService on trendingApi. ([135474ae](https://github.com/pixelfed/pixelfed/commit/135474ae))
- Updated PublicApiController, use UserFilterService in public timeline endpoint. ([ca6e491c](https://github.com/pixelfed/pixelfed/commit/ca6e491c))
- Updated ContextMenu, add View Profile link. ([8544bcbd](https://github.com/pixelfed/pixelfed/commit/8544bcbd))
- Updated presenters, improve content warnings. ([86422c81](https://github.com/pixelfed/pixelfed/commit/86422c81))
- Updated Timeline.vue, increase pagination limit from 3 to 12 and add empty feed placeholder. ([916e8f71](https://github.com/pixelfed/pixelfed/commit/916e8f71))
- Updated Timeline.vue, improve followed hashtags. ([728f10d7](https://github.com/pixelfed/pixelfed/commit/728f10d7))
- Updated PostComponent, use profileUrl method for comments. ([7ed65fc9](https://github.com/pixelfed/pixelfed/commit/7ed65fc9))
- Updated Timeline, fix empty timeline card. ([11eb6acd](https://github.com/pixelfed/pixelfed/commit/11eb6acd))
- Updated ap helpers, set text type when appropriate. ([9f4f983f](https://github.com/pixelfed/pixelfed/commit/9f4f983f))
- Updated StatusCard, add text support. ([ed14ee48](https://github.com/pixelfed/pixelfed/commit/ed14ee48))
- Updated PublicApiController, filter out text replies on home timeline. ([86219b57](https://github.com/pixelfed/pixelfed/commit/86219b57))
- Updated RemotePost.vue, improve text only post UI. ([b0257be2](https://github.com/pixelfed/pixelfed/commit/b0257be2))
- Updated Timeline, make text-only posts opt-in by default. ([0153ed6d](https://github.com/pixelfed/pixelfed/commit/0153ed6d))
- Updated LikeController, add UndoLikePipeline and federate Undo Like activities. ([8ac8fcad](https://github.com/pixelfed/pixelfed/commit/8ac8fcad))
- Updated Settings, add default license and enforced media descriptions. ([67e3f604](https://github.com/pixelfed/pixelfed/commit/67e3f604))
- Updated Compose Apis, make media descriptions/alt text length limit configurable. Default length: 1000. ([072d55d1](https://github.com/pixelfed/pixelfed/commit/072d55d1))
- Updated ApiV1Controller, add default license support. ([2a791f19](https://github.com/pixelfed/pixelfed/commit/2a791f19))
- Updated StatusTransformers, remove includes and use cached services. ([09d5198c](https://github.com/pixelfed/pixelfed/commit/09d5198c))
- Updated RemotePost component, update likes reaction bar. ([1060dd23](https://github.com/pixelfed/pixelfed/commit/1060dd23))
- Updated FollowPipeline, fix cache invalidation bug. ([c1f14f89](https://github.com/pixelfed/pixelfed/commit/c1f14f89))
- Updated PublicApiController, improve accountStatuses api perf. ([bce8edd9](https://github.com/pixelfed/pixelfed/commit/bce8edd9))
- Updated ApiControllers, use NotificationService. ([f9516ac3](https://github.com/pixelfed/pixelfed/commit/f9516ac3))
- Updated Notification components, fix old notifications with missing attributes. ([b6e226ae](https://github.com/pixelfed/pixelfed/commit/b6e226ae))
- Updated LikeController, improve query perf. ([f3d6023e](https://github.com/pixelfed/pixelfed/commit/f3d6023e))
- Updated License util, add nameToId method. ([f6131ed7](https://github.com/pixelfed/pixelfed/commit/f6131ed7))
- Updated RemoteProfile, add warning about potentially out of date information. ([7274574c](https://github.com/pixelfed/pixelfed/commit/7274574c))
- Updated NotifcationCard.vue component, add refresh button for cold notification cache. ([0e178a33](https://github.com/pixelfed/pixelfed/commit/0e178a33))
- Updated RemoteProfile component, add follower modals. ([c4146a30](https://github.com/pixelfed/pixelfed/commit/c4146a30))
- Updated FollowerService, cache audience. ([22257cc2](https://github.com/pixelfed/pixelfed/commit/22257cc2))
- Updated StatusService, add non-public option and improve cache invalidation. ([15c4fdd9](https://github.com/pixelfed/pixelfed/commit/15c4fdd9))
- Updated ContactAdmin mail, set New Support Message subject. ([bc3add05](https://github.com/pixelfed/pixelfed/commit/bc3add05))
- Updated StatusTransformer, prioritize scope over deprecated visibility attribute. ([6e45021f](https://github.com/pixelfed/pixelfed/commit/6e45021f))
- Updated StatusService, invalidate profile embed cache on deletion. ([acaf630d](https://github.com/pixelfed/pixelfed/commit/acaf630d))
- Updated status.reply view, fix archived post leakage. ([4fb3d1fa](https://github.com/pixelfed/pixelfed/commit/4fb3d1fa))
- Updated PostComponents, re-add time to timestamp. ([c5281dcd](https://github.com/pixelfed/pixelfed/commit/c5281dcd))
- Updated follow intent, fix follower count leak. ([03199e2f](https://github.com/pixelfed/pixelfed/commit/03199e2f))
- Updated Status model, add poll relation and allow up to 2 urls to autolink. ([2593cdee](https://github.com/pixelfed/pixelfed/commit/2593cdee))
- Updated snowflake id generation to improve randomness. ([e5aea490](https://github.com/pixelfed/pixelfed/commit/e5aea490))
- Updated Timeline, remove recent posts. ([7641b731](https://github.com/pixelfed/pixelfed/commit/7641b731))
- Updated InstanceCrawlPipeline, remove unused variable. ([e73cf531](https://github.com/pixelfed/pixelfed/commit/e73cf531))
- Updated StoryComposeController, fix expiry bug. ([7dee8f58](https://github.com/pixelfed/pixelfed/commit/7dee8f58))
- Updated Profile, fix following count bug. ([ee9f0795](https://github.com/pixelfed/pixelfed/commit/ee9f0795))
- Updated DirectMessageController, fix autocomplete bug. ([0f00be4d](https://github.com/pixelfed/pixelfed/commit/0f00be4d))
- Updated StoryService, fix division by zero bug. ([6ae1ba0a](https://github.com/pixelfed/pixelfed/commit/6ae1ba0a))
- Updated ApiV1Controller, fix empty public timeline bug. ([0584f9ee](https://github.com/pixelfed/pixelfed/commit/0584f9ee))
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
### Added
- Autocomplete Support (hashtags + mentions) ([de514f7d](https://github.com/pixelfed/pixelfed/commit/de514f7d))
- Creative Commons Licenses ([552e950](https://github.com/pixelfed/pixelfed/commit/552e950))
- Network Timeline ([af7face4](https://github.com/pixelfed/pixelfed/commit/af7face4))
- Admin config settings ([f2066b74](https://github.com/pixelfed/pixelfed/commit/f2066b74))
- Profile pronouns ([fabb57a9](https://github.com/pixelfed/pixelfed/commit/fabb57a9))
- Hashtag timeline api support ([241ae036](https://github.com/pixelfed/pixelfed/commit/241ae036))
- New admin dashboard layout ([eb7d5a4e](https://github.com/pixelfed/pixelfed/commit/eb7d5a4e))
- Fresh about page layout ([92dc7af6](https://github.com/pixelfed/pixelfed/commit/92dc7af6))
- Instance Rules ([a4efbb75](https://github.com/pixelfed/pixelfed/commit/a4efbb75))
- New Home Timeline ([56215be7](https://github.com/pixelfed/pixelfed/commit/56215be7))
### Updated
- Updated AdminController, fix variable name in updateSpam method. ([6edaf940](https://github.com/pixelfed/pixelfed/commit/6edaf940))
- Updated RemoteAvatarFetch, only dispatch jobs if cloud storage is enabled. ([4f40f6f5](https://github.com/pixelfed/pixelfed/commit/4f40f6f5))
- Updated StatusService, add ttl of 7 days. ([6e44ae0b](https://github.com/pixelfed/pixelfed/commit/6e44ae0b))
- Updated StatusHashtagService, use StatusService for statuses. ([0355b567](https://github.com/pixelfed/pixelfed/commit/0355b567))
- Updated StatusHashtagService, remove deprecated methods. ([aa4c718d](https://github.com/pixelfed/pixelfed/commit/aa4c718d))
- Updated ApiV1Controller, add StatusService del calls to update likes_count, reblogs_count and reply_count. ([05b9445c](https://github.com/pixelfed/pixelfed/commit/05b9445c))
- Updated Like, Status and Comment controllers to add StatusService del() method to update counts. ([eab4370c](https://github.com/pixelfed/pixelfed/commit/eab4370c))
- Updated ComposeController, use placeholder image for video media. Fixes #2595. ([789ed4b4](https://github.com/pixelfed/pixelfed/commit/789ed4b4))
- Updated DiscoverController, change api schema. ([2eea0409](https://github.com/pixelfed/pixelfed/commit/2eea0409))
- Updated StatusDelete pipeline, call StatusService::del() to remove status from cache. ([3f772ff8](https://github.com/pixelfed/pixelfed/commit/3f772ff8))
- Updated StatusHashtagTransformer, add blurhash attribute. ([899bbeba](https://github.com/pixelfed/pixelfed/commit/899bbeba))
- Updated status square previews, add blurhash and improved content warnings. ([39e389dd](https://github.com/pixelfed/pixelfed/commit/39e389dd))
- Updated Blurhash util, add default hash for invalid media. ([38a37c15](https://github.com/pixelfed/pixelfed/commit/38a37c15))
- Updated VideoThumbnail job, generate blurhash for videos. ([896452c7](https://github.com/pixelfed/pixelfed/commit/896452c7))
- Updated MediaTransformers, add default blurhash attribute. ([3f14a4c4](https://github.com/pixelfed/pixelfed/commit/3f14a4c4))
- Updated Timeline.vue, fix hashtag status previews. ([7768e844](https://github.com/pixelfed/pixelfed/commit/7768e844))
- Updated AP helpers, fix statusFetch 404s. ([3419379a](https://github.com/pixelfed/pixelfed/commit/3419379a))
- Updated InternalApiController, update discoverPosts method to improve performance. ([9862a855](https://github.com/pixelfed/pixelfed/commit/9862a855))
- Updated DiscoverComponent, add blurhash and like/comment counts. ([a8ebdd2e](https://github.com/pixelfed/pixelfed/commit/a8ebdd2e))
- Updated DiscoverComponent, add spinner loaders and remove deprecated sections. ([34869247](https://github.com/pixelfed/pixelfed/commit/34869247))
- Updated AccountController, add mutes and blocks endpoint to pixelfed api. ([1fb7e2b2](https://github.com/pixelfed/pixelfed/commit/1fb7e2b2))
- Updated AccountService, cache object and observe changes. ([b299da93](https://github.com/pixelfed/pixelfed/commit/b299da93))
- Updated webfinger util, fail on invalid webfinger url. Fixes ([#2613](https://github.com/pixelfed/pixelfed/issues/2613)) ([2d11317c](https://github.com/pixelfed/pixelfed/commit/2d11317c))
- Updated MediaStorageService, dispatch deletes to MediaDeletePipeline. ([37dbb3de](https://github.com/pixelfed/pixelfed/commit/37dbb3de))
- Updated ComposeController, use MediaStorageService for media deletes. ([ab5469ff](https://github.com/pixelfed/pixelfed/commit/ab5469ff))
- Updated StatusDeletePipeline, use MediaStorageService for media deletes. ([9fd90e17](https://github.com/pixelfed/pixelfed/commit/9fd90e17))
- Updated Discover, allow public discover access. ([1404ac6e](https://github.com/pixelfed/pixelfed/commit/1404ac6e))
- Updated pixelfed config, add media_fast_process setting. ([6bee5072](https://github.com/pixelfed/pixelfed/commit/6bee5072))
- Updated ComposeController, add mediaProcessingCheck method. ([33b625f5](https://github.com/pixelfed/pixelfed/commit/33b625f5))
- Updated ComposeModal, add processing step disabled by default. ([e6e76e80](https://github.com/pixelfed/pixelfed/commit/e6e76e80))
- Updated DiscoverComponent, allow unauthenticated if enabled. ([a1059a6e](https://github.com/pixelfed/pixelfed/commit/a1059a6e))
- Updated components, improve content warnings. ([a9e98965](https://github.com/pixelfed/pixelfed/commit/a9e98965))
- Updated ComposeModal, prevent tagging empty users. Fixes #2633. ([ceae664c](https://github.com/pixelfed/pixelfed/commit/ceae664c))
- Updated ComposeModal, show filter warning for unsupported browsers. ([12ce7602](https://github.com/pixelfed/pixelfed/commit/12ce7602))
- Updated Hashtag component, fix null infinite loading bug. Fixes #2637. ([55136518](https://github.com/pixelfed/pixelfed/commit/55136518))
- Updated filesystems config, add backup driver to store backups on other filesystems. ([ae90eef9](https://github.com/pixelfed/pixelfed/commit/ae90eef9))
- Updated Embeds. Fix Profile + Status embeds, remove following count and improve cache invalidation and hidden follower counts. ([5ac9d0e8](https://github.com/pixelfed/pixelfed/commit/5ac9d0e8))
- Updated FederationController, return 404 for invalid webfinger addresses. Fixes ([#2647](https://github.com/pixelfed/pixelfed/issues/2647)). ([deb6f115](https://github.com/pixelfed/pixelfed/commit/deb6f115))
- Updated InboxPipeline, fail earlier for invalid public keys. Fixes ([#2648](https://github.com/pixelfed/pixelfed/issues/2648)). ([d1c5e9b8](https://github.com/pixelfed/pixelfed/commit/d1c5e9b8))
- Updated Status model, refactor liked and shared methods to fix cache invalidation bug. ([f05c3b66](https://github.com/pixelfed/pixelfed/commit/f05c3b66))
- Updated Timeline component, add inline reports modal. ([e64b4bd3](https://github.com/pixelfed/pixelfed/commit/e64b4bd3))
- Updated federation pipeline, add locks. ([ddc76887](https://github.com/pixelfed/pixelfed/commit/ddc76887))
- Updated MediaStorageService, improve head checks to fix failed jobs. ([1769cdfd](https://github.com/pixelfed/pixelfed/commit/1769cdfd))
- Updated user admin, remove expensive db query and add search. ([8feeadbf](https://github.com/pixelfed/pixelfed/commit/8feeadbf))
- Updated Compose apis, prevent private accounts from posting public or unlisted scopes. ([f53bfa6f](https://github.com/pixelfed/pixelfed/commit/f53bfa6f))
- Updated font icons, use font-display:swap. ([77d4353a](https://github.com/pixelfed/pixelfed/commit/77d4353a))
- Updated ComposeModal, limit visibility scope for private accounts. ([001d4105](https://github.com/pixelfed/pixelfed/commit/001d4105))
- Updated ComposeController, add autocomplete apis for hashtags and mentions. ([f0e48a09](https://github.com/pixelfed/pixelfed/commit/f0e48a09))
- Updated StatusController, invalidate profile embed cache on status delete. ([9c8a87c3](https://github.com/pixelfed/pixelfed/commit/9c8a87c3))
- Updated moderation api, invalidate profile embed. ([b2501bfc](https://github.com/pixelfed/pixelfed/commit/b2501bfc))
- Updated Nodeinfo util, use last_active_at for monthly active user count. ([d200c12c](https://github.com/pixelfed/pixelfed/commit/d200c12c))
- Updated PhotoPresenter, add width and height to images. ([3f8202e2](https://github.com/pixelfed/pixelfed/commit/3f8202e2))
- Updated Compose Apis, refactor rate limits. ([42375b3d](https://github.com/pixelfed/pixelfed/commit/42375b3d))
- Updated PublicApiController, show unlisted comments. ([e1c6297e](https://github.com/pixelfed/pixelfed/commit/e1c6297e))
- Updated ApiV1Controller, add missing variable. ([886ea617](https://github.com/pixelfed/pixelfed/commit/886ea617))
- Updated PublicApiController, limit network pagination to 3 months. ([10119bbb](https://github.com/pixelfed/pixelfed/commit/10119bbb))
- Updated admin instance page, add search and improve performance. ([f5829373](https://github.com/pixelfed/pixelfed/commit/f5829373))
- Updated AdminInstanceController, invalidate banned domain cache when updated. ([35393edf](https://github.com/pixelfed/pixelfed/commit/35393edf))
- Updated AP Helpers, use instance filtering. ([66b4f8c7](https://github.com/pixelfed/pixelfed/commit/66b4f8c7))
- Updated ApiV1Controller, add missing instance api attributes. ([64b86546](https://github.com/pixelfed/pixelfed/commit/64b86546))
- Updated story garbage collection, handle non active stories and new ephemeral story media directory. ([c43f8bcc](https://github.com/pixelfed/pixelfed/commit/c43f8bcc))
- Updated Stories, add crop and duration settings to composer. ([c8edca69](https://github.com/pixelfed/pixelfed/commit/c8edca69))
- Updated instance endpoint, add custom description. ([668e936e](https://github.com/pixelfed/pixelfed/commit/668e936e))
- Updated StoryCompose component, improve full screen preview. ([39a76103](https://github.com/pixelfed/pixelfed/commit/39a76103))
- Updated Helpers, fix broken tests. ([22dddaa0](https://github.com/pixelfed/pixelfed/commit/22dddaa0))
- Updated StoryController, fix cache crop bug. ([c2f8faae](https://github.com/pixelfed/pixelfed/commit/c2f8faae))
- Updated StoryController, optimize photo size by resizing to 9:16 aspect. ([e66ed9a2](https://github.com/pixelfed/pixelfed/commit/e66ed9a2))
- Updated StoryCompose crop logic. ([2ead622c](https://github.com/pixelfed/pixelfed/commit/2ead622c))
- Updated StatusController, allow license edits without 24 hour limit. ([c799a01a](https://github.com/pixelfed/pixelfed/commit/c799a01a))
- Updated Settings, remove reports page. ([9cf962ff](https://github.com/pixelfed/pixelfed/commit/9cf962ff))
- Updated ProfileService, use account transformer. ([391b1287](https://github.com/pixelfed/pixelfed/commit/391b1287))
- Updated LikeController, hide like counts. ([ea687240](https://github.com/pixelfed/pixelfed/commit/ea687240))
- Updated StatusTransformers, add liked_by attribute. ([372bacb0](https://github.com/pixelfed/pixelfed/commit/372bacb0))
- Updated PostComponent, change like logic. ([0a35f5d6](https://github.com/pixelfed/pixelfed/commit/0a35f5d6))
- Updated Timeline component, change like logic. ([7bcbf96b](https://github.com/pixelfed/pixelfed/commit/7bcbf96b))
- Updated LikeService, fix likedBy method. ([a5e64da6](https://github.com/pixelfed/pixelfed/commit/a5e64da6))
- Updated PublicApiController, increase public timeline to 6 months from 3. ([8a736432](https://github.com/pixelfed/pixelfed/commit/8a736432))
- Updated LikeService, show like count to status owner. ([4408e2ef](https://github.com/pixelfed/pixelfed/commit/4408e2ef))
- Updated admin settings, add rules. ([a4efbb75](https://github.com/pixelfed/pixelfed/commit/a4efbb75))
- Updated LikeService, fix authentication bug. ([c9abd70e](https://github.com/pixelfed/pixelfed/commit/c9abd70e))
- Updated StatusTransformer, fix missing tags attribute. ([dac326e9](https://github.com/pixelfed/pixelfed/commit/dac326e9))
- Updated ComposeController, bail on empty attachments. ([061b145b](https://github.com/pixelfed/pixelfed/commit/061b145b))
- Updated landing and about page. ([92dc7af6](https://github.com/pixelfed/pixelfed/commit/92dc7af6))
- Updated AdminStatsService, fix postgres bug. ([af719135](https://github.com/pixelfed/pixelfed/commit/af719135))
- Updated api, remove auth requirement for hashtag timeline. ([c8e43c60](https://github.com/pixelfed/pixelfed/commit/c8e43c60))
- Updated NotificationCard component, fix default value. ([78ad4e77](https://github.com/pixelfed/pixelfed/commit/78ad4e77))
- Updated Timeline component, show counts and make sidebar footer lighter. ([0788bffa](https://github.com/pixelfed/pixelfed/commit/0788bffa))
- Updated AuthServiceProvider, increase default token + refresh token lifetime. ([178ed63d](https://github.com/pixelfed/pixelfed/commit/178ed63d))
- Updated liked by, fix remote username urls. ([f767d99a](https://github.com/pixelfed/pixelfed/commit/f767d99a))
- Updated StatusController, add cache invalidation for timeline cursor. ([f3bf2fd4](https://github.com/pixelfed/pixelfed/commit/f3bf2fd4))
- Updated PublicApiController, add recent feed support to home timeline. ([1e230e80](https://github.com/pixelfed/pixelfed/commit/1e230e80))
- Updated Inbox, fix reply/comment bug by moving attachment validation to Note with attachments. ([28df9f7e](https://github.com/pixelfed/pixelfed/commit/28df9f7e))
- Updated PrettyNumber, add decimal option. ([84520fe1](https://github.com/pixelfed/pixelfed/commit/84520fe1))
- Updated app config, change default descriptions. ([7d24560d](https://github.com/pixelfed/pixelfed/commit/7d24560d))
- Updated NotificationCard, fix loading bug. ([69567e19](https://github.com/pixelfed/pixelfed/commit/69567e19))
- Updated DirectMessageController, disable exception logging for invalid urls. Fixes ([#2752](https://github.com/pixelfed/pixelfed/issues/2752)). ([2d0a253e](https://github.com/pixelfed/pixelfed/commit/2d0a253e))
## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10)
### Added
- Direct Messages ([d63569c](https://github.com/pixelfed/pixelfed/commit/d63569c))
- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5))
- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5)) ([3ee1215a](https://github.com/pixelfed/pixelfed/commit/3ee1215a))
- Custom content warnings for remote posts ([6afc61a4](https://github.com/pixelfed/pixelfed/commit/6afc61a4))
- Thai translations ([74cd536](https://github.com/pixelfed/pixelfed/commit/74cd536))
- Added Bookmarks to v1 api ([99cb48c5](https://github.com/pixelfed/pixelfed/commit/99cb48c5))
- Added New Post notification to Timeline ([a0e7c4d5](https://github.com/pixelfed/pixelfed/commit/a0e7c4d5))
- Add Instagram Import ([e2a6bdd0](https://github.com/pixelfed/pixelfed/commit/e2a6bdd0))
- Add notification preview to NotificationCard ([28445e27](https://github.com/pixelfed/pixelfed/commit/28445e27))
- Add Grid Mode to Timelines ([c1853ca8](https://github.com/pixelfed/pixelfed/commit/c1853ca8))
- Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5))
- Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020))
- Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
@ -24,6 +373,7 @@
- Add autospam feature ([b892bcf0](https://github.com/pixelfed/pixelfed/commit/b892bcf0))
- Add hCaptcha ([082c1ccb](https://github.com/pixelfed/pixelfed/commit/082c1ccb))
- Add StatusView model to store views for discover algorithm ([7a68ee94](https://github.com/pixelfed/pixelfed/commit/7a68ee94))
- Add Year in Review feature (mysql only) ([f32072a3](https://github.com/pixelfed/pixelfed/commit/f32072a3))
### Updated
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
@ -145,8 +495,8 @@
- Updated avatars, use jpeg default. ([f6528c84](https://github.com/pixelfed/pixelfed/commit/f6528c84))
- Updated antispam bouncer, change recent from 1 week to 3 months. ([7d818197](https://github.com/pixelfed/pixelfed/commit/7d818197))
- Updated Post components, fix remote post and profile urls. ([cfcf17f3](https://github.com/pixelfed/pixelfed/commit/cfcf17f3))
- Update migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
- Update LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
- Updated migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
- Updated LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
- Updated Profile, fix status count. ([6dcd472b](https://github.com/pixelfed/pixelfed/commit/6dcd472b))
- Updated StatusService, cast response to array. ([0fbde91e](https://github.com/pixelfed/pixelfed/commit/0fbde91e))
- Updated status model, use scope over deprecated visibility attribute. ([f70826e1](https://github.com/pixelfed/pixelfed/commit/f70826e1))
@ -155,7 +505,44 @@
- Updated AP helpers, fixed federation bug. ([a52564f3](https://github.com/pixelfed/pixelfed/commit/a52564f3))
- Updated Helpers, cache profiles. ([1f672ecf](https://github.com/pixelfed/pixelfed/commit/1f672ecf))
- Updated DiscoverController, improve trending api performance. ([d8d3331f](https://github.com/pixelfed/pixelfed/commit/d8d3331f))
- Update InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
- Updated InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
- Updated StoryItemTransformer, increase story duration from 5 seconds to 10 seconds. ([5b0b14fc](https://github.com/pixelfed/pixelfed/commit/5b0b14fc))
- Updated StatusController, add view method. ([0cfc12c5](https://github.com/pixelfed/pixelfed/commit/0cfc12c5))
- Updated MediaPathService, add story method. ([aac44309](https://github.com/pixelfed/pixelfed/commit/aac44309))
- Updated StatusDelete job, handle cloud storage media deletes. ([4b1a0fd7](https://github.com/pixelfed/pixelfed/commit/4b1a0fd7))
- Updated ImageOptimizePipeline, add skip_optimize and MediaStorageService support. ([234f72f3](https://github.com/pixelfed/pixelfed/commit/234f72f3))
- Updated Media model, add cdn support to url and thumbnailUrl methods. ([57fa889d](https://github.com/pixelfed/pixelfed/commit/57fa889d))
- Updated MediaController, remove deprecated endpoint. ([8132db74](https://github.com/pixelfed/pixelfed/commit/8132db74))
- Updated api controllers, deprecate old endpoints. ([4415af1b](https://github.com/pixelfed/pixelfed/commit/4415af1b))
- Updated mobile apis, add blurhash. ([cf40526e](https://github.com/pixelfed/pixelfed/commit/cf40526e))
- Updated Image media util, store dimensions of media not thumbnail. ([40bd64aa](https://github.com/pixelfed/pixelfed/commit/40bd64aa))
- Updated MediaTransformers, include meta attribute with focus and dimensions. ([f8cbe1e4](https://github.com/pixelfed/pixelfed/commit/f8cbe1e4))
- Updated storage, add remote media cache directory. ([0eabbfdd](https://github.com/pixelfed/pixelfed/commit/0eabbfdd))
- Updated backup config, prevents gateway timeouts for large databases using mysql. ([9cd4bd74](https://github.com/pixelfed/pixelfed/commit/9cd4bd74))
- Updated MediaPipeline, handle cloud object storage. ([be6d12fc](https://github.com/pixelfed/pixelfed/commit/be6d12fc))
- Updated AP Helpers, use MediaStoragePipeline. ([01a1ffd6](https://github.com/pixelfed/pixelfed/commit/01a1ffd6))
- Updated RemoteProfile component, change thumbnail url. ([c1118956](https://github.com/pixelfed/pixelfed/commit/c1118956))
- Updated blade views. ([9683e846](https://github.com/pixelfed/pixelfed/commit/9683e846))
- Updated cache config, use phpredis by default. ([ed6877df](https://github.com/pixelfed/pixelfed/commit/ed6877df))
- Updated components, fix url rewriter. Closes #2538. ([e8cc66dc](https://github.com/pixelfed/pixelfed/commit/e8cc66dc))
- Updated UserCreate command, closes #2581. ([b2b8c9f9](https://github.com/pixelfed/pixelfed/commit/b2b8c9f9))
- Updated AvatarController, remove deprecated thumb_path. ([889c3d87](https://github.com/pixelfed/pixelfed/commit/889c3d87))
- Updated VideoThumbnail, add MediaStoragePipeline. ([98c44f7b](https://github.com/pixelfed/pixelfed/commit/98c44f7b))
- Updated StatusDelete pipeline, fix object storage thumbnail deletion. ([f930c4bd](https://github.com/pixelfed/pixelfed/commit/f930c4bd))
- Updated MediaStorageService, clear transformer cache after storing media. ([ce6ab80d](https://github.com/pixelfed/pixelfed/commit/ce6ab80d))
- Updated MediaTransformer, remove cache busting. ([258b2729](https://github.com/pixelfed/pixelfed/commit/258b2729))
- Updated AP helpers, only run MediaStoragePipeline if using cloud storage. ([77f21b4b](https://github.com/pixelfed/pixelfed/commit/77f21b4b))
- Updated AvatarObserver, add logic to delete avatars stored in S3. ([9eafc31e](https://github.com/pixelfed/pixelfed/commit/9eafc31e))
- Updated Profile model, use cdn_url for avatars. ([ea8e4261](https://github.com/pixelfed/pixelfed/commit/ea8e4261))
- Updated ActivityPubFetchService, add url validation. ([654b08d3](https://github.com/pixelfed/pixelfed/commit/654b08d3))
- Updated MediaStorageService, add avatar method. ([94a9f685](https://github.com/pixelfed/pixelfed/commit/94a9f685))
- Updated AvatarPipeline, add remote avatar fetch. ([4c148055](https://github.com/pixelfed/pixelfed/commit/4c148055))
- Updated ComposeController, update media version. ([cc2d4bf8](https://github.com/pixelfed/pixelfed/commit/cc2d4bf8))
- Updated AP Helpers, add blurhash and RemoteAvatarFetch. ([de8828e8](https://github.com/pixelfed/pixelfed/commit/de8828e8))
- Updated Timeline, prevent nextTick() when reloading same comment modal. Fixes #2584. ([cc84125b](https://github.com/pixelfed/pixelfed/commit/cc84125b))
- Updated site config, add labels to config. ([abe9cb3d](https://github.com/pixelfed/pixelfed/commit/abe9cb3d))
- Update StatusLabelService, change config key. ([4abfe76a](https://github.com/pixelfed/pixelfed/commit/4abfe76a))
## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
### Added
@ -213,7 +600,7 @@
- Updated StatusTransformer, fixes #[2113](https://github.com/pixelfed/pixelfed/issues/2113) ([eefa6e0d](https://github.com/pixelfed/pixelfed/commit/eefa6e0d))
- Updated InternalApiController, limit remote profile ui to remote profiles ([d918a68e](https://github.com/pixelfed/pixelfed/commit/d918a68e))
- Updated NotificationCard, fix pagination bug #[2019](https://github.com/pixelfed/pixelfed/issues/2019) ([32beaad5](https://github.com/pixelfed/pixelfed/commit/32beaad5))
-
## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
### Added
@ -304,7 +691,7 @@
- Updated CollectionController, increase limit from 18 to 50. ([c2826fd3](https://github.com/pixelfed/pixelfed/c2826fd3))
## Deprecated
## [v0.10.6 (2019-09-30)](https://github.com/pixelfed/pixelfed/compare/v0.10.5...v0.10.6)
@ -369,7 +756,7 @@
- Run ```php artisan passport:keys```
- Add ```OAUTH_ENABLED=true``` to .env
- Run ```php artisan config:cache```
## [v0.10.5 (2019-09-24)](https://github.com/pixelfed/pixelfed/compare/v0.10.4...v0.10.5)
@ -380,8 +767,8 @@
- Fixed cache bug in privacy and terms pages [#1712](https://github.com/pixelfed/pixelfed/commit/fe522da8db7a8b0d7c18d405abcb885f8678f35c)
### Changed
## [v0.10.4 (2019-09-24)](https://github.com/pixelfed/pixelfed/compare/v0.10.3...v0.10.4)
### Added
@ -412,17 +799,17 @@
## Deprecated
- Remove deprecated profile following/followers [#1697](https://github.com/pixelfed/pixelfed/pull/1697)
- Remove old comment permalink [05f6598](https://github.com/pixelfed/pixelfed/pull/1708/commits/05f659896d903e1ff41dba810f125d721fa057e7)
## [v0.10.3 (2019-09-08)](https://github.com/pixelfed/pixelfed/compare/v0.10.2...v0.10.3)
### Added
- Append ```.json``` to local status urls to view ActivityPub object [#1666](https://github.com/pixelfed/pixelfed/pull/1666)
### Fixed
- Reverted ```strict``` Same-Site Cookies to ```null``` to fix 2FA/session expiry [#1667](https://github.com/pixelfed/pixelfed/pull/1667)
- Fixed AP errors by storing ActivityPub object id and url [#1668](https://github.com/pixelfed/pixelfed/pull/1668) [#1683](https://github.com/pixelfed/pixelfed/pull/1683)
- Fixed content warnings that had filter applied [#1669](https://github.com/pixelfed/pixelfed/pull/1669)
- Reverted ```strict``` Same-Site Cookies to ```null``` to fix 2FA/session expiry [#1667](https://github.com/pixelfed/pixelfed/pull/1667)
- Fixed AP errors by storing ActivityPub object id and url [#1668](https://github.com/pixelfed/pixelfed/pull/1668) [#1683](https://github.com/pixelfed/pixelfed/pull/1683)
- Fixed content warnings that had filter applied [#1669](https://github.com/pixelfed/pixelfed/pull/1669)
### Changed
- Japanese Translations [#1673](https://github.com/pixelfed/pixelfed/pull/1673)
@ -432,7 +819,7 @@
### Deprecated
- Personalized Discover has been deprecated due to low use [#1670](https://github.com/pixelfed/pixelfed/pull/1670)
## [v0.10.2 (2019-09-06)](https://github.com/pixelfed/pixelfed/compare/v0.10.1...v0.10.2)
@ -452,7 +839,7 @@
- Loops! Discover short videos
- Preliminary support for profile PropertyValue metadata
- Preliminary support for Direct Messages
- Places! Run the artisan task `import:cities`
- Places! Run the artisan task `import:cities`
- Emails are now validated and banned email domains are disallowed at signup. Artisan task `email:bancheck` will validate existing users.
- .env vars `REDIS_SCHEME` and `REDIS_PATH` allow for using Redis over a Unix socket instead of TCP [#1602](https://github.com/pixelfed/pixelfed/pull/1602)
- .env var `IMAGE_DRIVER` allows using imagick instead of gd
@ -475,7 +862,7 @@
- Sample nginx.conf in contrib/ now uses HTTPS instead of HTTP. Docs updated to reference this file
- Updated register form
- Allow users to edit email after registrations
## [v0.10.0 (2019-07-17)](https://github.com/pixelfed/pixelfed/compare/v0.9.6...v0.10.0)
@ -496,7 +883,7 @@
### Fixed
- Hashtag post count off-by-one [#1485](https://github.com/pixelfed/pixelfed/pull/1485)
## [v0.9.5 (2019-07-10)](https://github.com/pixelfed/pixelfed/compare/v0.9.4...v0.9.5)
@ -532,8 +919,8 @@
### Removed
- Remove Classic Compose UI [#1434](https://github.com/pixelfed/pixelfed/pull/1434), [72bffd1](https://github.com/pixelfed/pixelfed/commit/72bffd1) [a2640af](https://github.com/pixelfed/pixelfed/commit/a2640af)
-
-
## [v0.9.4 (2019-06-03)](https://github.com/pixelfed/pixelfed/compare/v0.9.0...v0.9.4)
@ -573,7 +960,7 @@ php artisan config:cache
### Removed
- Google Recaptcha is no longer supported (#1231)
- Lightbox has been deprecated in favor of double-tap-to-like; it will return as a dedicated button in the future (#1277)
## [v0.9.0 (2019-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.8.6...v0.9.0)
@ -599,7 +986,7 @@ php artisan config:cache
### Removed
- Removed identicons due to SVG compatibility issues with federation. New users will instead be assigned a default avatar.
## [v0.8.6 (2019-04-06)](https://github.com/pixelfed/pixelfed/compare/v0.8.5...v0.8.6)

View file

@ -11,13 +11,19 @@
A free and ethical photo sharing platform, powered by ActivityPub federation.
<p align="center">
<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/Screen%20Shot%202019-09-08%20at%2010.40.54%20PM.png">
<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/pixelfed-screenshot.jpg">
</p>
## Official Documentation
Documentation for Pixelfed can be found on the [Pixelfed documentation website](https://docs.pixelfed.org/).
## Run on YunoHost
[![Install on YunoHost](https://user-images.githubusercontent.com/42862428/139559471-9495f1e9-e7a4-49f1-9a4b-675ddcc510a2.png 'Install on YunoHost')](https://install-app.yunohost.org/?app=pixelfed)
Pixelfed app for [YunoHost](https://yunohost.org 'YunoHost'). See [the package source code](https://github.com/YunoHost-Apps/pixelfed_ynh 'pixelfed_ynh repository on GitHub')
## License
Pixelfed is open-sourced software licensed under the AGPL license.
@ -27,7 +33,6 @@ Pixelfed is open-sourced software licensed under the AGPL license.
The ways you can communicate on the project are below. Before interacting, please
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
* IRC: [#pixelfed](irc://chat.freenode.net/pixelfed) on irc.freenode.net
* Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
@ -37,4 +42,4 @@ We would like to extend our thanks to the following sponsors for funding Pixelfe
- [NLnet Foundation](https://nlnet.nl) and [NGI0
Discovery](https://nlnet.nl/discovery/), part of the [Next Generation
Internet](https://ngi.eu) initiative.
Internet](https://ngi.eu) initiative.

3
SECURITY.md Normal file
View file

@ -0,0 +1,3 @@
## Reporting a Vulnerability
If you discover any security related issues, please email hello@pixelfed.org instead of using the issue tracker.

View file

@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Model;
class AccountLog extends Model
{
protected $fillable = ['*'];
public function user()
{
return $this->belongsTo(User::class);

View file

@ -14,9 +14,21 @@ class Avatar extends Model
*
* @var array
*/
protected $dates = ['deleted_at'];
protected $dates = [
'deleted_at',
'last_fetched_at',
'last_processed_at'
];
protected $fillable = ['profile_id'];
protected $visible = [
'id',
'profile_id',
'media_path',
'size',
];
public function profile()
{
return $this->belongsTo(Profile::class);

View file

@ -4,7 +4,7 @@ namespace App;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Pixelfed\Snowflake\HasSnowflakePrimary;
use App\HasSnowflakePrimary;
class Collection extends Model
{

View file

@ -3,7 +3,7 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Pixelfed\Snowflake\HasSnowflakePrimary;
use App\HasSnowflakePrimary;
class CollectionItem extends Model
{

View file

@ -0,0 +1,219 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Avatar;
use App\Profile;
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class AvatarSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'avatars:sync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform actions on avatars';
public $found = 0;
public $notFetched = 0;
public $fixed = 0;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Welcome to the avatar sync manager');
$actions = [
'Analyze',
'Full Analyze',
'Fetch - Fetch missing remote avatars',
'Fix - Fix remote accounts without avatar record',
'Sync - Store latest remote avatars',
];
$name = $this->choice(
'Select an action',
$actions,
0,
1,
false
);
$this->info('Selected: ' . $name);
switch($name) {
case $actions[0]:
$this->analyze();
break;
case $actions[1]:
$this->fullAnalyze();
break;
case $actions[2]:
$this->fetch();
break;
case $actions[3]:
$this->fix();
break;
case $actions[4]:
$this->sync();
break;
}
return Command::SUCCESS;
}
protected function incr($name)
{
switch($name) {
case 'found':
$this->found = $this->found + 1;
break;
case 'notFetched':
$this->notFetched = $this->notFetched + 1;
break;
case 'fixed':
$this->fixed++;
break;
}
}
protected function analyze()
{
$count = Avatar::whereIsRemote(true)->whereNull('cdn_url')->count();
$this->info('Found ' . $count . ' profiles with blank avatars.');
$this->line(' ');
$this->comment('We suggest running php artisan avatars:sync again and selecting the sync option');
$this->line(' ');
}
protected function fullAnalyze()
{
$count = Profile::count();
$bar = $this->output->createProgressBar($count);
$bar->start();
Profile::chunk(5000, function($profiles) use ($bar) {
foreach($profiles as $profile) {
if($profile->domain == null) {
$bar->advance();
continue;
}
$avatar = Avatar::whereProfileId($profile->id)->first();
if(!$avatar || $avatar->cdn_url == null) {
$this->incr('notFetched');
}
$this->incr('found');
$bar->advance();
}
});
$this->line(' ');
$this->line(' ');
$this->info('Found ' . $this->found . ' remote accounts');
$this->info('Found ' . $this->notFetched . ' remote avatars to fetch');
}
protected function fetch()
{
$this->info('Fetching ....');
Avatar::whereIsRemote(true)
->whereNull('cdn_url')
// ->with('profile')
->chunk(10, function($avatars) {
foreach($avatars as $avatar) {
if(!$avatar || !$avatar->profile) {
continue;
}
$url = $avatar->profile->remote_url;
if(!$url || !Helpers::validateUrl($url)) {
continue;
}
try {
$res = Helpers::fetchFromUrl($url);
if(
!is_array($res) ||
!isset($res['@context']) ||
!isset($res['icon']) ||
!isset($res['icon']['type']) ||
!isset($res['icon']['url']) ||
!Str::endsWith($res['icon']['url'], ['.png', '.jpg', '.jpeg'])
) {
continue;
}
} catch (\GuzzleHttp\Exception\RequestException $e) {
continue;
} catch(\Illuminate\Http\Client\ConnectionException $e) {
continue;
}
$avatar->remote_url = $res['icon']['url'];
$avatar->save();
RemoteAvatarFetch::dispatch($avatar->profile);
}
});
}
protected function fix()
{
Profile::chunk(5000, function($profiles) {
foreach($profiles as $profile) {
if($profile->domain == null || $profile->private_key) {
continue;
}
$avatar = Avatar::whereProfileId($profile->id)->first();
if($avatar) {
continue;
}
$avatar = new Avatar;
$avatar->is_remote = true;
$avatar->profile_id = $profile->id;
$avatar->save();
$this->incr('fixed');
}
});
$this->line(' ');
$this->line(' ');
$this->info('Fixed ' . $this->fixed . ' accounts with a blank avatar');
}
protected function sync()
{
Avatar::whereIsRemote(true)
->with('profile')
->chunk(10, function($avatars) {
foreach($avatars as $avatar) {
RemoteAvatarFetch::dispatch($avatar->profile);
}
});
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace App\Console\Commands;
use Illuminate\Http\File;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Spatie\Backup\BackupDestination\BackupDestination;
class BackupToCloud extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'backup:cloud';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send backups to cloud storage';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$localDisk = Storage::disk('local');
$cloudDisk = Storage::disk('backup');
$backupDestination = new BackupDestination($localDisk, '', 'local');
if(
empty(config('filesystems.disks.backup.key')) ||
empty(config('filesystems.disks.backup.secret')) ||
empty(config('filesystems.disks.backup.endpoint')) ||
empty(config('filesystems.disks.backup.region')) ||
empty(config('filesystems.disks.backup.bucket'))
) {
$this->error('Backup disk not configured.');
$this->error('See https://docs.pixelfed.org/technical-documentation/env.html#filesystem for more information.');
return Command::FAILURE;
}
$newest = $backupDestination->newestBackup();
$name = $newest->path();
$parts = explode('/', $name);
$fileName = array_pop($parts);
$storagePath = 'backups';
$path = storage_path('app/'. $name);
$file = $cloudDisk->putFileAs($storagePath, new File($path), $fileName, 'private');
$this->info("Backup file successfully saved!");
$url = $cloudDisk->url($file);
$this->table(
['Name', 'URL'],
[
[$fileName, $url]
],
);
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class DatabaseSessionGarbageCollector extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'gc:sessions';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Database sessions garbage collector';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if(config('session.driver') !== 'database') {
return Command::SUCCESS;
}
DB::transaction(function() {
DB::table('sessions')->whereNull('user_id')->delete();
});
DB::transaction(function() {
$ts = now()->subMonths(3)->timestamp;
DB::table('sessions')->where('last_activity', '<', $ts)->delete();
});
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ExportLanguages extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'i18n:export';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Build and export js localization files.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if(config('app.env') !== 'local') {
$this->error('This command is meant for development purposes and should only be run in a local environment');
return Command::FAILURE;
}
$path = base_path('resources/lang');
$langs = [];
foreach (new \DirectoryIterator($path) as $io) {
$name = $io->getFilename();
$skip = ['vendor'];
if($io->isDot() || in_array($name, $skip)) {
continue;
}
if($io->isDir()) {
array_push($langs, $name);
}
}
$exportDir = resource_path('assets/js/i18n/');
$exportDirAlt = public_path('_lang/');
foreach($langs as $lang) {
$strings = \Lang::get('web', [], $lang);
$json = json_encode($strings, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
$path = "{$exportDir}{$lang}.json";
file_put_contents($path, $json);
$pathAlt = "{$exportDirAlt}{$lang}.json";
file_put_contents($pathAlt, $json);
}
return Command::SUCCESS;
}
}

View file

@ -40,7 +40,7 @@ class FailedJobGC extends Command
{
FailedJob::chunk(50, function($jobs) {
foreach($jobs as $job) {
if($job->failed_at->lt(now()->subMonth())) {
if($job->failed_at->lt(now()->subHours(48))) {
$job->delete();
}
}

View file

@ -65,7 +65,6 @@ class ImportCities extends Command
public function __construct()
{
parent::__construct();
ini_set('memory_limit', '256M');
}
/**
@ -75,6 +74,8 @@ class ImportCities extends Command
*/
public function handle()
{
$old_memory_limit = ini_get('memory_limit');
ini_set('memory_limit', '256M');
$path = storage_path('app/cities.json');
if(hash_file('sha512', $path) !== self::CHECKSUM) {
@ -136,6 +137,7 @@ class ImportCities extends Command
$this->line('');
$this->info('Successfully imported ' . $cityCount . ' entries!');
$this->line('');
ini_set('memory_limit', $old_memory_limit);
return;
}

View file

@ -3,103 +3,82 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\{
DB,
Storage
};
use App\{
Story,
StoryView
};
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use App\Story;
use App\StoryView;
use App\Jobs\StoryPipeline\StoryExpire;
use App\Jobs\StoryPipeline\StoryRotateMedia;
use App\Services\StoryService;
class StoryGC extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'story:gc';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'story:gc';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear expired Stories';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear expired Stories';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->directoryScan();
$this->deleteViews();
$this->deleteStories();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->archiveExpiredStories();
$this->rotateMedia();
}
protected function directoryScan()
{
$day = now()->day;
protected function archiveExpiredStories()
{
$stories = Story::whereActive(true)
->where('created_at', '<', now()->subHours(24))
->get();
if($day != 3) {
return;
}
foreach($stories as $story) {
StoryExpire::dispatch($story)->onQueue('story');
}
}
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
protected function rotateMedia()
{
$queue = StoryService::rotateQueue();
$t1 = Storage::directories('public/_esm.t1');
$t2 = Storage::directories('public/_esm.t2');
if(!$queue || count($queue) == 0) {
return;
}
$dirs = array_merge($t1, $t2);
foreach($dirs as $dir) {
$hash = last(explode('/', $dir));
if($hash != $monthHash) {
$this->info('Found directory to delete: ' . $dir);
$this->deleteDirectory($dir);
}
}
}
protected function deleteDirectory($path)
{
Storage::deleteDirectory($path);
}
protected function deleteViews()
{
StoryView::where('created_at', '<', now()->subMinutes(1441))->delete();
}
protected function deleteStories()
{
$stories = Story::where('created_at', '<', now()->subMinutes(1441))->take(50)->get();
if($stories->count() == 0) {
exit;
}
foreach($stories as $story) {
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
DB::transaction(function() use($story) {
StoryView::whereStoryId($story->id)->delete();
$story->delete();
});
}
}
collect($queue)
->each(function($id) {
$story = StoryService::getById($id);
if(!$story) {
StoryService::removeRotateQueue($id);
return;
}
if($story->created_at->gt(now()->subMinutes(20))) {
return;
}
StoryRotateMedia::dispatch($story)->onQueue('story');
StoryService::removeRotateQueue($id);
});
}
}

View file

@ -12,7 +12,7 @@ class UserCreate extends Command
*
* @var string
*/
protected $signature = 'user:create';
protected $signature = 'user:create {--name=} {--username=} {--email=} {--password=} {--is_admin=0} {--confirm_email=0}';
/**
* The console command description.
@ -40,6 +40,26 @@ class UserCreate extends Command
{
$this->info('Creating a new user...');
$o = $this->options();
if( $o['name'] &&
$o['username'] &&
$o['email'] &&
$o['password']
) {
$user = new User;
$user->username = $o['username'];
$user->name = $o['name'];
$user->email = $o['email'];
$user->password = bcrypt($o['password']);
$user->is_admin = (bool) $o['is_admin'];
$user->email_verified_at = (bool) $o['confirm_email'] ? now() : null;
$user->save();
$this->info('Successfully created user!');
return;
}
$name = $this->ask('Name');
$username = $this->ask('Username');

View file

@ -31,6 +31,7 @@ class Kernel extends ConsoleKernel
$schedule->command('story:gc')->everyFiveMinutes();
$schedule->command('gc:failedjobs')->dailyAt(3);
$schedule->command('gc:passwordreset')->dailyAt('09:41');
$schedule->command('gc:sessions')->twiceDaily(13, 23);
}
/**

View file

@ -4,50 +4,75 @@ namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use League\OAuth2\Server\Exception\OAuthServerException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
OAuthServerException::class,
\Zttp\ConnectionException::class,
\GuzzleHttp\Exception\ConnectException::class,
\Illuminate\Http\Client\ConnectionException::class
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Exception $exception
*
* @return void
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Report or log an exception.
*
* @param \Exception $exception
*
* @return void
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
*
* @return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
return parent::render($request, $exception);
}
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (\BadMethodCallException $e) {
return app()->environment() !== 'production';
});
$this->reportable(function (\Illuminate\Http\Client\ConnectionException $e) {
return app()->environment() !== 'production';
});
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
*
* @return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
if ($request->wantsJson())
return response()->json(
['error' => $exception->getMessage()],
method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500
);
return parent::render($request, $exception);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App;
use App\Services\SnowflakeService;
trait HasSnowflakePrimary
{
public static function bootHasSnowflakePrimary()
{
static::saving(function ($model) {
if (is_null($model->getKey())) {
$keyName = $model->getKeyName();
$id = SnowflakeService::next();
$model->setAttribute($keyName, $id);
}
});
}
}

View file

@ -2,9 +2,9 @@
namespace App\Http\Controllers;
use Auth;
use Cache;
use Mail;
use Auth;
use Cache;
use Mail;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
use Carbon\Carbon;
@ -22,6 +22,13 @@ use App\{
User,
UserFilter
};
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Transformer\Api\Mastodon\v1\AccountTransformer;
use App\Services\AccountService;
use App\Services\UserFilterService;
use App\Services\RelationshipService;
class AccountController extends Controller
{
@ -30,6 +37,8 @@ class AccountController extends Controller
'user.block',
];
const FILTER_LIMIT = 'You cannot block or mute more than 100 accounts';
public function __construct()
{
$this->middleware('auth');
@ -67,7 +76,10 @@ class AccountController extends Controller
public function verifyEmail(Request $request)
{
return view('account.verify_email');
$recentSent = EmailVerification::whereUserId(Auth::id())
->whereDate('created_at', '>', now()->subHours(12))->count();
return view('account.verify_email', compact('recentSent'));
}
public function sendVerifyEmail(Request $request)
@ -77,7 +89,7 @@ class AccountController extends Controller
if ($recentAttempt > 0) {
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
}
}
EmailVerification::whereUserId(Auth::id())->delete();
@ -136,6 +148,12 @@ class AccountController extends Controller
]);
$user = Auth::user()->profile;
$count = UserFilterService::muteCount($user->id);
abort_if($count >= 100, 422, self::FILTER_LIMIT);
if($count == 0) {
$filterCount = UserFilter::whereUserId($user->id)->count();
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
}
$type = $request->input('type');
$item = $request->input('item');
$action = $type . '.mute';
@ -167,6 +185,7 @@ class AccountController extends Controller
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
return redirect()->back();
}
@ -217,6 +236,7 @@ class AccountController extends Controller
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
if($request->wantsJson()) {
return response()->json([200]);
@ -233,6 +253,12 @@ class AccountController extends Controller
]);
$user = Auth::user()->profile;
$count = UserFilterService::blockCount($user->id);
abort_if($count >= 100, 422, self::FILTER_LIMIT);
if($count == 0) {
$filterCount = UserFilter::whereUserId($user->id)->count();
abort_if($filterCount >= 100, 422, self::FILTER_LIMIT);
}
$type = $request->input('type');
$item = $request->input('item');
$action = $type.'.block';
@ -243,7 +269,7 @@ class AccountController extends Controller
switch ($type) {
case 'user':
$profile = Profile::findOrFail($item);
if ($profile->id == $user->id || $profile->user->is_admin == true) {
if ($profile->id == $user->id || ($profile->user && $profile->user->is_admin == true)) {
return abort(403);
}
$class = get_class($profile);
@ -265,6 +291,7 @@ class AccountController extends Controller
$pid = $user->id;
Cache::forget("user:filter:list:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
return redirect()->back();
}
@ -315,6 +342,7 @@ class AccountController extends Controller
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:posts:$pid");
Cache::forget("api:local:exp:rec:$pid");
RelationshipService::refresh($pid, $profile->id);
return redirect()->back();
}
@ -390,7 +418,7 @@ class AccountController extends Controller
$request->session()->pull('sudoModeAttempts');
Auth::logout();
return redirect(route('login'));
}
}
return view('auth.sudo');
}
@ -481,10 +509,88 @@ class AccountController extends Controller
}
} else {
return false;
}
}
}
public function accountRestored(Request $request)
{
}
public function accountMutes(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'limit' => 'nullable|integer|min:1|max:40'
]);
$user = $request->user();
$limit = $request->input('limit') ?? 40;
$mutes = UserFilter::whereUserId($user->profile_id)
->whereFilterableType('App\Profile')
->whereFilterType('mute')
->simplePaginate($limit)
->pluck('filterable_id');
$accounts = Profile::find($mutes);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
$res = $fractal->createData($resource)->toArray();
$url = $request->url();
$page = $request->input('page', 1);
$next = $page < 40 ? $page + 1 : 40;
$prev = $page > 1 ? $page - 1 : 1;
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
return response()->json($res, 200, ['Link' => $links]);
}
public function accountBlocks(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'limit' => 'nullable|integer|min:1|max:40',
'page' => 'nullable|integer|min:1|max:10'
]);
$user = $request->user();
$limit = $request->input('limit') ?? 40;
$blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
->whereUserId($user->profile_id)
->whereFilterableType('App\Profile')
->whereFilterType('block')
->simplePaginate($limit)
->pluck('filterable_id');
$profiles = Profile::findOrFail($blocked);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
$res = $fractal->createData($resource)->toArray();
$url = $request->url();
$page = $request->input('page', 1);
$next = $page < 40 ? $page + 1 : 40;
$prev = $page > 1 ? $page - 1 : 1;
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
return response()->json($res, 200, ['Link' => $links]);
}
public function accountBlocksV2(Request $request)
{
return response()->json(UserFilterService::blocks($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
}
public function accountMutesV2(Request $request)
{
return response()->json(UserFilterService::mutes($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
}
public function accountFiltersV2(Request $request)
{
return response()->json(UserFilterService::filters($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
}
}

View file

@ -14,29 +14,58 @@ trait AdminInstanceController
public function instances(Request $request)
{
$this->validate($request, [
'filter' => [
'nullable',
'string',
'min:1',
'max:20',
Rule::in(['autocw', 'unlisted', 'banned'])
Rule::in([
'cw',
'unlisted',
'banned',
// 'popular',
'new',
'all'
])
],
]);
if($request->has('filter') && $request->filled('filter')) {
if($request->has('q') && $request->filled('q')) {
$instances = Instance::where('domain', 'like', '%' . $request->input('q') . '%')->simplePaginate(10);
} else if($request->has('filter') && $request->filled('filter')) {
switch ($request->filter) {
case 'autocw':
$instances = Instance::whereAutoCw(true)->orderByDesc('id')->paginate(5);
case 'cw':
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereAutoCw(true)->orderByDesc('id')->simplePaginate(10);
break;
case 'unlisted':
$instances = Instance::whereUnlisted(true)->orderByDesc('id')->paginate(5);
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereUnlisted(true)->orderByDesc('id')->simplePaginate(10);
break;
case 'banned':
$instances = Instance::whereBanned(true)->orderByDesc('id')->paginate(5);
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->whereBanned(true)->orderByDesc('id')->simplePaginate(10);
break;
case 'new':
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->latest()->simplePaginate(10);
break;
// case 'popular':
// $popular = Profile::selectRaw('*, count(domain) as count')
// ->whereNotNull('domain')
// ->groupBy('domain')
// ->orderByDesc('count')
// ->take(10)
// ->get()
// ->pluck('domain')
// ->toArray();
// $instances = Instance::whereIn('domain', $popular)->simplePaginate(10);
// break;
default:
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10);
break;
}
} else {
$instances = Instance::orderByDesc('id')->paginate(5);
$instances = Instance::select('id', 'domain', 'unlisted', 'auto_cw', 'banned')->orderByDesc('id')->simplePaginate(10);
}
return view('admin.instances.home', compact('instances'));
}
@ -97,6 +126,10 @@ trait AdminInstanceController
break;
}
Cache::forget('instances:banned:domains');
Cache::forget('instances:unlisted:domains');
Cache::forget('instances:auto_cw:domains');
return response()->json([]);
}
}
}

View file

@ -27,6 +27,7 @@ trait AdminMediaController
],
'search' => 'nullable|string|min:1|max:20'
]);
if($request->filled('search')) {
$profiles = Profile::where('username', 'like', '%'.$request->input('search').'%')->pluck('id')->toArray();
$media = Media::whereHas('status')
@ -42,7 +43,8 @@ trait AdminMediaController
$media = MediaBlocklist::latest()->paginate(12);
return view('admin.media.home', compact('media'));
}
$media = Media::whereHas('status')->with('status')->orderby('id', 'desc')->paginate(12);
$media = Media::whereNull('remote_url')->orderby('id', 'desc')->simplePaginate(12);
return view('admin.media.home', compact('media'));
}
@ -51,4 +53,4 @@ trait AdminMediaController
$media = Media::findOrFail($id);
return view('admin.media.show', compact('media'));
}
}
}

View file

@ -3,12 +3,372 @@
namespace App\Http\Controllers\Admin;
use Cache;
use App\Report;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use App\Services\AccountService;
use App\Services\StatusService;
use App\{
AccountInterstitial,
Contact,
Hashtag,
Newsroom,
OauthClient,
Profile,
Report,
Status,
Story,
User
};
use Illuminate\Validation\Rule;
use App\Services\StoryService;
trait AdminReportController
{
public function reports(Request $request)
{
$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
$page = $request->input('page') ?? 1;
$ai = Cache::remember('admin-dash:reports:ai-count', 3600, function() {
return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count();
});
$spam = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
});
$mailVerifications = Redis::scard('email:manual');
if($filter == 'open' && $page == 1) {
$reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) {
return Report::whereHas('status')
->whereHas('reportedUser')
->whereHas('reporter')
->orderBy('created_at','desc')
->when($filter, function($q, $filter) {
return $filter == 'open' ?
$q->whereNull('admin_seen') :
$q->whereNotNull('admin_seen');
})
->paginate(6);
});
} else {
$reports = Report::whereHas('status')
->whereHas('reportedUser')
->whereHas('reporter')
->orderBy('created_at','desc')
->when($filter, function($q, $filter) {
return $filter == 'open' ?
$q->whereNull('admin_seen') :
$q->whereNotNull('admin_seen');
})
->paginate(6);
}
return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications'));
}
public function showReport(Request $request, $id)
{
$report = Report::findOrFail($id);
return view('admin.reports.show', compact('report'));
}
public function appeals(Request $request)
{
$appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->latest()
->paginate(6);
return view('admin.reports.appeals', compact('appeals'));
}
public function showAppeal(Request $request, $id)
{
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
return view('admin.reports.show_appeal', compact('appeal', 'meta'));
}
public function spam(Request $request)
{
$this->validate($request, [
'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions'
]);
$tab = $request->input('tab', 'home');
$openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
return AccountInterstitial::whereType('post.autospam')
->whereNull('appeal_handled_at')
->count();
});
$monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function() {
return AccountInterstitial::whereType('post.autospam')
->where('created_at', '>', now()->subMonth())
->count();
});
$totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function() {
return AccountInterstitial::whereType('post.autospam')->count();
});
$uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function() {
return AccountInterstitial::whereType('post.autospam')
->whereIsSpam(null)
->whereNotNull('appeal_handled_at')
->exists();
});
$avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function() {
if(config('database.default') != 'mysql') {
return 0;
}
return AccountInterstitial::selectRaw('*, count(id) as counter')
->whereType('post.autospam')
->groupBy('user_id')
->get()
->avg('counter');
});
$avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function() {
if(config('database.default') != 'mysql') {
return "0";
}
$seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get();
if(!$seconds) {
return "0";
}
$mins = floor($seconds->avg('timediff') / 60);
if($mins < 60) {
return $mins . ' min(s)';
}
if($mins < 2880) {
return floor($mins / 60) . ' hour(s)';
}
return floor($mins / 60 / 24) . ' day(s)';
});
$avgCount = $totalCount && $avg ? floor($totalCount / $avg) : "0";
if(in_array($tab, ['home', 'spam', 'not-spam'])) {
$appeals = AccountInterstitial::whereType('post.autospam')
->when($tab, function($q, $tab) {
switch($tab) {
case 'home':
return $q->whereNull('appeal_handled_at');
break;
case 'spam':
return $q->whereIsSpam(true);
break;
case 'not-spam':
return $q->whereIsSpam(false);
break;
}
})
->latest()
->paginate(6);
if($tab !== 'home') {
$appeals = $appeals->appends(['tab' => $tab]);
}
} else {
$appeals = new class {
public function count() {
return 0;
}
public function render() {
return;
}
};
}
return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized'));
}
public function showSpam(Request $request, $id)
{
$appeal = AccountInterstitial::whereType('post.autospam')
->findOrFail($id);
$meta = json_decode($appeal->meta);
return view('admin.reports.show_spam', compact('appeal', 'meta'));
}
public function fixUncategorizedSpam(Request $request)
{
if(Cache::get('admin-dash:reports:spam-sync-active')) {
return redirect('/i/admin/reports/autospam');
}
Cache::put('admin-dash:reports:spam-sync-active', 1, 900);
AccountInterstitial::chunk(500, function($reports) {
foreach($reports as $report) {
if($report->item_type != 'App\Status') {
continue;
}
if($report->type != 'post.autospam') {
continue;
}
if($report->is_spam != null) {
continue;
}
$status = StatusService::get($report->item_id, false);
if(!$status) {
return;
}
$scope = $status['visibility'];
$report->is_spam = $scope == 'unlisted';
$report->in_violation = $report->is_spam;
$report->severity_index = 1;
$report->save();
}
});
Cache::forget('admin-dash:reports:spam-sync');
return redirect('/i/admin/reports/autospam');
}
public function updateSpam(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all'
]);
$action = $request->input('action');
$appeal = AccountInterstitial::whereType('post.autospam')
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
$res = ['status' => 'success'];
$now = now();
Cache::forget('admin-dash:reports:spam-count:total');
Cache::forget('admin-dash:reports:spam-count:30d');
if($action == 'dismiss') {
$appeal->is_spam = true;
$appeal->appeal_handled_at = $now;
$appeal->save();
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
if($action == 'dismiss-all') {
AccountInterstitial::whereType('post.autospam')
->whereItemType('App\Status')
->whereNull('appeal_handled_at')
->whereUserId($appeal->user_id)
->update(['appeal_handled_at' => $now, 'is_spam' => true]);
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
if($action == 'approve-all') {
AccountInterstitial::whereType('post.autospam')
->whereItemType('App\Status')
->whereNull('appeal_handled_at')
->whereUserId($appeal->user_id)
->get()
->each(function($report) use($meta) {
$report->is_spam = false;
$report->appeal_handled_at = now();
$report->save();
$status = Status::find($report->item_id);
if($status) {
$status->is_nsfw = $meta->is_nsfw;
$status->scope = 'public';
$status->visibility = 'public';
$status->save();
StatusService::del($status->id, true);
}
});
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
$status = $appeal->status;
$status->is_nsfw = $meta->is_nsfw;
$status->scope = 'public';
$status->visibility = 'public';
$status->save();
$appeal->is_spam = false;
$appeal->appeal_handled_at = now();
$appeal->save();
StatusService::del($status->id);
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
public function updateAppeal(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve'
]);
$action = $request->input('action');
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->findOrFail($id);
if($action == 'dismiss') {
$appeal->appeal_handled_at = now();
$appeal->save();
Cache::forget('admin-dash:reports:ai-count');
return redirect('/i/admin/reports/appeals');
}
switch ($appeal->type) {
case 'post.cw':
$status = $appeal->status;
$status->is_nsfw = false;
$status->save();
break;
case 'post.unlist':
$status = $appeal->status;
$status->scope = 'public';
$status->visibility = 'public';
$status->save();
break;
default:
# code...
break;
}
$appeal->appeal_handled_at = now();
$appeal->save();
StatusService::del($status->id, true);
Cache::forget('admin-dash:reports:ai-count');
return redirect('/i/admin/reports/appeals');
}
public function updateReport(Request $request, $id)
{
$this->validate($request, [
@ -33,6 +393,7 @@ trait AdminReportController
$report = Report::findOrFail($id);
$this->handleReportAction($report, $action);
Cache::forget('admin-dash:reports:list-cache');
return response()->json(['msg'=> 'Success']);
}
@ -52,17 +413,20 @@ trait AdminReportController
$item->is_nsfw = true;
$item->save();
$report->nsfw = true;
StatusService::del($item->id, true);
break;
case 'unlist':
$item->visibility = 'unlisted';
$item->save();
Cache::forget('profiles:private');
StatusService::del($item->id, true);
break;
case 'delete':
// Todo: fire delete job
$report->admin_seen = null;
StatusService::del($item->id, true);
break;
case 'shadowban':
@ -115,4 +479,55 @@ trait AdminReportController
];
return response()->json($res);
}
public function reportMailVerifications(Request $request)
{
$ids = Redis::smembers('email:manual');
$ignored = Redis::smembers('email:manual-ignored');
$reports = [];
if($ids) {
$reports = collect($ids)
->filter(function($id) use($ignored) {
return !in_array($id, $ignored);
})
->map(function($id) {
$account = AccountService::get($id);
$user = User::whereProfileId($id)->first();
if(!$user) {
return [];
}
$account['email'] = $user->email;
return $account;
})
->filter(function($res) {
return $res && isset($res['id']);
})
->values();
}
return view('admin.reports.mail_verification', compact('reports', 'ignored'));
}
public function reportMailVerifyIgnore(Request $request)
{
$id = $request->input('id');
Redis::sadd('email:manual-ignored', $id);
return redirect('/i/admin/reports');
}
public function reportMailVerifyApprove(Request $request)
{
$id = $request->input('id');
$user = User::whereProfileId($id)->firstOrFail();
Redis::srem('email:manual', $id);
Redis::srem('email:manual-ignored', $id);
$user->email_verified_at = now();
$user->save();
return redirect('/i/admin/reports');
}
public function reportMailVerifyClearIgnored(Request $request)
{
Redis::del('email:manual-ignored');
return [200];
}
}

View file

@ -6,14 +6,192 @@ use Artisan, Cache, DB;
use Illuminate\Http\Request;
use Carbon\Carbon;
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
use App\Models\InstanceActor;
use App\Http\Controllers\Controller;
use App\Util\Lexer\PrettyNumber;
use App\Models\ConfigCache;
use App\Services\ConfigCacheService;
use App\Util\Site\Config;
trait AdminSettingsController
{
public function settings(Request $request)
{
return view('admin.settings.home');
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
$cloud_disk = config('filesystems.cloud');
$cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret'));
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null;
$jpeg = in_array('image/jpg', $types) ? true : in_array('image/jpeg', $types);
$png = in_array('image/png', $types);
$gif = in_array('image/gif', $types);
$mp4 = in_array('video/mp4', $types);
$webp = in_array('image/webp', $types);
// $system = [
// 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')),
// 'max_upload_size' => ini_get('post_max_size'),
// 'image_driver' => config('image.driver'),
// 'image_driver_loaded' => extension_loaded(config('image.driver'))
// ];
return view('admin.settings.home', compact(
'jpeg',
'png',
'gif',
'mp4',
'webp',
'rules',
'cloud_storage',
'cloud_disk',
'cloud_ready',
// 'system'
));
}
public function settingsHomeStore(Request $request)
{
$this->validate($request, [
'name' => 'nullable|string',
'short_description' => 'nullable',
'long_description' => 'nullable',
'max_photo_size' => 'nullable|integer|min:1',
'max_album_length' => 'nullable|integer|min:1|max:100',
'image_quality' => 'nullable|integer|min:1|max:100',
'type_jpeg' => 'nullable',
'type_png' => 'nullable',
'type_gif' => 'nullable',
'type_mp4' => 'nullable',
'type_webp' => 'nullable',
]);
if($request->filled('rule_delete')) {
$index = (int) $request->input('rule_delete');
$rules = ConfigCacheService::get('app.rules');
$json = json_decode($rules, true);
if(!$rules || empty($json)) {
return;
}
unset($json[$index]);
$json = json_encode(array_values($json));
ConfigCacheService::put('app.rules', $json);
return 200;
}
$media_types = explode(',', config_cache('pixelfed.media_types'));
$media_types_original = $media_types;
$mimes = [
'type_jpeg' => 'image/jpeg',
'type_png' => 'image/png',
'type_gif' => 'image/gif',
'type_mp4' => 'video/mp4',
'type_webp' => 'image/webp',
];
foreach ($mimes as $key => $value) {
if($request->input($key) == 'on') {
if(!in_array($value, $media_types)) {
array_push($media_types, $value);
}
} else {
$media_types = array_diff($media_types, [$value]);
}
}
if($media_types !== $media_types_original) {
ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
}
$keys = [
'name' => 'app.name',
'short_description' => 'app.short_description',
'long_description' => 'app.description',
'max_photo_size' => 'pixelfed.max_photo_size',
'max_album_length' => 'pixelfed.max_album_length',
'image_quality' => 'pixelfed.image_quality',
'account_limit' => 'pixelfed.max_account_size',
'custom_css' => 'uikit.custom.css',
'custom_js' => 'uikit.custom.js',
'about_title' => 'about.title'
];
foreach ($keys as $key => $value) {
$cc = ConfigCache::whereK($value)->first();
$val = $request->input($key);
if($cc && $cc->v != $val) {
ConfigCacheService::put($value, $val);
}
}
$bools = [
'activitypub' => 'federation.activitypub.enabled',
'open_registration' => 'pixelfed.open_registration',
'mobile_apis' => 'pixelfed.oauth_enabled',
'stories' => 'instance.stories.enabled',
'ig_import' => 'pixelfed.import.instagram.enabled',
'spam_detection' => 'pixelfed.bouncer.enabled',
'require_email_verification' => 'pixelfed.enforce_email_verification',
'enforce_account_limit' => 'pixelfed.enforce_account_limit',
'show_custom_css' => 'uikit.show_custom.css',
'show_custom_js' => 'uikit.show_custom.js',
'cloud_storage' => 'pixelfed.cloud_storage',
'account_autofollow' => 'account.autofollow'
];
foreach ($bools as $key => $value) {
$active = $request->input($key) == 'on';
if($key == 'activitypub' && $active && !InstanceActor::exists()) {
Artisan::call('instance:actor');
}
if( $key == 'mobile_apis' &&
$active &&
!file_exists(storage_path('oauth-public.key')) &&
!file_exists(storage_path('oauth-private.key'))
) {
Artisan::call('passport:keys');
Artisan::call('route:cache');
}
if(config_cache($value) !== $active) {
ConfigCacheService::put($value, (bool) $active);
}
}
if($request->filled('new_rule')) {
$rules = ConfigCacheService::get('app.rules');
$val = $request->input('new_rule');
if(!$rules) {
ConfigCacheService::put('app.rules', json_encode([$val]));
} else {
$json = json_decode($rules, true);
$json[] = $val;
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response');
}
if($request->filled('account_autofollow_usernames')) {
$usernames = explode(',', $request->input('account_autofollow_usernames'));
$names = [];
foreach($usernames as $n) {
$p = Profile::whereUsername($n)->first();
if(!$p) {
continue;
}
array_push($names, $p->username);
}
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
}
Cache::forget(Config::CACHE_KEY);
return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!');
}
public function settingsBackups(Request $request)
@ -23,51 +201,6 @@ trait AdminSettingsController
return view('admin.settings.backups', compact('files'));
}
public function settingsConfig(Request $request)
{
$editor = config('pixelfed.admin.env_editor');
$config = !$editor ? false : file_get_contents(base_path('.env'));
$backup = !$editor ? false : (is_file(base_path('.env.backup')) ? file_get_contents(base_path('.env.backup')) : false);
return view('admin.settings.config', compact('editor', 'config', 'backup'));
}
public function settingsConfigStore(Request $request)
{
if(config('pixelfed.admin.env_editor') !== true) {
abort(400);
}
$res = $request->input('res');
$old = file_get_contents(app()->environmentFilePath());
if(empty($old) || $old != $res) {
$oldFile = fopen(app()->environmentFilePath().'.backup', 'w');
fwrite($oldFile, $old);
fclose($oldFile);
}
$file = fopen(app()->environmentFilePath(), 'w');
fwrite($file, $res);
fclose($file);
Artisan::call('config:cache');
return ['msg' => 200];
}
public function settingsConfigRestore(Request $request)
{
if(config('pixelfed.admin.env_editor') !== true) {
abort(400);
}
$res = file_get_contents(app()->environmentFilePath().'.backup');
if(empty($res)) {
abort(400, 'No backup exists.');
}
$file = fopen(app()->environmentFilePath(), 'w');
fwrite($file, $res);
fclose($file);
Artisan::call('config:cache');
return ['msg' => 200];
}
public function settingsMaintenance(Request $request)
{
return view('admin.settings.maintenance');
@ -84,15 +217,6 @@ trait AdminSettingsController
return view('admin.settings.features');
}
public function settingsHomeStore(Request $request)
{
$this->validate($request, [
'APP_NAME' => 'required|string',
]);
// Artisan::call('config:clear');
return redirect()->back();
}
public function settingsPages(Request $request)
{
$pages = Page::orderByDesc('updated_at')->paginate(10);
@ -135,4 +259,4 @@ trait AdminSettingsController
}
return view('admin.settings.system', compact('sys'));
}
}
}

View file

@ -16,14 +16,27 @@ trait AdminUserController
{
public function users(Request $request)
{
$search = $request->has('a') && $request->query('a') == 'search' ? $request->query('q') : null;
$col = $request->query('col') ?? 'id';
$dir = $request->query('dir') ?? 'desc';
$users = User::select('id', 'username', 'status')
->withCount('statuses')
$offset = $request->has('page') ? $request->input('page') : 0;
$pagination = [
'prev' => $offset > 0 ? $offset - 1 : null,
'next' => $offset + 1,
'query' => $search ? '&a=search&q=' . $search : null
];
$users = User::select('id', 'username', 'status', 'profile_id')
->orderBy($col, $dir)
->simplePaginate(10);
->when($search, function($q, $search) {
return $q->where('username', 'like', "%{$search}%");
})
->when($offset, function($q, $offset) {
return $q->offset(($offset * 10));
})
->limit(10)
->get();
return view('admin.users.home', compact('users'));
return view('admin.users.home', compact('users', 'pagination'));
}
public function userShow(Request $request, $id)

View file

@ -11,30 +11,39 @@ use App\{
Profile,
Report,
Status,
Story,
User
};
use DB, Cache;
use DB, Cache, Storage;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use App\Http\Controllers\Admin\{
AdminDiscoverController,
AdminInstanceController,
AdminReportController,
// AdminGroupsController,
AdminMediaController,
AdminSettingsController,
// AdminStorageController,
AdminSupportController,
AdminUserController
};
use Illuminate\Validation\Rule;
use App\Services\AdminStatsService;
use App\Services\StatusService;
use App\Services\StoryService;
use App\Models\CustomEmoji;
class AdminController extends Controller
{
use AdminReportController,
AdminDiscoverController,
AdminDiscoverController,
// AdminGroupsController,
AdminMediaController,
AdminSettingsController,
AdminInstanceController,
// AdminStorageController,
AdminUserController;
public function __construct()
@ -52,9 +61,15 @@ class AdminController extends Controller
public function statuses(Request $request)
{
$statuses = Status::orderBy('id', 'desc')->simplePaginate(10);
return view('admin.statuses.home', compact('statuses'));
$statuses = Status::orderBy('id', 'desc')->cursorPaginate(10);
$data = $statuses->map(function($status) {
return StatusService::get($status->id, false);
})
->filter(function($s) {
return $s;
})
->toArray();
return view('admin.statuses.home', compact('statuses', 'data'));
}
public function showStatus(Request $request, $id)
@ -64,139 +79,6 @@ class AdminController extends Controller
return view('admin.statuses.show', compact('status'));
}
public function reports(Request $request)
{
$this->validate($request, [
'filter' => 'nullable|string|in:all,open,closed'
]);
$filter = $request->input('filter');
$reports = Report::orderBy('created_at','desc')
->when($filter, function($q, $filter) {
return $filter == 'open' ?
$q->whereNull('admin_seen') :
$q->whereNotNull('admin_seen');
})
->paginate(4);
return view('admin.reports.home', compact('reports'));
}
public function showReport(Request $request, $id)
{
$report = Report::findOrFail($id);
return view('admin.reports.show', compact('report'));
}
public function appeals(Request $request)
{
$appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->latest()
->paginate(6);
return view('admin.reports.appeals', compact('appeals'));
}
public function showAppeal(Request $request, $id)
{
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
return view('admin.reports.show_appeal', compact('appeal', 'meta'));
}
public function spam(Request $request)
{
$appeals = AccountInterstitial::whereType('post.autospam')
->whereNull('appeal_handled_at')
->latest()
->paginate(6);
return view('admin.reports.spam', compact('appeals'));
}
public function showSpam(Request $request, $id)
{
$appeal = AccountInterstitial::whereType('post.autospam')
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
return view('admin.reports.show_spam', compact('appeal', 'meta'));
}
public function updateSpam(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve'
]);
$action = $request->input('action');
$appeal = AccountInterstitial::whereType('post.autospam')
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
if($action == 'dismiss') {
$appeal->appeal_handled_at = now();
$appeal->save();
return redirect('/i/admin/reports/autospam');
}
$status = $appeal->status;
$status->is_nsfw = $meta->is_nsfw;
$status->scope = 'public';
$status->visibility = 'public';
$status->save();
$appeal->appeal_handled_at = now();
$appeal->save();
return redirect('/i/admin/reports/autospam');
}
public function updateAppeal(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve'
]);
$action = $request->input('action');
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->findOrFail($id);
if($action == 'dismiss') {
$appeal->appeal_handled_at = now();
$appeal->save();
return redirect('/i/admin/reports/appeals');
}
switch ($appeal->type) {
case 'post.cw':
$status = $appeal->status;
$status->is_nsfw = false;
$status->save();
break;
case 'post.unlist':
$status = $appeal->status;
$status->scope = 'public';
$status->visibility = 'public';
$status->save();
break;
default:
# code...
break;
}
$appeal->appeal_handled_at = now();
$appeal->save();
return redirect('/i/admin/reports/appeals');
}
public function profiles(Request $request)
{
$this->validate($request, [
@ -434,4 +316,173 @@ class AdminController extends Controller
$redirect = $news->published_at ? $news->permalink() : $news->editUrl();
return redirect($redirect);
}
public function diagnosticsHome(Request $request)
{
return view('admin.diagnostics.home');
}
public function diagnosticsDecrypt(Request $request)
{
$this->validate($request, [
'payload' => 'required'
]);
$key = 'exception_report:';
$decrypted = decrypt($request->input('payload'));
if(!starts_with($decrypted, $key)) {
abort(403, 'Can only decrypt error diagnostics');
}
$res = [
'decrypted' => substr($decrypted, strlen($key))
];
return response()->json($res);
}
public function stories(Request $request)
{
$stories = Story::with('profile')->latest()->paginate(10);
$stats = StoryService::adminStats();
return view('admin.stories.home', compact('stories', 'stats'));
}
public function customEmojiHome(Request $request)
{
if(!config('federation.custom_emoji.enabled')) {
return view('admin.custom-emoji.not-enabled');
}
$this->validate($request, [
'sort' => 'sometimes|in:all,local,remote,duplicates,disabled,search'
]);
if($request->has('cc')) {
Cache::forget('pf:admin:custom_emoji:stats');
Cache::forget('pf:custom_emoji');
return redirect(route('admin.custom-emoji'));
}
$sort = $request->input('sort') ?? 'all';
if($sort == 'search' && empty($request->input('q'))) {
return redirect(route('admin.custom-emoji'));
}
$pg = config('database.default') == 'pgsql';
$emojis = CustomEmoji::when($sort, function($query, $sort) use($request, $pg) {
if($sort == 'all') {
if($pg) {
return $query->latest();
} else {
return $query->groupBy('shortcode')->latest();
}
} else if($sort == 'local') {
return $query->latest()->where('domain', '=', config('pixelfed.domain.app'));
} else if($sort == 'remote') {
return $query->latest()->where('domain', '!=', config('pixelfed.domain.app'));
} else if($sort == 'duplicates') {
return $query->latest()->groupBy('shortcode')->havingRaw('count(*) > 1');
} else if($sort == 'disabled') {
return $query->latest()->whereDisabled(true);
} else if($sort == 'search') {
$q = $query
->latest()
->where('shortcode', 'like', '%' . $request->input('q') . '%')
->orWhere('domain', 'like', '%' . $request->input('q') . '%');
if(!$request->has('dups')) {
$q = $q->groupBy('shortcode');
}
return $q;
}
})
->simplePaginate(10)
->withQueryString();
$stats = Cache::remember('pf:admin:custom_emoji:stats', 43200, function() use($pg) {
$res = [
'total' => CustomEmoji::count(),
'active' => CustomEmoji::whereDisabled(false)->count(),
'remote' => CustomEmoji::where('domain', '!=', config('pixelfed.domain.app'))->count(),
];
if($pg) {
$res['duplicate'] = CustomEmoji::select('shortcode')->groupBy('shortcode')->havingRaw('count(*) > 1')->count();
} else {
$res['duplicate'] = CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count();
}
return $res;
});
return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats'));
}
public function customEmojiToggleActive(Request $request, $id)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id);
$emoji->disabled = !$emoji->disabled;
$emoji->save();
$key = CustomEmoji::CACHE_KEY . str_replace(':', '', $emoji->shortcode);
Cache::forget($key);
return redirect()->back();
}
public function customEmojiAdd(Request $request)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
return view('admin.custom-emoji.add');
}
public function customEmojiStore(Request $request)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
$this->validate($request, [
'shortcode' => [
'required',
'min:3',
'max:80',
'starts_with::',
'ends_with::',
Rule::unique('custom_emoji')->where(function ($query) use($request) {
return $query->whereDomain(config('pixelfed.domain.app'))
->whereShortcode($request->input('shortcode'));
})
],
'emoji' => 'required|file|mimes:jpg,png|max:' . (config('federation.custom_emoji.max_size') / 1000)
]);
$emoji = new CustomEmoji;
$emoji->shortcode = $request->input('shortcode');
$emoji->domain = config('pixelfed.domain.app');
$emoji->save();
$fileName = $emoji->id . '.' . $request->emoji->extension();
$request->emoji->storeAs('public/emoji', $fileName);
$emoji->media_path = 'emoji/' . $fileName;
$emoji->save();
Cache::forget('pf:custom_emoji');
return redirect(route('admin.custom-emoji'));
}
public function customEmojiDelete(Request $request, $id)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id);
Storage::delete("public/{$emoji->media_path}");
Cache::forget('pf:custom_emoji');
$emoji->delete();
return redirect(route('admin.custom-emoji'));
}
public function customEmojiShowDuplicates(Request $request, $id)
{
abort_unless(config('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));
}
}

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,8 @@ use App\{
Media,
Notification,
Profile,
Status
Status,
StatusArchived
};
use App\Transformer\Api\{
AccountTransformer,
@ -36,9 +37,11 @@ use App\Jobs\VideoPipeline\{
VideoPostProcess,
VideoThumbnail
};
use App\Services\AccountService;
use App\Services\NotificationService;
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Services\StatusService;
class BaseApiController extends Controller
{
@ -54,26 +57,40 @@ class BaseApiController extends Controller
public function notifications(Request $request)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$pg = $request->input('pg');
if($pg == true) {
$timeago = Carbon::now()->subMonths(6);
$notifications = Notification::whereProfileId($pid)
->whereDate('created_at', '>', $timeago)
->latest()
->simplePaginate(10);
$resource = new Fractal\Resource\Collection($notifications, new NotificationTransformer());
$res = $this->fractal->createData($resource)->toArray();
} else {
$this->validate($request, [
'page' => 'nullable|integer|min:1|max:10',
'limit' => 'nullable|integer|min:1|max:40'
]);
$limit = $request->input('limit') ?? 10;
$page = $request->input('page') ?? 1;
$end = (int) $page * $limit;
$start = (int) $end - $limit;
$res = NotificationService::get($pid, $start, $end);
$pid = $request->user()->profile_id;
$limit = $request->input('limit', 20);
$since = $request->input('since_id');
$min = $request->input('min_id');
$max = $request->input('max_id');
if(!$since && !$min && !$max) {
$min = 1;
}
$maxId = null;
$minId = null;
if($max) {
$res = NotificationService::getMax($pid, $max, $limit);
$ids = NotificationService::getRankedMaxId($pid, $max, $limit);
if(!empty($ids)) {
$maxId = max($ids);
$minId = min($ids);
}
} else {
$res = NotificationService::getMin($pid, $min ?? $since, $limit);
$ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
if(!empty($ids)) {
$maxId = max($ids);
$minId = min($ids);
}
}
if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
NotificationService::warmCache($pid, 400, true);
}
return response()->json($res);
@ -183,7 +200,6 @@ class BaseApiController extends Controller
$avatar = Avatar::whereProfileId($profile->id)->firstOrFail();
$opath = $avatar->media_path;
$avatar->media_path = "$public/$name";
$avatar->thumb_path = null;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = null;
$avatar->save();
@ -201,117 +217,17 @@ class BaseApiController extends Controller
public function showTempMedia(Request $request, $profileId, $mediaId, $timestamp)
{
abort_if(!$request->user(), 403);
abort_if(!$request->hasValidSignature(), 404);
abort_if(Auth::user()->profile_id != $profileId, 404);
$media = Media::whereProfileId(Auth::user()->profile_id)->findOrFail($mediaId);
$path = storage_path('app/'.$media->media_path);
return response()->file($path);
abort(400, 'Endpoint deprecated');
}
public function uploadMedia(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'file.*' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24'
]);
$user = Auth::user();
$profile = $user->profile;
if(config('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
$photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
return;
}
$storagePath = MediaPathService::get($user, 2);
$path = $photo->store($storagePath);
$hash = \hash_file('sha256', $photo);
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media();
$media->status_id = null;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $photo->getMimeType();
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
$media->save();
$url = URL::temporarySignedRoute(
'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id, 'timestamp' => time()]
);
switch ($media->mime) {
case 'image/jpeg':
case 'image/png':
ImageOptimize::dispatch($media);
break;
case 'video/mp4':
VideoThumbnail::dispatch($media);
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
default:
break;
}
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
$res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $url;
$res['url'] = $url;
return response()->json($res);
abort(400, 'Endpoint deprecated');
}
public function deleteMedia(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:media,id'
]);
$media = Media::whereNull('status_id')
->whereUserId(Auth::id())
->findOrFail($request->input('id'));
Storage::delete($media->media_path);
Storage::delete($media->thumbnail_path);
$media->forceDelete();
return response()->json([
'msg' => 'Successfully deleted',
'code' => 200
]);
abort(400, 'Endpoint deprecated');
}
public function verifyCredentials(Request $request)
@ -320,17 +236,9 @@ class BaseApiController extends Controller
abort_if(!$user, 403);
if($user->status != null) {
Auth::logout();
return redirect('/login');
abort(403);
}
$key = 'user:last_active_at:id:'.$user->id;
$ttl = now()->addMinutes(5);
Cache::remember($key, $ttl, function() use($user) {
$user->last_active_at = now();
$user->save();
return;
});
$resource = new Fractal\Resource\Item($user->profile, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
$res = AccountService::get($user->profile_id);
return response()->json($res);
}
@ -351,26 +259,98 @@ class BaseApiController extends Controller
public function accountLikes(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'page' => 'sometimes|int|min:1|max:20',
'limit' => 'sometimes|int|min:1|max:10'
]);
$user = $request->user();
$limit = $request->input('limit', 10);
$res = \DB::table('likes')
->whereProfileId($user->profile_id)
->latest()
->simplePaginate($limit)
->map(function($id) {
$status = StatusService::get($id->status_id, false);
$status['favourited'] = true;
return $status;
})
->filter(function($post) {
return $post && isset($post['account']);
})
->values();
return response()->json($res);
}
public function archive(Request $request, $id)
{
abort_if(!$request->user(), 403);
$limit = 10;
$page = (int) $request->input('page', 1);
$status = Status::whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId($request->user()->profile_id)
->findOrFail($id);
if($page > 20) {
return [];
if($status->scope === 'archived') {
return [200];
}
$favourites = $user->profile->likes()
->latest()
->simplePaginate($limit)
->pluck('status_id');
$archive = new StatusArchived;
$archive->status_id = $status->id;
$archive->profile_id = $status->profile_id;
$archive->original_scope = $status->scope;
$archive->save();
$statuses = Status::find($favourites)->reverse();
$status->scope = 'archived';
$status->visibility = 'draft';
$status->save();
StatusService::del($status->id, true);
AccountService::syncPostCount($status->profile_id);
return [200];
}
public function unarchive(Request $request, $id)
{
abort_if(!$request->user(), 403);
$status = Status::whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId($request->user()->profile_id)
->findOrFail($id);
if($status->scope !== 'archived') {
return [200];
}
$archive = StatusArchived::whereStatusId($status->id)
->whereProfileId($status->profile_id)
->firstOrFail();
$status->scope = $archive->original_scope;
$status->visibility = $archive->original_scope;
$status->save();
$archive->delete();
StatusService::del($status->id, true);
AccountService::syncPostCount($status->profile_id);
return [200];
}
public function archivedPosts(Request $request)
{
abort_if(!$request->user(), 403);
$statuses = Status::whereProfileId($request->user()->profile_id)
->whereScope('archived')
->orderByDesc('id')
->simplePaginate(10);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
return $fractal->createData($resource)->toArray();
}
}

View file

@ -34,7 +34,7 @@ class InstanceApiController extends Controller {
$res = [
'uri' => config('pixelfed.domain.app'),
'title' => config('app.name'),
'title' => config_cache('app.name'),
'description' => '',
'version' => config('pixelfed.version'),
'urls' => [],

View file

@ -27,7 +27,10 @@ class LoginController extends Controller
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectTo = '/i/web';
protected $maxAttempts = 5;
protected $decayMinutes = 60;
/**
* Create a new controller instance.

View file

@ -14,183 +14,200 @@ use App\Services\EmailService;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/';
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/i/web';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']);
}
public function getRegisterToken()
{
return \Cache::remember('pf:register:rt', 900, function() {
return str_random(40);
});
}
$usernameRules = [
'required',
'min:2',
'max:15',
'unique:users',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
/**
* Get a validator for an incoming registration request.
*
* @param array $data
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']);
}
if(ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
$usernameRules = [
'required',
'min:2',
'max:15',
'unique:users',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
if(($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if(ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
if (!ctype_alpha($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.');
}
if(($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if (!ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
if (!ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start 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 (_).');
}
if (!ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
];
$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 (_).');
}
$emailRules = [
'required',
'string',
'email',
'max:255',
'unique:users',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
];
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
];
$rules = [
'agecheck' => 'required|accepted',
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
'username' => $usernameRules,
'email' => $emailRules,
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
];
$emailRules = [
'required',
'string',
'email',
'max:255',
'unique:users',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
];
if(config('captcha.enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
}
$rt = [
'required',
function ($attribute, $value, $fail) {
if($value !== $this->getRegisterToken()) {
return $fail('Something went wrong');
}
}
];
return Validator::make($data, $rules);
}
$rules = [
'agecheck' => 'required|accepted',
'rt' => $rt,
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
'username' => $usernameRules,
'email' => $emailRules,
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
];
/**
* Create a new user instance after a valid registration.
*
* @param array $data
*
* @return \App\User
*/
protected function create(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']);
}
if(config('captcha.enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
}
return User::create([
'name' => $data['name'],
'username' => $data['username'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
return Validator::make($data, $rules);
}
/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm()
{
if(config('pixelfed.open_registration')) {
$limit = config('pixelfed.max_users');
if($limit) {
abort_if($limit <= User::count(), 404);
return view('auth.register');
} else {
return view('auth.register');
}
} else {
abort(404);
}
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
*
* @return \App\User
*/
protected function create(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']);
}
/**
* Handle a registration request for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function register(Request $request)
{
abort_if(config('pixelfed.open_registration') == false, 400);
return User::create([
'name' => $data['name'],
'username' => $data['username'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
$count = User::count();
$limit = config('pixelfed.max_users');
/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm()
{
if(config_cache('pixelfed.open_registration')) {
$limit = config('pixelfed.max_users');
if($limit) {
abort_if($limit <= User::count(), 404);
return view('auth.register');
} else {
return view('auth.register');
}
} else {
abort(404);
}
}
if(false == config('pixelfed.open_registration') || $limit && $limit <= $count) {
return abort(403);
}
/**
* Handle a registration request for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function register(Request $request)
{
abort_if(config_cache('pixelfed.open_registration') == false, 400);
$this->validator($request->all())->validate();
$count = User::count();
$limit = config('pixelfed.max_users');
event(new Registered($user = $this->create($request->all())));
if(false == config_cache('pixelfed.open_registration') || $limit && $limit <= $count) {
return abort(403);
}
$this->guard()->login($user);
$this->validator($request->all())->validate();
return $this->registered($request, $user)
?: redirect($this->redirectPath());
}
event(new Registered($user = $this->create($request->all())));
$this->guard()->login($user);
return $this->registered($request, $user)
?: redirect($this->redirectPath());
}
}

View file

@ -35,7 +35,6 @@ class AvatarController extends Controller
$avatar = Avatar::firstOrNew(['profile_id' => $profile->id]);
$currentAvatar = $avatar->recentlyCreated ? null : storage_path('app/'.$profile->avatar->media_path);
$avatar->media_path = "$public/$name";
$avatar->thumb_path = null;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = null;
$avatar->save();
@ -121,10 +120,7 @@ class AvatarController extends Controller
$avatar = $profile->avatar;
if( $avatar->media_path == 'public/avatars/default.png' ||
$avatar->thumb_path == 'public/avatars/default.png' ||
$avatar->media_path == 'public/avatars/default.jpg' ||
$avatar->thumb_path == 'public/avatars/default.jpg'
$avatar->media_path == 'public/avatars/default.jpg'
) {
return;
}
@ -133,12 +129,7 @@ class AvatarController extends Controller
@unlink(storage_path('app/' . $avatar->media_path));
}
if(is_file(storage_path('app/' . $avatar->thumb_path))) {
@unlink(storage_path('app/' . $avatar->thumb_path));
}
$avatar->media_path = 'public/avatars/default.jpg';
$avatar->thumb_path = 'public/avatars/default.jpg';
$avatar->change_count = $avatar->change_count + 1;
$avatar->save();

View file

@ -6,6 +6,7 @@ use App\Bookmark;
use App\Status;
use Auth;
use Illuminate\Http\Request;
use App\Services\BookmarkService;
class BookmarkController extends Controller
{
@ -28,7 +29,10 @@ class BookmarkController extends Controller
);
if (!$bookmark->wasRecentlyCreated) {
BookmarkService::del($profile->id, $status->id);
$bookmark->delete();
} else {
BookmarkService::add($profile->id, $status->id);
}
if ($request->ajax()) {

View file

@ -18,6 +18,7 @@ use League\Fractal;
use App\Transformer\Api\StatusTransformer;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\StatusService;
class CommentController extends Controller
{
@ -72,13 +73,11 @@ class CommentController extends Controller
$reply->visibility = $scope;
$reply->save();
$status->reply_count++;
$status->save();
return $reply;
});
NewStatusPipeline::dispatch($reply, false);
StatusService::del($status->id);
NewStatusPipeline::dispatch($reply);
CommentPipeline::dispatch($status, $reply);
if ($request->ajax()) {
@ -87,11 +86,11 @@ class CommentController extends Controller
$entity = new Fractal\Resource\Item($reply, new StatusTransformer());
$entity = $fractal->createData($entity)->toArray();
$response = [
'code' => 200,
'msg' => 'Comment saved',
'username' => $profile->username,
'url' => $reply->url(),
'profile' => $profile->url(),
'code' => 200,
'msg' => 'Comment saved',
'username' => $profile->username,
'url' => $reply->url(),
'profile' => $profile->url(),
'comment' => $reply->caption,
'entity' => $entity,
];

View file

@ -0,0 +1,735 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth, Cache, Storage, URL;
use Carbon\Carbon;
use App\{
Avatar,
Hashtag,
Like,
Media,
MediaTag,
Notification,
Profile,
Place,
Status,
UserFilter,
UserSetting
};
use App\Models\Poll;
use App\Transformer\Api\{
MediaTransformer,
MediaDraftTransformer,
StatusTransformer,
StatusStatelessTransformer
};
use League\Fractal;
use App\Util\Media\Filter;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Jobs\AvatarPipeline\AvatarOptimize;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\VideoPipeline\{
VideoOptimize,
VideoPostProcess,
VideoThumbnail
};
use App\Services\AccountService;
use App\Services\NotificationService;
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Services\MediaStorageService;
use App\Services\MediaTagService;
use App\Services\StatusService;
use Illuminate\Support\Str;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
use App\Util\Media\License;
class ComposeController extends Controller
{
protected $fractal;
public function __construct()
{
$this->middleware('auth');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
public function show(Request $request)
{
return view('status.compose');
}
public function mediaUpload(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'file.*' => function() {
return [
'required',
'mimes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
];
},
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24'
]);
$user = Auth::user();
$profile = $user->profile;
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
return $dailyLimit >= 250;
});
abort_if($limitReached == true, 429);
if(config_cache('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config_cache('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
$photo = $request->file('file');
$mimes = explode(',', config_cache('pixelfed.media_types'));
abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
$storagePath = MediaPathService::get($user, 2);
$path = $photo->store($storagePath);
$hash = \hash_file('sha256', $photo);
$mime = $photo->getMimeType();
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media();
$media->status_id = null;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $mime;
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
$media->version = 3;
$media->save();
$preview_url = $media->url() . '?v=' . time();
$url = $media->url() . '?v=' . time();
switch ($media->mime) {
case 'image/jpeg':
case 'image/png':
case 'image/webp':
ImageOptimize::dispatch($media);
break;
case 'video/mp4':
VideoThumbnail::dispatch($media);
$preview_url = '/storage/no-preview.png';
$url = '/storage/no-preview.png';
break;
default:
break;
}
Cache::forget($limitKey);
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
$res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $preview_url;
$res['url'] = $url;
return response()->json($res);
}
public function mediaUpdate(Request $request)
{
$this->validate($request, [
'id' => 'required',
'file' => function() {
return [
'required',
'mimes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
];
},
]);
$user = Auth::user();
$limitKey = 'compose:rate-limit:media-updates:' . $user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
return $dailyLimit >= 500;
});
abort_if($limitReached == true, 429);
$photo = $request->file('file');
$id = $request->input('id');
$media = Media::whereUserId($user->id)
->whereProfileId($user->profile_id)
->whereNull('status_id')
->findOrFail($id);
$media->save();
$fragments = explode('/', $media->media_path);
$name = last($fragments);
array_pop($fragments);
$dir = implode('/', $fragments);
$path = $photo->storeAs($dir, $name);
$res = [
'url' => $media->url() . '?v=' . time()
];
ImageOptimize::dispatch($media);
Cache::forget($limitKey);
return $res;
}
public function mediaDelete(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:media,id'
]);
$media = Media::whereNull('status_id')
->whereUserId(Auth::id())
->findOrFail($request->input('id'));
MediaStorageService::delete($media, true);
$media->forceDelete();
return response()->json([
'msg' => 'Successfully deleted',
'code' => 200
]);
}
public function searchTag(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:50'
]);
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
return [];
}
$q = mb_substr($q, 1);
}
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
->whereFilterableId($request->user()->profile_id)
->pluck('user_id');
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->whereNull('domain')
->where('username','like','%'.$q.'%')
->limit(15)
->get()
->map(function($r) {
return [
'id' => (string) $r->id,
'name' => $r->username,
'privacy' => true,
'avatar' => $r->avatarUrl()
];
});
return $results;
}
public function searchUntag(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'status_id' => 'required',
'profile_id' => 'required'
]);
$user = $request->user();
$status_id = $request->input('status_id');
$profile_id = (int) $request->input('profile_id');
abort_if((int) $user->profile_id !== $profile_id, 400);
$tag = MediaTag::whereStatusId($status_id)
->whereProfileId($profile_id)
->first();
if(!$tag) {
return [];
}
Notification::whereItemType('App\MediaTag')
->whereItemId($tag->id)
->whereProfileId($profile_id)
->whereAction('tagged')
->delete();
MediaTagService::untag($status_id, $profile_id);
return [200];
}
public function searchLocation(Request $request)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'q' => 'required|string|max:100'
]);
$q = filter_var($request->input('q'), FILTER_SANITIZE_STRING);
$hash = hash('sha256', $q);
$key = 'search:location:id:' . $hash;
$places = Cache::remember($key, now()->addMinutes(15), function() use($q) {
$q = '%' . $q . '%';
return Place::where('name', 'like', $q)
->take(80)
->get()
->map(function($r) {
return [
'id' => $r->id,
'name' => $r->name,
'country' => $r->country,
'url' => $r->url()
];
});
});
return $places;
}
public function searchMentionAutocomplete(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:2|max:50'
]);
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
return [];
}
}
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
->whereFilterableId($request->user()->profile_id)
->pluck('user_id');
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->where('username','like','%'.$q.'%')
->groupBy('domain')
->limit(15)
->get()
->map(function($profile) {
$username = $profile->domain ? substr($profile->username, 1) : $profile->username;
return [
'key' => '@' . str_limit($username, 30),
'value' => $username,
];
});
return $results;
}
public function searchHashtagAutocomplete(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:2|max:50'
]);
$q = $request->input('q');
$results = Hashtag::select('slug')
->where('slug', 'like', '%'.$q.'%')
->whereIsNsfw(false)
->whereIsBanned(false)
->limit(5)
->get()
->map(function($tag) {
return [
'key' => '#' . $tag->slug,
'value' => $tag->slug
];
});
return $results;
}
public function store(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'media.*' => 'required',
'media.*.id' => 'required|integer|min:1',
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
'media.*.license' => 'nullable|string|max:140',
'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
'comments_disabled' => 'nullable',
'tagged' => 'nullable',
'license' => 'nullable|integer|min:1|max:16'
// 'optimize_media' => 'nullable'
]);
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($request->caption, $kw) == true) {
abort(400, 'Invalid object');
}
}
}
}
$user = Auth::user();
$profile = $user->profile;
$limitKey = 'compose:rate-limit:store:' . $user->id;
$limitTtl = now()->addMinutes(15);
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Status::whereProfileId($user->profile_id)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->where('created_at', '>', now()->subDays(1))
->count();
return $dailyLimit >= 100;
});
abort_if($limitReached == true, 429);
$license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null;
$visibility = $request->input('visibility');
$medias = $request->input('media');
$attachments = [];
$status = new Status;
$mimes = [];
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
$optimize_media = (bool) $request->input('optimize_media');
foreach($medias as $k => $media) {
if($k + 1 > config_cache('pixelfed.max_album_length')) {
continue;
}
$m = Media::findOrFail($media['id']);
if($m->profile_id !== $profile->id || $m->status_id) {
abort(403, 'Invalid media id');
}
$m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
$m->license = $license;
$m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
$m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
// if($optimize_media == false) {
// $m->skip_optimize = true;
// ImageThumbnail::dispatch($m);
// } else {
// ImageOptimize::dispatch($m);
// }
if($cw == true || $profile->cw == true) {
$m->is_nsfw = $cw;
$status->is_nsfw = $cw;
}
$m->save();
$attachments[] = $m;
array_push($mimes, $m->mime);
}
abort_if(empty($attachments), 422);
$mediaType = StatusController::mimeTypeCheck($mimes);
if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
abort(400, __('exception.compose.invalid.album'));
}
if($place && is_array($place)) {
$status->place_id = $place['id'];
}
if($request->filled('comments_disabled')) {
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
$status->caption = strip_tags($request->caption);
$status->rendered = Autolink::create()->autolink($status->caption);
$status->scope = 'draft';
$status->profile_id = $profile->id;
$status->save();
foreach($attachments as $media) {
$media->status_id = $status->id;
$media->save();
}
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
$visibility = $profile->is_private ? 'private' : $visibility;
$cw = $profile->cw == true ? true : $cw;
$status->is_nsfw = $cw;
$status->visibility = $visibility;
$status->scope = $visibility;
$status->type = $mediaType;
$status->save();
foreach($tagged as $tg) {
$mt = new MediaTag;
$mt->status_id = $status->id;
$mt->media_id = $status->media->first()->id;
$mt->profile_id = $tg['id'];
$mt->tagged_username = $tg['name'];
$mt->is_public = true;
$mt->metadata = json_encode([
'_v' => 1,
]);
$mt->save();
MediaTagService::set($mt->status_id, $mt->profile_id);
MediaTagService::sendNotification($mt);
}
NewStatusPipeline::dispatch($status);
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
Cache::forget('status:transformer:media:attachments:'.$status->id);
Cache::forget($user->storageUsedKey());
Cache::forget('profile:embed:' . $status->profile_id);
Cache::forget($limitKey);
return $status->url();
}
public function storeText(Request $request)
{
abort_unless(config('exp.top'), 404);
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
'comments_disabled' => 'nullable',
'tagged' => 'nullable',
]);
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($request->caption, $kw) == true) {
abort(400, 'Invalid object');
}
}
}
}
$user = Auth::user();
$profile = $user->profile;
$visibility = $request->input('visibility');
$status = new Status;
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
if($place && is_array($place)) {
$status->place_id = $place['id'];
}
if($request->filled('comments_disabled')) {
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
$status->caption = strip_tags($request->caption);
$status->profile_id = $profile->id;
$entities = Extractor::create()->extract($status->caption);
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
$cw = $profile->cw == true ? true : $cw;
$status->is_nsfw = $cw;
$status->visibility = $visibility;
$status->scope = $visibility;
$status->type = 'text';
$status->rendered = Autolink::create()->autolink($status->caption);
$status->entities = json_encode(array_merge([
'timg' => [
'version' => 0,
'bg_id' => 1,
'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
'length' => strlen($status->caption),
]
], $entities), JSON_UNESCAPED_SLASHES);
$status->save();
foreach($tagged as $tg) {
$mt = new MediaTag;
$mt->status_id = $status->id;
$mt->media_id = $status->media->first()->id;
$mt->profile_id = $tg['id'];
$mt->tagged_username = $tg['name'];
$mt->is_public = true;
$mt->metadata = json_encode([
'_v' => 1,
]);
$mt->save();
MediaTagService::set($mt->status_id, $mt->profile_id);
MediaTagService::sendNotification($mt);
}
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
return $status->url();
}
public function mediaProcessingCheck(Request $request)
{
$this->validate($request, [
'id' => 'required|integer|min:1'
]);
$media = Media::whereUserId($request->user()->id)
->whereNull('status_id')
->findOrFail($request->input('id'));
if(config('pixelfed.media_fast_process')) {
return [
'finished' => true
];
}
$finished = false;
switch ($media->mime) {
case 'image/jpeg':
case 'image/png':
case 'video/mp4':
$finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
break;
default:
# code...
break;
}
return [
'finished' => $finished
];
}
public function composeSettings(Request $request)
{
$uid = $request->user()->id;
$default = [
'default_license' => 1,
'media_descriptions' => false,
'max_altext_length' => config_cache('pixelfed.max_altext_length')
];
$settings = AccountService::settings($uid);
if(isset($settings['other']) && isset($settings['other']['scope'])) {
$s = $settings['compose_settings'];
$s['default_scope'] = $settings['other']['scope'];
$settings['compose_settings'] = $s;
}
return array_merge($default, $settings['compose_settings']);
}
public function createPoll(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private',
'comments_disabled' => 'nullable',
'expiry' => 'required|in:60,360,1440,10080',
'pollOptions' => 'required|array|min:1|max:4'
]);
abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
abort_if(Status::whereType('poll')
->whereProfileId($request->user()->profile_id)
->whereCaption($request->input('caption'))
->where('created_at', '>', now()->subDays(2))
->exists()
, 422, 'Duplicate detected.');
$status = new Status;
$status->profile_id = $request->user()->profile_id;
$status->caption = $request->input('caption');
$status->rendered = Autolink::create()->autolink($status->caption);
$status->visibility = 'draft';
$status->scope = 'draft';
$status->type = 'poll';
$status->local = true;
$status->save();
$poll = new Poll;
$poll->status_id = $status->id;
$poll->profile_id = $status->profile_id;
$poll->poll_options = $request->input('pollOptions');
$poll->expires_at = now()->addMinutes($request->input('expiry'));
$poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
return 0;
})->toArray();
$poll->save();
$status->visibility = $request->input('visibility');
$status->scope = $request->input('visibility');
$status->save();
NewStatusPipeline::dispatch($status);
return ['url' => $status->url()];
}
}

View file

@ -160,7 +160,7 @@ class DirectMessageController extends Controller
'messages' => []
];
});
}
}
} elseif(config('database.default') == 'mysql') {
if($action == 'inbox') {
$dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
@ -334,7 +334,7 @@ class DirectMessageController extends Controller
$dm->type = 'link';
$dm->meta = [
'domain' => parse_url($msg, PHP_URL_HOST),
'local' => parse_url($msg, PHP_URL_HOST) ==
'local' => parse_url($msg, PHP_URL_HOST) ==
parse_url(config('app.url'), PHP_URL_HOST)
];
$dm->save();
@ -390,7 +390,6 @@ class DirectMessageController extends Controller
$min_id = $request->input('min_id');
$r = Profile::findOrFail($pid);
// $r = Profile::whereNull('domain')->findOrFail($pid);
if($min_id) {
$res = DirectMessage::select('*')
@ -500,8 +499,8 @@ class DirectMessageController extends Controller
'file' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
'mimes:' . config_cache('pixelfed.media_types'),
'max:' . config_cache('pixelfed.max_photo_size'),
];
},
'to_id' => 'required'
@ -522,18 +521,18 @@ class DirectMessageController extends Controller
$hidden = false;
}
if(config('pixelfed.enforce_account_limit') == true) {
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('pixelfed.max_account_size');
});
$limit = (int) config_cache('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types'));
$mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
abort(403, 'Invalid or unsupported mime type.');
}
@ -590,11 +589,15 @@ class DirectMessageController extends Controller
{
$this->validate($request, [
'q' => 'required|string|min:2|max:50',
'remote' => 'nullable|boolean',
'remote' => 'nullable',
]);
$q = $request->input('q');
$r = $request->input('remote');
$r = $request->input('remote', false);
if($r && !Str::of($q)->contains('.')) {
return [];
}
if($r && Helpers::validateUrl($q)) {
Helpers::profileFetch($q);

View file

@ -3,14 +3,14 @@
namespace App\Http\Controllers;
use App\{
DiscoverCategory,
Follower,
Hashtag,
HashtagFollow,
Profile,
Status,
StatusHashtag,
UserFilter
DiscoverCategory,
Follower,
Hashtag,
HashtagFollow,
Profile,
Status,
StatusHashtag,
UserFilter
};
use Auth, DB, Cache;
use Illuminate\Http\Request;
@ -24,232 +24,172 @@ use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\StatusHashtagService;
use App\Services\SnowflakeService;
use App\Services\StatusService;
use App\Services\UserFilterService;
class DiscoverController extends Controller
{
protected $fractal;
protected $fractal;
public function __construct()
{
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
public function __construct()
{
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
public function home(Request $request)
{
abort_if(!Auth::check(), 403);
return view('discover.home');
}
public function home(Request $request)
{
abort_if(!Auth::check() && config('instance.discover.public') == false, 403);
return view('discover.home');
}
public function showTags(Request $request, $hashtag)
{
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
public function showTags(Request $request, $hashtag)
{
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
$tag = Hashtag::whereName($hashtag)
->orWhere('slug', $hashtag)
->firstOrFail();
$tagCount = StatusHashtagService::count($tag->id);
return view('discover.tags.show', compact('tag', 'tagCount'));
}
$tag = Hashtag::whereName($hashtag)
->orWhere('slug', $hashtag)
->firstOrFail();
$tagCount = StatusHashtagService::count($tag->id);
return view('discover.tags.show', compact('tag', 'tagCount'));
}
public function showCategory(Request $request, $slug)
{
abort_if(!Auth::check(), 403);
public function showCategory(Request $request, $slug)
{
abort(404);
}
$tag = DiscoverCategory::whereActive(true)
->whereSlug($slug)
->firstOrFail();
public function showLoops(Request $request)
{
abort(404);
}
$posts = Cache::remember('discover:category-'.$tag->id.':posts', now()->addMinutes(15), function() use ($tag) {
$tagids = $tag->hashtags->pluck('id')->toArray();
$sids = StatusHashtag::whereIn('hashtag_id', $tagids)->orderByDesc('status_id')->take(500)->pluck('status_id')->toArray();
$posts = Status::whereScope('public')->whereIn('id', $sids)->whereNull('uri')->whereType('photo')->whereNull('in_reply_to_id')->whereNull('reblog_of_id')->orderByDesc('created_at')->take(39)->get();
return $posts;
});
$tag->posts_count = Cache::remember('discover:category-'.$tag->id.':posts_count', now()->addMinutes(30), function() use ($tag) {
return $tag->posts()->whereScope('public')->count();
});
return view('discover.tags.category', compact('tag', 'posts'));
}
public function loopsApi(Request $request)
{
abort(404);
}
public function showLoops(Request $request)
{
if(config('exp.loops') != true) {
return redirect('/');
}
return view('discover.loops.home');
}
public function loopWatch(Request $request)
{
return response()->json(200);
}
public function loopsApi(Request $request)
{
abort_if(!config('exp.loops'), 403);
// todo proper pagination, maybe LoopService
$res = Cache::remember('discover:loops:recent', now()->addHours(6), function() {
$loops = Status::whereType('video')
->whereNull('uri')
->whereScope('public')
->latest()
->take(18)
->get();
public function getHashtags(Request $request)
{
$auth = Auth::check();
abort_if(!config('instance.discover.tags.is_public') && !$auth, 403);
$resource = new Fractal\Resource\Collection($loops, new StatusStatelessTransformer());
return $this->fractal->createData($resource)->toArray();
});
return $res;
}
$this->validate($request, [
'hashtag' => 'required|string|min:1|max:124',
'page' => 'nullable|integer|min:1|max:' . ($auth ? 29 : 10)
]);
public function loopWatch(Request $request)
{
abort_if(!Auth::check(), 403);
abort_if(!config('exp.loops'), 403);
$page = $request->input('page') ?? '1';
$end = $page > 1 ? $page * 9 : 0;
$tag = $request->input('hashtag');
$this->validate($request, [
'id' => 'integer|min:1'
]);
$id = $request->input('id');
$hashtag = Hashtag::whereName($tag)->firstOrFail();
if($page == 1) {
$res['follows'] = HashtagFollow::whereUserId(Auth::id())
->whereHashtagId($hashtag->id)
->exists();
}
$res['hashtag'] = [
'name' => $hashtag->name,
'url' => $hashtag->url()
];
$res['tags'] = StatusHashtagService::get($hashtag->id, $page, $end);
return $res;
}
// todo log loops
public function profilesDirectory(Request $request)
{
return redirect('/')
->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
}
return response()->json(200);
}
public function profilesDirectoryApi(Request $request)
{
return ['error' => 'Temporarily unavailable.'];
}
public function getHashtags(Request $request)
{
$auth = Auth::check();
abort_if(!config('instance.discover.tags.is_public') && !$auth, 403);
public function trendingApi(Request $request)
{
abort_if(config('instance.discover.public') == false && !Auth::check(), 403);
$this->validate($request, [
'hashtag' => 'required|string|min:1|max:124',
'page' => 'nullable|integer|min:1|max:' . ($auth ? 29 : 10)
]);
$this->validate($request, [
'range' => 'nullable|string|in:daily,monthly,yearly',
]);
$page = $request->input('page') ?? '1';
$end = $page > 1 ? $page * 9 : 0;
$tag = $request->input('hashtag');
$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;
$hashtag = Hashtag::whereName($tag)->firstOrFail();
$res['tags'] = StatusHashtagService::get($hashtag->id, $page, $end);
if($page == 1) {
$res['follows'] = HashtagFollow::whereUserId(Auth::id())->whereHashtagId($hashtag->id)->exists();
}
return $res;
}
$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');
});
public function profilesDirectory(Request $request)
{
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
return view('discover.profiles.home');
}
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
public function profilesDirectoryApi(Request $request)
{
$this->validate($request, [
'page' => 'integer|max:10'
]);
$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 ['error' => 'Temporarily unavailable.'];
return response()->json($res);
}
$page = $request->input('page') ?? 1;
$key = 'discover:profiles:page:' . $page;
$ttl = now()->addHours(12);
public function trendingHashtags(Request $request)
{
$res = StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
->groupBy('hashtag_id')
->orderBy('total','desc')
->where('created_at', '>', now()->subDays(90))
->take(9)
->get()
->map(function($h) {
$hashtag = $h->hashtag;
return [
'id' => $hashtag->id,
'total' => $h->total,
'name' => '#'.$hashtag->name,
'url' => $hashtag->url('?src=dsh1')
];
});
return $res;
}
$res = Cache::remember($key, $ttl, function() {
$profiles = Profile::whereNull('domain')
->whereNull('status')
->whereIsPrivate(false)
->has('statuses')
->whereIsSuggestable(true)
// ->inRandomOrder()
->simplePaginate(8);
$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
return $this->fractal->createData($resource)->toArray();
});
return $res;
}
public function trendingApi(Request $request)
{
$this->validate($request, [
'range' => 'nullable|string|in:daily,monthly'
]);
$range = $request->input('range') == 'monthly' ? 31 : 1;
$key = ':api:discover:trending:v2.8:range:' . $range;
$ttl = now()->addMinutes(15);
$ids = Cache::remember($key, $ttl, function() use($range) {
$days = $range == 1 ? 2 : 31;
$min_id = SnowflakeService::byDate(now()->subDays($days));
return Status::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(15)
->pluck('id');
});
$res = $ids->map(function($s) {
return StatusService::get($s);
});
return response()->json($res);
}
public function trendingHashtags(Request $request)
{
$res = StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
->groupBy('hashtag_id')
->orderBy('total','desc')
->where('created_at', '>', now()->subDays(4))
->take(9)
->get()
->map(function($h) {
$hashtag = $h->hashtag;
return [
'id' => $hashtag->id,
'total' => $h->total,
'name' => '#'.$hashtag->name,
'url' => $hashtag->url('?src=dsh1')
];
});
return $res;
}
public function trendingPlaces(Request $request)
{
$res = Status::select('place_id',DB::raw('count(place_id) as total'))
->whereNotNull('place_id')
->where('created_at','>',now()->subDays(14))
->groupBy('place_id')
->orderBy('total')
->limit(4)
->get()
->map(function($s){
$p = $s->place;
return [
'name' => $p->name,
'country' => $p->country,
'url' => $p->url()
];
});
return $res;
}
public function trendingPlaces(Request $request)
{
return [];
}
}

View file

@ -3,16 +3,17 @@
namespace App\Http\Controllers;
use App\Jobs\InboxPipeline\{
InboxWorker,
InboxValidator
DeleteWorker,
InboxWorker,
InboxValidator
};
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
use App\{
AccountLog,
Like,
Profile,
Status,
User
AccountLog,
Like,
Profile,
Status,
User
};
use App\Util\Lexer\Nickname;
use App\Util\Webfinger\Webfinger;
@ -23,146 +24,164 @@ use Illuminate\Http\Request;
use League\Fractal;
use App\Util\Site\Nodeinfo;
use App\Util\ActivityPub\{
Helpers,
HttpSignature,
Outbox
Helpers,
HttpSignature,
Outbox
};
use Zttp\Zttp;
class FederationController extends Controller
{
public function nodeinfoWellKnown()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::wellKnown())
->header('Access-Control-Allow-Origin','*');
}
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 nodeinfo()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::get())
->header('Access-Control-Allow-Origin','*');
}
public function nodeinfo()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
public function webfinger(Request $request)
{
abort_if(!config('federation.webfinger.enabled'), 400);
public function webfinger(Request $request)
{
abort_if(!config('federation.webfinger.enabled'), 400);
abort_if(!$request->filled('resource'), 400);
abort_if(!$request->has('resource') || !$request->filled('resource'), 400);
$resource = $request->input('resource');
$parsed = Nickname::normalizeProfileUrl($resource);
if($parsed['domain'] !== config('pixelfed.domain.app')) {
abort(400);
}
$username = $parsed['username'];
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($profile->status != null) {
return ProfileController::accountCheck($profile);
}
$webfinger = (new Webfinger($profile))->generate();
$resource = $request->input('resource');
$hash = hash('sha256', $resource);
$key = 'federation:webfinger:sha256:' . $hash;
if($cached = Cache::get($key)) {
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
}
$domain = config('pixelfed.domain.app');
abort_if(strpos($resource, $domain) == false, 400);
$parsed = Nickname::normalizeProfileUrl($resource);
if(empty($parsed) || $parsed['domain'] !== $domain) {
abort(400);
}
$username = $parsed['username'];
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
abort_if($profile->status != null, 400);
$webfinger = (new Webfinger($profile))->generate();
Cache::put($key, $webfinger, 1209600);
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT)
->header('Access-Control-Allow-Origin','*');
}
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
public function hostMeta(Request $request)
{
abort_if(!config('federation.webfinger.enabled'), 404);
public function hostMeta(Request $request)
{
abort_if(!config('federation.webfinger.enabled'), 404);
$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>';
$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($xml)->header('Content-Type', 'application/xrd+xml');
}
return response($xml)->header('Content-Type', 'application/xrd+xml');
}
public function userOutbox(Request $request, $username)
{
abort_if(!config('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404);
public function userOutbox(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404);
$profile = Profile::whereNull('domain')
->whereNull('status')
->whereIsPrivate(false)
->whereUsername($username)
->firstOrFail();
$profile = Profile::whereNull('domain')
->whereNull('status')
->whereIsPrivate(false)
->whereUsername($username)
->firstOrFail();
$key = 'ap:outbox:latest_10:pid:' . $profile->id;
$ttl = now()->addMinutes(15);
$res = Cache::remember($key, $ttl, function() use($profile) {
return Outbox::get($profile);
});
$key = 'ap:outbox:latest_10:pid:' . $profile->id;
$ttl = now()->addMinutes(15);
$res = Cache::remember($key, $ttl, function() use($profile) {
return Outbox::get($profile);
});
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
}
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
}
public function userInbox(Request $request, $username)
{
abort_if(!config('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.inbox'), 404);
public function userInbox(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.inbox'), 404);
$headers = $request->headers->all();
$payload = $request->getContent();
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
return;
}
$headers = $request->headers->all();
$payload = $request->getContent();
$obj = json_decode($payload, true, 8);
public function sharedInbox(Request $request)
{
abort_if(!config('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.sharedInbox'), 404);
if(isset($obj['type']) && $obj['type'] === 'Delete') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
} else {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
}
return;
}
$headers = $request->headers->all();
$payload = $request->getContent();
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
return;
}
public function sharedInbox(Request $request)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.sharedInbox'), 404);
public function userFollowing(Request $request, $username)
{
abort_if(!config('federation.activitypub.enabled'), 404);
$headers = $request->headers->all();
$payload = $request->getContent();
$obj = json_decode($payload, true, 8);
$profile = Profile::whereNull('remote_url')
->whereUsername($username)
->whereIsPrivate(false)
->firstOrFail();
if($profile->status != null) {
abort(404);
}
if(isset($obj['type']) && $obj['type'] === 'Delete') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
} else {
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
}
return;
}
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
return response()->json($obj);
}
public function userFollowing(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
public function userFollowers(Request $request, $username)
{
abort_if(!config('federation.activitypub.enabled'), 404);
$profile = Profile::whereNull('remote_url')
->whereUsername($username)
->whereIsPrivate(false)
->firstOrFail();
$profile = Profile::whereNull('remote_url')
->whereUsername($username)
->whereIsPrivate(false)
->firstOrFail();
if($profile->status != null) {
abort(404);
}
if($profile->status != null) {
abort(404);
}
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
return response()->json($obj);
}
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
public function userFollowers(Request $request, $username)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
return response()->json($obj);
}
$profile = Profile::whereNull('remote_url')
->whereUsername($username)
->whereIsPrivate(false)
->firstOrFail();
if($profile->status != null) {
abort(404);
}
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
return response()->json($obj);
}
}

View file

@ -12,6 +12,7 @@ use Auth, Cache;
use Illuminate\Http\Request;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Util\ActivityPub\Helpers;
use App\Services\FollowerService;
class FollowerController extends Controller
{
@ -70,7 +71,9 @@ class FollowerController extends Controller
]);
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target);
}
}
FollowerService::add($user->id, $target->id);
} elseif ($private == false && $isFollowing == 0) {
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
@ -87,6 +90,7 @@ class FollowerController extends Controller
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target);
}
FollowerService::add($user->id, $target->id);
FollowPipeline::dispatch($follower);
} else {
if($force == true) {
@ -101,6 +105,7 @@ class FollowerController extends Controller
Follower::whereProfileId($user->id)
->whereFollowingId($target->id)
->delete();
FollowerService::remove($user->id, $target->id);
}
}

View file

@ -15,10 +15,13 @@ use App\Jobs\ImportPipeline\ImportInstagram;
trait Instagram
{
public function instagram()
{
return view('settings.import.instagram.home');
}
public function instagram()
{
if(config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
return view('settings.import.instagram.home');
}
public function instagramStart(Request $request)
{

View file

@ -6,8 +6,11 @@ use Illuminate\Http\Request;
trait Mastodon
{
public function mastodon()
{
return view('settings.import.mastodon.home');
}
public function mastodon()
{
if(config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
return view('settings.import.mastodon.home');
}
}

View file

@ -11,10 +11,6 @@ class ImportController extends Controller
public function __construct()
{
$this->middleware('auth');
if(config('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
}
}

View file

@ -12,7 +12,7 @@ class InstanceActorController extends Controller
{
$res = Cache::rememberForever(InstanceActor::PROFILE_KEY, function() {
$res = (new InstanceActor())->first()->getActor();
return json_encode($res);
return json_encode($res, JSON_UNESCAPED_SLASHES);
});
return response($res)->header('Content-Type', 'application/json');
}

View file

@ -4,30 +4,34 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{
AccountInterstitial,
DirectMessage,
DiscoverCategory,
Hashtag,
Follower,
Like,
Media,
MediaTag,
Notification,
Profile,
StatusHashtag,
Status,
UserFilter,
AccountInterstitial,
Bookmark,
DirectMessage,
DiscoverCategory,
Hashtag,
Follower,
Like,
Media,
MediaTag,
Notification,
Profile,
StatusHashtag,
Status,
User,
UserFilter,
};
use Auth,Cache;
use Illuminate\Support\Facades\Redis;
use Carbon\Carbon;
use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
StatusTransformer,
// StatusMediaContainerTransformer,
AccountTransformer,
StatusTransformer,
// StatusMediaContainerTransformer,
};
use App\Util\Media\Filter;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\ModPipeline\HandleSpammerPipeline;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Illuminate\Validation\Rule;
@ -35,518 +39,405 @@ use Illuminate\Support\Str;
use App\Services\MediaTagService;
use App\Services\ModLogService;
use App\Services\PublicTimelineService;
use App\Services\SnowflakeService;
use App\Services\StatusService;
use App\Services\UserFilterService;
use App\Services\DiscoverService;
use App\Services\BookmarkService;
class InternalApiController extends Controller
{
protected $fractal;
protected $fractal;
public function __construct()
{
$this->middleware('auth');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
public function __construct()
{
$this->middleware('auth');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
// deprecated v2 compose api
public function compose(Request $request)
{
return redirect('/');
}
// deprecated v2 compose api
public function compose(Request $request)
{
return redirect('/');
}
// deprecated
public function discover(Request $request)
{
return;
}
// deprecated
public function discover(Request $request)
{
return;
}
public function discoverPosts(Request $request)
{
$profile = Auth::user()->profile;
$pid = $profile->id;
$following = Cache::remember('feature:discover:following:'.$pid, now()->addMinutes(15), function() use ($pid) {
return Follower::whereProfileId($pid)->pluck('following_id')->toArray();
});
$filters = Cache::remember("user:filter:list:$pid", now()->addMinutes(15), function() use($pid) {
$private = Profile::whereIsPrivate(true)
->orWhere('unlisted', true)
->orWhere('status', '!=', null)
->pluck('id')
->toArray();
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')
->toArray();
return array_merge($private, $filters);
});
$following = array_merge($following, $filters);
public function discoverPosts(Request $request)
{
$pid = $request->user()->profile_id;
$filters = UserFilterService::filters($pid);
$forYou = DiscoverService::getForYou();
$posts = $forYou->take(50)->map(function($post) {
return StatusService::get($post);
})
->filter(function($post) use($filters) {
return $post &&
isset($post['account']) &&
isset($post['account']['id']) &&
!in_array($post['account']['id'], $filters);
})
->take(12)
->values();
return response()->json(compact('posts'));
}
$sql = config('database.default') !== 'pgsql';
public function directMessage(Request $request, $profileId, $threadId)
{
$profile = Auth::user()->profile;
$posts = Status::select(
'id',
'caption',
'is_nsfw',
'profile_id',
'type',
'uri',
'created_at'
)
->whereNull('uri')
->whereIn('type', ['photo','photo:album', 'video'])
->whereIsNsfw(false)
->whereVisibility('public')
->whereNotIn('profile_id', $following)
->when($sql, function($q, $s) {
return $q->where('created_at', '>', now()->subMonths(3));
})
->with('media')
->inRandomOrder()
->latest()
->take(39)
->get();
if($profileId != $profile->id) {
abort(403);
}
$res = [
'posts' => $posts->map(function($post) {
return [
'type' => $post->type,
'url' => $post->url(),
'thumb' => $post->thumb(),
];
})
];
return response()->json($res);
}
$msg = DirectMessage::whereToId($profile->id)
->orWhere('from_id',$profile->id)
->findOrFail($threadId);
public function directMessage(Request $request, $profileId, $threadId)
{
$profile = Auth::user()->profile;
$thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
->whereIn('from_id', [$profile->id,$msg->from_id])
->orderBy('created_at', 'asc')
->paginate(30);
if($profileId != $profile->id) {
abort(403);
}
return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
}
$msg = DirectMessage::whereToId($profile->id)
->orWhere('from_id',$profile->id)
->findOrFail($threadId);
public function statusReplies(Request $request, int $id)
{
$this->validate($request, [
'limit' => 'nullable|int|min:1|max:6'
]);
$parent = Status::whereScope('public')->findOrFail($id);
$limit = $request->input('limit') ?? 3;
$children = Status::whereInReplyToId($parent->id)
->orderBy('created_at', 'desc')
->take($limit)
->get();
$resource = new Fractal\Resource\Collection($children, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
$thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
->whereIn('from_id', [$profile->id,$msg->from_id])
->orderBy('created_at', 'asc')
->paginate(30);
return response()->json($res);
}
return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
}
public function stories(Request $request)
{
public function statusReplies(Request $request, int $id)
{
$parent = Status::whereScope('public')->findOrFail($id);
}
$children = Status::whereInReplyToId($parent->id)
->orderBy('created_at', 'desc')
->take(3)
->get();
public function discoverCategories(Request $request)
{
$categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
$res = $categories->map(function($item) {
return [
'name' => $item->name,
'url' => $item->url(),
'thumb' => $item->thumb()
];
});
return response()->json($res);
}
$resource = new Fractal\Resource\Collection($children, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
public function modAction(Request $request)
{
abort_unless(Auth::user()->is_admin, 400);
$this->validate($request, [
'action' => [
'required',
'string',
Rule::in([
'addcw',
'remcw',
'unlist',
'spammer'
])
],
'item_id' => 'required|integer|min:1',
'item_type' => [
'required',
'string',
Rule::in(['profile', 'status'])
]
]);
return response()->json($res);
}
$action = $request->input('action');
$item_id = $request->input('item_id');
$item_type = $request->input('item_type');
public function stories(Request $request)
{
}
$status = Status::findOrFail($item_id);
$author = User::whereProfileId($status->profile_id)->first();
abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
public function discoverCategories(Request $request)
{
$categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
$res = $categories->map(function($item) {
return [
'name' => $item->name,
'url' => $item->url(),
'thumb' => $item->thumb()
];
});
return response()->json($res);
}
switch($action) {
case 'addcw':
$status->is_nsfw = true;
$status->save();
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'cw',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
public function modAction(Request $request)
{
abort_unless(Auth::user()->is_admin, 400);
$this->validate($request, [
'action' => [
'required',
'string',
Rule::in([
'addcw',
'remcw',
'unlist'
])
],
'item_id' => 'required|integer|min:1',
'item_type' => [
'required',
'string',
Rule::in(['profile', 'status'])
]
]);
if($status->uri == null) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.cw';
$ai->view = 'account.moderation.post.cw';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$action = $request->input('action');
$item_id = $request->input('item_id');
$item_type = $request->input('item_type');
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
break;
switch($action) {
case 'addcw':
$status = Status::findOrFail($item_id);
$status->is_nsfw = true;
$status->save();
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'cw',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
case 'remcw':
$status->is_nsfw = false;
$status->save();
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'remove_cw',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
if($status->uri == null) {
$ai = AccountInterstitial::whereUserId($status->profile->user_id)
->whereType('post.cw')
->whereItemId($status->id)
->whereItemType('App\Status')
->first();
$ai->delete();
}
break;
case 'unlist':
$status->scope = $status->visibility = 'unlisted';
$status->save();
PublicTimelineService::del($status->id);
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'unlist',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
if($status->uri == null) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.cw';
$ai->view = 'account.moderation.post.cw';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
if($status->uri == null) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.unlist';
$ai->view = 'account.moderation.post.unlist';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
break;
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
break;
case 'remcw':
$status = Status::findOrFail($item_id);
$status->is_nsfw = false;
$status->save();
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'remove_cw',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
if($status->uri == null) {
$ai = AccountInterstitial::whereUserId($status->profile->user_id)
->whereType('post.cw')
->whereItemId($status->id)
->whereItemType('App\Status')
->first();
$ai->delete();
}
break;
case 'spammer':
HandleSpammerPipeline::dispatch($status->profile);
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\User::class')
->action('admin.status.moderate')
->metadata([
'action' => 'spammer',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
break;
}
case 'unlist':
$status = Status::whereScope('public')->findOrFail($item_id);
$status->scope = $status->visibility = 'unlisted';
$status->save();
PublicTimelineService::del($status->id);
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'unlist',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
StatusService::del($status->id, true);
return ['msg' => 200];
}
if($status->uri == null) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.unlist';
$ai->view = 'account.moderation.post.unlist';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
public function composePost(Request $request)
{
abort(400, 'Endpoint deprecated');
}
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
break;
}
return ['msg' => 200];
}
public function bookmarks(Request $request)
{
$pid = $request->user()->profile_id;
$res = Bookmark::whereProfileId($pid)
->orderByDesc('created_at')
->simplePaginate(10)
->map(function($bookmark) use($pid) {
$status = StatusService::get($bookmark->status_id, false);
if(!$status) {
return false;
}
$status['bookmarked_at'] = $bookmark->created_at->format('c');
public function composePost(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'media.*' => 'required',
'media.*.id' => 'required|integer|min:1',
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
'media.*.license' => 'nullable|string|max:140',
'media.*.alt' => 'nullable|string|max:140',
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
'comments_disabled' => 'nullable',
'tagged' => 'nullable'
]);
if($status) {
BookmarkService::add($pid, $status['id']);
}
return $status;
})
->filter(function($bookmark) {
return $bookmark && isset($bookmark['id']);
})
->values();
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
$keywords = config('costar.keyword.block');
foreach($keywords as $kw) {
if(Str::contains($request->caption, $kw) == true) {
abort(400, 'Invalid object');
}
}
}
}
return response()->json($res);
}
$user = Auth::user();
$profile = $user->profile;
$visibility = $request->input('visibility');
$medias = $request->input('media');
$attachments = [];
$status = new Status;
$mimes = [];
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
public function accountStatuses(Request $request, $id)
{
$this->validate($request, [
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'nullable|integer|min:1|max:24'
]);
foreach($medias as $k => $media) {
if($k + 1 > config('pixelfed.max_album_length')) {
continue;
}
$m = Media::findOrFail($media['id']);
if($m->profile_id !== $profile->id || $m->status_id) {
abort(403, 'Invalid media id');
}
$m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
$m->license = $media['license'];
$m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
$m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
if($cw == true || $profile->cw == true) {
$m->is_nsfw = $cw;
$status->is_nsfw = $cw;
}
$m->save();
$attachments[] = $m;
array_push($mimes, $m->mime);
}
$profile = Profile::whereNull('status')->findOrFail($id);
$mediaType = StatusController::mimeTypeCheck($mimes);
$limit = $request->limit ?? 9;
$max_id = $request->max_id;
$min_id = $request->min_id;
$scope = $request->only_media == true ?
['photo', 'photo:album', 'video', 'video:album'] :
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
abort(400, __('exception.compose.invalid.album'));
}
if($profile->is_private) {
if(!Auth::check()) {
return response()->json([]);
}
$pid = Auth::user()->profile->id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
return $following->push($pid)->toArray();
});
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
} else {
if(Auth::check()) {
$pid = Auth::user()->profile->id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
return $following->push($pid)->toArray();
});
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
} else {
$visibility = ['public', 'unlisted'];
}
}
if($place && is_array($place)) {
$status->place_id = $place['id'];
}
if($request->filled('comments_disabled')) {
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
$dir = $min_id ? '>' : '<';
$id = $min_id ?? $max_id;
$timeline = Status::select(
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'likes_count',
'reblogs_count',
'scope',
'local',
'created_at',
'updated_at'
)->whereProfileId($profile->id)
->whereIn('type', $scope)
->where('id', $dir, $id)
->whereIn('visibility', $visibility)
->latest()
->limit($limit)
->get();
$status->caption = strip_tags($request->caption);
$status->scope = 'draft';
$status->profile_id = $profile->id;
$status->save();
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
foreach($attachments as $media) {
$media->status_id = $status->id;
$media->save();
}
return response()->json($res);
}
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
$cw = $profile->cw == true ? true : $cw;
$status->is_nsfw = $cw;
$status->visibility = $visibility;
$status->scope = $visibility;
$status->type = $mediaType;
$status->save();
public function remoteProfile(Request $request, $id)
{
return redirect('/i/web/profile/' . $id);
}
foreach($tagged as $tg) {
$mt = new MediaTag;
$mt->status_id = $status->id;
$mt->media_id = $status->media->first()->id;
$mt->profile_id = $tg['id'];
$mt->tagged_username = $tg['name'];
$mt->is_public = true; // (bool) $tg['privacy'] ?? 1;
$mt->metadata = json_encode([
'_v' => 1,
]);
$mt->save();
MediaTagService::set($mt->status_id, $mt->profile_id);
MediaTagService::sendNotification($mt);
}
public function remoteStatus(Request $request, $profileId, $statusId)
{
return redirect('/i/web/post/' . $statusId);
}
NewStatusPipeline::dispatch($status);
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
Cache::forget($user->storageUsedKey());
return $status->url();
}
public function requestEmailVerification(Request $request)
{
$pid = $request->user()->profile_id;
$exists = Redis::sismember('email:manual', $pid);
return view('account.email.request_verification', compact('exists'));
}
public function bookmarks(Request $request)
{
$statuses = Auth::user()->profile
->bookmarks()
->withCount(['likes','comments'])
->orderBy('created_at', 'desc')
->simplePaginate(10);
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
public function accountStatuses(Request $request, $id)
{
$this->validate($request, [
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'nullable|integer|min:1|max:24'
]);
$profile = Profile::whereNull('status')->findOrFail($id);
$limit = $request->limit ?? 9;
$max_id = $request->max_id;
$min_id = $request->min_id;
$scope = $request->only_media == true ?
['photo', 'photo:album', 'video', 'video:album'] :
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
if($profile->is_private) {
if(!Auth::check()) {
return response()->json([]);
}
$pid = Auth::user()->profile->id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
return $following->push($pid)->toArray();
});
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
} else {
if(Auth::check()) {
$pid = Auth::user()->profile->id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
return $following->push($pid)->toArray();
});
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
} else {
$visibility = ['public', 'unlisted'];
}
}
$dir = $min_id ? '>' : '<';
$id = $min_id ?? $max_id;
$timeline = Status::select(
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'likes_count',
'reblogs_count',
'scope',
'local',
'created_at',
'updated_at'
)->whereProfileId($profile->id)
->whereIn('type', $scope)
->where('id', $dir, $id)
->whereIn('visibility', $visibility)
->latest()
->limit($limit)
->get();
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
public function remoteProfile(Request $request, $id)
{
$profile = Profile::whereNull('status')
->whereNotNull('domain')
->findOrFail($id);
$user = Auth::user();
return view('profile.remote', compact('profile', 'user'));
}
public function remoteStatus(Request $request, $profileId, $statusId)
{
$user = Profile::whereNull('status')
->whereNotNull('domain')
->findOrFail($profileId);
$status = Status::whereProfileId($user->id)
->whereNull('reblog_of_id')
->whereIn('visibility', ['public', 'unlisted'])
->findOrFail($statusId);
$template = $status->in_reply_to_id ? 'status.reply' : 'status.remote';
return view($template, compact('user', 'status'));
}
public function requestEmailVerificationStore(Request $request)
{
$pid = $request->user()->profile_id;
Redis::sadd('email:manual', $pid);
return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']);
}
}

View file

@ -3,68 +3,67 @@
namespace App\Http\Controllers;
use App\Jobs\LikePipeline\LikePipeline;
use App\Jobs\LikePipeline\UnlikePipeline;
use App\Like;
use App\Status;
use App\User;
use Auth;
use Cache;
use Illuminate\Http\Request;
use App\Services\StatusService;
class LikeController extends Controller
{
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',
]);
$user = Auth::user();
$profile = $user->profile;
$status = Status::findOrFail($request->input('item'));
$user = Auth::user();
$profile = $user->profile;
$status = Status::findOrFail($request->input('item'));
$count = $status->likes()->count();
if (Like::whereStatusId($status->id)->whereProfileId($profile->id)->exists()) {
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
UnlikePipeline::dispatch($like);
} else {
$count = $status->likes_count > 4 ? $status->likes_count : $status->likes()->count();
$like = Like::firstOrCreate([
'profile_id' => $user->profile_id,
'status_id' => $status->id
]);
if($like->wasRecentlyCreated == true) {
$count++;
$status->likes_count = $count;
$like->status_profile_id = $status->profile_id;
$like->is_comment = in_array($status->type, [
'photo',
'photo:album',
'video',
'video:album',
'photo:video:album'
]) == false;
$like->save();
$status->save();
LikePipeline::dispatch($like);
}
}
if ($status->likes()->whereProfileId($profile->id)->count() !== 0) {
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
$like->forceDelete();
$count--;
$status->likes_count = $count;
$status->save();
} else {
$like = Like::firstOrCreate([
'profile_id' => $user->profile_id,
'status_id' => $status->id
]);
if($like->wasRecentlyCreated == true) {
$count++;
$status->likes_count = $count;
$like->status_profile_id = $status->profile_id;
$like->is_comment = in_array($status->type, [
'photo',
'photo:album',
'video',
'video:album',
'photo:video:album'
]) == false;
$like->save();
$status->save();
LikePipeline::dispatch($like);
}
}
Cache::forget('status:'.$status->id.':likedby:userid:'.$user->id);
StatusService::refresh($status->id);
Cache::forget('status:'.$status->id.':likedby:userid:'.$user->id);
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => 0];
} else {
$response = redirect($status->url());
}
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
return $response;
}
return $response;
}
}

View file

@ -22,39 +22,6 @@ class MediaController extends Controller
public function composeUpdate(Request $request, $id)
{
$this->validate($request, [
'file' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
]);
$user = Auth::user();
$photo = $request->file('file');
$media = Media::whereUserId($user->id)
->whereProfileId($user->profile_id)
->whereNull('status_id')
->findOrFail($id);
$media->version = 2;
$media->save();
$fragments = explode('/', $media->media_path);
$name = last($fragments);
array_pop($fragments);
$dir = implode('/', $fragments);
$path = $photo->storeAs($dir, $name);
$res = [];
$res['url'] = URL::temporarySignedRoute(
'temp-media', now()->addHours(1), ['profileId' => $media->profile_id, 'mediaId' => $media->id, 'timestamp' => time()]
);
ImageOptimize::dispatch($media);
return $res;
abort(400, 'Endpoint deprecated');
}
}

View file

@ -20,44 +20,7 @@ class MediaTagController extends Controller
public function usernameLookup(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:50'
]);
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
if(strlen($q) < 3) {
return [];
}
$q = mb_substr($q, 1);
}
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
->whereFilterableId($request->user()->profile_id)
->pluck('user_id');
$blocked->push($request->user()->profile_id);
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->whereNull('domain')
->where('username','like','%'.$q.'%')
->limit(15)
->get()
->map(function($r) {
return [
'id' => (string) $r->id,
'name' => $r->username,
'privacy' => true,
'avatar' => $r->avatarUrl()
];
});
return $results;
abort(404);
}
public function untagProfile(Request $request)

View file

@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Status;
use App\Models\Poll;
use App\Models\PollVote;
use App\Services\PollService;
use App\Services\FollowerService;
class PollController extends Controller
{
public function getPoll(Request $request, $id)
{
abort_if(!config_cache('instance.polls.enabled'), 404);
$poll = Poll::findOrFail($id);
$status = Status::findOrFail($poll->status_id);
if($status->scope != 'public') {
abort_if(!$request->user(), 403);
if($request->user()->profile_id != $status->profile_id) {
abort_if(!FollowerService::follows($request->user()->profile_id, $status->profile_id), 404);
}
}
$pid = $request->user() ? $request->user()->profile_id : false;
$poll = PollService::getById($id, $pid);
return $poll;
}
public function vote(Request $request, $id)
{
abort_if(!config_cache('instance.polls.enabled'), 404);
abort_unless($request->user(), 403);
$this->validate($request, [
'choices' => 'required|array'
]);
$pid = $request->user()->profile_id;
$poll_id = $id;
$choices = $request->input('choices');
// todo: implement multiple choice
$choice = $choices[0];
$poll = Poll::findOrFail($poll_id);
abort_if(now()->gt($poll->expires_at), 422, 'Poll expired.');
abort_if(PollVote::wherePollId($poll_id)->whereProfileId($pid)->exists(), 400, 'Already voted.');
$vote = new PollVote;
$vote->status_id = $poll->status_id;
$vote->profile_id = $pid;
$vote->poll_id = $poll->id;
$vote->choice = $choice;
$vote->save();
$poll->votes_count = $poll->votes_count + 1;
$poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($choice) {
return $choice == $key ? $tally + 1 : $tally;
})->toArray();
$poll->save();
PollService::del($poll->status_id);
$res = PollService::get($poll->status_id, $pid);
return $res;
}
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use Cache;
use DB;
use View;
use App\Follower;
use App\FollowRequest;
@ -13,6 +14,9 @@ use App\Story;
use App\User;
use App\UserFilter;
use League\Fractal;
use App\Services\AccountService;
use App\Services\FollowerService;
use App\Services\StatusService;
use App\Util\Lexer\Nickname;
use App\Util\Webfinger\Webfinger;
use App\Transformer\ActivityPub\ProfileOutbox;
@ -20,229 +24,247 @@ use App\Transformer\ActivityPub\ProfileTransformer;
class ProfileController extends Controller
{
public function show(Request $request, $username)
{
$user = Profile::whereNull('domain')
->whereNull('status')
->whereUsername($username)
->firstOrFail();
if($request->wantsJson() && config('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
return $this->buildProfile($request, $user);
}
public function show(Request $request, $username)
{
$user = Profile::whereNull('domain')
->whereNull('status')
->whereUsername($username)
->firstOrFail();
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;
});
if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
return $this->buildProfile($request, $user);
}
if ($user->is_private == true) {
abort(404);
}
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;
});
$owner = false;
$is_following = false;
if ($user->is_private == true) {
$profile = null;
return view('profile.private', compact('user'));
}
$is_admin = $user->user->is_admin;
$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
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
$owner = false;
$is_following = false;
return view($ui, 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;
});
$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
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
if ($user->is_private == true) {
$isPrivate = $this->privateProfileCheck($user, $loggedIn);
}
return view($ui, 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;
});
$isBlocked = $this->blockedProfileCheck($user);
if ($user->is_private == true) {
$isPrivate = $this->privateProfileCheck($user, $loggedIn);
}
$owner = $loggedIn && Auth::id() === $user->user_id;
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
$isBlocked = $this->blockedProfileCheck($user);
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'));
}
$owner = $loggedIn && Auth::id() === $user->user_id;
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : 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
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
return view($ui, compact('profile', 'settings'));
}
}
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'));
}
public function permalinkRedirect(Request $request, $username)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$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
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
return view($ui, compact('profile', 'settings'));
}
}
if ($request->wantsJson() && config('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
public function permalinkRedirect(Request $request, $username)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
return redirect($user->url());
}
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
protected function privateProfileCheck(Profile $profile, $loggedIn)
{
if (!Auth::check()) {
return true;
}
return redirect($user->url());
}
$user = Auth::user()->profile;
if($user->id == $profile->id || !$profile->is_private) {
return false;
}
protected function privateProfileCheck(Profile $profile, $loggedIn)
{
if (!Auth::check()) {
return true;
}
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
if ($follows == false) {
return true;
}
return false;
}
$user = Auth::user()->profile;
if($user->id == $profile->id || !$profile->is_private) {
return false;
}
public static function accountCheck(Profile $profile)
{
switch ($profile->status) {
case 'disabled':
case 'suspended':
case 'delete':
return view('profile.disabled');
break;
default:
break;
}
return abort(404);
}
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
if ($follows == false) {
return 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 false;
}
return false;
}
public static function accountCheck(Profile $profile)
{
switch ($profile->status) {
case 'disabled':
case 'suspended':
case 'delete':
return view('profile.disabled');
break;
public function showActivityPub(Request $request, $user)
{
abort_if(!config('federation.activitypub.enabled'), 404);
abort_if($user->domain, 404);
default:
break;
}
return abort(404);
}
$fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
$res = $fractal->createData($resource)->toArray();
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
}
protected function 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;
}
public function showAtomFeed(Request $request, $user)
{
abort_if(!config('federation.atom.enabled'), 404);
return false;
}
$profile = $user = Profile::whereNull('status')->whereNull('domain')->whereUsername($user)->whereIsPrivate(false)->firstOrFail();
if($profile->status != null) {
return $this->accountCheck($profile);
}
if($profile->is_private || Auth::check()) {
$blocked = $this->blockedProfileCheck($profile);
$check = $this->privateProfileCheck($profile, null);
if($check || $blocked) {
return redirect($profile->url());
}
}
$items = $profile->statuses()->whereHas('media')->whereIn('visibility',['public', 'unlisted'])->orderBy('created_at', 'desc')->take(10)->get();
return response()->view('atom.user', compact('profile', 'items'))
->header('Content-Type', 'application/atom+xml');
}
public function showActivityPub(Request $request, $user)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if($user->domain, 404);
public function meRedirect()
{
abort_if(!Auth::check(), 404);
return redirect(Auth::user()->url());
}
$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');
}
public function embed(Request $request, $username)
{
$res = view('profile.embed-removed');
public function showAtomFeed(Request $request, $user)
{
abort_if(!config('federation.atom.enabled'), 404);
if(strlen($username) > 15 || strlen($username) < 2) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$pid = AccountService::usernameToId($user);
$profile = Profile::whereUsername($username)
->whereIsPrivate(false)
->whereNull('status')
->whereNull('domain')
->first();
abort_if(!$pid, 404);
if(!$profile) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = AccountService::get($pid);
$content = Cache::remember('profile:embed:'.$profile->id, now()->addHours(12), function() use($profile) {
return View::make('profile.embed')->with(compact('profile'))->render();
});
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
public function stories(Request $request, $username)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$pid = $profile->id;
$authed = Auth::user()->profile;
abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404);
$exists = Story::whereProfileId($pid)
->where('expires_at', '>', now())
->count();
abort_unless($exists > 0, 404);
return view('profile.story', compact('pid', 'profile'));
}
$items = DB::table('statuses')
->whereProfileId($pid)
->whereVisibility('public')
->whereType('photo')
->latest()
->take(10)
->get()
->map(function($status) {
return StatusService::get($status->id);
})
->filter(function($status) {
return $status &&
isset($status['account']) &&
isset($status['media_attachments']) &&
count($status['media_attachments']);
})
->values();
$permalink = config('app.url') . "/users/{$profile['username']}.atom";
return response()
->view('atom.user', compact('profile', 'items', 'permalink'))
->header('Content-Type', 'application/atom+xml');
}
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(strlen($username) > 15 || strlen($username) < 2) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = Profile::whereUsername($username)
->whereIsPrivate(false)
->whereNull('status')
->whereNull('domain')
->first();
if(!$profile) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
if(AccountService::canEmbed($profile->user_id) == false) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = AccountService::get($profile->id);
$res = view('profile.embed', compact('profile'));
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function stories(Request $request, $username)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$pid = $profile->id;
$authed = Auth::user()->profile_id;
abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404);
$exists = Story::whereProfileId($pid)
->whereActive(true)
->exists();
abort_unless($exists, 404);
return view('profile.story', compact('pid', 'profile'));
}
}

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,7 @@ class ReportController extends Controller
$this->validate($request, [
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
]);
]);
return view('report.form');
}
@ -86,11 +86,11 @@ class ReportController extends Controller
public function formStore(Request $request)
{
$this->validate($request, [
'report' => 'required|alpha_dash',
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
'msg' => 'nullable|string|max:150',
]);
'report' => 'required|alpha_dash',
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
'msg' => 'nullable|string|max:150',
]);
$profile = Auth::user()->profile;
$reportType = $request->input('report');
@ -98,10 +98,26 @@ class ReportController extends Controller
$object_type = $request->input('type');
$msg = $request->input('msg');
$object = null;
$types = ['spam', 'sensitive', 'abusive'];
$types = [
// original 3
'spam',
'sensitive',
'abusive',
// new
'underage',
'copyright',
'impersonation',
'scam',
'terrorism'
];
if (!in_array($reportType, $types)) {
return redirect('/timeline')->with('error', 'Invalid report type');
if($request->wantsJson()) {
return abort(400, 'Invalid report type');
} else {
return redirect('/timeline')->with('error', 'Invalid report type');
}
}
switch ($object_type) {
@ -115,16 +131,28 @@ class ReportController extends Controller
break;
default:
return redirect('/timeline')->with('error', 'Invalid report type');
if($request->wantsJson()) {
return abort(400, 'Invalid report type');
} else {
return redirect('/timeline')->with('error', 'Invalid report type');
}
break;
}
if ($exists !== 0) {
return redirect('/timeline')->with('error', 'You have already reported this!');
if($request->wantsJson()) {
return response()->json(200);
} else {
return redirect('/timeline')->with('error', 'You have already reported this!');
}
}
if ($object->profile_id == $profile->id) {
return redirect('/timeline')->with('error', 'You cannot report your own content!');
if($request->wantsJson()) {
return response()->json(200);
} else {
return redirect('/timeline')->with('error', 'You cannot report your own content!');
}
}
$report = new Report();
@ -134,9 +162,13 @@ class ReportController extends Controller
$report->object_type = $object_type;
$report->reported_profile_id = $object->profile_id;
$report->type = $request->input('report');
$report->message = $request->input('msg');
$report->message = e($request->input('msg'));
$report->save();
return redirect('/timeline')->with('status', 'Report successfully sent!');
if($request->wantsJson()) {
return response()->json(200);
} else {
return redirect('/timeline')->with('status', 'Report successfully sent!');
}
}
}

View file

@ -12,348 +12,358 @@ use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use App\Transformer\Api\{
AccountTransformer,
HashtagTransformer,
StatusTransformer,
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 $term = '';
public $hash = '';
public $cacheKey = 'api:search:tag:';
public function __construct()
{
$this->middleware('auth');
}
public function __construct()
{
$this->middleware('auth');
}
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 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'
]);
$scope = $request->input('scope') ?? 'all';
$this->term = e(urldecode($request->input('q')));
$this->hash = hash('sha256', $this->term);
$scope = $request->input('scope') ?? 'all';
$this->term = e(urldecode($request->input('q')));
$this->hash = hash('sha256', $this->term);
switch ($scope) {
case 'all':
$this->getHashtags();
$this->getPosts();
$this->getProfiles();
// $this->getPlaces();
break;
switch ($scope) {
case 'all':
$this->getHashtags();
$this->getPosts();
$this->getProfiles();
// $this->getPlaces();
break;
case 'hashtag':
$this->getHashtags();
break;
case 'hashtag':
$this->getHashtags();
break;
case 'profile':
$this->getProfiles();
break;
case 'profile':
$this->getProfiles();
break;
case 'webfinger':
$this->webfingerSearch();
break;
case 'webfinger':
$this->webfingerSearch();
break;
case 'remote':
$this->remoteLookupSearch();
break;
case 'remote':
$this->remoteLookupSearch();
break;
case 'place':
$this->getPlaces();
break;
case 'place':
$this->getPlaces();
break;
default:
break;
}
default:
break;
}
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
}
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
}
protected function getPosts()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
config('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();
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();
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;
}
}
}
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;
}
}
}
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;
}
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;
}
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 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 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('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;
});
}
}
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;
});
}
}
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();
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($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()
]
];
});
}
});
}
}
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()
]
];
});
}
});
}
}
public function results(Request $request)
{
$this->validate($request, [
'q' => 'required|string|min:1',
]);
return view('search.results');
}
public function results(Request $request)
{
$this->validate($request, [
'q' => 'required|string|min:1',
]);
protected function webfingerSearch()
{
$wfs = WebfingerService::lookup($this->term);
return view('search.results');
}
if(empty($wfs)) {
return;
}
protected function webfingerSearch()
{
$wfs = WebfingerService::lookup($this->term);
$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;
}
if(empty($wfs)) {
return;
}
protected function remotePostLookup()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
$local = Helpers::validateLocalUrl($tag);
$valid = Helpers::validateUrl($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']
]
]
];
return;
}
if($valid == false || $local == true) {
return;
}
if(Status::whereUri($tag)->whereLocal(false)->exists()) {
$item = Status::whereUri($tag)->first();
$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' => $item->firstMedia()->remote_url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
protected function remotePostLookup()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
$local = Helpers::validateLocalUrl($tag);
$valid = Helpers::validateUrl($tag);
$remote = Helpers::fetchFromUrl($tag);
if($valid == false || $local == true) {
return;
}
if(isset($remote['type']) && $remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$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' => $item->firstMedia()->remote_url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
}
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()
]];
}
protected function remoteLookupSearch()
{
if(!Helpers::validateUrl($this->term)) {
return;
}
$this->getProfiles();
$this->remotePostLookup();
}
}
$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();
}
}

View file

@ -4,17 +4,236 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\AccountLog;
use App\Follower;
use App\Like;
use App\Status;
use App\StatusHashtag;
use Illuminate\Support\Facades\Cache;
class SeasonalController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function __construct()
{
$this->middleware('auth');
}
public function yearInReview()
{
$profile = Auth::user()->profile;
return view('account.yir', compact('profile'));
}
public function yearInReview()
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
$profile = Auth::user()->profile;
return view('account.yir', compact('profile'));
}
public function getData(Request $request)
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
$uid = $request->user()->id;
$pid = $request->user()->profile_id;
$epoch = '2020-01-01 00:00:00';
$epochStart = '2020-01-01 00:00:00';
$epochEnd = '2020-12-31 23:59:59';
$siteKey = 'seasonal:my2020:shared';
$siteTtl = now()->addMonths(3);
$userKey = 'seasonal:my2020:user:' . $uid;
$userTtl = now()->addMonths(3);
$shared = Cache::remember($siteKey, $siteTtl, function() use($epochStart, $epochEnd) {
return [
'average' => [
'posts' => round(Status::selectRaw('*, count(profile_id) as count')
->whereNull('uri')
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->pluck('count')
->avg()),
'likes' => round(Like::selectRaw('*, count(profile_id) as count')
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->pluck('count')
->avg()),
],
'popular' => [
'hashtag' => StatusHashtag::selectRaw('*,count(hashtag_id) as count')
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('hashtag_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->hashtag->name,
'count' => $sh->count
];
})
->first(),
'post' => Status::whereScope('public')
->where('likes_count', '>', 1)
->whereIsNsfw(false)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->orderByDesc('likes_count')
->take(1)
->get()
->map(function($status) {
return [
'id' => (string) $status->id,
'username' => (string) $status->profile->username,
'created_at' => $status->created_at->format('M d, Y'),
'type' => $status->type,
'url' => $status->url(),
'thumb' => $status->thumb(),
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
'reply_count' => $status->reply_count ?? 0,
];
})
->first(),
'places' => Status::selectRaw('*, count(place_id) as count')
->whereNotNull('place_id')
->having('count', '>', 1)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('place_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->place->getName(),
'url' => $sh->place->url(),
'count' => $sh->count
];
})
->first()
],
];
});
$res = Cache::remember($userKey, $userTtl, function() use($uid, $pid, $epochStart, $epochEnd, $request) {
return [
'account' => [
'user_id' => $request->user()->id,
'created_at' => $request->user()->created_at->format('M d, Y'),
'created_this_year' => $request->user()->created_at->gt('2020-01-01 00:00:00'),
'created_months_ago' => $request->user()->created_at->diffInMonths(now()),
'followers_this_year' => Follower::whereFollowingId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'followed_this_year' => Follower::whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'most_popular' => Status::whereProfileId($pid)
->where('likes_count', '>', 1)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->orderByDesc('likes_count')
->take(1)
->get()
->map(function($status) {
return [
'id' => (string) $status->id,
'username' => (string) $status->profile->username,
'created_at' => $status->created_at->format('M d, Y'),
'type' => $status->type,
'url' => $status->url(),
'thumb' => $status->thumb(),
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
'reply_count' => $status->reply_count ?? 0,
];
})
->first(),
'posts_count' => Status::whereProfileId($pid)
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'likes_count' => Like::whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->count(),
'hashtag' => StatusHashtag::selectRaw('*, count(hashtag_id) as count')
->whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->hashtag->name,
'count' => $sh->count
];
})
->first(),
'places' => Status::selectRaw('*, count(place_id) as count')
->whereNotNull('place_id')
->having('count', '>', 1)
->whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('place_id')
->orderByDesc('count')
->take(1)
->get()
->map(function($sh) {
return [
'name' => $sh->place->getName(),
'url' => $sh->place->url(),
'count' => $sh->count
];
})
->first(),
'places_total' => Status::whereProfileId($pid)
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->whereNotNull('place_id')
->count()
]
];
});
return response()->json(array_merge($res, $shared));
}
public function store(Request $request)
{
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
abort_if(config('database.default') != 'mysql', 404);
$user = $request->user();
$log = AccountLog::firstOrCreate([
[
'item_type' => 'App\User',
'item_id' => $user->id,
'user_id' => $user->id,
'action' => 'seasonal.my2020.view'
],
[
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent()
]
]);
return response()->json(200);
}
}

View file

@ -8,6 +8,7 @@ use App\Media;
use App\Profile;
use App\User;
use App\UserFilter;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\PrettyNumber;
use Auth;
use Cache;
@ -16,181 +17,194 @@ use Mail;
use Purify;
use App\Mail\PasswordChange;
use Illuminate\Http\Request;
use App\Services\PronounService;
trait HomeSettings
{
public function home()
{
$id = Auth::user()->profile->id;
$storage = [];
$used = Media::whereProfileId($id)->sum('size');
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
$storage['used'] = $used;
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
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'));
}
return view('settings.home', compact('storage', 'pronouns'));
}
public function homeUpdate(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
'website' => 'nullable|url',
'language' => 'nullable|string|min:2|max:5'
]);
public function homeUpdate(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
'website' => 'nullable|url',
'language' => 'nullable|string|min:2|max:5',
'pronouns' => 'nullable|array|max:4'
]);
$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;
$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('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 ($profile->bio != $bio) {
$changes = true;
$profile->bio = $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 ($changes === true) {
Cache::forget('user:account:id:'.$user->id);
$user->save();
$profile->save();
if($existingPronouns != $pronouns) {
if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
PronounService::clear($profile->id);
} else {
PronounService::put($profile->id, $pronouns);
}
}
}
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
}
if ($changes === true) {
Cache::forget('user:account:id:'.$user->id);
$user->save();
$profile->save();
return redirect('/settings/home');
}
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
}
public function password()
{
return view('settings.password');
}
return redirect('/settings/home');
}
public function passwordUpdate(Request $request)
{
$this->validate($request, [
'current' => 'required|string',
'password' => 'required|string',
'password_confirmation' => 'required|string',
]);
public function password()
{
return view('settings.password');
}
$current = $request->input('current');
$new = $request->input('password');
$confirm = $request->input('password_confirmation');
public function passwordUpdate(Request $request)
{
$this->validate($request, [
'current' => 'required|string',
'password' => 'required|string',
'password_confirmation' => 'required|string',
]);
$user = Auth::user();
$current = $request->input('current');
$new = $request->input('password');
$confirm = $request->input('password_confirmation');
if (password_verify($current, $user->password) && $new === $confirm) {
$user->password = bcrypt($new);
$user->save();
$user = Auth::user();
$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();
if (password_verify($current, $user->password) && $new === $confirm) {
$user->password = bcrypt($new);
$user->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.');
}
$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.');
}
public function email()
{
return view('settings.email');
}
}
public function emailUpdate(Request $request)
{
$this->validate($request, [
'email' => 'required|email',
]);
$changes = false;
$email = $request->input('email');
$user = Auth::user();
$profile = $user->profile;
public function email()
{
return view('settings.email');
}
$validate = config('pixelfed.enforce_email_verification');
public function emailUpdate(Request $request)
{
$this->validate($request, [
'email' => 'required|email',
]);
$changes = false;
$email = $request->input('email');
$user = Auth::user();
$profile = $user->profile;
if ($user->email != $email) {
$changes = true;
$user->email = $email;
$validate = config_cache('pixelfed.enforce_email_verification');
if ($validate) {
$user->email_verified_at = null;
// Prevent old verifications from working
EmailVerification::whereUserId($user->id)->delete();
}
if ($user->email != $email) {
$changes = true;
$user->email = $email;
$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 ($validate) {
$user->email_verified_at = null;
// Prevent old verifications from working
EmailVerification::whereUserId($user->id)->delete();
}
if ($changes === true) {
Cache::forget('user:account:id:'.$user->id);
$user->save();
$profile->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();
}
return redirect('/settings/home')->with('status', 'Email successfully updated!');
} else {
return redirect('/settings/email');
}
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');
}
public function avatar()
{
return view('settings.avatar');
}
}
}
public function avatar()
{
return view('settings.avatar');
}
}

View file

@ -74,6 +74,11 @@ trait PrivacySettings
}
Cache::forget('profile:settings:' . $profile->id);
Cache::forget('user:account:id:' . $profile->user_id);
Cache::forget('profile:follower_count:' . $profile->id);
Cache::forget('profile:following_count:' . $profile->id);
Cache::forget('profile:embed:' . $profile->id);
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
}
@ -222,4 +227,4 @@ trait PrivacySettings
Cache::forget('profiles:private');
return [200];
}
}
}

View file

@ -7,228 +7,314 @@ use App\Following;
use App\ProfileSponsor;
use App\Report;
use App\UserFilter;
use App\UserSetting;
use Auth, Cookie, DB, Cache, Purify;
use Illuminate\Support\Facades\Redis;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Http\Controllers\Settings\{
ExportSettings,
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings
ExportSettings,
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings
};
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
class SettingsController extends Controller
{
use ExportSettings,
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings;
use ExportSettings,
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings;
public function __construct()
{
$this->middleware('auth');
}
public function __construct()
{
$this->middleware('auth');
}
public function accessibility()
{
$settings = Auth::user()->settings;
public function accessibility()
{
$settings = Auth::user()->settings;
return view('settings.accessibility', compact('settings'));
}
return view('settings.accessibility', compact('settings'));
}
public function accessibilityStore(Request $request)
{
$settings = Auth::user()->settings;
$fields = [
'compose_media_descriptions',
'reduce_motion',
'optimize_screen_reader',
'high_contrast_mode',
'video_autoplay',
];
foreach ($fields as $field) {
$form = $request->input($field);
if ($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
$settings->save();
}
public function accessibilityStore(Request $request)
{
$settings = Auth::user()->settings;
$fields = [
'compose_media_descriptions',
'reduce_motion',
'optimize_screen_reader',
'high_contrast_mode',
'video_autoplay',
];
foreach ($fields as $field) {
$form = $request->input($field);
if ($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
$settings->save();
}
return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!');
}
return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!');
}
public function notifications()
{
return view('settings.notifications');
}
public function notifications()
{
return view('settings.notifications');
}
public function applications()
{
return view('settings.applications');
}
public function applications()
{
return view('settings.applications');
}
public function dataImport()
{
abort_if(!config('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.home');
}
public function dataImport()
{
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.home');
}
public function dataImportInstagram()
{
abort_if(!config('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.instagram.home');
}
public function dataImportInstagram()
{
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.instagram.home');
}
public function developers()
{
return view('settings.developers');
}
public function developers()
{
return view('settings.developers');
}
public function removeAccountTemporary(Request $request)
{
$user = Auth::user();
abort_if(!config('pixelfed.account_deletion'), 403);
abort_if($user->is_admin, 403);
public function removeAccountTemporary(Request $request)
{
$user = Auth::user();
abort_if(!config('pixelfed.account_deletion'), 403);
abort_if($user->is_admin, 403);
return view('settings.remove.temporary');
}
return view('settings.remove.temporary');
}
public function removeAccountTemporarySubmit(Request $request)
{
$user = Auth::user();
abort_if(!config('pixelfed.account_deletion'), 403);
abort_if($user->is_admin, 403);
$profile = $user->profile;
$user->status = 'disabled';
$profile->status = 'disabled';
$user->save();
$profile->save();
Auth::logout();
Cache::forget('profiles:private');
return redirect('/');
}
public function removeAccountTemporarySubmit(Request $request)
{
$user = Auth::user();
abort_if(!config('pixelfed.account_deletion'), 403);
abort_if($user->is_admin, 403);
$profile = $user->profile;
$user->status = 'disabled';
$profile->status = 'disabled';
$user->save();
$profile->save();
Auth::logout();
Cache::forget('profiles:private');
return redirect('/');
}
public function removeAccountPermanent(Request $request)
{
$user = Auth::user();
abort_if($user->is_admin, 403);
return view('settings.remove.permanent');
}
public function removeAccountPermanent(Request $request)
{
$user = Auth::user();
abort_if($user->is_admin, 403);
return view('settings.remove.permanent');
}
public function removeAccountPermanentSubmit(Request $request)
{
if(config('pixelfed.account_deletion') == false) {
abort(404);
}
$user = Auth::user();
abort_if(!config('pixelfed.account_deletion'), 403);
abort_if($user->is_admin, 403);
$profile = $user->profile;
$ts = Carbon::now()->addMonth();
$user->status = 'delete';
$profile->status = 'delete';
$user->delete_after = $ts;
$profile->delete_after = $ts;
$user->save();
$profile->save();
Cache::forget('profiles:private');
Auth::logout();
DeleteAccountPipeline::dispatch($user)->onQueue('high');
return redirect('/');
}
public function removeAccountPermanentSubmit(Request $request)
{
if(config('pixelfed.account_deletion') == false) {
abort(404);
}
$user = Auth::user();
abort_if(!config('pixelfed.account_deletion'), 403);
abort_if($user->is_admin, 403);
$profile = $user->profile;
$ts = Carbon::now()->addMonth();
$user->status = 'delete';
$profile->status = 'delete';
$user->delete_after = $ts;
$profile->delete_after = $ts;
$user->save();
$profile->save();
Cache::forget('profiles:private');
Auth::logout();
DeleteAccountPipeline::dispatch($user)->onQueue('high');
return redirect('/');
}
public function requestFullExport(Request $request)
{
$user = Auth::user();
return view('settings.export.show');
}
public function requestFullExport(Request $request)
{
$user = Auth::user();
return view('settings.export.show');
}
public function reportsHome(Request $request)
{
$profile = Auth::user()->profile;
$reports = Report::whereProfileId($profile->id)->orderByDesc('created_at')->paginate(10);
return view('settings.reports', compact('reports'));
}
public function metroDarkMode(Request $request)
{
$this->validate($request, [
'mode' => 'required|string|in:light,dark'
]);
public function metroDarkMode(Request $request)
{
$this->validate($request, [
'mode' => 'required|string|in:light,dark'
]);
$mode = $request->input('mode');
$mode = $request->input('mode');
if($mode == 'dark') {
$cookie = Cookie::make('dark-mode', true, 43800);
} else {
$cookie = Cookie::forget('dark-mode');
}
if($mode == 'dark') {
$cookie = Cookie::make('dark-mode', true, 43800);
} else {
$cookie = Cookie::forget('dark-mode');
}
return response()->json([200])->cookie($cookie);
}
return response()->json([200])->cookie($cookie);
}
public function sponsor()
{
$default = [
'patreon' => null,
'liberapay' => null,
'opencollective' => null
];
$sponsors = ProfileSponsor::whereProfileId(Auth::user()->profile->id)->first();
$sponsors = $sponsors ? json_decode($sponsors->sponsors, true) : $default;
return view('settings.sponsor', compact('sponsors'));
}
public function sponsor()
{
$default = [
'patreon' => null,
'liberapay' => null,
'opencollective' => null
];
$sponsors = ProfileSponsor::whereProfileId(Auth::user()->profile->id)->first();
$sponsors = $sponsors ? json_decode($sponsors->sponsors, true) : $default;
return view('settings.sponsor', compact('sponsors'));
}
public function sponsorStore(Request $request)
{
$this->validate($request, [
'patreon' => 'nullable|string',
'liberapay' => 'nullable|string',
'opencollective' => 'nullable|string'
]);
public function sponsorStore(Request $request)
{
$this->validate($request, [
'patreon' => 'nullable|string',
'liberapay' => 'nullable|string',
'opencollective' => 'nullable|string'
]);
$patreon = Str::startsWith($request->input('patreon'), 'https://') ?
substr($request->input('patreon'), 8) :
$request->input('patreon');
$patreon = Str::startsWith($request->input('patreon'), 'https://') ?
substr($request->input('patreon'), 8) :
$request->input('patreon');
$liberapay = Str::startsWith($request->input('liberapay'), 'https://') ?
substr($request->input('liberapay'), 8) :
$request->input('liberapay');
$opencollective = Str::startsWith($request->input('opencollective'), 'https://') ?
substr($request->input('opencollective'), 8) :
$request->input('opencollective');
$liberapay = Str::startsWith($request->input('liberapay'), 'https://') ?
substr($request->input('liberapay'), 8) :
$request->input('liberapay');
$patreon = Str::startsWith($patreon, 'patreon.com/') ? e($patreon) : null;
$liberapay = Str::startsWith($liberapay, 'liberapay.com/') ? e($liberapay) : null;
$opencollective = Str::startsWith($opencollective, 'opencollective.com/') ? e($opencollective) : null;
$opencollective = Str::startsWith($request->input('opencollective'), 'https://') ?
substr($request->input('opencollective'), 8) :
$request->input('opencollective');
if(empty($patreon) && empty($liberapay) && empty($opencollective)) {
return redirect(route('settings'))->with('error', 'An error occured. Please try again later.');;
}
$patreon = Str::startsWith($patreon, 'patreon.com/') ? e($patreon) : null;
$liberapay = Str::startsWith($liberapay, 'liberapay.com/') ? e($liberapay) : null;
$opencollective = Str::startsWith($opencollective, 'opencollective.com/') ? e($opencollective) : null;
$res = [
'patreon' => $patreon,
'liberapay' => $liberapay,
'opencollective' => $opencollective
];
if(empty($patreon) && empty($liberapay) && empty($opencollective)) {
return redirect(route('settings'))->with('error', 'An error occured. Please try again later.');;
}
$sponsors = ProfileSponsor::firstOrCreate([
'profile_id' => Auth::user()->profile_id ?? Auth::user()->profile->id
]);
$sponsors->sponsors = json_encode($res);
$sponsors->save();
$sponsors = $res;
return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!');;
}
$res = [
'patreon' => $patreon,
'liberapay' => $liberapay,
'opencollective' => $opencollective
];
$sponsors = ProfileSponsor::firstOrCreate([
'profile_id' => Auth::user()->profile_id ?? Auth::user()->profile->id
]);
$sponsors->sponsors = json_encode($res);
$sponsors->save();
$sponsors = $res;
return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!');
}
public function timelineSettings(Request $request)
{
$pid = $request->user()->profile_id;
$top = Redis::zscore('pf:tl:top', $pid) != false;
$replies = Redis::zscore('pf:tl:replies', $pid) != false;
return view('settings.timeline', compact('top', 'replies'));
}
public function updateTimelineSettings(Request $request)
{
$pid = $request->user()->profile_id;
$top = $request->has('top') && $request->input('top') === 'on';
$replies = $request->has('replies') && $request->input('replies') === 'on';
if($top) {
Redis::zadd('pf:tl:top', $pid, $pid);
} else {
Redis::zrem('pf:tl:top', $pid);
}
if($replies) {
Redis::zadd('pf:tl:replies', $pid, $pid);
} else {
Redis::zrem('pf:tl:replies', $pid);
}
return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!');;
}
public function mediaSettings(Request $request)
{
$setting = UserSetting::whereUserId($request->user()->id)->firstOrFail();
$compose = $setting->compose_settings ? json_decode($setting->compose_settings, true) : [
'default_license' => null,
'media_descriptions' => false
];
return view('settings.media', compact('compose'));
}
public function updateMediaSettings(Request $request)
{
$this->validate($request, [
'default' => 'required|int|min:1|max:16',
'sync' => 'nullable',
'media_descriptions' => 'nullable'
]);
$license = $request->input('default');
$sync = $request->input('sync') == 'on';
$media_descriptions = $request->input('media_descriptions') == 'on';
$uid = $request->user()->id;
$setting = UserSetting::whereUserId($uid)->firstOrFail();
$compose = json_decode($setting->compose_settings, true);
$changed = false;
if($sync) {
$key = 'pf:settings:mls_recently:'.$uid;
if(Cache::get($key) == 2) {
$msg = 'You can only sync licenses twice per 24 hours. Try again later.';
return redirect(route('settings'))
->with('error', $msg);
}
}
if(!isset($compose['default_license']) || $compose['default_license'] !== $license) {
$compose['default_license'] = (int) $license;
$changed = true;
}
if(!isset($compose['media_descriptions']) || $compose['media_descriptions'] !== $media_descriptions) {
$compose['media_descriptions'] = $media_descriptions;
$changed = true;
}
if($changed) {
$setting->compose_settings = json_encode($compose);
$setting->save();
Cache::forget('profile:compose:settings:' . $request->user()->id);
}
if($sync) {
$val = Cache::has($key) ? 2 : 1;
Cache::put($key, $val, 86400);
MediaSyncLicensePipeline::dispatch($uid, $license);
return redirect(route('settings'))->with('status', 'Media licenses successfully synced! It may take a few minutes to take effect for every post.');
}
return redirect(route('settings'))->with('status', 'Media settings successfully updated!');
}
}

View file

@ -9,144 +9,149 @@ use App\Util\Lexer\PrettyNumber;
use App\{Follower, Page, Profile, Status, User, UserFilter};
use App\Util\Localization\Localization;
use App\Services\FollowerService;
use App\Util\ActivityPub\Helpers;
class SiteController extends Controller
{
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()
{
$data = Cache::remember('site:landing:data', now()->addHours(3), function() {
return [
'stats' => [
'posts' => App\Util\Lexer\PrettyNumber::convert(App\Status::count()),
'likes' => App\Util\Lexer\PrettyNumber::convert(App\Like::count()),
'hashtags' => App\Util\Lexer\PrettyNumber::convert(App\StatusHashtag::count())
],
];
});
return view('site.index', compact('data'));
}
public function homeGuest()
{
return view('site.index');
}
public function homeTimeline(Request $request)
{
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.home', compact('layout'));
}
public function homeTimeline(Request $request)
{
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.home', compact('layout'));
}
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()
{
$page = Page::whereSlug('/site/about')->whereActive(true)->first();
$stats = Cache::remember('site:about:stats-v1', now()->addHours(12), function() {
return [
'posts' => Status::count(),
'users' => User::whereNull('status')->count(),
'admin' => User::whereIsAdmin(true)->first()
];
});
$path = $page ? 'site.about-custom' : 'site.about';
return view($path, compact('page', 'stats'));
}
public function 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 language()
{
return view('site.language');
}
public function language()
{
return view('site.language');
}
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 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 privacy(Request $request)
{
$page = Cache::remember('site:privacy', now()->addDays(120), function() {
$slug = '/site/privacy';
$page = Page::whereSlug($slug)->whereActive(true)->first();
});
return View::make('site.privacy')->with(compact('page'))->render();
}
public function privacy(Request $request)
{
$page = Cache::remember('site:privacy', now()->addDays(120), function() {
$slug = '/site/privacy';
$page = Page::whereSlug($slug)->whereActive(true)->first();
});
return View::make('site.privacy')->with(compact('page'))->render();
}
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();
}
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();
}
public function redirectUrl(Request $request)
{
$this->validate($request, [
'url' => 'required|url'
]);
$url = request()->input('url');
return view('site.redirect', compact('url'));
}
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 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'));
}
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'));
}
public function legacyProfileRedirect(Request $request, $username)
{
$username = Str::contains($username, '@') ? '@' . $username : $username;
if(str_contains($username, '@')) {
$profile = Profile::whereUsername($username)
->firstOrFail();
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}";
}
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";
}
} else {
$profile = Profile::whereUsername($username)
->whereNull('domain')
->firstOrFail();
$url = "/$profile->username";
}
return redirect($url);
}
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);
}
}

View file

@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Cache;
use DB;
use League\CommonMark\CommonMarkConverter;
use App\Services\AccountService;
use App\Services\StatusService;
use App\Services\SnowflakeService;
use App\Util\Localization\Localization;
class SpaController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
abort_unless(config('exp.spa'), 404);
return view('layouts.spa');
}
public function webPost(Request $request, $id)
{
abort_unless(config('exp.spa'), 404);
if($request->user()) {
return view('layouts.spa');
}
if(SnowflakeService::byDate(now()->subDays(30)) > $id) {
abort(404);
}
$post = StatusService::get($id);
if(
$post &&
isset($post['url']) &&
isset($post['local']) &&
$post['local'] === true
) {
return redirect($post['url']);
}
abort(404);
}
public function webProfile(Request $request, $id)
{
abort_unless(config('exp.spa'), 404);
if($request->user()) {
if(substr($id, 0, 1) == '@') {
$id = AccountService::usernameToId(substr($id, 1));
return redirect("/i/web/profile/{$id}");
}
return view('layouts.spa');
}
$account = AccountService::get($id);
if($account && isset($account['url'])) {
return redirect($account['url']);
}
return redirect('404');
}
public function updateLanguage(Request $request)
{
$this->validate($request, [
'v' => 'required|in:0.1,0.2',
'l' => 'required|alpha_dash|max:5'
]);
$lang = $request->input('l');
$user = $request->user();
abort_if(!in_array($lang, Localization::languages()), 400);
$user->language = $lang;
$user->save();
session()->put('locale', $lang);
return ['language' => $lang];
}
public function getPrivacy()
{
$body = $this->markdownToHtml('views/page/privacy.md');
return [
'body' => $body
];
}
public function getTerms()
{
$body = $this->markdownToHtml('views/page/terms.md');
return [
'body' => $body
];
}
protected function markdownToHtml($src, $ttl = 600)
{
return Cache::remember(
'pf:doc_cache:markdown:' . $src,
$ttl,
function() use($src) {
$path = resource_path($src);
$file = file_get_contents($path);
$converter = new CommonMarkConverter();
return (string) $converter->convertToHtml($file);
});
}
public function usernameRedirect(Request $request, $username)
{
$id = AccountService::usernameToId($username);
if(!$id) {
return redirect('/i/web/404');
}
return redirect('/i/web/profile/' . $id);
}
}

View file

@ -6,406 +6,414 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\SharePipeline\SharePipeline;
use App\Jobs\SharePipeline\UndoSharePipeline;
use App\AccountInterstitial;
use App\Media;
use App\Profile;
use App\Status;
use App\StatusArchived;
use App\StatusView;
use App\Transformer\ActivityPub\StatusTransformer;
use App\Transformer\ActivityPub\Verb\Note;
use App\Transformer\ActivityPub\Verb\Question;
use App\User;
use Auth, Cache;
use Auth, DB, Cache;
use Illuminate\Http\Request;
use League\Fractal;
use App\Util\Media\Filter;
use Illuminate\Support\Str;
use App\Services\HashidService;
use App\Services\StatusService;
use App\Util\Media\License;
use App\Services\ReblogService;
class StatusController extends Controller
{
public function show(Request $request, $username, int $id)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
public function show(Request $request, $username, $id)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($user->status != null) {
return ProfileController::accountCheck($user);
}
if($user->status != null) {
return ProfileController::accountCheck($user);
}
$status = Status::whereProfileId($user->id)
->whereNull('reblog_of_id')
->whereIn('scope', ['public','unlisted', 'private'])
->findOrFail($id);
$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->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->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($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->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('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $status);
}
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'));
}
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
return view($template, compact('user', 'status'));
}
public function shortcodeRedirect(Request $request, $id)
{
abort_if(strlen($id) < 5, 404);
if(!Auth::check()) {
return redirect('/login?next='.urlencode('/' . $request->path()));
}
$id = HashidService::decode($id);
$status = Status::find($id);
if(!$status) {
return redirect('/404');
}
return redirect($status->url());
}
public function 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 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)
{
$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');
}
$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 showEmbed(Request $request, $username, int $id)
{
$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');
}
$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();
public function showObject(Request $request, $username, int $id)
{
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($user->status != null) {
return ProfileController::accountCheck($user);
}
if($user->status != null) {
return ProfileController::accountCheck($user);
}
$status = Status::whereProfileId($user->id)
->whereNotIn('visibility',['draft','direct'])
->findOrFail($id);
$status = Status::whereProfileId($user->id)
->whereNotIn('visibility',['draft','direct'])
->findOrFail($id);
abort_if($status->uri, 404);
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);
}
}
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);
}
return $this->showActivityPub($request, $status);
}
public function compose()
{
$this->authCheck();
public function compose()
{
$this->authCheck();
return view('status.compose');
}
return view('status.compose');
}
public function store(Request $request)
{
return;
}
public function store(Request $request)
{
return;
}
public function delete(Request $request)
{
$this->authCheck();
public function delete(Request $request)
{
$this->authCheck();
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$status = Status::findOrFail($request->input('item'));
$status = Status::findOrFail($request->input('item'));
$user = Auth::user();
$user = Auth::user();
if($status->profile_id != $user->profile->id &&
$user->is_admin == true &&
$status->uri == null
) {
$media = $status->media;
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();
$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();
}
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
Cache::forget('profile:status_count:' . $status->profile_id);
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
Cache::forget('profile:status_count:'.$status->profile_id);
StatusDelete::dispatch($status);
}
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
Cache::forget('profile:status_count:' . $status->profile_id);
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
Cache::forget('profile:status_count:'.$status->profile_id);
StatusDelete::dispatch($status);
}
if($request->wantsJson()) {
return response()->json(['Status successfully deleted.']);
} else {
return redirect($user->url());
}
}
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',
]);
public function storeShare(Request $request)
{
$this->authCheck();
$user = Auth::user();
$profile = $user->profile;
$status = Status::withCount('shares')
->whereIn('scope', ['public', 'unlisted'])
->findOrFail($request->input('item'));
$this->validate($request, [
'item' => 'required|integer|min:1',
]);
$count = $status->shares()->count();
$user = Auth::user();
$profile = $user->profile;
$status = Status::whereScope('public')
->findOrFail($request->input('item'));
$exists = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->count();
if ($exists !== 0) {
$shares = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->get();
foreach ($shares as $share) {
$share->delete();
$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->save();
$count++;
SharePipeline::dispatch($share);
}
if($count >= 0) {
$status->reblogs_count = $count;
$status->save();
}
Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
$count = $status->reblogs_count;
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
$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->save();
$count++;
SharePipeline::dispatch($share);
ReblogService::add($profile->id, $status->id);
}
return $response;
}
Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
StatusService::del($status->id);
public function showActivityPub(Request $request, $status)
{
$fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($status, new Note());
$res = $fractal->createData($resource)->toArray();
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT);
}
return $response;
}
public function edit(Request $request, $username, $id)
{
$this->authCheck();
$user = Auth::user()->profile;
$status = Status::whereProfileId($user->id)
->with(['media'])
->where('created_at', '>', now()->subHours(24))
->findOrFail($id);
return view('status.edit', compact('user', 'status'));
}
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();
public function editStore(Request $request, $username, $id)
{
$this->authCheck();
$user = Auth::user()->profile;
$status = Status::whereProfileId($user->id)
->with(['media'])
->where('created_at', '>', now()->subHours(24))
->findOrFail($id);
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
$this->validate($request, [
'id' => 'required|integer|min:1',
'caption' => 'nullable',
'filter' => 'nullable|alpha_dash|max:30',
]);
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'));
}
$id = $request->input('id');
$caption = $request->input('caption');
$filter = $request->input('filter');
public function editStore(Request $request, $username, $id)
{
$this->authCheck();
$user = Auth::user()->profile;
$status = Status::whereProfileId($user->id)
->with(['media'])
->findOrFail($id);
$media = Media::whereProfileId($user->id)
->whereStatusId($status->id)
->findOrFail($id);
$this->validate($request, [
'license' => 'nullable|integer|min:1|max:16',
]);
$changed = false;
$licenseId = $request->input('license');
if ($media->caption != $caption) {
$media->caption = $caption;
$changed = true;
}
$status->media->each(function($media) use($licenseId) {
$media->license = $licenseId;
$media->save();
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
});
if ($media->filter_class != $filter && in_array($filter, Filter::classes())) {
$media->filter_class = $filter;
$changed = true;
}
return redirect($status->url());
}
if ($changed === true) {
$media->save();
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
}
protected function authCheck()
{
if (Auth::check() == false) {
abort(403);
}
}
return response()->json([], 200);
}
protected function validateVisibility($visibility)
{
$allowed = ['public', 'unlisted', 'private'];
return in_array($visibility, $allowed) ? $visibility : 'public';
}
protected function authCheck()
{
if (Auth::check() == false) {
abort(403);
}
}
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';
}
protected function validateVisibility($visibility)
{
$allowed = ['public', 'unlisted', 'private'];
return in_array($visibility, $allowed) ? $visibility : 'public';
}
return 'text';
}
public static function mimeTypeCheck($mimes)
{
$allowed = explode(',', config('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';
}
}
public function toggleVisibility(Request $request) {
$this->authCheck();
$this->validate($request, [
'item' => 'required|string|min:1|max:20',
'disableComments' => 'required|boolean'
]);
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');
$user = Auth::user();
$id = $request->input('item');
$state = $request->input('disableComments');
$status = Status::findOrFail($id);
$status = Status::findOrFail($id);
if($status->profile_id != $user->profile->id && $user->is_admin == false) {
abort(403);
}
if($status->profile_id != $user->profile->id && $user->is_admin == false) {
abort(403);
}
$status->comments_disabled = $status->comments_disabled == true ? false : true;
$status->save();
$status->comments_disabled = $status->comments_disabled == true ? false : true;
$status->save();
return response()->json([200]);
}
return response()->json([200]);
}
public function storeView(Request $request)
{
abort_if(!$request->user(), 403);
$views = $request->input('_v');
$uid = $request->user()->profile_id;
if(empty($views) || !is_array($views)) {
return response()->json(0);
}
Cache::forget('profile:home-timeline-cursor:' . $request->user()->id);
foreach($views as $view) {
if(!isset($view['sid']) || !isset($view['pid'])) {
continue;
}
DB::transaction(function () use($view, $uid) {
StatusView::firstOrCreate([
'status_id' => $view['sid'],
'status_profile_id' => $view['pid'],
'profile_id' => $uid
]);
});
}
return response()->json(1);
}
}

View file

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

View file

@ -4,248 +4,128 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\DirectMessage;
use App\Follower;
use App\Notification;
use App\Media;
use App\Profile;
use App\Status;
use App\Story;
use App\StoryView;
use App\Services\PollService;
use App\Services\ProfileService;
use App\Services\StoryService;
use Cache, Storage;
use Image as Intervention;
use App\Services\AccountService;
use App\Services\FollowerService;
use App\Services\MediaPathService;
use FFMpeg;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\Format\Video\X264;
use League\Fractal\Manager;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Resource\Item;
use App\Transformer\ActivityPub\Verb\StoryVerb;
use App\Jobs\StoryPipeline\StoryViewDeliver;
class StoryController extends Controller
class StoryController extends StoryComposeController
{
public function apiV1Add(Request $request)
public function recent(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'file' => function() {
return [
'required',
'mimes:image/jpeg,image/png,video/mp4',
'max:' . config('pixelfed.max_photo_size'),
];
},
]);
$user = $request->user();
if(Story::whereProfileId($user->profile_id)->where('expires_at', '>', now())->count() >= Story::MAX_PER_DAY) {
abort(400, 'You have reached your limit for new Stories today.');
}
$photo = $request->file('file');
$path = $this->storePhoto($photo);
$story = new Story();
$story->duration = 3;
$story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
$story->mime = $photo->getMimeType();
$story->path = $path;
$story->local = true;
$story->size = $photo->getSize();
$story->expires_at = now()->addHours(24);
$story->save();
return [
'code' => 200,
'msg' => 'Successfully added',
'media_url' => url(Storage::url($story->path))
];
}
protected function storePhoto($photo)
{
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
$sid = (string) Str::uuid();
$rid = Str::random(9).'.'.Str::random(9);
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [
'image/jpeg',
'image/png',
'video/mp4'
]) == false) {
abort(400, 'Invalid media type');
return;
}
$storagePath = "public/_esm.t2/{$monthHash}/{$sid}/{$rid}";
$path = $photo->store($storagePath);
if(in_array($photo->getMimeType(), ['image/jpeg','image/png',])) {
$fpath = storage_path('app/' . $path);
$img = Intervention::make($fpath);
$img->orientate();
$img->save($fpath, config('pixelfed.image_quality'));
$img->destroy();
}
return $path;
}
public function apiV1Delete(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
$story->delete();
return [
'code' => 200,
'msg' => 'Successfully deleted'
];
}
public function apiV1Recent(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$profile = $request->user()->profile;
$following = $profile->following->pluck('id')->toArray();
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$pid = $request->user()->profile_id;
if(config('database.default') == 'pgsql') {
$db = Story::with('profile')
->whereIn('profile_id', $following)
->where('expires_at', '>', now())
->distinct('profile_id')
->take(9)
->get();
$s = Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->get()
->map(function($s) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $s->profile_id;
$r->type = $s->type;
$r->path = $s->path;
return $r;
})
->unique('profile_id');
} else {
$db = Story::with('profile')
->whereIn('profile_id', $following)
->where('expires_at', '>', now())
->orderByDesc('expires_at')
->groupBy('profile_id')
->take(9)
->get();
$s = Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->groupBy('followers.following_id')
->orderByDesc('id')
->get();
}
$stories = $db->map(function($s, $k) {
$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 [
'id' => (string) $s->id,
'photo' => $s->profile->avatarUrl(),
'name' => $s->profile->username,
'link' => $s->profile->url(),
'lastUpdated' => (int) $s->created_at->format('U'),
'seen' => $s->seen(),
'items' => [],
'pid' => (string) $s->profile->id
'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
];
});
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function apiV1Fetch(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile;
$profile = Profile::findOrFail($id);
if($id == $authed->id) {
$publicOnly = true;
} else {
$publicOnly = (bool) $profile->followedBy($authed);
}
$stories = Story::whereProfileId($profile->id)
->orderBy('expires_at', 'desc')
->where('expires_at', '>', now())
->when(!$publicOnly, function($query, $publicOnly) {
return $query->wherePublic(true);
})
->get()
->map(function($s, $k) {
return [
'id' => (string) $s->id,
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
'length' => 3,
'src' => url(Storage::url($s->path)),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $s->created_at->format('U'),
'expires_at' => (int) $s->expires_at->format('U'),
'seen' => $s->seen()
];
})->toArray();
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function apiV1Item(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile;
$story = Story::with('profile')
->where('expires_at', '>', now())
->findOrFail($id);
$profile = $story->profile;
if($story->profile_id == $authed->id) {
$publicOnly = true;
} else {
$publicOnly = (bool) $profile->followedBy($authed);
}
abort_if(!$publicOnly, 403);
$res = [
'id' => (string) $story->id,
'type' => Str::endsWith($story->path, '.mp4') ? 'video' :'photo',
'length' => 3,
'src' => url(Storage::url($story->path)),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $story->created_at->format('U'),
'expires_at' => (int) $story->expires_at->format('U'),
'seen' => $story->seen()
];
->sortBy('seen')
->values();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function apiV1Profile(Request $request, $id)
public function profile(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile;
$authed = $request->user()->profile_id;
$profile = Profile::findOrFail($id);
if($id == $authed->id) {
$publicOnly = true;
} else {
$publicOnly = (bool) $profile->followedBy($authed);
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
return [];
}
$stories = Story::whereProfileId($profile->id)
->whereActive(true)
->orderBy('expires_at')
->where('expires_at', '>', now())
->when(!$publicOnly, function($query, $publicOnly) {
return $query->wherePublic(true);
})
->get()
->map(function($s, $k) {
return [
'id' => $s->id,
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
'length' => 3,
->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)),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $s->created_at->format('U'),
'expires_at' => (int) $s->expires_at->format('U'),
'seen' => $s->seen()
'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);
}
}
return $res;
})->toArray();
if(count($stories) == 0) {
return [];
@ -253,110 +133,159 @@ class StoryController extends Controller
$cursor = count($stories) - 1;
$stories = [[
'id' => (string) $stories[$cursor]['id'],
'photo' => $profile->avatarUrl(),
'name' => $profile->username,
'link' => $profile->url(),
'lastUpdated' => (int) now()->format('U'),
'seen' => null,
'items' => $stories,
'nodes' => $stories,
'account' => AccountService::get($profile->id),
'pid' => (string) $profile->id
]];
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function apiV1Viewed(Request $request)
public function viewed(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:stories',
'id' => 'required|min:1',
]);
$id = $request->input('id');
$authed = $request->user()->profile;
$story = Story::with('profile')
->where('expires_at', '>', now())
->orderByDesc('expires_at')
->findOrFail($id);
$exp = $story->expires_at;
$profile = $story->profile;
if($story->profile_id == $authed->id) {
$publicOnly = true;
} else {
$publicOnly = (bool) $profile->followedBy($authed);
return [];
}
$publicOnly = (bool) $profile->followedBy($authed);
abort_if(!$publicOnly, 403);
StoryView::firstOrCreate([
$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 apiV1Exists(Request $request, $id)
public function exists(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$res = (bool) Story::whereProfileId($id)
->where('expires_at', '>', now())
->count();
return response()->json($res);
}
public function apiV1Me(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$profile = $request->user()->profile;
$stories = Story::whereProfileId($profile->id)
->orderBy('expires_at')
->where('expires_at', '>', now())
->get()
->map(function($s, $k) {
return [
'id' => $s->id,
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
'length' => 3,
'src' => url(Storage::url($s->path)),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $s->created_at->format('U'),
'expires_at' => (int) $s->expires_at->format('U'),
'seen' => true
];
})->toArray();
$ts = count($stories) ? last($stories)['time'] : null;
$res = [
'id' => (string) $profile->id,
'photo' => $profile->avatarUrl(),
'name' => $profile->username,
'link' => $profile->url(),
'lastUpdated' => $ts,
'seen' => true,
'items' => $stories
];
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function compose(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
return view('stories.compose');
return response()->json(Story::whereProfileId($id)
->whereActive(true)
->exists());
}
public function iRedirect(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
abort_if(!$user, 404);
$username = $user->username;
return redirect("/stories/{$username}");
}
public function viewers(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required|string'
]);
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
$viewers = StoryView::whereStoryId($story->id)
->latest()
->simplePaginate(10)
->map(function($view) {
return AccountService::get($view->profile_id);
})
->values();
return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function remoteStory(Request $request, $id)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::findOrFail($id);
if($profile->user_id != null || $profile->domain == null) {
return redirect('/stories/' . $profile->username);
}
$pid = $profile->id;
return view('stories.show_remote', compact('pid'));
}
public function pollResults(Request $request)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required|string'
]);
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
return PollService::storyResults($sid);
}
public function getActivityObject(Request $request, $username, $id)
{
abort_if(!config_cache('instance.stories.enabled'), 404);
if(!$request->wantsJson()) {
return redirect('/stories/' . $username);
}
abort_if(!$request->hasHeader('Authorization'), 404);
$profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail();
$story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id);
abort_if($story->bearcap_token == null, 404);
abort_if(now()->gt($story->expires_at), 404);
$token = substr($request->header('Authorization'), 7);
abort_if(hash_equals($story->bearcap_token, $token) === false, 404);
abort_if($story->created_at->lt(now()->subMinutes(20)), 404);
$fractal = new Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Item($story, new StoryVerb());
$res = $fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function showSystemStory()
{
// return view('stories.system');
}
}

View file

@ -2,37 +2,32 @@
namespace App\Http\Controllers;
use Auth, Cache;
use App\Follower;
use App\Profile;
use App\Status;
use App\User;
use App\UserFilter;
use Illuminate\Http\Request;
class TimelineController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('twofactor');
}
public function __construct()
{
$this->middleware('auth');
$this->middleware('twofactor');
}
public function local(Request $request)
{
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.local', compact('layout'));
}
public function local(Request $request)
{
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.local', compact('layout'));
}
public function network(Request $request)
{
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.network', compact('layout'));
}
public function network(Request $request)
{
abort_if(config('federation.network_timeline') == false, 404);
$this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.network', compact('layout'));
}
}

View file

@ -9,19 +9,16 @@ use Illuminate\Support\Str;
class UserInviteController extends Controller
{
public function __construct()
{
abort_if(!config('pixelfed.user_invites.enabled'), 404);
}
public function create(Request $request)
{
abort_if(!config('pixelfed.user_invites.enabled'), 404);
abort_unless(Auth::check(), 403);
return view('settings.invites.create');
}
public function show(Request $request)
{
abort_if(!config('pixelfed.user_invites.enabled'), 404);
abort_unless(Auth::check(), 403);
$invites = UserInvite::whereUserId(Auth::id())->paginate(10);
$limit = config('pixelfed.user_invites.limit.total');
@ -31,6 +28,7 @@ class UserInviteController extends Controller
public function store(Request $request)
{
abort_if(!config('pixelfed.user_invites.enabled'), 404);
abort_unless(Auth::check(), 403);
$this->validate($request, [
'email' => 'required|email|unique:users|unique:user_invites',

View file

@ -32,10 +32,11 @@ class Kernel extends HttpKernel
\App\Http\Middleware\FrameGuard::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
// 'restricted',
],

View file

@ -6,31 +6,32 @@ use Closure;
class EmailVerificationCheck
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->user() &&
config('pixelfed.enforce_email_verification') &&
is_null($request->user()->email_verified_at) &&
!$request->is(
'i/auth/*',
'i/verify-email',
'log*',
'i/confirm-email/*',
'settings/home',
'settings/email'
)
) {
return redirect('/i/verify-email');
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->user() &&
config_cache('pixelfed.enforce_email_verification') &&
is_null($request->user()->email_verified_at) &&
!$request->is(
'i/auth/*',
'i/verify-email*',
'log*',
'site*',
'i/confirm-email/*',
'settings/home',
'settings/email'
)
) {
return redirect('/i/verify-email');
}
return $next($request);
}
return $next($request);
}
}

View file

@ -16,68 +16,67 @@ use Image as Intervention;
class AvatarOptimize implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile;
protected $current;
protected $profile;
protected $current;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Profile $profile, $current)
{
$this->profile = $profile;
$this->current = $current;
}
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Profile $profile, $current)
{
$this->profile = $profile;
$this->current = $current;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$avatar = $this->profile->avatar;
$file = storage_path("app/$avatar->media_path");
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$avatar = $this->profile->avatar;
$file = storage_path("app/$avatar->media_path");
try {
$img = Intervention::make($file)->orientate();
$img->fit(200, 200, function ($constraint) {
$constraint->upsize();
});
$quality = config('pixelfed.image_quality');
$img->save($file, $quality);
try {
$img = Intervention::make($file)->orientate();
$img->fit(200, 200, function ($constraint) {
$constraint->upsize();
});
$quality = config_cache('pixelfed.image_quality');
$img->save($file, $quality);
$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
$avatar->thumb_path = $avatar->media_path;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = Carbon::now();
$avatar->save();
Cache::forget('avatar:' . $avatar->profile_id);
$this->deleteOldAvatar($avatar->media_path, $this->current);
} catch (Exception $e) {
}
}
$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = Carbon::now();
$avatar->save();
Cache::forget('avatar:' . $avatar->profile_id);
$this->deleteOldAvatar($avatar->media_path, $this->current);
} catch (Exception $e) {
}
}
protected function deleteOldAvatar($new, $current)
{
if ( storage_path('app/'.$new) == $current ||
Str::endsWith($current, 'avatars/default.png') ||
Str::endsWith($current, 'avatars/default.jpg'))
{
return;
}
if (is_file($current)) {
@unlink($current);
}
}
protected function deleteOldAvatar($new, $current)
{
if ( storage_path('app/'.$new) == $current ||
Str::endsWith($current, 'avatars/default.png') ||
Str::endsWith($current, 'avatars/default.jpg'))
{
return;
}
if (is_file($current)) {
@unlink($current);
}
}
}

View file

@ -45,7 +45,6 @@ class CreateAvatar implements ShouldQueue
$avatar = new Avatar();
$avatar->profile_id = $profile->id;
$avatar->media_path = $path;
$avatar->thumb_path = $path;
$avatar->change_count = 0;
$avatar->last_processed_at = \Carbon\Carbon::now();
$avatar->save();

View file

@ -0,0 +1,113 @@
<?php
namespace App\Jobs\AvatarPipeline;
use App\Avatar;
use App\Profile;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use Zttp\Zttp;
use App\Http\Controllers\AvatarController;
use Storage;
use Log;
use Illuminate\Http\File;
use App\Services\MediaStorageService;
use App\Services\ActivityPubFetchService;
class RemoteAvatarFetch implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 1;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Profile $profile)
{
$this->profile = $profile;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->profile;
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
return 1;
}
if($profile->domain == null || $profile->private_key) {
return 1;
}
$avatar = Avatar::whereProfileId($profile->id)->first();
if(!$avatar) {
$avatar = new Avatar;
$avatar->profile_id = $profile->id;
$avatar->save();
}
if($avatar->media_path == null && $avatar->remote_url == null) {
$avatar->media_path = 'public/avatars/default.jpg';
$avatar->is_remote = true;
$avatar->save();
}
$person = Helpers::fetchFromUrl($profile->remote_url);
if(!$person || !isset($person['@context'])) {
return 1;
}
if( !isset($person['icon']) ||
!isset($person['icon']['type']) ||
!isset($person['icon']['url'])
) {
return 1;
}
if($person['icon']['type'] !== 'Image') {
return 1;
}
if(!Helpers::validateUrl($person['icon']['url'])) {
return 1;
}
$icon = $person['icon'];
$avatar->remote_url = $icon['url'];
$avatar->save();
MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false);
return 1;
}
}

View file

@ -8,6 +8,7 @@ use App\{
UserFilter
};
use App\Services\NotificationService;
use App\Services\StatusService;
use DB, Cache, Log;
use Illuminate\Support\Facades\Redis;
@ -58,6 +59,11 @@ class CommentPipeline implements ShouldQueue
$target = $status->profile;
$actor = $comment->profile;
DB::transaction(function() use($status) {
$status->reply_count = DB::table('statuses')->whereInReplyToId($status->id)->count();
$status->save();
});
if ($actor->id === $target->id || $status->comments_disabled == true) {
return true;
}
@ -85,6 +91,16 @@ class CommentPipeline implements ShouldQueue
NotificationService::setNotification($notification);
NotificationService::set($notification->profile_id, $notification->id);
StatusService::del($comment->id);
});
if($exists = Cache::get('status:replies:all:' . $status->id)) {
if($exists && $exists->count() == 3) {
} else {
Cache::forget('status:replies:all:' . $status->id);
}
} else {
Cache::forget('status:replies:all:' . $status->id);
}
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace App\Jobs\DeletePipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Cache;
use DB;
use Illuminate\Support\Str;
use App\Profile;
use App\Util\ActivityPub\Helpers;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use App\Util\ActivityPub\HttpSignature;
class FanoutDeletePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile;
public $timeout = 300;
public $tries = 1;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($profile)
{
$this->profile = $profile;
}
public function handle()
{
$profile = $this->profile;
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$audience = Cache::remember('pf:ap:known_instances', now()->addHours(6), function() {
return Profile::whereNotNull('sharedInbox')->groupBy('sharedInbox')->pluck('sharedInbox')->toArray();
});
$activity = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => $profile->permalink('#delete'),
"type" => "Delete",
"actor" => $profile->permalink(),
"to" => [
"https://www.w3.org/ns/activitystreams#Public",
],
"object" => $profile->permalink(),
];
$payload = json_encode($activity);
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
return 1;
}
}

View file

@ -14,59 +14,62 @@ use Illuminate\Support\Facades\Redis;
class FollowPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $follower;
protected $follower;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($follower)
{
$this->follower = $follower;
}
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($follower)
{
$this->follower = $follower;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$follower = $this->follower;
$actor = $follower->actor;
$target = $follower->target;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$follower = $this->follower;
$actor = $follower->actor;
$target = $follower->target;
if($target->domain || !$target->private_key) {
return;
}
Cache::forget('profile:following:' . $actor->id);
Cache::forget('profile:following:' . $target->id);
try {
$notification = new Notification();
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'follow';
$notification->message = $follower->toText();
$notification->rendered = $follower->toHtml();
$notification->item_id = $target->id;
$notification->item_type = "App\Profile";
$notification->save();
if($target->domain || !$target->private_key) {
return;
}
$redis = Redis::connection();
try {
$notification = new Notification();
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'follow';
$notification->message = $follower->toText();
$notification->rendered = $follower->toHtml();
$notification->item_id = $target->id;
$notification->item_type = "App\Profile";
$notification->save();
$nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
$redis->lpush($nkey, $notification->id);
} catch (Exception $e) {
Log::error($e);
}
}
$redis = Redis::connection();
$nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
$redis->lpush($nkey, $notification->id);
} catch (Exception $e) {
Log::error($e);
}
}
}

View file

@ -41,7 +41,7 @@ class ImageOptimize implements ShouldQueue
{
$media = $this->media;
$path = storage_path('app/'.$media->media_path);
if (!is_file($path)) {
if (!is_file($path) || $media->skip_optimize) {
return;
}

View file

@ -45,7 +45,7 @@ class ImageResize implements ShouldQueue
return;
}
$path = storage_path('app/'.$media->media_path);
if (!is_file($path)) {
if (!is_file($path) || $media->skip_optimize) {
return;
}

View file

@ -11,81 +11,73 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use ImageOptimizer;
use Illuminate\Http\File;
use App\Services\MediaPathService;
use App\Jobs\MediaPipeline\MediaStoragePipeline;
class ImageUpdate implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media;
protected $media;
protected $protectedMimes = [
'image/jpeg',
'image/png',
];
protected $protectedMimes = [
'image/jpeg',
'image/png',
'image/webp'
];
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Media $media)
{
$this->media = $media;
}
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$media = $this->media;
if(!$media) {
return;
}
$path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path);
if (!is_file($path)) {
return;
}
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Media $media)
{
$this->media = $media;
}
if (in_array($media->mime, $this->protectedMimes) == true) {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$media = $this->media;
if(!$media) {
return;
}
$path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path);
if (!is_file($path) || !is_file($thumb)) {
return;
}
if (!is_file($path)) {
return;
}
$photo_size = filesize($path);
$thumb_size = filesize($thumb);
$total = ($photo_size + $thumb_size);
$media->size = $total;
$media->save();
if (in_array($media->mime, $this->protectedMimes) == true) {
ImageOptimizer::optimize($thumb);
if(!$media->skip_optimize) {
ImageOptimizer::optimize($path);
}
}
if(config('pixelfed.cloud_storage') == true) {
$p = explode('/', $media->media_path);
$monthHash = $p[2];
$userHash = $p[3];
$storagePath = "public/m/{$monthHash}/{$userHash}";
$file = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($path), 'public');
$url = Storage::disk(config('filesystems.cloud'))->url($file);
$thumbFile = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($thumb), 'public');
$thumbUrl = Storage::disk(config('filesystems.cloud'))->url($thumbFile);
$media->thumbnail_url = $thumbUrl;
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->save();
}
}
if (!is_file($path) || !is_file($thumb)) {
return;
}
$photo_size = filesize($path);
$thumb_size = filesize($thumb);
$total = ($photo_size + $thumb_size);
$media->size = $total;
$media->save();
MediaStoragePipeline::dispatch($media);
}
}

View file

@ -12,116 +12,120 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\{
ImportJob,
ImportData,
Media,
Profile,
Status,
ImportJob,
ImportData,
Media,
Profile,
Status,
};
class ImportInstagram implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $import;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(ImportJob $import)
{
$this->import = $import;
}
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if(config('pixelfed.import.instagram.enabled') != true) {
return;
}
protected $import;
$job = ImportJob::findOrFail($this->import->id);
$profile = Profile::findOrFail($job->profile_id);
$user = $profile->user;
$json = $job->mediaJson();
$collection = array_reverse($json['photos']);
$files = $job->files;
$monthHash = hash('sha1', date('Y').date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);
$fs = new Filesystem;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
foreach($collection as $import)
{
$caption = $import['caption'];
try {
$min = Carbon::create(2010, 10, 6, 0, 0, 0);
$taken_at = Carbon::parse($import['taken_at']);
if(!$min->lt($taken_at)) {
$taken_at = Carbon::now();
}
} catch (Exception $e) {
}
$filename = last( explode('/', $import['path']) );
$importData = ImportData::whereJobId($job->id)
->whereOriginalName($filename)
->first();
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(ImportJob $import)
{
$this->import = $import;
}
if(empty($importData) || is_file(storage_path("app/$importData->path")) == false) {
continue;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if(config_cache('pixelfed.import.instagram.enabled') != true) {
return;
}
DB::transaction(function() use(
$fs, $job, $profile, $caption, $taken_at, $filename,
$monthHash, $userHash, $importData
) {
$status = new Status();
$status->profile_id = $profile->id;
$status->caption = strip_tags($caption);
$status->is_nsfw = false;
$status->type = 'photo';
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->created_at = $taken_at;
$status->save();
$job = ImportJob::findOrFail($this->import->id);
$profile = Profile::findOrFail($job->profile_id);
$user = $profile->user;
$json = $job->mediaJson();
$collection = array_reverse($json['photos']);
$files = $job->files;
$monthHash = hash('sha1', date('Y').date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);
$fs = new Filesystem;
foreach($collection as $import)
{
$caption = $import['caption'];
try {
$min = Carbon::create(2010, 10, 6, 0, 0, 0);
$taken_at = Carbon::parse($import['taken_at']);
if(!$min->lt($taken_at)) {
$taken_at = Carbon::now();
}
} catch (Exception $e) {
}
$filename = last( explode('/', $import['path']) );
$importData = ImportData::whereJobId($job->id)
->whereOriginalName($filename)
->first();
if(empty($importData) || is_file(storage_path("app/$importData->path")) == false) {
continue;
}
DB::transaction(function() use(
$fs, $job, $profile, $caption, $taken_at, $filename,
$monthHash, $userHash, $importData
) {
$status = new Status();
$status->profile_id = $profile->id;
$status->caption = strip_tags($caption);
$status->is_nsfw = false;
$status->type = 'photo';
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->created_at = $taken_at;
$status->save();
$path = storage_path("app/$importData->path");
$storagePath = "public/m/{$monthHash}/{$userHash}";
$newPath = "app/$storagePath/$filename";
$fs->move($path,storage_path($newPath));
$path = $newPath;
$hash = \hash_file('sha256', storage_path($path));
$media = new Media();
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$media->user_id = $profile->user->id;
$media->media_path = "$storagePath/$filename";
$media->original_sha256 = $hash;
$media->size = $fs->size(storage_path($path));
$media->mime = $fs->mimeType(storage_path($path));
$media->filter_class = null;
$media->filter_name = null;
$media->order = 1;
$media->save();
ImageOptimize::dispatch($media);
});
}
$path = storage_path("app/$importData->path");
$storagePath = "public/m/{$monthHash}/{$userHash}";
$dir = "app/$storagePath";
if(!is_dir(storage_path($dir))) {
mkdir(storage_path($dir), 0755, true);
}
$newPath = "$dir/$filename";
$fs->move($path,storage_path($newPath));
$path = $newPath;
$hash = \hash_file('sha256', storage_path($path));
$media = new Media();
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$media->user_id = $profile->user->id;
$media->media_path = "$storagePath/$filename";
$media->original_sha256 = $hash;
$media->size = $fs->size(storage_path($path));
$media->mime = $fs->mimeType(storage_path($path));
$media->filter_class = null;
$media->filter_name = null;
$media->order = 1;
$media->save();
ImageOptimize::dispatch($media);
});
}
$job->completed_at = Carbon::now();
$job->save();
}
$job->completed_at = Carbon::now();
$job->save();
}
}

View file

@ -0,0 +1,223 @@
<?php
namespace App\Jobs\InboxPipeline;
use Cache;
use App\Profile;
use App\Util\ActivityPub\{
Helpers,
HttpSignature,
Inbox
};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Zttp\Zttp;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
class DeleteWorker implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $headers;
protected $payload;
public $timeout = 60;
public $tries = 1;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($headers, $payload)
{
$this->headers = $headers;
$this->payload = $payload;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = null;
$headers = $this->headers;
$payload = json_decode($this->payload, true, 8);
if(isset($payload['id'])) {
$lockKey = 'pf:ap:del-lock:' . hash('sha256', $payload['id']);
if(Cache::get($lockKey) !== null) {
// Job processed already
return 1;
}
Cache::put($lockKey, 1, 300);
}
if(!isset($headers['signature']) || !isset($headers['date'])) {
return;
}
if(empty($headers) || empty($payload)) {
return;
}
if( $payload['type'] === 'Delete' &&
( ( is_string($payload['object']) &&
$payload['object'] === $payload['actor'] ) ||
( is_array($payload['object']) &&
isset($payload['object']['id'], $payload['object']['type']) &&
$payload['object']['type'] === 'Person' &&
$payload['actor'] === $payload['object']['id']
))
) {
$actor = $payload['actor'];
$hash = strlen($actor) <= 48 ?
'b:' . base64_encode($actor) :
'h:' . hash('sha256', $actor);
$lockKey = 'ap:inbox:actor-delete-exists:lock:' . $hash;
Cache::lock($lockKey, 10)->block(5, function () use(
$headers,
$payload,
$actor,
$hash
) {
$key = 'ap:inbox:actor-delete-exists:' . $hash;
$actorDelete = Cache::remember($key, now()->addMinutes(15), function() use($actor) {
return Profile::whereRemoteUrl($actor)
->whereNotNull('domain')
->exists();
});
if($actorDelete) {
if($this->verifySignature($headers, $payload) == true) {
Cache::set($key, false);
$profile = Profile::whereNotNull('domain')
->whereNull('status')
->whereRemoteUrl($actor)
->first();
if($profile) {
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('delete');
}
return;
} else {
// Signature verification failed, exit.
return;
}
} else {
// Remote user doesn't exist, exit early.
return;
}
});
return;
}
$profile = null;
if($this->verifySignature($headers, $payload) == true) {
(new Inbox($headers, $profile, $payload))->handle();
return;
} else if($this->blindKeyRotation($headers, $payload) == true) {
(new Inbox($headers, $profile, $payload))->handle();
return;
} else {
return;
}
}
protected function verifySignature($headers, $payload)
{
$body = $this->payload;
$bodyDecoded = $payload;
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
if(!$signature) {
return;
}
if(!$date) {
return;
}
if(!now()->parse($date)->gt(now()->subDays(1)) ||
!now()->parse($date)->lt(now()->addDays(1))
) {
return;
}
$signatureData = HttpSignature::parseSignatureHeader($signature);
$keyId = Helpers::validateUrl($signatureData['keyId']);
$id = Helpers::validateUrl($bodyDecoded['id']);
$keyDomain = parse_url($keyId, PHP_URL_HOST);
$idDomain = parse_url($id, PHP_URL_HOST);
if(isset($bodyDecoded['object'])
&& is_array($bodyDecoded['object'])
&& isset($bodyDecoded['object']['attributedTo'])
) {
if(parse_url($bodyDecoded['object']['attributedTo'], PHP_URL_HOST) !== $keyDomain) {
return;
}
}
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
return;
}
$actor = Profile::whereKeyId($keyId)->first();
if(!$actor) {
$actorUrl = is_array($bodyDecoded['actor']) ? $bodyDecoded['actor'][0] : $bodyDecoded['actor'];
$actor = Helpers::profileFirstOrNew($actorUrl);
}
if(!$actor) {
return;
}
$pkey = openssl_pkey_get_public($actor->public_key);
if(!$pkey) {
return 0;
}
$inboxPath = "/f/inbox";
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
if($verified == 1) {
return true;
} else {
return false;
}
}
protected function blindKeyRotation($headers, $payload)
{
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
if(!$signature) {
return;
}
if(!$date) {
return;
}
if(!now()->parse($date)->gt(now()->subDays(1)) ||
!now()->parse($date)->lt(now()->addDays(1))
) {
return;
}
$signatureData = HttpSignature::parseSignatureHeader($signature);
$keyId = Helpers::validateUrl($signatureData['keyId']);
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
if(!$actor) {
return;
}
if(Helpers::validateUrl($actor->remote_url) == false) {
return;
}
$res = Zttp::timeout(5)->withHeaders([
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
])->get($actor->remote_url);
$res = json_decode($res->body(), true, 8);
if($res['publicKey']['id'] !== $actor->key_id) {
return;
}
$actor->public_key = $res['publicKey']['publicKeyPem'];
$actor->save();
return $this->verifySignature($headers, $payload);
}
}

View file

@ -53,6 +53,15 @@ class InboxValidator implements ShouldQueue
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
if(isset($payload['id'])) {
$lockKey = hash('sha256', $payload['id']);
if(Cache::get($lockKey) !== null) {
// Job processed already
return 1;
}
Cache::put($lockKey, 1, 300);
}
if(!isset($headers['signature']) || !isset($headers['date'])) {
return;
}
@ -173,6 +182,9 @@ class InboxValidator implements ShouldQueue
return;
}
$pkey = openssl_pkey_get_public($actor->public_key);
if(!$pkey) {
return 0;
}
$inboxPath = "/users/{$profile->username}/inbox";
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
if($verified == 1) {

View file

@ -49,6 +49,15 @@ class InboxWorker implements ShouldQueue
$headers = $this->headers;
$payload = json_decode($this->payload, true, 8);
if(isset($payload['id'])) {
$lockKey = hash('sha256', $payload['id']);
if(Cache::get($lockKey) !== null) {
// Job processed already
return 1;
}
Cache::put($lockKey, 1, 300);
}
if(!isset($headers['signature']) || !isset($headers['date'])) {
return;
}
@ -161,6 +170,9 @@ class InboxWorker implements ShouldQueue
return;
}
$pkey = openssl_pkey_get_public($actor->public_key);
if(!$pkey) {
return 0;
}
$inboxPath = "/f/inbox";
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
if($verified == 1) {

View file

@ -0,0 +1,56 @@
<?php
namespace App\Jobs\InstancePipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use App\Instance;
use App\Profile;
use App\Services\NodeinfoService;
class FetchNodeinfoPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $instance;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Instance $instance)
{
$this->instance = $instance;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$instance = $this->instance;
$ni = NodeinfoService::get($instance->domain);
if($ni) {
if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) {
$software = $ni['software']['name'];
$instance->software = strtolower(strip_tags($software));
$instance->last_crawled_at = now();
$instance->user_count = Profile::whereDomain($instance->domain)->count();
$instance->save();
}
} else {
$instance->user_count = Profile::whereDomain($instance->domain)->count();
$instance->last_crawled_at = now();
$instance->save();
}
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Jobs\InstancePipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use App\Instance;
use App\Profile;
use App\Services\NodeinfoService;
class InstanceCrawlPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Instance::whereNull('last_crawled_at')->whereNull('software')->chunk(50, function($instances) {
foreach($instances as $instance) {
FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
}
});
}
}

View file

@ -2,7 +2,7 @@
namespace App\Jobs\LikePipeline;
use Cache, Log;
use Cache, DB, Log;
use Illuminate\Support\Facades\Redis;
use App\{Like, Notification};
use Illuminate\Bus\Queueable;
@ -14,6 +14,7 @@ use App\Util\ActivityPub\Helpers;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\Like as LikeTransformer;
use App\Services\StatusService;
class LikePipeline implements ShouldQueue
{
@ -58,6 +59,11 @@ class LikePipeline implements ShouldQueue
return;
}
$status->likes_count = DB::table('likes')->whereStatusId($status->id)->count();
$status->save();
StatusService::refresh($status->id);
if($status->url && $actor->domain == null) {
return $this->remoteLikeDeliver();
}

View file

@ -0,0 +1,108 @@
<?php
namespace App\Jobs\LikePipeline;
use Cache, DB, Log;
use Illuminate\Support\Facades\Redis;
use App\{Like, Notification};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\UndoLike as LikeTransformer;
use App\Services\StatusService;
class UnlikePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $like;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
public $timeout = 5;
public $tries = 1;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Like $like)
{
$this->like = $like;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$like = $this->like;
$status = $this->like->status;
$actor = $this->like->actor;
if (!$status) {
// Ignore notifications to deleted statuses
return;
}
$status->likes_count = DB::table('likes')->whereStatusId($status->id)->count();
$status->save();
StatusService::refresh($status->id);
if($actor->id !== $status->profile_id && $status->url && $actor->domain == null) {
$this->remoteLikeDeliver();
}
$exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id)
->whereAction('like')
->whereItemId($status->id)
->whereItemType('App\Status')
->first();
if($exists) {
$exists->delete();
}
$like = Like::whereProfileId($actor->id)->whereStatusId($status->id)->first();
if(!$like) {
return;
}
$like->forceDelete();
return;
}
public function remoteLikeDeliver()
{
$like = $this->like;
$status = $this->like->status;
$actor = $this->like->actor;
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($like, new LikeTransformer());
$activity = $fractal->createData($resource)->toArray();
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
Helpers::sendSignedObject($actor, $url, $activity);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Jobs\MediaPipeline;
use App\Media;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
class MediaDeletePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media;
public function __construct(Media $media)
{
$this->media = $media;
}
public function handle()
{
$media = $this->media;
$path = $media->media_path;
$thumb = $media->thumbnail_path;
if(!$path) {
return 1;
}
$e = explode('/', $path);
array_pop($e);
$i = implode('/', $e);
if(config_cache('pixelfed.cloud_storage') == true) {
$disk = Storage::disk(config('filesystems.cloud'));
if($disk->exists($path)) {
$disk->delete($path);
}
if($disk->exists($thumb)) {
$disk->delete($thumb);
}
if(count($e) > 4 && count($disk->files($i)) == 0) {
$disk->deleteDirectory($i);
}
}
$disk = Storage::disk(config('filesystems.local'));
if($disk->exists($path)) {
$disk->delete($path);
}
if($disk->exists($thumb)) {
$disk->delete($thumb);
}
if(count($e) > 4 && count($disk->files($i)) == 0) {
$disk->deleteDirectory($i);
}
return 1;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Jobs\MediaPipeline;
use App\Media;
use Cache;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use App\Services\MediaStorageService;
class MediaStoragePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media;
public function __construct(Media $media)
{
$this->media = $media;
}
public function handle()
{
MediaStorageService::store($this->media);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Jobs\MediaPipeline;
use App\Media;
use App\User;
use Cache;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Services\StatusService;
class MediaSyncLicensePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $userId;
protected $licenseId;
public function __construct($userId, $licenseId)
{
$this->userId = $userId;
$this->licenseId = $licenseId;
}
public function handle()
{
$licenseId = $this->licenseId;
if(!$licenseId || !$this->userId) {
return 1;
}
Media::whereUserId($this->userId)
->chunk(100, function($medias) use($licenseId) {
foreach($medias as $media) {
$media->license = $licenseId;
$media->save();
Cache::forget('status:transformer:media:attachments:'. $media->status_id);
StatusService::del($media->status_id);
}
});
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Jobs\ModPipeline;
use Cache;
use App\Profile;
use App\Status;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Services\StatusService;
class HandleSpammerPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile;
public $deleteWhenMissingModels = true;
public function __construct(Profile $profile)
{
$this->profile = $profile;
}
public function handle()
{
$profile = $this->profile;
$profile->unlisted = true;
$profile->cw = true;
$profile->no_autolink = true;
$profile->save();
Status::whereProfileId($profile->id)
->chunk(50, function($statuses) {
foreach($statuses as $status) {
$status->is_nsfw = true;
$status->scope = $status->scope === 'public' ? 'unlisted' : $status->scope;
$status->visibility = $status->scope;
$status->save();
StatusService::del($status->id, true);
}
});
Cache::forget('_api:statuses:recent_9:'.$profile->id);
return 1;
}
}

View file

@ -15,135 +15,141 @@ use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\Announce;
use GuzzleHttp\{Pool, Client, Promise};
use App\Util\ActivityPub\HttpSignature;
use App\Services\StatusService;
class SharePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$actor = $status->profile;
$target = $status->parent()->profile;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$parent = $this->status->parent();
$actor = $status->profile;
$target = $parent->profile;
if ($status->uri !== null) {
// Ignore notifications to remote statuses
return;
}
if ($status->uri !== null) {
// Ignore notifications to remote statuses
return;
}
$exists = Notification::whereProfileId($target->id)
->whereActorId($status->profile_id)
->whereAction('share')
->whereItemId($status->reblog_of_id)
->whereItemType('App\Status')
->count();
$exists = Notification::whereProfileId($target->id)
->whereActorId($status->profile_id)
->whereAction('share')
->whereItemId($status->reblog_of_id)
->whereItemType('App\Status')
->exists();
if ($target->id === $status->profile_id) {
$this->remoteAnnounceDeliver();
return true;
}
if($target->id === $status->profile_id) {
$this->remoteAnnounceDeliver();
return true;
}
if( $exists !== 0) {
return true;
}
if($exists === true) {
return true;
}
$this->remoteAnnounceDeliver();
$this->remoteAnnounceDeliver();
try {
$notification = new Notification;
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'share';
$notification->message = $status->shareToText();
$notification->rendered = $status->shareToHtml();
$notification->item_id = $status->reblog_of_id ?? $status->id;
$notification->item_type = "App\Status";
$notification->save();
$parent->reblogs_count = $parent->shares()->count();
$parent->save();
StatusService::del($parent->id);
$redis = Redis::connection();
$key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
$redis->lpush($key, $notification->id);
} catch (Exception $e) {
Log::error($e);
}
}
try {
$notification = new Notification;
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'share';
$notification->message = $status->shareToText();
$notification->rendered = $status->shareToHtml();
$notification->item_id = $status->reblog_of_id ?? $status->id;
$notification->item_type = "App\Status";
$notification->save();
public function remoteAnnounceDeliver()
{
if(config('federation.activitypub.enabled') == false) {
return true;
}
$status = $this->status;
$profile = $status->profile;
$redis = Redis::connection();
$key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
$redis->lpush($key, $notification->id);
} catch (Exception $e) {
Log::error($e);
}
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new Announce());
$activity = $fractal->createData($resource)->toArray();
public function remoteAnnounceDeliver()
{
if(config_cache('federation.activitypub.enabled') == false) {
return true;
}
$status = $this->status;
$profile = $status->profile;
$audience = $status->profile->getAudienceInbox();
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new Announce());
$activity = $fractal->createData($resource)->toArray();
if(empty($audience) || $status->scope != 'public') {
// Return on profiles with no remote followers
return;
}
$audience = $status->profile->getAudienceInbox();
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
if(empty($audience) || $status->scope != 'public') {
// Return on profiles with no remote followers
return;
}
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$payload = json_encode($activity);
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$promise->wait();
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
}
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace App\Jobs\SharePipeline;
use Cache, Log;
use Illuminate\Support\Facades\Redis;
use App\{Status, Notification};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\UndoAnnounce;
use GuzzleHttp\{Pool, Client, Promise};
use App\Util\ActivityPub\HttpSignature;
use App\Services\StatusService;
class UndoSharePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
public $deleteWhenMissingModels = true;
public function __construct(Status $status)
{
$this->status = $status;
}
public function handle()
{
$status = $this->status;
$actor = $status->profile;
$parent = $status->parent();
$target = $status->parent()->profile;
if ($status->uri !== null) {
return;
}
if($target->domain === null) {
Notification::whereProfileId($target->id)
->whereActorId($status->profile_id)
->whereAction('share')
->whereItemId($status->reblog_of_id)
->whereItemType('App\Status')
->delete();
}
$this->remoteAnnounceDeliver();
if($parent->reblogs_count > 0) {
$parent->reblogs_count = $parent->reblogs_count - 1;
$parent->save();
StatusService::del($parent->id);
}
$status->forceDelete();
return 1;
}
public function remoteAnnounceDeliver()
{
if(config_cache('federation.activitypub.enabled') == false) {
return 1;
}
$status = $this->status;
$profile = $status->profile;
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new UndoAnnounce());
$activity = $fractal->createData($resource)->toArray();
$audience = $status->profile->getAudienceInbox();
if(empty($audience) || $status->scope != 'public') {
return 1;
}
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
}

View file

@ -12,6 +12,7 @@ use Illuminate\Queue\SerializesModels;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\CreateNote;
use App\Transformer\ActivityPub\Verb\CreateQuestion;
use App\Util\ActivityPub\Helpers;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
@ -20,85 +21,97 @@ use App\Util\ActivityPub\HttpSignature;
class StatusActivityPubDeliver implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
protected $status;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$profile = $status->profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
if($status->local == false || $status->url || $status->uri) {
return;
}
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
$audience = $status->profile->getAudienceInbox();
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$profile = $status->profile;
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
// Return on profiles with no remote followers
return;
}
if($status->local == false || $status->url || $status->uri) {
return;
}
$audience = $status->profile->getAudienceInbox();
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
// Return on profiles with no remote followers
return;
}
switch($status->type) {
case 'poll':
$activitypubObject = new CreateQuestion();
break;
default:
$activitypubObject = new CreateNote();
break;
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new CreateNote());
$activity = $fractal->createData($resource)->toArray();
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, $activitypubObject);
$activity = $fractal->createData($resource)->toArray();
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$payload = json_encode($activity);
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false
]
]);
};
}
};
$promise->wait();
}
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
}

View file

@ -2,14 +2,14 @@
namespace App\Jobs\StatusPipeline;
use DB;
use DB, Storage;
use App\{
AccountInterstitial,
MediaTag,
Notification,
Report,
Status,
StatusHashtag,
AccountInterstitial,
MediaTag,
Notification,
Report,
Status,
StatusHashtag,
};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -17,6 +17,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use League\Fractal;
use Illuminate\Support\Str;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\DeleteNote;
use App\Util\ActivityPub\Helpers;
@ -24,163 +25,149 @@ use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use App\Util\ActivityPub\HttpSignature;
use App\Services\StatusService;
use App\Services\MediaStorageService;
class StatusDelete implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
protected $status;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$profile = $this->status->profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
$count = $profile->statuses()
->getQuery()
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->count();
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
$profile->status_count = ($count - 1);
$profile->save();
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$profile = $this->status->profile;
if(config('federation.activitypub.enabled') == true) {
$this->fanoutDelete($status);
} else {
$this->unlinkRemoveMedia($status);
}
StatusService::del($status->id, true);
}
if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
$profile->status_count = $profile->status_count - 1;
$profile->save();
}
public function unlinkRemoveMedia($status)
{
foreach ($status->media as $media) {
$thumbnail = storage_path("app/{$media->thumbnail_path}");
$photo = storage_path("app/{$media->media_path}");
if(config_cache('federation.activitypub.enabled') == true) {
$this->fanoutDelete($status);
} else {
$this->unlinkRemoveMedia($status);
}
try {
if (is_file($thumbnail)) {
unlink($thumbnail);
}
if (is_file($photo)) {
unlink($photo);
}
$media->delete();
} catch (Exception $e) {
}
}
if($status->in_reply_to_id) {
DB::transaction(function() use($status) {
$parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count;
$parent->save();
});
}
DB::transaction(function() use($status) {
$comments = Status::where('in_reply_to_id', $status->id)->get();
foreach ($comments as $comment) {
$comment->in_reply_to_id = null;
$comment->save();
Notification::whereItemType('App\Status')
->whereItemId($comment->id)
->delete();
}
$status->likes()->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->delete();
StatusHashtag::whereStatusId($status->id)->delete();
Report::whereObjectType('App\Status')
->whereObjectId($status->id)
->delete();
}
MediaTag::where('status_id', $status->id)
->cursor()
->each(function($tag) {
Notification::where('item_type', 'App\MediaTag')
->where('item_id', $tag->id)
->forceDelete();
$tag->delete();
});
public function unlinkRemoveMedia($status)
{
foreach ($status->media as $media) {
MediaStorageService::delete($media, true);
}
AccountInterstitial::where('item_type', 'App\Status')
->where('item_id', $status->id)
->delete();
if($status->in_reply_to_id) {
DB::transaction(function() use($status) {
$parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count;
$parent->save();
});
}
DB::transaction(function() use($status) {
$comments = Status::where('in_reply_to_id', $status->id)->get();
foreach ($comments as $comment) {
$comment->in_reply_to_id = null;
$comment->save();
Notification::whereItemType('App\Status')
->whereItemId($comment->id)
->delete();
}
$status->likes()->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->delete();
StatusHashtag::whereStatusId($status->id)->delete();
Report::whereObjectType('App\Status')
->whereObjectId($status->id)
->delete();
MediaTag::where('status_id', $status->id)
->cursor()
->each(function($tag) {
Notification::where('item_type', 'App\MediaTag')
->where('item_id', $tag->id)
->forceDelete();
$tag->delete();
});
AccountInterstitial::where('item_type', 'App\Status')
->where('item_id', $status->id)
->delete();
$status->forceDelete();
});
$status->forceDelete();
});
return true;
}
return true;
}
protected function fanoutDelete($status)
{
$audience = $status->profile->getAudienceInbox();
$profile = $status->profile;
protected function fanoutDelete($status)
{
$audience = $status->profile->getAudienceInbox();
$profile = $status->profile;
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new DeleteNote());
$activity = $fractal->createData($resource)->toArray();
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new DeleteNote());
$activity = $fractal->createData($resource)->toArray();
$this->unlinkRemoveMedia($status);
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$this->unlinkRemoveMedia($status);
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$payload = json_encode($activity);
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$promise->wait();
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
}
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
}

View file

@ -21,146 +21,158 @@ use Illuminate\Queue\SerializesModels;
class StatusEntityLexer implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
protected $entities;
protected $autolink;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
protected $status;
protected $entities;
protected $autolink;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->status->profile;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
$count = $profile->statuses()
->getQuery()
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->count();
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
$profile->status_count = $count;
$profile->save();
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->status->profile;
$status = $this->status;
if($profile->no_autolink == false) {
$this->parseEntities();
}
}
if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
$profile->status_count = $profile->status_count + 1;
$profile->save();
}
public function parseEntities()
{
$this->extractEntities();
}
if($profile->no_autolink == false) {
$this->parseEntities();
}
}
public function extractEntities()
{
$this->entities = Extractor::create()->extract($this->status->caption);
$this->autolinkStatus();
}
public function parseEntities()
{
$this->extractEntities();
}
public function autolinkStatus()
{
$this->autolink = Autolink::create()->autolink($this->status->caption);
$this->storeEntities();
}
public function extractEntities()
{
$this->entities = Extractor::create()->extract($this->status->caption);
$this->autolinkStatus();
}
public function storeEntities()
{
$this->storeHashtags();
DB::transaction(function () {
$status = $this->status;
$status->rendered = nl2br($this->autolink);
$status->entities = json_encode($this->entities);
$status->save();
});
}
public function autolinkStatus()
{
$this->autolink = Autolink::create()->autolink($this->status->caption);
$this->storeEntities();
}
public function storeHashtags()
{
$tags = array_unique($this->entities['hashtags']);
$status = $this->status;
public function storeEntities()
{
$this->storeHashtags();
DB::transaction(function () {
$status = $this->status;
$status->rendered = nl2br($this->autolink);
$status->entities = json_encode($this->entities);
$status->save();
});
}
foreach ($tags as $tag) {
if(mb_strlen($tag) > 124) {
continue;
}
DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag, '-', false);
$hashtag = Hashtag::firstOrCreate(
['name' => $tag, 'slug' => $slug]
);
StatusHashtag::firstOrCreate(
[
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
'status_visibility' => $status->visibility,
]
);
});
}
$this->storeMentions();
}
public function storeHashtags()
{
$tags = array_unique($this->entities['hashtags']);
$status = $this->status;
public function storeMentions()
{
$mentions = array_unique($this->entities['mentions']);
$status = $this->status;
foreach ($tags as $tag) {
if(mb_strlen($tag) > 124) {
continue;
}
DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag, '-', false);
$hashtag = Hashtag::where('slug', $slug)->first();
if (!$hashtag) {
$hashtag = Hashtag::create(
['name' => $tag, 'slug' => $slug]
);
}
foreach ($mentions as $mention) {
$mentioned = Profile::whereUsername($mention)->first();
StatusHashtag::firstOrCreate(
[
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
'status_visibility' => $status->visibility,
]
);
});
}
$this->storeMentions();
}
if (empty($mentioned) || !isset($mentioned->id)) {
continue;
}
public function storeMentions()
{
$mentions = array_unique($this->entities['mentions']);
$status = $this->status;
DB::transaction(function () use ($status, $mentioned) {
$m = new Mention();
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
foreach ($mentions as $mention) {
$mentioned = Profile::whereUsername($mention)->first();
MentionPipeline::dispatch($status, $m);
});
}
$this->deliver();
}
if (empty($mentioned) || !isset($mentioned->id)) {
continue;
}
public function deliver()
{
$status = $this->status;
DB::transaction(function () use ($status, $mentioned) {
$m = new Mention();
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
if(config('pixelfed.bouncer.enabled')) {
Bouncer::get($status);
}
MentionPipeline::dispatch($status, $m);
});
}
$this->deliver();
}
if($status->uri == null && $status->scope == 'public') {
PublicTimelineService::add($status->id);
}
public function deliver()
{
$status = $this->status;
$types = [
'photo',
'photo:album',
'video',
'video:album',
'photo:video:album'
];
if(config('federation.activitypub.enabled') == true && config('app.env') == 'production') {
StatusActivityPubDeliver::dispatch($this->status);
}
}
if(config_cache('pixelfed.bouncer.enabled')) {
Bouncer::get($status);
}
if( $status->uri == null &&
$status->scope == 'public' &&
in_array($status->type, $types) &&
$status->in_reply_to_id === null &&
$status->reblog_of_id === null
) {
PublicTimelineService::add($status->id);
}
if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
StatusActivityPubDeliver::dispatch($status);
}
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace App\Jobs\StatusPipeline;
use App\Notification;
use App\Status;
use Cache;
use DB;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use App\Services\NotificationService;
class StatusReplyPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
public $timeout = 5;
public $tries = 1;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$actor = $status->profile;
$reply = Status::find($status->in_reply_to_id);
if(!$actor || !$reply) {
return 1;
}
$target = $reply->profile;
$exists = Notification::whereProfileId($target->id)
->whereActorId($actor->id)
->whereIn('action', ['mention', 'comment'])
->whereItemId($status->id)
->whereItemType('App\Status')
->count();
if ($actor->id === $target || $exists !== 0) {
return 1;
}
DB::transaction(function() use($target, $actor, $status) {
$notification = new Notification();
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'comment';
$notification->message = $status->replyToText();
$notification->rendered = $status->replyToHtml();
$notification->item_id = $status->id;
$notification->item_type = "App\Status";
$notification->save();
NotificationService::setNotification($notification);
NotificationService::set($notification->profile_id, $notification->id);
});
return 1;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Jobs\StatusPipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Services\CustomEmojiService;
use App\Services\StatusService;
class StatusTagsPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $activity;
protected $status;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($activity, $status)
{
$this->activity = $activity;
$this->status = $status;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$res = $this->activity;
collect($res['tag'])
->filter(function($tag) {
// todo: finish hashtag + mention import
// return in_array($tag['type'], ['Emoji', 'Hashtag', 'Mention']);
return $tag && $tag['type'] == 'Emoji';
})
->map(function($tag) {
CustomEmojiService::import($tag['id'], $this->status->id);
});
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace App\Jobs\StoryPipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Storage;
use App\Story;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\DeleteStory;
use App\Util\ActivityPub\Helpers;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use App\Util\ActivityPub\HttpSignature;
use App\Services\FollowerService;
use App\Services\StoryService;
class StoryDelete implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $story;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Story $story)
{
$this->story = $story;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$story = $this->story;
if($story->local == false) {
return;
}
StoryService::removeRotateQueue($story->id);
StoryService::delLatest($story->profile_id);
StoryService::delById($story->id);
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
$story->views()->delete();
$profile = $story->profile;
$activity = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $story->url() . '#delete',
'type' => 'Delete',
'actor' => $profile->permalink(),
'object' => [
'id' => $story->url(),
'type' => 'Story',
],
];
$this->fanoutExpiry($profile, $activity);
// delete notifications
// delete polls
// delete reports
$story->delete();
return;
}
protected function fanoutExpiry($profile, $activity)
{
$audience = FollowerService::softwareAudience($profile->id, 'pixelfed');
if(empty($audience)) {
// Return on profiles with no remote followers
return;
}
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
}

View file

@ -0,0 +1,169 @@
<?php
namespace App\Jobs\StoryPipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Storage;
use App\Story;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\DeleteStory;
use App\Util\ActivityPub\Helpers;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use App\Util\ActivityPub\HttpSignature;
use App\Services\FollowerService;
use App\Services\StoryService;
class StoryExpire implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $story;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Story $story)
{
$this->story = $story;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$story = $this->story;
if($story->local == false) {
$this->handleRemoteExpiry();
return;
}
if($story->active == false) {
return;
}
if($story->expires_at->gt(now())) {
return;
}
$story->active = false;
$story->save();
$this->rotateMediaPath();
$this->fanoutExpiry();
StoryService::delLatest($story->profile_id);
}
protected function rotateMediaPath()
{
$story = $this->story;
$date = date('Y').date('m');
$old = $story->path;
$base = "story_archives/{$story->profile_id}/{$date}/";
$paths = explode('/', $old);
$path = array_pop($paths);
$newPath = $base . $path;
if(Storage::exists($old) == true) {
$dir = implode('/', $paths);
Storage::move($old, $newPath);
Storage::delete($old);
$story->bearcap_token = null;
$story->path = $newPath;
$story->save();
Storage::deleteDirectory($dir);
}
}
protected function fanoutExpiry()
{
$story = $this->story;
$profile = $story->profile;
if($story->local == false || $story->remote_url) {
return;
}
$audience = FollowerService::softwareAudience($story->profile_id, 'pixelfed');
if(empty($audience)) {
// Return on profiles with no remote followers
return;
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($story, new DeleteStory());
$activity = $fractal->createData($resource)->toArray();
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
protected function handleRemoteExpiry()
{
$story = $this->story;
$story->active = false;
$story->save();
$path = $story->path;
if(Storage::exists($path) == true) {
Storage::delete($path);
}
$story->views()->delete();
$story->delete();
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace App\Jobs\StoryPipeline;
use Cache, Log;
use App\Story;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\CreateStory;
use App\Util\ActivityPub\Helpers;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use App\Util\ActivityPub\HttpSignature;
use App\Services\FollowerService;
use App\Services\StoryService;
class StoryFanout implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $story;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Story $story)
{
$this->story = $story;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$story = $this->story;
$profile = $story->profile;
if($story->local == false || $story->remote_url) {
return;
}
StoryService::delLatest($story->profile_id);
$audience = FollowerService::softwareAudience($story->profile_id, 'pixelfed');
if(empty($audience)) {
// Return on profiles with no remote followers
return;
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($story, new CreateStory());
$activity = $fractal->createData($resource)->toArray();
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace App\Jobs\StoryPipeline;
use Cache, Log;
use App\Story;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
use App\Services\FollowerService;
use App\Util\Lexer\Bearcap;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\ConnectionException;
use App\Util\ActivityPub\Validator\StoryValidator;
use App\Services\StoryService;
use App\Services\MediaPathService;
use Illuminate\Support\Str;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
class StoryFetch implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $activity;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($activity)
{
$this->activity = $activity;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$activity = $this->activity;
$activityId = $activity['id'];
$activityActor = $activity['actor'];
if(parse_url($activityId, PHP_URL_HOST) !== parse_url($activityActor, PHP_URL_HOST)) {
return;
}
$bearcap = Bearcap::decode($activity['object']['object']);
if(!$bearcap) {
return;
}
$url = $bearcap['url'];
$token = $bearcap['token'];
if(parse_url($activityId, PHP_URL_HOST) !== parse_url($url, PHP_URL_HOST)) {
return;
}
$version = config('pixelfed.version');
$appUrl = config('app.url');
$headers = [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token,
'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})",
];
try {
$res = Http::withHeaders($headers)
->timeout(30)
->get($url);
} catch (RequestException $e) {
return false;
} catch (ConnectionException $e) {
return false;
} catch (\Exception $e) {
return false;
}
$payload = $res->json();
if(StoryValidator::validate($payload) == false) {
return;
}
if(Helpers::validateUrl($payload['attachment']['url']) == false) {
return;
}
$type = $payload['attachment']['type'] == 'Image' ? 'photo' : 'video';
$profile = Helpers::profileFetch($payload['attributedTo']);
$ext = pathinfo($payload['attachment']['url'], PATHINFO_EXTENSION);
$storagePath = MediaPathService::story($profile);
$fileName = Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $ext;
$contextOptions = [
'ssl' => [
'verify_peer' => false,
'verify_peername' => false
]
];
$ctx = stream_context_create($contextOptions);
$data = file_get_contents($payload['attachment']['url'], false, $ctx);
$tmpBase = storage_path('app/remcache/');
$tmpPath = $profile->id . '-' . $fileName;
$tmpName = $tmpBase . $tmpPath;
file_put_contents($tmpName, $data);
$disk = Storage::disk(config('filesystems.default'));
$path = $disk->putFileAs($storagePath, new File($tmpName), $fileName, 'public');
$size = filesize($tmpName);
unlink($tmpName);
$story = new Story;
$story->profile_id = $profile->id;
$story->object_id = $payload['id'];
$story->size = $size;
$story->mime = $payload['attachment']['mediaType'];
$story->duration = $payload['duration'];
$story->media_url = $payload['attachment']['url'];
$story->type = $type;
$story->public = false;
$story->local = false;
$story->active = true;
$story->path = $path;
$story->view_count = 0;
$story->can_reply = $payload['can_reply'];
$story->can_react = $payload['can_react'];
$story->created_at = now()->parse($payload['published']);
$story->expires_at = now()->parse($payload['expiresAt']);
$story->save();
StoryService::delLatest($story->profile_id);
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace App\Jobs\StoryPipeline;
use App\Story;
use App\Status;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
class StoryReactionDeliver implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $story;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Story $story, Status $status)
{
$this->story = $story;
$this->status = $status;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$story = $this->story;
$status = $this->status;
if($story->local == true) {
return;
}
$target = $story->profile;
$actor = $status->profile;
$to = $target->inbox_url;
$payload = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $status->permalink(),
'type' => 'Story:Reaction',
'to' => $target->permalink(),
'actor' => $actor->permalink(),
'content' => $status->caption,
'inReplyTo' => $story->object_id,
'published' => $status->created_at->toAtomString()
];
Helpers::sendSignedObject($actor, $to, $payload);
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace App\Jobs\StoryPipeline;
use App\Story;
use App\Status;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
class StoryReplyDeliver implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $story;
protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Story $story, Status $status)
{
$this->story = $story;
$this->status = $status;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$story = $this->story;
$status = $this->status;
if($story->local == true) {
return;
}
$target = $story->profile;
$actor = $status->profile;
$to = $target->inbox_url;
$payload = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $status->permalink(),
'type' => 'Story:Reply',
'to' => $target->permalink(),
'actor' => $actor->permalink(),
'content' => $status->caption,
'inReplyTo' => $story->object_id,
'published' => $status->created_at->toAtomString()
];
Helpers::sendSignedObject($actor, $to, $payload);
}
}

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