Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
TeDomum
Pixelfed
Commits
700c7805
Unverified
Commit
700c7805
authored
Jan 07, 2020
by
daniel
Committed by
GitHub
Jan 07, 2020
Browse files
Merge pull request #1906 from pixelfed/staging
Add S3 + Stories
parents
f1e15bac
e6419297
Changes
41
Hide whitespace changes
Inline
Side-by-side
app/Console/Commands/StoryGC.php
0 → 100644
View file @
700c7805
<?php
namespace
App\Console\Commands
;
use
Illuminate\Console\Command
;
use
Illuminate\Support\Facades\
{
DB
,
Storage
};
use
App\
{
Story
,
StoryView
};
class
StoryGC
extends
Command
{
/**
* 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'
;
/**
* Create a new command instance.
*
* @return void
*/
public
function
__construct
()
{
parent
::
__construct
();
}
/**
* Execute the console command.
*
* @return mixed
*/
public
function
handle
()
{
$stories
=
Story
::
where
(
'expires_at'
,
'<'
,
now
())
->
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
();
});
}
}
}
app/Console/Kernel.php
View file @
700c7805
...
...
@@ -30,6 +30,7 @@ class Kernel extends ConsoleKernel
$schedule
->
command
(
'media:gc'
)
->
hourly
();
$schedule
->
command
(
'horizon:snapshot'
)
->
everyFiveMinutes
();
$schedule
->
command
(
'story:gc'
)
->
everyFiveMinutes
();
}
/**
...
...
app/Http/Controllers/FollowerController.php
View file @
700c7805
...
...
@@ -111,6 +111,10 @@ class FollowerController extends Controller
Cache
::
forget
(
'api:local:exp:rec:'
.
$user
->
id
);
Cache
::
forget
(
'user:account:id:'
.
$target
->
user_id
);
Cache
::
forget
(
'user:account:id:'
.
$user
->
user_id
);
Cache
::
forget
(
'px:profile:followers-v1.3:'
.
$user
->
id
);
Cache
::
forget
(
'px:profile:followers-v1.3:'
.
$target
->
id
);
Cache
::
forget
(
'px:profile:following-v1.3:'
.
$user
->
id
);
Cache
::
forget
(
'px:profile:following-v1.3:'
.
$target
->
id
);
return
$target
->
url
();
}
...
...
app/Http/Controllers/InternalApiController.php
View file @
700c7805
...
...
@@ -244,7 +244,7 @@ class InternalApiController extends Controller
'cw'
=>
'nullable|boolean'
,
'visibility'
=>
'required|string|in:public,private,unlisted|min:2|max:10'
,
'place'
=>
'nullable'
,
'comments_disabled'
=>
'nullable
|boolean
'
'comments_disabled'
=>
'nullable'
]);
if
(
config
(
'costar.enabled'
)
==
true
)
{
...
...
@@ -301,7 +301,7 @@ class InternalApiController extends Controller
}
if
(
$request
->
filled
(
'comments_disabled'
))
{
$status
->
comments_disabled
=
$request
->
input
(
'comments_disabled'
);
$status
->
comments_disabled
=
(
bool
)
$request
->
input
(
'comments_disabled'
);
}
$status
->
caption
=
strip_tags
(
$request
->
caption
);
...
...
@@ -314,10 +314,6 @@ class InternalApiController extends Controller
$media
->
save
();
}
// $resource = new Fractal\Resource\Collection($status->media()->orderBy('order')->get(), new StatusMediaContainerTransformer());
// $mediaContainer = $this->fractal->createData($resource)->toArray();
// $status->media_container = json_encode($mediaContainer);
$visibility
=
$profile
->
unlisted
==
true
&&
$visibility
==
'public'
?
'unlisted'
:
$visibility
;
$cw
=
$profile
->
cw
==
true
?
true
:
$cw
;
$status
->
is_nsfw
=
$cw
;
...
...
app/Http/Controllers/ProfileController.php
View file @
700c7805
...
...
@@ -9,6 +9,7 @@ use View;
use
App\Follower
;
use
App\FollowRequest
;
use
App\Profile
;
use
App\Story
;
use
App\User
;
use
App\UserFilter
;
use
League\Fractal
;
...
...
@@ -135,6 +136,21 @@ class ProfileController extends Controller
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
);
}
protected
function
blockedProfileCheck
(
Profile
$profile
)
{
$pid
=
Auth
::
user
()
->
profile
->
id
;
...
...
@@ -215,4 +231,18 @@ class ProfileController extends Controller
return
response
(
$content
)
->
withHeaders
([
'X-Frame-Options'
=>
'ALLOWALL'
]);
}
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'
));
}
}
app/Http/Controllers/StoryController.php
View file @
700c7805
...
...
@@ -3,6 +3,15 @@
namespace
App\Http\Controllers
;
use
Illuminate\Http\Request
;
use
Illuminate\Support\Str
;
use
App\Media
;
use
App\Profile
;
use
App\Story
;
use
App\StoryView
;
use
App\Services\StoryService
;
use
Cache
,
Storage
;
use
App\Services\FollowerService
;
class
StoryController
extends
Controller
{
...
...
@@ -12,8 +21,235 @@ class StoryController extends Controller
$this
->
middleware
(
'auth'
);
}
public
function
home
(
Request
$request
)
public
function
apiV1Add
(
Request
$request
)
{
abort_if
(
!
config
(
'instance.stories.enabled'
)
||
!
$request
->
user
(),
404
);
$this
->
validate
(
$request
,
[
'file.*'
=>
function
()
{
return
[
'required'
,
'mimes:image/jpeg,image/png'
,
'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.'
);
}
$story
=
new
Story
();
$story
->
profile_id
=
$user
->
profile_id
;
$story
->
save
();
$monthHash
=
substr
(
hash
(
'sha1'
,
date
(
'Y'
)
.
date
(
'm'
)),
0
,
12
);
$rid
=
Str
::
random
(
6
)
.
'.'
.
Str
::
random
(
9
);
$photo
=
$request
->
file
(
'file'
);
$mimes
=
explode
(
','
,
config
(
'pixelfed.media_types'
));
if
(
in_array
(
$photo
->
getMimeType
(),
[
'image/jpeg'
,
'image/png'
])
==
false
)
{
abort
(
400
,
'Invalid media type'
);
return
;
}
$storagePath
=
"public/_esm.t1/
{
$monthHash
}
/
{
$story
->
id
}
/
{
$rid
}
"
;
$path
=
$photo
->
store
(
$storagePath
);
$story
->
path
=
$path
;
$story
->
local
=
true
;
$story
->
expires_at
=
now
()
->
addHours
(
24
);
$story
->
save
();
return
[
'code'
=>
200
,
'msg'
=>
'Successfully added'
,
'media_url'
=>
url
(
Storage
::
url
(
$story
->
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
=
FollowerService
::
build
()
->
profile
(
$profile
)
->
following
();
$stories
=
Story
::
with
(
'profile'
)
->
whereIn
(
'profile_id'
,
$following
)
->
groupBy
(
'profile_id'
)
->
where
(
'expires_at'
,
'>'
,
now
())
->
orderByDesc
(
'expires_at'
)
->
take
(
9
)
->
get
()
->
map
(
function
(
$s
,
$k
)
{
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
];
});
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
);
$profile
=
$request
->
user
()
->
profile
;
if
(
$id
==
$profile
->
id
)
{
$publicOnly
=
true
;
}
else
{
$following
=
FollowerService
::
build
()
->
profile
(
$profile
)
->
following
();
$publicOnly
=
in_array
(
$id
,
$following
);
}
$stories
=
Story
::
whereProfileId
(
$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'
=>
'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
apiV1Profile
(
Request
$request
,
$id
)
{
abort_if
(
!
config
(
'instance.stories.enabled'
)
||
!
$request
->
user
(),
404
);
$authed
=
$request
->
user
()
->
profile
;
$profile
=
Profile
::
findOrFail
(
$id
);
if
(
$id
==
$authed
->
id
)
{
$publicOnly
=
true
;
}
else
{
$following
=
FollowerService
::
build
()
->
profile
(
$authed
)
->
following
();
$publicOnly
=
in_array
(
$id
,
$following
);
}
$stories
=
Story
::
whereProfileId
(
$profile
->
id
)
->
orderBy
(
'expires_at'
)
->
where
(
'expires_at'
,
'>'
,
now
())
->
when
(
!
$publicOnly
,
function
(
$query
,
$publicOnly
)
{
return
$query
->
wherePublic
(
true
);
})
->
get
()
->
map
(
function
(
$s
,
$k
)
{
return
[
'id'
=>
$s
->
id
,
'type'
=>
'photo'
,
'length'
=>
3
,
'src'
=>
url
(
Storage
::
url
(
$s
->
path
)),
'preview'
=>
null
,
'link'
=>
null
,
'linkText'
=>
null
,
'time'
=>
$s
->
created_at
->
format
(
'U'
),
'expires_at'
=>
(
int
)
$s
->
expires_at
->
format
(
'U'
),
'seen'
=>
$s
->
seen
()
];
})
->
toArray
();
if
(
count
(
$stories
)
==
0
)
{
return
[];
}
$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
,
'pid'
=>
(
string
)
$profile
->
id
]];
return
response
()
->
json
(
$stories
,
200
,
[],
JSON_PRETTY_PRINT
|
JSON_UNESCAPED_SLASHES
);
}
public
function
apiV1Viewed
(
Request
$request
)
{
abort_if
(
!
config
(
'instance.stories.enabled'
)
||
!
$request
->
user
(),
404
);
$this
->
validate
(
$request
,
[
'id'
=>
'required|integer|min:1|exists:stories'
,
]);
StoryView
::
firstOrCreate
([
'story_id'
=>
$request
->
input
(
'id'
),
'profile_id'
=>
$request
->
user
()
->
profile_id
]);
return
[
'code'
=>
200
];
}
public
function
compose
(
Request
$request
)
{
abort_if
(
!
config
(
'instance.stories.enabled'
)
||
!
$request
->
user
(),
404
);
return
view
(
'stories.compose'
);
}
public
function
apiV1Exists
(
Request
$request
,
$id
)
{
abort_if
(
!
config
(
'instance.stories.enabled'
),
404
);
$res
=
(
bool
)
Story
::
whereProfileId
(
$id
)
->
where
(
'expires_at'
,
'>'
,
now
())
->
count
();
return
response
()
->
json
(
$res
);
}
public
function
iRedirect
(
Request
$request
)
{
return
view
(
'stories.home'
);
$user
=
$request
->
user
();
abort_if
(
!
$user
,
404
);
$username
=
$user
->
username
;
return
redirect
(
"/stories/
{
$username
}
"
);
}
}
app/Profile.php
View file @
700c7805
...
...
@@ -303,4 +303,9 @@ class Profile extends Model
->
whereFollowingId
(
$this
->
id
)
->
exists
();
}
public
function
stories
()
{
return
$this
->
hasMany
(
Story
::
class
);
}
}
app/Providers/AuthServiceProvider.php
View file @
700c7805
...
...
@@ -36,7 +36,6 @@ class AuthServiceProvider extends ServiceProvider
'read'
,
'write'
,
'follow'
,
'push'
]);
Passport
::
tokensCan
([
...
...
app/Status.php
View file @
700c7805
...
...
@@ -131,13 +131,9 @@ class Status extends Model
$media
=
$this
->
firstMedia
();
$path
=
$media
->
media_path
;
$hash
=
is_null
(
$media
->
processed_at
)
?
md5
(
'unprocessed'
)
:
md5
(
$media
->
created_at
);
if
(
config
(
'pixelfed.cloud_storage'
)
==
true
)
{
$url
=
Storage
::
disk
(
config
(
'filesystems.cloud'
))
->
url
(
$path
)
.
"?v=
{
$hash
}
"
;
}
else
{
$url
=
Storage
::
url
(
$path
)
.
"?v=
{
$hash
}
"
;
}
$url
=
$media
->
cdn_url
?
$media
->
cdn_url
.
"?v=
{
$hash
}
"
:
url
(
Storage
::
url
(
$path
)
.
"?v=
{
$hash
}
"
);
return
url
(
$url
)
;
return
$url
;
}
public
function
likes
()
...
...
app/Story.php
View file @
700c7805
...
...
@@ -10,6 +10,8 @@ class Story extends Model
{
use
HasSnowflakePrimary
;
public
const
MAX_PER_DAY
=
10
;
/**
* Indicates if the IDs are auto-incrementing.
*
...
...
@@ -24,6 +26,8 @@ class Story extends Model
*/
protected
$dates
=
[
'published_at'
,
'expires_at'
];
protected
$fillable
=
[
'profile_id'
];
protected
$visible
=
[
'id'
];
public
function
profile
()
...
...
@@ -31,16 +35,6 @@ class Story extends Model
return
$this
->
belongsTo
(
Profile
::
class
);
}
public
function
items
()
{
return
$this
->
hasMany
(
StoryItem
::
class
);
}
public
function
reactions
()
{
return
$this
->
hasMany
(
StoryReaction
::
class
);
}
public
function
views
()
{
return
$this
->
hasMany
(
StoryView
::
class
);
...
...
@@ -48,7 +42,13 @@ class Story extends Model
public
function
seen
(
$pid
=
false
)
{
$id
=
$pid
??
Auth
::
user
()
->
profile
->
id
;
return
$this
->
views
()
->
whereProfileId
(
$id
)
->
exists
();
return
StoryView
::
whereStoryId
(
$this
->
id
)
->
whereProfileId
(
Auth
::
user
()
->
profile
->
id
)
->
exists
();
}
public
function
permalink
()
{
return
url
(
"/story/
$this->id
"
);
}
}
app/Transformer/Api/StatusTransformer.php
View file @
700c7805
...
...
@@ -62,7 +62,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
public
function
includeMediaAttachments
(
Status
$status
)
{
return
Cache
::
remember
(
'status:transformer:media:attachments:'
.
$status
->
id
,
now
()
->
add
Day
s
(
14
),
function
()
use
(
$status
)
{
return
Cache
::
remember
(
'status:transformer:media:attachments:'
.
$status
->
id
,
now
()
->
add
Minute
s
(
14
),
function
()
use
(
$status
)
{
if
(
in_array
(
$status
->
type
,
[
'photo'
,
'video'
,
'video:album'
,
'photo:album'
,
'loop'
,
'photo:video:album'
]))
{
$media
=
$status
->
media
()
->
orderBy
(
'order'
)
->
get
();
return
$this
->
collection
(
$media
,
new
MediaTransformer
());
...
...
app/Util/ActivityPub/Helpers.php
View file @
700c7805
...
...
@@ -406,7 +406,6 @@ class Helpers {
$remoteUsername
=
"@
{
$username
}
@
{
$domain
}
"
;
abort_if
(
!
self
::
validateUrl
(
$res
[
'inbox'
]),
400
);
abort_if
(
!
self
::
validateUrl
(
$res
[
'outbox'
]),
400
);
abort_if
(
!
self
::
validateUrl
(
$res
[
'id'
]),
400
);
$profile
=
Profile
::
whereRemoteUrl
(
$res
[
'id'
])
->
first
();
...
...
@@ -451,4 +450,20 @@ class Helpers {
$response
=
curl_exec
(
$ch
);
return
;
}
public
static
function
apSignedPostRequest
(
$senderProfile
,
$url
,
$body
)
{
abort_if
(
!
self
::
validateUrl
(
$url
),
400
);
$payload
=
json_encode
(
$body
);
$headers
=
HttpSignature
::
sign
(
$senderProfile
,
$url
,
$body
);
$ch
=
curl_init
(
$url
);
curl_setopt
(
$ch
,
CURLOPT_RETURNTRANSFER
,
true
);
curl_setopt
(
$ch
,
CURLOPT_HTTPHEADER
,
$headers
);
curl_setopt
(
$ch
,
CURLOPT_POSTFIELDS
,
$payload
);
curl_setopt
(
$ch
,
CURLOPT_HEADER
,
true
);
$response
=
curl_exec
(
$ch
);
return
;
}
}
app/Util/Lexer/RestrictedNames.php
View file @
700c7805
...
...
@@ -12,7 +12,6 @@ class RestrictedNames
'download'
,
'domainadmin'
,
'domainadministrator'
,
'email'
,
'errors'
,
'events'
,
'example'
,
...
...
@@ -26,7 +25,7 @@ class RestrictedNames
'hostmaster'
,
'imap'
,
'info'
,
'info'
,
'info
rmation
'
,
'is'
,
'isatap'
,
'it'
,
...
...
@@ -142,6 +141,8 @@ class RestrictedNames
'drives'
,
'driver'
,
'e'
,
'email'
,
'emails'
,
'error'
,
'explore'
,
'export'
,
...
...
@@ -206,6 +207,10 @@ class RestrictedNames
'news'
,
'news'
,
'newsfeed'
,
'newsroom'
,
'newsrooms'
,
'news-room'
,
'news-rooms'
,
'o'
,
'oauth'
,
'official'
,
...
...
app/Util/RateLimit/User.php
View file @
700c7805
...
...
@@ -6,7 +6,7 @@ trait User {
public
function
isTrustedAccount
()
{
return
$this
->
created_at
->
lt
(
now
()
->
subDays
(
2
0
));
return
$this
->
created_at
->
lt
(
now
()
->
subDays
(
6
0
));
}
public
function
getMaxPostsPerHourAttribute
()
...
...
@@ -98,4 +98,19 @@ trait User {
{
return
5000
;
}
public
function
getMaxStoriesPerHourAttribute
()
{
return
20
;
}
public
function
getMaxStoriesPerDayAttribute
()
{
return
30
;
}
public
function
getMaxStoryDeletePerDayAttribute
()
{
return
35
;