mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 22:41:27 +00:00
commit
a87c236c00
21 changed files with 538 additions and 80 deletions
|
@ -10,6 +10,8 @@
|
||||||
- Updated RateLimit, add max post edits per hour and day ([51fbfcdc](https://github.com/pixelfed/pixelfed/commit/51fbfcdc))
|
- Updated RateLimit, add max post edits per hour and day ([51fbfcdc](https://github.com/pixelfed/pixelfed/commit/51fbfcdc))
|
||||||
- Updated Timeline.vue, move announcements from sidebar to top of timeline ([228f5044](https://github.com/pixelfed/pixelfed/commit/228f5044))
|
- Updated Timeline.vue, move announcements from sidebar to top of timeline ([228f5044](https://github.com/pixelfed/pixelfed/commit/228f5044))
|
||||||
- Updated lexer autolinker and extractor, add support for mentioned usernames containing dashes, periods and underscore characters ([f911c96d](https://github.com/pixelfed/pixelfed/commit/f911c96d))
|
- Updated lexer autolinker and extractor, add support for mentioned usernames containing dashes, periods and underscore characters ([f911c96d](https://github.com/pixelfed/pixelfed/commit/f911c96d))
|
||||||
|
- Updated Story apis, move FE to v0 and add v1 for oauth clients ([92654fab](https://github.com/pixelfed/pixelfed/commit/92654fab))
|
||||||
|
- Updated robots.txt ([25101901](https://github.com/pixelfed/pixelfed/commit/25101901))
|
||||||
|
|
||||||
## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
|
## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -44,6 +44,47 @@ class StoryGC extends Command
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->directoryScan();
|
||||||
|
$this->deleteViews();
|
||||||
|
$this->deleteStories();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function directoryScan()
|
||||||
|
{
|
||||||
|
$day = now()->day;
|
||||||
|
|
||||||
|
if($day !== 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
|
||||||
|
|
||||||
|
$t1 = Storage::directories('public/_esm.t1');
|
||||||
|
$t2 = Storage::directories('public/_esm.t2');
|
||||||
|
|
||||||
|
$dirs = array_merge($t1, $t2);
|
||||||
|
|
||||||
|
foreach($dirs as $dir) {
|
||||||
|
$hash = last(explode('/', $dir));
|
||||||
|
if($hash != $monthHash) {
|
||||||
|
$this->info('Found directory to delete: ' . $dir);
|
||||||
|
$this->deleteDirectory($dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function deleteDirectory($path)
|
||||||
|
{
|
||||||
|
Storage::deleteDirectory($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function deleteViews()
|
||||||
|
{
|
||||||
|
StoryView::where('created_at', '<', now()->subDays(2))->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function deleteStories()
|
||||||
{
|
{
|
||||||
$stories = Story::where('expires_at', '<', now())->take(50)->get();
|
$stories = Story::where('expires_at', '<', now())->take(50)->get();
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,10 @@ use App\Jobs\VideoPipeline\{
|
||||||
VideoPostProcess,
|
VideoPostProcess,
|
||||||
VideoThumbnail
|
VideoThumbnail
|
||||||
};
|
};
|
||||||
use App\Services\NotificationService;
|
use App\Services\{
|
||||||
|
NotificationService,
|
||||||
|
SearchApiV2Service
|
||||||
|
};
|
||||||
|
|
||||||
class ApiV1Controller extends Controller
|
class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
|
@ -367,15 +370,15 @@ class ApiV1Controller extends Controller
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$target = Profile::where('id', '!=', $user->id)
|
$target = Profile::where('id', '!=', $user->profile_id)
|
||||||
->whereNull('status')
|
->whereNull('status')
|
||||||
->findOrFail($item);
|
->findOrFail($id);
|
||||||
|
|
||||||
$private = (bool) $target->is_private;
|
$private = (bool) $target->is_private;
|
||||||
$remote = (bool) $target->domain;
|
$remote = (bool) $target->domain;
|
||||||
$blocked = UserFilter::whereUserId($target->id)
|
$blocked = UserFilter::whereUserId($target->id)
|
||||||
->whereFilterType('block')
|
->whereFilterType('block')
|
||||||
->whereFilterableId($user->id)
|
->whereFilterableId($user->profile_id)
|
||||||
->whereFilterableType('App\Profile')
|
->whereFilterableType('App\Profile')
|
||||||
->exists();
|
->exists();
|
||||||
|
|
||||||
|
@ -383,7 +386,7 @@ class ApiV1Controller extends Controller
|
||||||
abort(400, 'You cannot follow this user.');
|
abort(400, 'You cannot follow this user.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$isFollowing = Follower::whereProfileId($user->id)
|
$isFollowing = Follower::whereProfileId($user->profile_id)
|
||||||
->whereFollowingId($target->id)
|
->whereFollowingId($target->id)
|
||||||
->exists();
|
->exists();
|
||||||
|
|
||||||
|
@ -396,42 +399,42 @@ class ApiV1Controller extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limits, max 7500 followers per account
|
// Rate limits, max 7500 followers per account
|
||||||
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
|
if($user->profile->following()->count() >= Follower::MAX_FOLLOWING) {
|
||||||
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
|
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limits, follow 30 accounts per hour max
|
// Rate limits, follow 30 accounts per hour max
|
||||||
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
if($user->profile->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
||||||
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
||||||
}
|
}
|
||||||
|
|
||||||
if($private == true) {
|
if($private == true) {
|
||||||
$follow = FollowRequest::firstOrCreate([
|
$follow = FollowRequest::firstOrCreate([
|
||||||
'follower_id' => $user->id,
|
'follower_id' => $user->profile_id,
|
||||||
'following_id' => $target->id
|
'following_id' => $target->id
|
||||||
]);
|
]);
|
||||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||||
(new FollowerController())->sendFollow($user, $target);
|
(new FollowerController())->sendFollow($user->profile, $target);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$follower = new Follower();
|
$follower = new Follower();
|
||||||
$follower->profile_id = $user->id;
|
$follower->profile_id = $user->profile_id;
|
||||||
$follower->following_id = $target->id;
|
$follower->following_id = $target->id;
|
||||||
$follower->save();
|
$follower->save();
|
||||||
|
|
||||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||||
(new FollowerController())->sendFollow($user, $target);
|
(new FollowerController())->sendFollow($user->profile, $target);
|
||||||
}
|
}
|
||||||
FollowPipeline::dispatch($follower);
|
FollowPipeline::dispatch($follower);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache::forget('profile:following:'.$target->id);
|
Cache::forget('profile:following:'.$target->id);
|
||||||
Cache::forget('profile:followers:'.$target->id);
|
Cache::forget('profile:followers:'.$target->id);
|
||||||
Cache::forget('profile:following:'.$user->id);
|
Cache::forget('profile:following:'.$user->profile_id);
|
||||||
Cache::forget('profile:followers:'.$user->id);
|
Cache::forget('profile:followers:'.$user->profile_id);
|
||||||
Cache::forget('api:local:exp:rec:'.$user->id);
|
Cache::forget('api:local:exp:rec:'.$user->profile_id);
|
||||||
Cache::forget('user:account:id:'.$target->user_id);
|
Cache::forget('user:account:id:'.$target->user_id);
|
||||||
Cache::forget('user:account:id:'.$user->user_id);
|
Cache::forget('user:account:id:'.$user->id);
|
||||||
|
|
||||||
$resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
|
$resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
|
||||||
$res = $this->fractal->createData($resource)->toArray();
|
$res = $this->fractal->createData($resource)->toArray();
|
||||||
|
@ -452,14 +455,14 @@ class ApiV1Controller extends Controller
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$target = Profile::where('id', '!=', $user->id)
|
$target = Profile::where('id', '!=', $user->profile_id)
|
||||||
->whereNull('status')
|
->whereNull('status')
|
||||||
->findOrFail($item);
|
->findOrFail($id);
|
||||||
|
|
||||||
$private = (bool) $target->is_private;
|
$private = (bool) $target->is_private;
|
||||||
$remote = (bool) $target->domain;
|
$remote = (bool) $target->domain;
|
||||||
|
|
||||||
$isFollowing = Follower::whereProfileId($user->id)
|
$isFollowing = Follower::whereProfileId($user->profile_id)
|
||||||
->whereFollowingId($target->id)
|
->whereFollowingId($target->id)
|
||||||
->exists();
|
->exists();
|
||||||
|
|
||||||
|
@ -471,29 +474,29 @@ class ApiV1Controller extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limits, follow 30 accounts per hour max
|
// Rate limits, follow 30 accounts per hour max
|
||||||
if($user->following()->where('followers.updated_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
if($user->profile->following()->where('followers.updated_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
||||||
abort(400, 'You can only follow or unfollow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
abort(400, 'You can only follow or unfollow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
||||||
}
|
}
|
||||||
|
|
||||||
FollowRequest::whereFollowerId($user->id)
|
FollowRequest::whereFollowerId($user->profile_id)
|
||||||
->whereFollowingId($target->id)
|
->whereFollowingId($target->id)
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
Follower::whereProfileId($user->id)
|
Follower::whereProfileId($user->profile_id)
|
||||||
->whereFollowingId($target->id)
|
->whereFollowingId($target->id)
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||||
(new FollowerController())->sendUndoFollow($user, $target);
|
(new FollowerController())->sendUndoFollow($user->profile, $target);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache::forget('profile:following:'.$target->id);
|
Cache::forget('profile:following:'.$target->id);
|
||||||
Cache::forget('profile:followers:'.$target->id);
|
Cache::forget('profile:followers:'.$target->id);
|
||||||
Cache::forget('profile:following:'.$user->id);
|
Cache::forget('profile:following:'.$user->profile_id);
|
||||||
Cache::forget('profile:followers:'.$user->id);
|
Cache::forget('profile:followers:'.$user->profile_id);
|
||||||
Cache::forget('api:local:exp:rec:'.$user->id);
|
Cache::forget('api:local:exp:rec:'.$user->profile_id);
|
||||||
Cache::forget('user:account:id:'.$target->user_id);
|
Cache::forget('user:account:id:'.$target->user_id);
|
||||||
Cache::forget('user:account:id:'.$user->user_id);
|
Cache::forget('user:account:id:'.$user->id);
|
||||||
|
|
||||||
$resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
|
$resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
|
||||||
$res = $this->fractal->createData($resource)->toArray();
|
$res = $this->fractal->createData($resource)->toArray();
|
||||||
|
@ -1164,34 +1167,43 @@ class ApiV1Controller extends Controller
|
||||||
public function accountNotifications(Request $request)
|
public function accountNotifications(Request $request)
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'page' => 'nullable|integer|min:1|max:10',
|
|
||||||
'limit' => 'nullable|integer|min:1|max:80',
|
'limit' => 'nullable|integer|min:1|max:80',
|
||||||
'max_id' => 'nullable|integer|min:1',
|
'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
||||||
'min_id' => 'nullable|integer|min:0',
|
'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
||||||
|
'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
$limit = $request->input('limit') ?? 20;
|
$limit = $request->input('limit', 20);
|
||||||
$timeago = now()->subMonths(6);
|
$timeago = now()->subMonths(6);
|
||||||
|
|
||||||
|
$since = $request->input('since_id');
|
||||||
$min = $request->input('min_id');
|
$min = $request->input('min_id');
|
||||||
$max = $request->input('max_id');
|
$max = $request->input('max_id');
|
||||||
if($min || $max) {
|
|
||||||
$dir = $min ? '>' : '<';
|
abort_if(!$since && !$min && !$max, 400);
|
||||||
$id = $min ?? $max;
|
|
||||||
|
$dir = $since ? '>' : ($min ? '>=' : '<');
|
||||||
|
$id = $since ?? $min ?? $max;
|
||||||
|
|
||||||
$notifications = Notification::whereProfileId($pid)
|
$notifications = Notification::whereProfileId($pid)
|
||||||
->whereDate('created_at', '>', $timeago)
|
|
||||||
->where('id', $dir, $id)
|
->where('id', $dir, $id)
|
||||||
->orderByDesc('created_at')
|
->whereDate('created_at', '>', $timeago)
|
||||||
|
->orderByDesc('id')
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
->get();
|
->get();
|
||||||
} else {
|
|
||||||
$notifications = Notification::whereProfileId($pid)
|
$resource = new Fractal\Resource\Collection(
|
||||||
->whereDate('created_at', '>', $timeago)
|
$notifications,
|
||||||
->orderByDesc('created_at')
|
new NotificationTransformer()
|
||||||
->simplePaginate($limit);
|
);
|
||||||
}
|
|
||||||
$resource = new Fractal\Resource\Collection($notifications, new NotificationTransformer());
|
$res = $this->fractal
|
||||||
$res = $this->fractal->createData($resource)->toArray();
|
->createData($resource)
|
||||||
|
->toArray();
|
||||||
|
|
||||||
return response()->json($res);
|
return response()->json($res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1696,4 +1708,30 @@ class ApiV1Controller extends Controller
|
||||||
$res = [];
|
$res = [];
|
||||||
return response()->json($res);
|
return response()->json($res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v2/search
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function searchV2(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'q' => 'required|string|min:1|max:80',
|
||||||
|
'account_id' => 'nullable|string',
|
||||||
|
'max_id' => 'nullable|string',
|
||||||
|
'min_id' => 'nullable|string',
|
||||||
|
'type' => 'nullable|in:accounts,hashtags,statuses',
|
||||||
|
'exclude_unreviewed' => 'nullable',
|
||||||
|
'resolve' => 'nullable',
|
||||||
|
'limit' => 'nullable|integer|max:40',
|
||||||
|
'offset' => 'nullable|integer',
|
||||||
|
'following' => 'nullable'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return SearchApiV2Service::query($request);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,12 +16,6 @@ use App\Services\FollowerService;
|
||||||
|
|
||||||
class StoryController extends Controller
|
class StoryController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
public function construct()
|
|
||||||
{
|
|
||||||
$this->middleware('auth');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function apiV1Add(Request $request)
|
public function apiV1Add(Request $request)
|
||||||
{
|
{
|
||||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||||
|
@ -66,8 +60,8 @@ class StoryController extends Controller
|
||||||
protected function storePhoto($photo)
|
protected function storePhoto($photo)
|
||||||
{
|
{
|
||||||
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
|
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
|
||||||
$sid = Str::uuid();
|
$sid = (string) Str::uuid();
|
||||||
$rid = Str::random(6).'.'.Str::random(9);
|
$rid = Str::random(9).'.'.Str::random(9);
|
||||||
$mimes = explode(',', config('pixelfed.media_types'));
|
$mimes = explode(',', config('pixelfed.media_types'));
|
||||||
if(in_array($photo->getMimeType(), [
|
if(in_array($photo->getMimeType(), [
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
|
@ -77,7 +71,7 @@ class StoryController extends Controller
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$storagePath = "public/_esm.t1/{$monthHash}/{$sid}/{$rid}";
|
$storagePath = "public/_esm.t2/{$monthHash}/{$sid}/{$rid}";
|
||||||
$path = $photo->store($storagePath);
|
$path = $photo->store($storagePath);
|
||||||
$fpath = storage_path('app/' . $path);
|
$fpath = storage_path('app/' . $path);
|
||||||
$img = Intervention::make($fpath);
|
$img = Intervention::make($fpath);
|
||||||
|
@ -175,6 +169,39 @@ class StoryController extends Controller
|
||||||
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
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' => 'photo',
|
||||||
|
'length' => 3,
|
||||||
|
'src' => url(Storage::url($story->path)),
|
||||||
|
'preview' => null,
|
||||||
|
'link' => null,
|
||||||
|
'linkText' => null,
|
||||||
|
'time' => $story->created_at->format('U'),
|
||||||
|
'expires_at' => (int) $story->expires_at->format('U'),
|
||||||
|
'seen' => $story->seen()
|
||||||
|
];
|
||||||
|
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
public function apiV1Profile(Request $request, $id)
|
public function apiV1Profile(Request $request, $id)
|
||||||
{
|
{
|
||||||
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||||
|
@ -232,24 +259,33 @@ class StoryController extends Controller
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'id' => 'required|integer|min:1|exists:stories',
|
'id' => 'required|integer|min:1|exists:stories',
|
||||||
]);
|
]);
|
||||||
|
$id = $request->input('id');
|
||||||
|
$authed = $request->user()->profile;
|
||||||
|
$story = Story::with('profile')
|
||||||
|
->where('expires_at', '>', now())
|
||||||
|
->orderByDesc('expires_at')
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
$profile = $story->profile;
|
||||||
|
if($story->profile_id == $authed->id) {
|
||||||
|
$publicOnly = true;
|
||||||
|
} else {
|
||||||
|
$publicOnly = (bool) $profile->followedBy($authed);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort_if(!$publicOnly, 403);
|
||||||
|
|
||||||
StoryView::firstOrCreate([
|
StoryView::firstOrCreate([
|
||||||
'story_id' => $request->input('id'),
|
'story_id' => $id,
|
||||||
'profile_id' => $request->user()->profile_id
|
'profile_id' => $authed->id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return ['code' => 200];
|
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)
|
public function apiV1Exists(Request $request, $id)
|
||||||
{
|
{
|
||||||
abort_if(!config('instance.stories.enabled'), 404);
|
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||||
|
|
||||||
$res = (bool) Story::whereProfileId($id)
|
$res = (bool) Story::whereProfileId($id)
|
||||||
->where('expires_at', '>', now())
|
->where('expires_at', '>', now())
|
||||||
|
@ -258,8 +294,54 @@ class StoryController extends Controller
|
||||||
return response()->json($res);
|
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' => 'photo',
|
||||||
|
'length' => 3,
|
||||||
|
'src' => url(Storage::url($s->path)),
|
||||||
|
'preview' => null,
|
||||||
|
'link' => null,
|
||||||
|
'linkText' => null,
|
||||||
|
'time' => $s->created_at->format('U'),
|
||||||
|
'expires_at' => (int) $s->expires_at->format('U'),
|
||||||
|
'seen' => true
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
$ts = count($stories) ? last($stories)['time'] : null;
|
||||||
|
$res = [
|
||||||
|
'id' => (string) $profile->id,
|
||||||
|
'photo' => $profile->avatarUrl(),
|
||||||
|
'name' => $profile->username,
|
||||||
|
'link' => $profile->url(),
|
||||||
|
'lastUpdated' => $ts,
|
||||||
|
'seen' => true,
|
||||||
|
'items' => $stories
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||||
|
|
||||||
|
return view('stories.compose');
|
||||||
|
}
|
||||||
|
|
||||||
public function iRedirect(Request $request)
|
public function iRedirect(Request $request)
|
||||||
{
|
{
|
||||||
|
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
abort_if(!$user, 404);
|
abort_if(!$user, 404);
|
||||||
$username = $user->username;
|
$username = $user->username;
|
||||||
|
|
234
app/Services/SearchApiV2Service.php
Normal file
234
app/Services/SearchApiV2Service.php
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Cache;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use App\{Hashtag, Profile, Status};
|
||||||
|
use App\Transformer\Api\AccountTransformer;
|
||||||
|
use App\Transformer\Api\StatusTransformer;
|
||||||
|
use League\Fractal;
|
||||||
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
|
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class SearchApiV2Service
|
||||||
|
{
|
||||||
|
private $query;
|
||||||
|
|
||||||
|
public static function query($query)
|
||||||
|
{
|
||||||
|
return (new self)->run($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run($query)
|
||||||
|
{
|
||||||
|
$this->query = $query;
|
||||||
|
|
||||||
|
if($query->has('resolve') &&
|
||||||
|
$query->resolve == true &&
|
||||||
|
Helpers::validateUrl(urldecode($query->input('q')))
|
||||||
|
) {
|
||||||
|
return $this->resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($query->has('type')) {
|
||||||
|
switch ($query->input('type')) {
|
||||||
|
case 'accounts':
|
||||||
|
return [
|
||||||
|
'accounts' => $this->accounts(),
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => []
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'hashtags':
|
||||||
|
return [
|
||||||
|
'accounts' => [],
|
||||||
|
'hashtags' => $this->hashtags(),
|
||||||
|
'statuses' => []
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'statuses':
|
||||||
|
return [
|
||||||
|
'accounts' => [],
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => $this->statuses()
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($query->has('account_id')) {
|
||||||
|
return [
|
||||||
|
'accounts' => [],
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => $this->statusesById()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'accounts' => $this->accounts(),
|
||||||
|
'hashtags' => $this->hashtags(),
|
||||||
|
'statuses' => $this->statuses()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolve()
|
||||||
|
{
|
||||||
|
$query = urldecode($this->query->input('q'));
|
||||||
|
if(Str::startsWith($query, '@') == true) {
|
||||||
|
return WebfingerService::lookup($this->query->input('q'));
|
||||||
|
} else if (Str::startsWith($query, 'https://') == true) {
|
||||||
|
return $this->resolveQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function accounts()
|
||||||
|
{
|
||||||
|
$limit = $this->query->input('limit', 20);
|
||||||
|
$query = '%' . $this->query->input('q') . '%';
|
||||||
|
$results = Profile::whereNull('status')
|
||||||
|
->where('username', 'like', $query)
|
||||||
|
->when($this->query->input('offset') != null, function($q, $offset) {
|
||||||
|
return $q->offset($offset);
|
||||||
|
})
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Collection($results, new AccountTransformer());
|
||||||
|
return $fractal->createData($resource)->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function hashtags()
|
||||||
|
{
|
||||||
|
$limit = $this->query->input('limit', 20);
|
||||||
|
$query = '%' . $this->query->input('q') . '%';
|
||||||
|
return Hashtag::whereIsBanned(false)
|
||||||
|
->where('name', 'like', $query)
|
||||||
|
->when($this->query->input('offset') != null, function($q, $offset) {
|
||||||
|
return $q->offset($offset);
|
||||||
|
})
|
||||||
|
->limit($limit)
|
||||||
|
->get()
|
||||||
|
->map(function($tag) {
|
||||||
|
return [
|
||||||
|
'name' => $tag->name,
|
||||||
|
'url' => $tag->url(),
|
||||||
|
'history' => []
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function statuses()
|
||||||
|
{
|
||||||
|
$limit = $this->query->input('limit', 20);
|
||||||
|
$query = '%' . $this->query->input('q') . '%';
|
||||||
|
$results = Status::where('caption', 'like', $query)
|
||||||
|
->whereScope('public')
|
||||||
|
->when($this->query->input('offset') != null, function($q, $offset) {
|
||||||
|
return $q->offset($offset);
|
||||||
|
})
|
||||||
|
->limit($limit)
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Collection($results, new StatusTransformer());
|
||||||
|
return $fractal->createData($resource)->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function statusesById()
|
||||||
|
{
|
||||||
|
$accountId = $this->query->input('account_id');
|
||||||
|
$limit = $this->query->input('limit', 20);
|
||||||
|
$query = '%' . $this->query->input('q') . '%';
|
||||||
|
$results = Status::where('caption', 'like', $query)
|
||||||
|
->whereProfileId($accountId)
|
||||||
|
->when($this->query->input('offset') != null, function($q, $offset) {
|
||||||
|
return $q->offset($offset);
|
||||||
|
})
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Collection($results, new StatusTransformer());
|
||||||
|
return $fractal->createData($resource)->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveQuery()
|
||||||
|
{
|
||||||
|
$query = urldecode($this->query->input('q'));
|
||||||
|
if(Helpers::validateLocalUrl($query)) {
|
||||||
|
if(Str::contains($query, '/p/')) {
|
||||||
|
return $this->resolveLocalStatus();
|
||||||
|
} else {
|
||||||
|
return $this->resolveLocalProfile();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
'accounts' => [],
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveLocalStatus()
|
||||||
|
{
|
||||||
|
$query = urldecode($this->query->input('q'));
|
||||||
|
$query = last(explode('/', $query));
|
||||||
|
$status = Status::whereNull('uri')
|
||||||
|
->whereScope('public')
|
||||||
|
->find($query);
|
||||||
|
|
||||||
|
if(!$status) {
|
||||||
|
return [
|
||||||
|
'accounts' => [],
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Item($status, new StatusTransformer());
|
||||||
|
return [
|
||||||
|
'accounts' => [],
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => $fractal->createData($resource)->toArray()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveLocalProfile()
|
||||||
|
{
|
||||||
|
$query = urldecode($this->query->input('q'));
|
||||||
|
$query = last(explode('/', $query));
|
||||||
|
$profile = Profile::whereNull('status')
|
||||||
|
->whereNull('domain')
|
||||||
|
->whereUsername($query)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if(!$profile) {
|
||||||
|
return [
|
||||||
|
'accounts' => [],
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
|
||||||
|
return [
|
||||||
|
'accounts' => $fractal->createData($resource)->toArray(),
|
||||||
|
'hashtags' => [],
|
||||||
|
'statuses' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
app/Services/WebfingerService.php
Normal file
40
app/Services/WebfingerService.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Cache;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use App\Util\Webfinger\WebfingerUrl;
|
||||||
|
use Zttp\Zttp;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
|
use App\Transformer\Api\AccountTransformer;
|
||||||
|
use League\Fractal;
|
||||||
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
|
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||||
|
|
||||||
|
class WebfingerService
|
||||||
|
{
|
||||||
|
public static function lookup($query)
|
||||||
|
{
|
||||||
|
return (new self)->run($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run($query)
|
||||||
|
{
|
||||||
|
$url = WebfingerUrl::generateWebfingerUrl($query);
|
||||||
|
if(!Helpers::validateUrl($url)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$res = Zttp::get($url);
|
||||||
|
$webfinger = $res->json();
|
||||||
|
if(!isset($webfinger['links'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$profile = Helpers::profileFetch($webfinger['links'][0]['href']);
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
|
||||||
|
$res = $fractal->createData($resource)->toArray();
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
}
|
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/story-compose.js
vendored
BIN
public/js/story-compose.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -1,2 +1,4 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow:
|
Disallow: /discover/places/
|
||||||
|
Disallow: /stories/
|
||||||
|
Disallow: /i/
|
|
@ -680,6 +680,7 @@ export default {
|
||||||
let self = this;
|
let self = this;
|
||||||
self.status = response.data.status;
|
self.status = response.data.status;
|
||||||
self.user = response.data.user;
|
self.user = response.data.user;
|
||||||
|
window._sharedData.curUser = self.user;
|
||||||
self.media = self.status.media_attachments;
|
self.media = self.status.media_attachments;
|
||||||
self.reactions = response.data.reactions;
|
self.reactions = response.data.reactions;
|
||||||
self.likes = response.data.likes;
|
self.likes = response.data.likes;
|
||||||
|
|
|
@ -642,7 +642,7 @@
|
||||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
|
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
|
||||||
this.user = res.data;
|
this.user = res.data;
|
||||||
if(res.data.id == this.profileId || this.relationship.following == true) {
|
if(res.data.id == this.profileId || this.relationship.following == true) {
|
||||||
axios.get('/api/stories/v1/exists/' + this.profileId)
|
axios.get('/api/stories/v0/exists/' + this.profileId)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.hasStory = res.data == true;
|
this.hasStory = res.data == true;
|
||||||
})
|
})
|
||||||
|
|
|
@ -177,7 +177,7 @@
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.mediaWatcher();
|
this.mediaWatcher();
|
||||||
axios.get('/api/stories/v1/fetch/' + this.profileId)
|
axios.get('/api/stories/v0/fetch/' + this.profileId)
|
||||||
.then(res => this.stories = res.data);
|
.then(res => this.stories = res.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.post('/api/stories/v1/add', form, xhrConfig)
|
axios.post('/api/stories/v0/add', form, xhrConfig)
|
||||||
.then(function(e) {
|
.then(function(e) {
|
||||||
self.uploadProgress = 100;
|
self.uploadProgress = 100;
|
||||||
self.uploading = false;
|
self.uploading = false;
|
||||||
|
@ -264,7 +264,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.delete('/api/stories/v1/delete/' + story.id)
|
axios.delete('/api/stories/v0/delete/' + story.id)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.stories.splice(index, 1);
|
this.stories.splice(index, 1);
|
||||||
if(this.stories.length == 0) {
|
if(this.stories.length == 0) {
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
fetchStories() {
|
fetchStories() {
|
||||||
axios.get('/api/stories/v1/recent')
|
axios.get('/api/stories/v0/recent')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
let stories = new Zuck('storyContainer', {
|
let stories = new Zuck('storyContainer', {
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
data.forEach(d => {
|
data.forEach(d => {
|
||||||
let url = '/api/stories/v1/fetch/' + d.pid;
|
let url = '/api/stories/v0/fetch/' + d.pid;
|
||||||
axios.get(url)
|
axios.get(url)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
res.data.forEach(item => {
|
res.data.forEach(item => {
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
fetchStories() {
|
fetchStories() {
|
||||||
axios.get('/api/stories/v1/profile/' + this.pid)
|
axios.get('/api/stories/v0/profile/' + this.pid)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
if(data.length == 0) {
|
if(data.length == 0) {
|
||||||
|
|
|
@ -1424,7 +1424,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
hasStory() {
|
hasStory() {
|
||||||
axios.get('/api/stories/v1/exists/'+this.profile.id)
|
axios.get('/api/stories/v0/exists/'+this.profile.id)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.userStory = res.data;
|
this.userStory = res.data;
|
||||||
})
|
})
|
||||||
|
|
2
resources/assets/sass/_variables.scss
vendored
2
resources/assets/sass/_variables.scss
vendored
|
@ -21,3 +21,5 @@ $white: white;
|
||||||
$theme-colors: (
|
$theme-colors: (
|
||||||
'primary': #08d
|
'primary': #08d
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$card-cap-bg: $white;
|
||||||
|
|
|
@ -67,4 +67,18 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
|
||||||
Route::get('timelines/public', 'Api\ApiV1Controller@timelinePublic');
|
Route::get('timelines/public', 'Api\ApiV1Controller@timelinePublic');
|
||||||
Route::get('timelines/tag/{hashtag}', 'Api\ApiV1Controller@timelineHashtag')->middleware($middleware);
|
Route::get('timelines/tag/{hashtag}', 'Api\ApiV1Controller@timelineHashtag')->middleware($middleware);
|
||||||
});
|
});
|
||||||
|
Route::group(['prefix' => 'stories'], function () use($middleware) {
|
||||||
|
Route::get('v1/me', 'StoryController@apiV1Me');
|
||||||
|
Route::get('v1/recent', 'StoryController@apiV1Recent');
|
||||||
|
Route::post('v1/add', 'StoryController@apiV1Add')->middleware(array_merge($middleware, ['throttle:maxStoriesPerDay,1440']));
|
||||||
|
Route::get('v1/item/{id}', 'StoryController@apiV1Item');
|
||||||
|
Route::get('v1/fetch/{id}', 'StoryController@apiV1Fetch');
|
||||||
|
Route::get('v1/profile/{id}', 'StoryController@apiV1Profile');
|
||||||
|
Route::get('v1/exists/{id}', 'StoryController@apiV1Exists');
|
||||||
|
Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete')->middleware(array_merge($middleware, ['throttle:maxStoryDeletePerDay,1440']));
|
||||||
|
Route::post('v1/viewed', 'StoryController@apiV1Viewed');
|
||||||
|
});
|
||||||
|
Route::group(['prefix' => 'v2'], function() use($middleware) {
|
||||||
|
Route::get('search', 'Api\ApiV1Controller@searchV2')->middleware($middleware);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -179,12 +179,14 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
||||||
Route::post('moderate', 'Api\AdminApiController@moderate');
|
Route::post('moderate', 'Api\AdminApiController@moderate');
|
||||||
});
|
});
|
||||||
Route::group(['prefix' => 'stories'], function () {
|
Route::group(['prefix' => 'stories'], function () {
|
||||||
Route::get('v1/recent', 'StoryController@apiV1Recent');
|
Route::get('v0/recent', 'StoryController@apiV1Recent');
|
||||||
Route::post('v1/add', 'StoryController@apiV1Add')->middleware('throttle:maxStoriesPerDay,1440');
|
Route::post('v0/add', 'StoryController@apiV1Add')->middleware('throttle:maxStoriesPerDay,1440');
|
||||||
Route::get('v1/fetch/{id}', 'StoryController@apiV1Fetch');
|
Route::get('v0/fetch/{id}', 'StoryController@apiV1Fetch');
|
||||||
Route::get('v1/profile/{id}', 'StoryController@apiV1Profile');
|
Route::get('v0/profile/{id}', 'StoryController@apiV1Profile');
|
||||||
Route::get('v1/exists/{id}', 'StoryController@apiV1Exists');
|
Route::get('v0/exists/{id}', 'StoryController@apiV1Exists');
|
||||||
Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete')->middleware('throttle:maxStoryDeletePerDay,1440');
|
Route::delete('v0/delete/{id}', 'StoryController@apiV1Delete')->middleware('throttle:maxStoryDeletePerDay,1440');
|
||||||
|
Route::get('v0/me', 'StoryController@apiV1Me');
|
||||||
|
Route::get('v0/item/{id}', 'StoryController@apiV1Item');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue