diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e865b260..baf201a1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,10 +3,66 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.5...dev)
### Added
+- Added ```/api/v1/accounts/update_credentials``` endpoint [6afd6970](https://github.com/pixelfed/pixelfed/commit/6afd6970)
+- Added ```/api/v1/accounts/{id}/followers``` endpoint [41c91cba](https://github.com/pixelfed/pixelfed/commit/41c91cba)
+- Added ```/api/v1/accounts/{id}/following``` endpoint [607eb51b](https://github.com/pixelfed/pixelfed/commit/607eb51b)
+- Added ```/api/v1/accounts/{id}/statuses``` endpoint [8ce6c1f2](https://github.com/pixelfed/pixelfed/commit/8ce6c1f2)
+- Added ```/api/v1/accounts/{id}/follow``` endpoint [f3839026](https://github.com/pixelfed/pixelfed/commit/f3839026)
+- Added ```/api/v1/accounts/{id}/unfollow``` endpoint [fadc96b2](https://github.com/pixelfed/pixelfed/commit/fadc96b2)
+- Added ```/api/v1/accounts/relationships``` endpoint [4b9f7d6b](https://github.com/pixelfed/pixelfed/commit/4b9f7d6b)
+- Added ```/api/v1/accounts/search``` endpoint [b1fccf6d](https://github.com/pixelfed/pixelfed/commit/b1fccf6d)
+- Added ```/api/v1/blocks``` endpoint [ac9f1bc0](https://github.com/pixelfed/pixelfed/commit/ac9f1bc0)
+- Added ```/api/v1/accounts/{id}/block``` endpoint [c6b1ed97](https://github.com/pixelfed/pixelfed/commit/c6b1ed97)
+- Added ```/api/v1/accounts/{id}/unblock``` endpoint [35226c99](https://github.com/pixelfed/pixelfed/commit/35226c99)
+- Added ```/api/v1/custom_emojis``` endpoint [6e43431a](https://github.com/pixelfed/pixelfed/commit/6e43431a)
+- Added ```/api/v1/domain_blocks``` endpoint [83a6313f](https://github.com/pixelfed/pixelfed/commit/83a6313f)
+- Added ```/api/v1/endorsements``` endpoint [1f16221e](https://github.com/pixelfed/pixelfed/commit/1f16221e)
+- Added ```/api/v1/favourites``` endpoint [b9cc06da](https://github.com/pixelfed/pixelfed/commit/b9cc06da)
+- Added ```/api/v1/statuses/{id}/favourite``` endpoint [4edeba17](https://github.com/pixelfed/pixelfed/commit/4edeba17)
+- Added ```/api/v1/statuses/{id}/unfavourite``` endpoint [437e18e3](https://github.com/pixelfed/pixelfed/commit/437e18e3)
+- Added ```/api/v1/filters``` endpoint [b3d82edd](https://github.com/pixelfed/pixelfed/commit/b3d82edd)
+- Added ```/api/v1/follow_requests``` endpoint [97269136](https://github.com/pixelfed/pixelfed/commit/97269136)
+- Added ```/api/v1/follow_requests/{id}/authorize``` endpoint [7bdd9b2a](https://github.com/pixelfed/pixelfed/commit/7bdd9b2a)
+- Added ```/api/v1/follow_requests/{id}/reject``` endpoint [62aa922a](https://github.com/pixelfed/pixelfed/commit/62aa922a)
+- Added ```/api/v1/suggestions``` endpoint [e52aeeed](https://github.com/pixelfed/pixelfed/commit/e52aeeed)
+- Added ```/api/v1/lists``` endpoint [2a106c4e](https://github.com/pixelfed/pixelfed/commit/2a106c4e)
+- Added ```/api/v1/accounts/{id}/lists``` endpoint [dba172df](https://github.com/pixelfed/pixelfed/commit/dba172df)
+- Added ```/api/v1/lists/{id}/accounts``` endpoint [dba172df](https://github.com/pixelfed/pixelfed/commit/dba172df)
+- Added ```/api/v1/media``` endpoint [39f3e313](https://github.com/pixelfed/pixelfed/commit/39f3e313)
+- Added ```/api/v1/media/{id}``` endpoint [fcf231f4](https://github.com/pixelfed/pixelfed/commit/fcf231f4)
+- Added ```/api/v1/mutes``` endpoint [b280d183](https://github.com/pixelfed/pixelfed/commit/b280d183)
+- Added ```/api/v1/accounts/{id}/mute``` endpoint [3e98dce4](https://github.com/pixelfed/pixelfed/commit/3e98dce4)
+- Added ```/api/v1/accounts/{id}/unmute``` endpoint [41c96ddd](https://github.com/pixelfed/pixelfed/commit/41c96ddd)
+- Added ```/api/v1/notifications``` endpoint [39449f36](https://github.com/pixelfed/pixelfed/commit/39449f36)
+- Added ```/api/v1/timelines/home``` endpoint [cf3405d8](https://github.com/pixelfed/pixelfed/commit/cf3405d8)
+- Added ```/api/v1/conversations``` endpoint [336f9069](https://github.com/pixelfed/pixelfed/commit/336f9069)
+- Added ```/api/v1/timelines/public``` endpoint [f3eeb9c9](https://github.com/pixelfed/pixelfed/commit/f3eeb9c9)
+- Added ```/api/v1/statuses/{id}/card``` endpoint [92251208](https://github.com/pixelfed/pixelfed/commit/92251208)
+- Added ```/api/v1/statuses/{id}/reblogged_by``` endpoint [118006ed](https://github.com/pixelfed/pixelfed/commit/118006ed)
+- Added ```/api/v1/statuses/{id}/favourited_by``` endpoint [5cdff57d](https://github.com/pixelfed/pixelfed/commit/5cdff57d)
+- Added POST ```/api/v1/statuses``` endpoint [3aa729a3](https://github.com/pixelfed/pixelfed/commit/3aa729a3)
+- Added DELETE ```/api/v1/statuses``` endpoint [0a20b832](https://github.com/pixelfed/pixelfed/commit/0a20b832)
+- Added POST ```/api/v1/statuses/{id}/reblog``` endpoint [43cef282](https://github.com/pixelfed/pixelfed/commit/43cef282)
+- Added POST ```/api/v1/statuses/{id}/unreblog``` endpoint [3147fe5c](https://github.com/pixelfed/pixelfed/commit/3147fe5c)
+- Added GET ```/api/v1/timelines/tag/{hashtag}``` endpoint [2ff53be4](https://github.com/pixelfed/pixelfed/commit/2ff53be4)
### Fixed
+- Update developer settings pages, fix vue bug [cd365ab3](https://github.com/pixelfed/pixelfed/commit/cd365ab3)
+- Update User model, fix filter relationship [5a0c295e](https://github.com/pixelfed/pixelfed/commit/5a0c295e)
### Changed
+- Updated Inbox Accept.Follow to use id of remote object [#1715](https://github.com/pixelfed/pixelfed/pull/1715)
+- Update StatusTransformer, make spoiler_text non-nullable [b66cf9cd](https://github.com/pixelfed/pixelfed/commit/b66cf9cd)
+- Update FollowerController, make follow and unfollow methods public [6237897d](https://github.com/pixelfed/pixelfed/commit/6237897d)
+- Update DiscoverComponent, change api namespace [35275572](https://github.com/pixelfed/pixelfed/commit/35275572)
+
+## Deprecated
+- Removed deprecated AttachmentTransformer, superceeded by MediaTransformer [9b5aac4f](https://github.com/pixelfed/pixelfed/commit/9b5aac4f)
+
+### To enable mobile app support
+- 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)
diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php
index 3cc31514e..a90327f4d 100644
--- a/app/Http/Controllers/Api/ApiV1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Controller.php
@@ -5,24 +5,45 @@ namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Str;
-use App\Jobs\StatusPipeline\StatusDelete;
+use App\Util\ActivityPub\Helpers;
+use App\Util\Media\Filter;
use Laravel\Passport\Passport;
-use Auth, Cache, DB;
+use Auth, Cache, DB, URL;
use App\{
+ Follower,
+ FollowRequest,
Like,
Media,
+ Notification,
Profile,
- Status
+ Status,
+ UserFilter,
};
use League\Fractal;
-use App\Transformer\Api\{
+use App\Transformer\Api\Mastodon\v1\{
AccountTransformer,
- RelationshipTransformer,
+ MediaTransformer,
+ NotificationTransformer,
StatusTransformer,
};
+use App\Transformer\Api\{
+ RelationshipTransformer,
+};
+use App\Http\Controllers\FollowerController;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
-
+use App\Http\Controllers\StatusController;
+use App\Jobs\LikePipeline\LikePipeline;
+use App\Jobs\SharePipeline\SharePipeline;
+use App\Jobs\StatusPipeline\NewStatusPipeline;
+use App\Jobs\StatusPipeline\StatusDelete;
+use App\Jobs\FollowPipeline\FollowPipeline;
+use App\Jobs\ImageOptimizePipeline\ImageOptimize;
+use App\Jobs\VideoPipeline\{
+ VideoOptimize,
+ VideoPostProcess,
+ VideoThumbnail
+};
use App\Services\NotificationService;
class ApiV1Controller extends Controller
@@ -34,6 +55,7 @@ class ApiV1Controller extends Controller
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
+
public function apps(Request $request)
{
abort_if(!config('pixelfed.oauth_enabled'), 404);
@@ -66,9 +88,46 @@ class ApiV1Controller extends Controller
'client_secret' => $client->secret,
'vapid_key' => null
];
- return $res;
+ return response()->json($res, 200, [
+ 'Access-Control-Allow-Origin' => '*'
+ ]);
}
+ /**
+ * GET /api/v1/accounts/verify_credentials
+ *
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
+ public function verifyCredentials(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+ $id = $request->user()->id;
+
+ //$res = Cache::remember('mastoapi:user:account:id:'.$id, now()->addHours(6), function() use($id) {
+ $profile = Profile::whereNull('status')->whereUserId($id)->firstOrFail();
+ $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ $res['source'] = [
+ 'privacy' => $profile->is_private ? 'private' : 'public',
+ 'sensitive' => $profile->cw ? true : false,
+ 'language' => null,
+ 'note' => '',
+ 'fields' => []
+ ];
+ // return $res;
+ // });
+
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/accounts/{id}
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
public function accountById(Request $request, $id)
{
$profile = Profile::whereNull('status')->findOrFail($id);
@@ -78,15 +137,744 @@ class ApiV1Controller extends Controller
return response()->json($res);
}
- public function statusById(Request $request, $id)
+ /**
+ * PATCH /api/v1/accounts/update_credentials
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
+ public function accountUpdateCredentials(Request $request)
{
- $status = Status::whereVisibility('public')->findOrFail($id);
- $resource = new Fractal\Resource\Item($status, new StatusTransformer());
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request, [
+ 'display_name' => 'nullable|string',
+ 'note' => 'nullable|string',
+ 'locked' => 'nullable|boolean',
+ // 'source.privacy' => 'nullable|in:unlisted,public,private',
+ // 'source.sensitive' => 'nullable|boolean'
+ ]);
+
+ $user = $request->user();
+ $profile = $user->profile;
+
+ $displayName = $request->input('display_name');
+ $note = $request->input('note');
+ $locked = $request->input('locked');
+ // $privacy = $request->input('source.privacy');
+ // $sensitive = $request->input('source.sensitive');
+
+ $changes = false;
+
+ if($displayName !== $user->name) {
+ $user->name = $displayName;
+ $profile->name = $displayName;
+ $changes = true;
+ }
+
+ if($note !== $profile->bio) {
+ $profile->bio = e($note);
+ $changes = true;
+ }
+
+ if(!is_null($locked)) {
+ $profile->is_private = $locked;
+ $changes = true;
+ }
+
+ if($changes) {
+ $user->save();
+ $profile->save();
+ }
+
+ $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
+ /**
+ * GET /api/v1/accounts/{id}/followers
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
+ public function accountFollowersById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+ $profile = Profile::whereNull('status')->findOrFail($id);
+
+ if($profile->domain) {
+ $res = [];
+ } else {
+ $settings = $profile->user->settings;
+ if($settings->show_profile_followers == true) {
+ $limit = $request->input('limit') ?? 40;
+ $followers = $profile->followers()->paginate($limit);
+ $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ } else {
+ $res = [];
+ }
+ }
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/accounts/{id}/following
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
+ public function accountFollowingById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+ $profile = Profile::whereNull('status')->findOrFail($id);
+
+ if($profile->domain) {
+ $res = [];
+ } else {
+ $settings = $profile->user->settings;
+ if($settings->show_profile_following == true) {
+ $limit = $request->input('limit') ?? 40;
+ $following = $profile->following()->paginate($limit);
+ $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ } else {
+ $res = [];
+ }
+ }
+
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/accounts/{id}/statuses
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\StatusTransformer
+ */
+ public function accountStatusesById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $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:40'
+ ]);
+
+ $profile = Profile::whereNull('status')->findOrFail($id);
+
+ $limit = $request->limit ?? 20;
+ $max_id = $request->max_id;
+ $min_id = $request->min_id;
+ $pid = $request->user()->profile_id;
+ $scope = $request->only_media == true ?
+ ['photo', 'photo:album', 'video', 'video:album'] :
+ ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
+
+ if($pid == $profile->id) {
+ $visibility = ['public', 'unlisted', 'private'];
+ } else if($profile->is_private) {
+ $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 {
+ $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'];
+ }
+
+ if($min_id || $max_id) {
+ $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',
+ 'scope',
+ 'local',
+ 'place_id',
+ 'created_at',
+ 'updated_at'
+ )->whereProfileId($profile->id)
+ ->whereIn('type', $scope)
+ ->where('id', $dir, $id)
+ ->whereIn('visibility', $visibility)
+ ->latest()
+ ->limit($limit)
+ ->get();
+ } else {
+ $timeline = Status::select(
+ 'id',
+ 'uri',
+ 'caption',
+ 'rendered',
+ 'profile_id',
+ 'type',
+ 'in_reply_to_id',
+ 'reblog_of_id',
+ 'is_nsfw',
+ 'scope',
+ 'local',
+ 'place_id',
+ 'created_at',
+ 'updated_at'
+ )->whereProfileId($profile->id)
+ ->whereIn('type', $scope)
+ ->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);
+ }
+
+ /**
+ * POST /api/v1/accounts/{id}/follow
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\RelationshipTransformer
+ */
+ public function accountFollowById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+
+ $target = Profile::where('id', '!=', $user->id)
+ ->whereNull('status')
+ ->findOrFail($item);
+
+ $private = (bool) $target->is_private;
+ $remote = (bool) $target->domain;
+ $blocked = UserFilter::whereUserId($target->id)
+ ->whereFilterType('block')
+ ->whereFilterableId($user->id)
+ ->whereFilterableType('App\Profile')
+ ->exists();
+
+ if($blocked == true) {
+ abort(400, 'You cannot follow this user.');
+ }
+
+ $isFollowing = Follower::whereProfileId($user->id)
+ ->whereFollowingId($target->id)
+ ->exists();
+
+ // Following already, return empty relationship
+ if($isFollowing == true) {
+ $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ // Rate limits, max 7500 followers per account
+ if($user->following()->count() >= Follower::MAX_FOLLOWING) {
+ abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
+ }
+
+ // Rate limits, follow 30 accounts per hour max
+ if($user->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');
+ }
+
+ if($private == true) {
+ $follow = FollowRequest::firstOrCreate([
+ 'follower_id' => $user->id,
+ 'following_id' => $target->id
+ ]);
+ if($remote == true && config('federation.activitypub.remoteFollow') == true) {
+ (new FollowerController())->sendFollow($user, $target);
+ }
+ } else {
+ $follower = new Follower();
+ $follower->profile_id = $user->id;
+ $follower->following_id = $target->id;
+ $follower->save();
+
+ if($remote == true && config('federation.activitypub.remoteFollow') == true) {
+ (new FollowerController())->sendFollow($user, $target);
+ }
+ FollowPipeline::dispatch($follower);
+ }
+
+ Cache::forget('profile:following:'.$target->id);
+ Cache::forget('profile:followers:'.$target->id);
+ Cache::forget('profile:following:'.$user->id);
+ Cache::forget('profile:followers:'.$user->id);
+ Cache::forget('api:local:exp:rec:'.$user->id);
+ Cache::forget('user:account:id:'.$target->user_id);
+ Cache::forget('user:account:id:'.$user->user_id);
+
+ $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/accounts/{id}/unfollow
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\RelationshipTransformer
+ */
+ public function accountUnfollowById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+
+ $target = Profile::where('id', '!=', $user->id)
+ ->whereNull('status')
+ ->findOrFail($item);
+
+ $private = (bool) $target->is_private;
+ $remote = (bool) $target->domain;
+
+ $isFollowing = Follower::whereProfileId($user->id)
+ ->whereFollowingId($target->id)
+ ->exists();
+
+ if($isFollowing == false) {
+ $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ // Rate limits, follow 30 accounts per hour max
+ if($user->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');
+ }
+
+ FollowRequest::whereFollowerId($user->id)
+ ->whereFollowingId($target->id)
+ ->delete();
+
+ Follower::whereProfileId($user->id)
+ ->whereFollowingId($target->id)
+ ->delete();
+
+ if($remote == true && config('federation.activitypub.remoteFollow') == true) {
+ (new FollowerController())->sendUndoFollow($user, $target);
+ }
+
+ Cache::forget('profile:following:'.$target->id);
+ Cache::forget('profile:followers:'.$target->id);
+ Cache::forget('profile:following:'.$user->id);
+ Cache::forget('profile:followers:'.$user->id);
+ Cache::forget('api:local:exp:rec:'.$user->id);
+ Cache::forget('user:account:id:'.$target->user_id);
+ Cache::forget('user:account:id:'.$user->user_id);
+
+ $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/accounts/relationships
+ *
+ * @param array|integer $id
+ *
+ * @return \App\Transformer\Api\RelationshipTransformer
+ */
+ public function accountRelationshipsById(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request, [
+ 'id' => 'required|array|min:1|max:20',
+ 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX
+ ]);
+ $pid = $request->user()->profile_id ?? $request->user()->profile->id;
+ $ids = collect($request->input('id'));
+ $filtered = $ids->filter(function($v) use($pid) {
+ return $v != $pid;
+ });
+ $relations = Profile::whereNull('status')->findOrFail($filtered->values());
+ $fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
+ $res = $this->fractal->createData($fractal)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/accounts/search
+ *
+ *
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
+ public function accountSearch(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request, [
+ 'q' => 'required|string|min:1|max:255',
+ 'limit' => 'nullable|integer|min:1|max:40',
+ 'resolve' => 'nullable'
+ ]);
+
+ $user = $request->user();
+ $query = $request->input('q');
+ $limit = $request->input('limit') ?? 20;
+ $resolve = (bool) $request->input('resolve', false);
+ $q = '%' . $query . '%';
+
+ $profiles = Profile::whereNull('status')
+ ->where('username', 'like', $q)
+ ->orWhere('name', 'like', $q)
+ ->limit($limit)
+ ->get();
+
+ $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/blocks
+ *
+ *
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
+ 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);
+ $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/accounts/{id}/block
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\RelationshipTransformer
+ */
+ public function accountBlockById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+ $pid = $user->profile_id ?? $user->profile->id;
+
+ if($id == $pid) {
+ abort(400, 'You cannot block yourself');
+ }
+
+ $profile = Profile::findOrFail($id);
+
+ Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
+ Follower::whereProfileId($pid)->whereFollowingId($profile->id)->delete();
+ Notification::whereProfileId($pid)->whereActorId($profile->id)->delete();
+
+ $filter = UserFilter::firstOrCreate([
+ 'user_id' => $pid,
+ 'filterable_id' => $profile->id,
+ 'filterable_type' => 'App\Profile',
+ 'filter_type' => 'block',
+ ]);
+
+ Cache::forget("user:filter:list:$pid");
+ Cache::forget("api:local:exp:rec:$pid");
+
+ $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/accounts/{id}/unblock
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\RelationshipTransformer
+ */
+ public function accountUnblockById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+ $pid = $user->profile_id ?? $user->profile->id;
+
+ if($id == $pid) {
+ abort(400, 'You cannot unblock yourself');
+ }
+
+ $profile = Profile::findOrFail($id);
+
+ UserFilter::whereUserId($pid)
+ ->whereFilterableId($profile->id)
+ ->whereFilterableType('App\Profile')
+ ->whereFilterType('block')
+ ->delete();
+
+ Cache::forget("user:filter:list:$pid");
+ Cache::forget("api:local:exp:rec:$pid");
+
+ $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/custom_emojis
+ *
+ * Return empty array, we don't support custom emoji
+ *
+ * @return array
+ */
+ public function customEmojis()
+ {
+ return response()->json([]);
+ }
+
+ /**
+ * GET /api/v1/domain_blocks
+ *
+ * Return empty array
+ *
+ * @return array
+ */
+ public function accountDomainBlocks(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+ return response()->json([]);
+ }
+
+ /**
+ * GET /api/v1/endorsements
+ *
+ * Return empty array
+ *
+ * @return array
+ */
+ public function accountEndorsements(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+ return response()->json([]);
+ }
+
+ /**
+ * GET /api/v1/favourites
+ *
+ * Returns collection of liked statuses
+ *
+ * @return \App\Transformer\Api\StatusTransformer
+ */
+ public function accountFavourites(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+
+ $limit = $request->input('limit') ?? 20;
+ $favourites = Like::whereProfileId($user->profile_id)
+ ->latest()
+ ->simplePaginate($limit)
+ ->pluck('status_id');
+
+ $statuses = Status::findOrFail($favourites);
+ $resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/statuses/{id}/favourite
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\StatusTransformer
+ */
+ public function statusFavouriteById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+
+ $status = Status::findOrFail($id);
+
+ $like = Like::firstOrCreate([
+ 'profile_id' => $user->profile_id,
+ 'status_id' => $status->id
+ ]);
+
+ if($like->wasRecentlyCreated == true) {
+ LikePipeline::dispatch($like);
+ }
+
+ $resource = new Fractal\Resource\Item($status, new StatusTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/statuses/{id}/unfavourite
+ *
+ * @param integer $id
+ *
+ * @return \App\Transformer\Api\StatusTransformer
+ */
+ public function statusUnfavouriteById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+
+ $status = Status::findOrFail($id);
+
+ $like = Like::whereProfileId($user->profile_id)
+ ->whereStatusId($status->id)
+ ->first();
+
+ if($like) {
+ $like->delete();
+ }
+
+ $resource = new Fractal\Resource\Item($status, new StatusTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/filters
+ *
+ * Return empty response since we filter server side
+ *
+ * @return array
+ */
+ public function accountFilters(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ return response()->json([]);
+ }
+
+ /**
+ * GET /api/v1/follow_requests
+ *
+ * Return array of Accounts that have sent follow requests
+ *
+ * @return \App\Transformer\Api\AccountTransformer
+ */
+ public function accountFollowRequests(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+
+ $followRequests = FollowRequest::whereFollowingId($user->profile->id)->pluck('follower_id');
+
+ $profiles = Profile::find($followRequests);
+
+ $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/follow_requests/{id}/authorize
+ *
+ * @param integer $id
+ *
+ * @return null
+ */
+ public function accountFollowRequestAccept(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ // todo
+
+ return response()->json([]);
+ }
+
+ /**
+ * POST /api/v1/follow_requests/{id}/reject
+ *
+ * @param integer $id
+ *
+ * @return null
+ */
+ public function accountFollowRequestReject(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ // todo
+
+ return response()->json([]);
+ }
+
+ /**
+ * GET /api/v1/suggestions
+ *
+ * Return empty array as we don't support suggestions
+ *
+ * @return null
+ */
+ public function accountSuggestions(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ // todo
+
+ return response()->json([]);
+ }
+
+ /**
+ * GET /api/v1/instance
+ *
+ * Information about the server.
+ *
+ * @return Instance
+ */
public function instance(Request $request)
{
$res = [
@@ -109,15 +897,497 @@ class ApiV1Controller extends Controller
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
- public function filters(Request $request)
+ /**
+ * GET /api/v1/lists
+ *
+ * Return empty array as we don't support lists
+ *
+ * @return null
+ */
+ public function accountLists(Request $request)
{
- // Pixelfed does not yet support keyword filters
+ abort_if(!$request->user(), 403);
+
+ return response()->json([]);
+ }
+
+ /**
+ * GET /api/v1/accounts/{id}/lists
+ *
+ * @param integer $id
+ *
+ * @return null
+ */
+ public function accountListsById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ return response()->json([]);
+ }
+
+ /**
+ * POST /api/v1/media
+ *
+ *
+ * @return MediaTransformer
+ */
+ public function mediaUpload(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',
+ 'description' => 'nullable|string|max:420'
+ ]);
+
+ $user = $request->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.');
+ }
+ }
+
+ $monthHash = hash('sha1', date('Y').date('m'));
+ $userHash = hash('sha1', $user->id . (string) $user->created_at);
+
+ $photo = $request->file('file');
+
+ $mimes = explode(',', config('pixelfed.media_types'));
+ if(in_array($photo->getMimeType(), $mimes) == false) {
+ abort(403, 'Invalid or unsupported mime type.');
+ }
+
+ $storagePath = "public/m/{$monthHash}/{$userHash}";
+ $path = $photo->store($storagePath);
+ $hash = \hash_file('sha256', $photo);
+
+ $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->caption = $request->input('description');
+ $media->filter_class = $request->input('filter_class');
+ $media->filter_name = $request->input('filter_name');
+ $media->save();
+
+ 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;
+ }
+
+ $resource = new Fractal\Resource\Item($media, new MediaTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ $res['preview_url'] = url('/storage/no-preview.png');
+ $res['url'] = url('/storage/no-preview.png');
+ return response()->json($res);
+ }
+
+ /**
+ * PUT /api/v1/media/{id}
+ *
+ * @param integer $id
+ *
+ * @return MediaTransformer
+ */
+ public function mediaUpdate(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request, [
+ 'description' => 'nullable|string|max:420'
+ ]);
+
+ $user = $request->user();
+
+ $media = Media::whereUserId($user->id)
+ ->whereNull('status_id')
+ ->findOrFail($id);
+
+ $media->caption = $request->input('description');
+ $media->save();
+
+ $resource = new Fractal\Resource\Item($media, new MediaTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ $res['preview_url'] = url('/storage/no-preview.png');
+ $res['url'] = url('/storage/no-preview.png');
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/mutes
+ *
+ *
+ * @return AccountTransformer
+ */
+ 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);
+
+ $resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/accounts/{id}/mute
+ *
+ * @param integer $id
+ *
+ * @return RelationshipTransformer
+ */
+ public function accountMuteById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+ $pid = $user->profile_id;
+
+ $account = Profile::findOrFail($id);
+
+ $filter = UserFilter::firstOrCreate([
+ 'user_id' => $pid,
+ 'filterable_id' => $account->id,
+ 'filterable_type' => 'App\Profile',
+ 'filter_type' => 'mute',
+ ]);
+
+ Cache::forget("user:filter:list:$pid");
+ Cache::forget("feature:discover:posts:$pid");
+ Cache::forget("api:local:exp:rec:$pid");
+
+ $resource = new Fractal\Resource\Item($account, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/accounts/{id}/unmute
+ *
+ * @param integer $id
+ *
+ * @return RelationshipTransformer
+ */
+ public function accountUnmuteById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+ $pid = $user->profile_id;
+
+ $account = Profile::findOrFail($id);
+
+ $filter = UserFilter::whereUserId($pid)
+ ->whereFilterableId($account->id)
+ ->whereFilterableType('App\Profile')
+ ->whereFilterType('mute')
+ ->first();
+
+ if($filter) {
+ $filter->delete();
+ Cache::forget("user:filter:list:$pid");
+ Cache::forget("feature:discover:posts:$pid");
+ Cache::forget("api:local:exp:rec:$pid");
+ }
+
+ $resource = new Fractal\Resource\Item($account, new RelationshipTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/notifications
+ *
+ *
+ * @return NotificationTransformer
+ */
+ public function accountNotifications(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+ $this->validate($request, [
+ 'page' => 'nullable|integer|min:1|max:10',
+ 'limit' => 'nullable|integer|min:1|max:80',
+ 'max_id' => 'nullable|integer|min:1',
+ 'min_id' => 'nullable|integer|min:0',
+ ]);
+ $pid = $request->user()->profile_id;
+ $limit = $request->input('limit') ?? 20;
+ $timeago = now()->subMonths(6);
+ $min = $request->input('min_id');
+ $max = $request->input('max_id');
+ if($min || $max) {
+ $dir = $min ? '>' : '<';
+ $id = $min ?? $max;
+ $notifications = Notification::whereProfileId($pid)
+ ->whereDate('created_at', '>', $timeago)
+ ->latest()
+ ->where('id', $dir, $id)
+ ->limit($limit)
+ ->get();
+ } else {
+ $notifications = Notification::whereProfileId($pid)
+ ->whereDate('created_at', '>', $timeago)
+ ->latest()
+ ->simplePaginate($limit);
+ }
+ $resource = new Fractal\Resource\Collection($notifications, new NotificationTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/timelines/home
+ *
+ *
+ * @return StatusTransformer
+ */
+ public function timelineHome(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request,[
+ 'page' => 'nullable|integer|max:40',
+ 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
+ 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
+ 'limit' => 'nullable|integer|max:80'
+ ]);
+
+ $page = $request->input('page');
+ $min = $request->input('min_id');
+ $max = $request->input('max_id');
+ $limit = $request->input('limit') ?? 3;
+
+ $pid = $request->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();
+ });
+
+ if($min || $max) {
+ $dir = $min ? '>' : '<';
+ $id = $min ?? $max;
+ $timeline = Status::select(
+ 'id',
+ 'uri',
+ 'caption',
+ 'rendered',
+ 'profile_id',
+ 'type',
+ 'in_reply_to_id',
+ 'reblog_of_id',
+ 'is_nsfw',
+ 'scope',
+ 'local',
+ 'reply_count',
+ 'comments_disabled',
+ 'place_id',
+ 'created_at',
+ 'updated_at'
+ )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
+ ->with('profile', 'hashtags', 'mentions')
+ ->where('id', $dir, $id)
+ ->whereIn('profile_id', $following)
+ ->whereIn('visibility',['public', 'unlisted', 'private'])
+ ->latest()
+ ->limit($limit)
+ ->get();
+ } else {
+ $timeline = Status::select(
+ 'id',
+ 'uri',
+ 'caption',
+ 'rendered',
+ 'profile_id',
+ 'type',
+ 'in_reply_to_id',
+ 'reblog_of_id',
+ 'is_nsfw',
+ 'scope',
+ 'local',
+ 'reply_count',
+ 'comments_disabled',
+ 'place_id',
+ 'created_at',
+ 'updated_at'
+ )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
+ ->with('profile', 'hashtags', 'mentions')
+ ->whereIn('profile_id', $following)
+ ->whereIn('visibility',['public', 'unlisted', 'private'])
+ ->latest()
+ ->simplePaginate($limit);
+ }
+
+ $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
+ $res = $this->fractal->createData($fractal)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/conversations
+ *
+ * Not implemented
+ *
+ * @return array
+ */
+ public function conversations(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
return response()->json([]);
}
- public function context(Request $request)
+ /**
+ * GET /api/v1/timelines/public
+ *
+ *
+ * @return StatusTransformer
+ */
+ public function timelinePublic(Request $request)
{
- // todo
+ $this->validate($request,[
+ 'page' => 'nullable|integer|max:40',
+ 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
+ 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
+ 'limit' => 'nullable|integer|max:80'
+ ]);
+
+ $page = $request->input('page');
+ $min = $request->input('min_id');
+ $max = $request->input('max_id');
+ $limit = $request->input('limit') ?? 3;
+
+ if($min || $max) {
+ $dir = $min ? '>' : '<';
+ $id = $min ?? $max;
+ $timeline = Status::select(
+ 'id',
+ 'uri',
+ 'caption',
+ 'rendered',
+ 'profile_id',
+ 'type',
+ 'in_reply_to_id',
+ 'reblog_of_id',
+ 'is_nsfw',
+ 'scope',
+ 'local',
+ 'reply_count',
+ 'comments_disabled',
+ 'place_id',
+ 'created_at',
+ 'updated_at'
+ )->whereNull('uri')
+ ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
+ ->with('profile', 'hashtags', 'mentions')
+ ->where('id', $dir, $id)
+ ->whereVisibility('public')
+ ->latest()
+ ->limit($limit)
+ ->get();
+ } else {
+ $timeline = Status::select(
+ 'id',
+ 'uri',
+ 'caption',
+ 'rendered',
+ 'profile_id',
+ 'type',
+ 'in_reply_to_id',
+ 'reblog_of_id',
+ 'is_nsfw',
+ 'scope',
+ 'local',
+ 'reply_count',
+ 'comments_disabled',
+ 'place_id',
+ 'created_at',
+ 'updated_at'
+ )->whereNull('uri')
+ ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
+ ->with('profile', 'hashtags', 'mentions')
+ ->whereVisibility('public')
+ ->latest()
+ ->simplePaginate($limit);
+ }
+
+ $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
+ $res = $this->fractal->createData($fractal)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/statuses/{id}
+ *
+ * @param integer $id
+ *
+ * @return StatusTransformer
+ */
+ public function statusById(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $status = Status::whereVisibility('public')->findOrFail($id);
+ $resource = new Fractal\Resource\Item($status, new StatusTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/statuses/{id}/context
+ *
+ * @param integer $id
+ *
+ * @return StatusTransformer
+ */
+ public function statusContext(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $status = Status::whereVisibility('public')->findOrFail($id);
+
+ // Return empty response since we don't handle threading like this
$res = [
'ancestors' => [],
'descendants' => []
@@ -125,4 +1395,262 @@ class ApiV1Controller extends Controller
return response()->json($res);
}
+
+ /**
+ * GET /api/v1/statuses/{id}/card
+ *
+ * @param integer $id
+ *
+ * @return StatusTransformer
+ */
+ public function statusCard(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $status = Status::whereVisibility('public')->findOrFail($id);
+
+ // Return empty response since we don't handle support cards
+ $res = [];
+
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/statuses/{id}/reblogged_by
+ *
+ * @param integer $id
+ *
+ * @return AccountTransformer
+ */
+ public function statusRebloggedBy(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request, [
+ 'limit' => 'nullable|integer|min:1|max:80'
+ ]);
+
+ $limit = $request->input('limit') ?? 40;
+ $status = Status::whereVisibility('public')->findOrFail($id);
+ $shared = $status->sharedBy()->latest()->simplePaginate($limit);
+ $resource = new Fractal\Resource\Collection($shared, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/statuses/{id}/favourited_by
+ *
+ * @param integer $id
+ *
+ * @return AccountTransformer
+ */
+ public function statusFavouritedBy(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request, [
+ 'limit' => 'nullable|integer|min:1|max:80'
+ ]);
+
+ $limit = $request->input('limit') ?? 40;
+ $status = Status::whereVisibility('public')->findOrFail($id);
+ $liked = $status->likedBy()->latest()->simplePaginate($limit);
+ $resource = new Fractal\Resource\Collection($liked, new AccountTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/statuses
+ *
+ *
+ * @return StatusTransformer
+ */
+ public function statusCreate(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+
+ $this->validate($request, [
+ 'status' => 'nullable|string',
+ 'in_reply_to_id' => 'nullable|integer',
+ 'media_ids' => 'array|max:' . config('pixelfed.max_album_length'),
+ 'media_ids.*' => 'integer|min:1',
+ 'sensitive' => 'nullable|boolean',
+ 'visibility' => 'string|in:private,unlisted,public',
+ ]);
+
+ if(config('costar.enabled') == true) {
+ $blockedKeywords = config('costar.keyword.block');
+ if($blockedKeywords !== null && $request->status) {
+ $keywords = config('costar.keyword.block');
+ foreach($keywords as $kw) {
+ if(Str::contains($request->status, $kw) == true) {
+ abort(400, 'Invalid object. Contains banned keyword.');
+ }
+ }
+ }
+ }
+
+ if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) {
+ abort(403, 'Empty statuses are not allowed');
+ }
+
+ $ids = $request->input('media_ids');
+ $in_reply_to_id = $request->input('in_reply_to_id');
+ $user = $request->user();
+
+ if($in_reply_to_id) {
+ $parent = Status::findOrFail($in_reply_to_id);
+
+ $status = new Status;
+ $status->caption = strip_tags($request->input('status'));
+ $status->scope = $request->input('visibility');
+ $status->visibility = $request->input('visibility');
+ $status->profile_id = $user->profile_id;
+ $status->is_nsfw = $user->profile->cw == true ? true : $request->input('sensitive', false);
+ $status->in_reply_to_id = $parent->id;
+ $status->in_reply_to_profile_id = $parent->profile_id;
+ $status->save();
+ } else if($ids) {
+ $status = new Status;
+ $status->caption = strip_tags($request->input('status'));
+ $status->profile_id = $user->profile_id;
+ $status->scope = 'draft';
+ $status->is_nsfw = $user->profile->cw == true ? true : $request->input('sensitive', false);
+ $status->save();
+
+ $mimes = [];
+
+ foreach($ids as $k => $v) {
+ if($k + 1 > config('pixelfed.max_album_length')) {
+ continue;
+ }
+ $m = Media::findOrFail($v);
+ if($m->profile_id !== $user->profile_id || $m->status_id) {
+ abort(403, 'Invalid media id');
+ }
+ $m->status_id = $status->id;
+ $m->save();
+ array_push($mimes, $m->mime);
+ }
+
+ if(empty($mimes)) {
+ $status->delete();
+ abort(500, 'Invalid media ids');
+ }
+
+ $status->scope = $request->input('visibility');
+ $status->visibility = $request->input('visibility');
+ $status->type = StatusController::mimeTypeCheck($mimes);
+ $status->save();
+ }
+
+ if(!$status) {
+ $oops = 'An error occured. RefId: '.time().'-'.$user->profile_id.':'.Str::random(5).':'.Str::random(10);
+ abort(500, $oops);
+ }
+
+ NewStatusPipeline::dispatch($status);
+ Cache::forget('user:account:id:'.$user->id);
+ Cache::forget('profile:status_count:'.$user->profile_id);
+ Cache::forget($user->storageUsedKey());
+
+ $resource = new Fractal\Resource\Item($status, new StatusTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * DELETE /api/v1/statuses
+ *
+ * @param integer $id
+ *
+ * @return null
+ */
+ public function statusDelete(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $status = Status::whereProfileId($request->user()->profile->id)
+ ->findOrFail($id);
+
+ Cache::forget('profile:status_count:'.$status->profile_id);
+ StatusDelete::dispatch($status);
+
+ return response()->json(['Status successfully deleted.']);
+ }
+
+ /**
+ * POST /api/v1/statuses/{id}/reblog
+ *
+ * @param integer $id
+ *
+ * @return StatusTransformer
+ */
+ public function statusShare(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+ $status = Status::findOrFail($id);
+
+ $share = Status::firstOrCreate([
+ 'profile_id' => $user->profile_id,
+ 'reblog_of_id' => $status->id,
+ 'in_reply_to_profile_id' => $status->profile_id
+ ]);
+
+ if($share->wasRecentlyCreated == true) {
+ SharePipeline::dispatch($share);
+ }
+
+ $resource = new Fractal\Resource\Item($status, new StatusTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * POST /api/v1/statuses/{id}/unreblog
+ *
+ * @param integer $id
+ *
+ * @return StatusTransformer
+ */
+ public function statusUnshare(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+
+ $user = $request->user();
+ $status = Status::findOrFail($id);
+
+ Status::whereProfileId($user->profile_id)
+ ->whereReblogOfId($status->id)
+ ->delete();
+ $count = $status->reblogs_count;
+ $status->reblogs_count = $count > 0 ? $count - 1 : 0;
+ $status->save();
+
+ $resource = new Fractal\Resource\Item($status, new StatusTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ return response()->json($res);
+ }
+
+ /**
+ * GET /api/v1/timelines/tag/{hashtag}
+ *
+ * @param string $hashtag
+ *
+ * @return StatusTransformer
+ */
+ public function timelineHashtag(Request $request, $hashtag)
+ {
+ abort_if(!$request->user(), 403);
+
+ // todo
+ $res = [];
+ return response()->json($res);
+ }
}
\ No newline at end of file
diff --git a/app/Http/Controllers/FollowerController.php b/app/Http/Controllers/FollowerController.php
index 97a3700d2..b3b6b2e4a 100644
--- a/app/Http/Controllers/FollowerController.php
+++ b/app/Http/Controllers/FollowerController.php
@@ -109,7 +109,7 @@ class FollowerController extends Controller
Cache::forget('user:account:id:'.$user->user_id);
}
- protected function sendFollow($user, $target)
+ public function sendFollow($user, $target)
{
if($target->domain == null || $user->domain != null) {
return;
@@ -117,7 +117,7 @@ class FollowerController extends Controller
$payload = [
'@context' => 'https://www.w3.org/ns/activitystreams',
- 'id' => $user->permalink('#follow/'.$target->id.''),
+ 'id' => $user->permalink('#follow/'.$target->id),
'type' => 'Follow',
'actor' => $user->permalink(),
'object' => $target->permalink()
@@ -128,7 +128,7 @@ class FollowerController extends Controller
Helpers::sendSignedObject($user, $inbox, $payload);
}
- protected function sendUndoFollow($user, $target)
+ public function sendUndoFollow($user, $target)
{
if($target->domain == null || $user->domain != null) {
return;
diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php
index 0c13b8548..f44fd7ac4 100644
--- a/app/Http/Middleware/VerifyCsrfToken.php
+++ b/app/Http/Middleware/VerifyCsrfToken.php
@@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
protected $except = [
- //
+ '/api/v1/*'
];
}
diff --git a/app/Transformer/Api/AttachmentTransformer.php b/app/Transformer/Api/AttachmentTransformer.php
deleted file mode 100644
index 76c5b6ae3..000000000
--- a/app/Transformer/Api/AttachmentTransformer.php
+++ /dev/null
@@ -1,28 +0,0 @@
- (string) $media->id,
- 'type' => $media->activityVerb(),
- 'url' => $media->url(),
- 'remote_url' => null,
- 'preview_url' => $media->thumbnailUrl(),
- 'text_url' => null,
- 'meta' => null,
- 'description' => $media->caption,
- 'license' => $media->license,
- 'is_nsfw' => $media->is_nsfw,
- 'orientation' => $media->orientation,
- 'filter_name' => $media->filter_name,
- 'filter_class' => $media->filter_class,
- 'mime' => $media->mime,
- ];
- }
-}
diff --git a/app/Transformer/Api/Mastodon/v1/AccountTransformer.php b/app/Transformer/Api/Mastodon/v1/AccountTransformer.php
new file mode 100644
index 000000000..0d062851e
--- /dev/null
+++ b/app/Transformer/Api/Mastodon/v1/AccountTransformer.php
@@ -0,0 +1,40 @@
+domain == null;
+ $is_admin = !$local ? false : $profile->user->is_admin;
+ $acct = $local ? $profile->username . '@' . config('pixelfed.domain.app') : substr($profile->username, 1);
+ $username = $local ? $profile->username : explode('@', $acct)[0];
+ return [
+ 'id' => (string) $profile->id,
+ 'username' => $username,
+ 'acct' => $acct,
+ 'display_name' => $profile->name,
+ 'locked' => (bool) $profile->is_private,
+ 'created_at' => $profile->created_at->toJSON(),
+ 'followers_count' => $profile->followerCount(),
+ 'following_count' => $profile->followingCount(),
+ 'statuses_count' => (int) $profile->statusCount(),
+ 'note' => $profile->bio ?? '',
+ 'url' => $profile->url(),
+ 'avatar' => $profile->avatarUrl(),
+ 'avatar_static' => $profile->avatarUrl(),
+ 'header' => '',
+ 'header_static' => '',
+ 'emojis' => [],
+ 'moved' => null,
+ 'fields' => null,
+ 'bot' => null,
+ 'software' => 'pixelfed',
+ 'is_admin' => (bool) $is_admin,
+ ];
+ }
+}
diff --git a/app/Transformer/Api/Mastodon/v1/HashtagTransformer.php b/app/Transformer/Api/Mastodon/v1/HashtagTransformer.php
new file mode 100644
index 000000000..7956a8adc
--- /dev/null
+++ b/app/Transformer/Api/Mastodon/v1/HashtagTransformer.php
@@ -0,0 +1,17 @@
+ $hashtag->name,
+ 'url' => $hashtag->url(),
+ ];
+ }
+}
diff --git a/app/Transformer/Api/Mastodon/v1/MediaTransformer.php b/app/Transformer/Api/Mastodon/v1/MediaTransformer.php
new file mode 100644
index 000000000..ceb96abec
--- /dev/null
+++ b/app/Transformer/Api/Mastodon/v1/MediaTransformer.php
@@ -0,0 +1,23 @@
+ (string) $media->id,
+ 'type' => lcfirst($media->activityVerb()),
+ 'url' => $media->url(),
+ 'remote_url' => null,
+ 'preview_url' => $media->thumbnailUrl(),
+ 'text_url' => null,
+ 'meta' => null,
+ 'description' => $media->caption
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Transformer/Api/Mastodon/v1/MentionTransformer.php b/app/Transformer/Api/Mastodon/v1/MentionTransformer.php
new file mode 100644
index 000000000..774f4122e
--- /dev/null
+++ b/app/Transformer/Api/Mastodon/v1/MentionTransformer.php
@@ -0,0 +1,19 @@
+ (string) $profile->id,
+ 'url' => $profile->url(),
+ 'username' => $profile->username,
+ 'acct' => $profile->username,
+ ];
+ }
+}
diff --git a/app/Transformer/Api/Mastodon/v1/NotificationTransformer.php b/app/Transformer/Api/Mastodon/v1/NotificationTransformer.php
new file mode 100644
index 000000000..595ac17df
--- /dev/null
+++ b/app/Transformer/Api/Mastodon/v1/NotificationTransformer.php
@@ -0,0 +1,58 @@
+ (string) $notification->id,
+ 'type' => $this->replaceTypeVerb($notification->action),
+ 'created_at' => (string) $notification->created_at->toJSON(),
+ ];
+ }
+
+ public function includeAccount(Notification $notification)
+ {
+ return $this->item($notification->actor, new AccountTransformer());
+ }
+
+ public function includeStatus(Notification $notification)
+ {
+ $item = $notification;
+ if($item->item_id && $item->item_type == 'App\Status') {
+ $status = Status::with('media')->find($item->item_id);
+ if($status) {
+ return $this->item($status, new StatusTransformer());
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public function replaceTypeVerb($verb)
+ {
+ $verbs = [
+ 'follow' => 'follow',
+ 'mention' => 'mention',
+ 'share' => 'reblog',
+ 'like' => 'favourite',
+ 'comment' => 'mention',
+ ];
+ return $verbs[$verb];
+ }
+}
diff --git a/app/Transformer/Api/Mastodon/v1/StatusTransformer.php b/app/Transformer/Api/Mastodon/v1/StatusTransformer.php
new file mode 100644
index 000000000..2cd6f123f
--- /dev/null
+++ b/app/Transformer/Api/Mastodon/v1/StatusTransformer.php
@@ -0,0 +1,82 @@
+ (string) $status->id,
+ 'uri' => $status->url(),
+ 'url' => $status->url(),
+ 'in_reply_to_id' => $status->in_reply_to_id,
+ 'in_reply_to_account_id' => $status->in_reply_to_profile_id,
+ 'reblog' => null,
+ 'content' => $status->rendered ?? $status->caption,
+ 'created_at' => $status->created_at->toJSON(),
+ 'emojis' => [],
+ 'replies_count' => 0,
+ 'reblogs_count' => $status->reblogs_count != 0 ? $status->reblogs_count: $status->shares()->count(),
+ 'favourites_count' => $status->likes_count != 0 ? $status->likes_count: $status->likes()->count(),
+ 'reblogged' => null,
+ 'favourited' => null,
+ 'muted' => null,
+ 'sensitive' => (bool) $status->is_nsfw,
+ 'spoiler_text' => $status->cw_summary ?? '',
+ 'visibility' => $status->visibility ?? $status->scope,
+ 'mentions' => [],
+ 'tags' => [],
+ 'card' => null,
+ 'poll' => null,
+ 'application' => [
+ 'name' => 'web',
+ 'website' => null
+ ],
+ 'language' => null,
+ 'pinned' => null,
+ ];
+ }
+
+ public function includeAccount(Status $status)
+ {
+ $account = $status->profile;
+
+ return $this->item($account, new AccountTransformer());
+ }
+
+ public function includeMediaAttachments(Status $status)
+ {
+ return Cache::remember('mastoapi:status:transformer:media:attachments:'.$status->id, now()->addDays(14), function() use($status) {
+ if(in_array($status->type, ['photo', 'video', 'photo:album', 'loop', 'photo:video:album'])) {
+ $media = $status->media()->orderBy('order')->get();
+ return $this->collection($media, new MediaTransformer());
+ }
+ });
+ }
+
+ public function includeMentions(Status $status)
+ {
+ $mentions = $status->mentions;
+
+ return $this->collection($mentions, new MentionTransformer());
+ }
+
+ public function includeTags(Status $status)
+ {
+ $hashtags = $status->hashtags;
+
+ return $this->collection($hashtags, new HashtagTransformer());
+ }
+}
\ No newline at end of file
diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php
index 27f3f555f..5b251501e 100644
--- a/app/Transformer/Api/StatusTransformer.php
+++ b/app/Transformer/Api/StatusTransformer.php
@@ -31,7 +31,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'favourited' => $status->liked(),
'muted' => null,
'sensitive' => (bool) $status->is_nsfw,
- 'spoiler_text' => $status->cw_summary,
+ 'spoiler_text' => $status->cw_summary ?? '',
'visibility' => $status->visibility ?? $status->scope,
'application' => [
'name' => 'web',
diff --git a/app/User.php b/app/User.php
index 896ad12d6..72963400f 100644
--- a/app/User.php
+++ b/app/User.php
@@ -65,7 +65,7 @@ class User extends Authenticatable
public function filters()
{
- return $this->hasMany(UserFilter::class);
+ return $this->hasMany(UserFilter::class, 'user_id', 'profile_id');
}
public function receivesBroadcastNotificationsOn()
diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php
index 52205bb7d..fdd39d61b 100644
--- a/app/Util/ActivityPub/Inbox.php
+++ b/app/Util/ActivityPub/Inbox.php
@@ -197,7 +197,7 @@ class Inbox
'type' => 'Accept',
'actor' => $target->permalink(),
'object' => [
- 'id' => $actor->permalink('#follows/' . $follower->id),
+ 'id' => $this->payload['id'],
'actor' => $actor->permalink(),
'type' => 'Follow',
'object' => $target->permalink()
diff --git a/composer.json b/composer.json
index 8aebac042..93aad5904 100644
--- a/composer.json
+++ b/composer.json
@@ -9,6 +9,7 @@
"ext-bcmath": "*",
"ext-ctype": "*",
"ext-curl": "*",
+ "ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
@@ -18,7 +19,7 @@
"intervention/image": "^2.4",
"jenssegers/agent": "^2.6",
"laravel/framework": "5.8.*",
- "laravel/horizon": "^3.1",
+ "laravel/horizon": "^3.3",
"laravel/passport": "^7.0",
"laravel/tinker": "^1.0",
"league/flysystem-aws-s3-v3": "~1.0",
diff --git a/config/pixelfed.php b/config/pixelfed.php
index ed10f11fc..a9710a2aa 100644
--- a/config/pixelfed.php
+++ b/config/pixelfed.php
@@ -23,7 +23,7 @@ return [
| This value is the version of your Pixelfed instance.
|
*/
- 'version' => '0.10.5',
+ 'version' => '0.10.6',
/*
|--------------------------------------------------------------------------
diff --git a/public/js/discover.js b/public/js/discover.js
index b62111b0f..a483b7997 100644
Binary files a/public/js/discover.js and b/public/js/discover.js differ
diff --git a/public/mix-manifest.json b/public/mix-manifest.json
index 344c2149b..abdb49fdb 100644
Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ
diff --git a/resources/assets/js/components/DiscoverComponent.vue b/resources/assets/js/components/DiscoverComponent.vue
index 7b5df69c5..a22c3b0d3 100644
--- a/resources/assets/js/components/DiscoverComponent.vue
+++ b/resources/assets/js/components/DiscoverComponent.vue
@@ -80,7 +80,7 @@
methods: {
fetchData() {
- axios.get('/api/v2/discover/posts')
+ axios.get('/api/pixelfed/v2/discover/posts')
.then((res) => {
this.posts = res.data.posts;
this.loaded = true;
diff --git a/resources/views/settings/applications.blade.php b/resources/views/settings/applications.blade.php
index be40dfdf7..912b108b6 100644
--- a/resources/views/settings/applications.blade.php
+++ b/resources/views/settings/applications.blade.php
@@ -16,9 +16,4 @@
@push('scripts')
-
@endpush
\ No newline at end of file
diff --git a/resources/views/settings/developers.blade.php b/resources/views/settings/developers.blade.php
index 14f80094e..ade660d77 100644
--- a/resources/views/settings/developers.blade.php
+++ b/resources/views/settings/developers.blade.php
@@ -16,9 +16,4 @@
@push('scripts')
-
@endpush
\ No newline at end of file
diff --git a/routes/api.php b/routes/api.php
index 65380a01a..78c263701 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -9,8 +9,5 @@ Route::group(['prefix' => 'api'], function() {
Route::group(['prefix' => 'v1'], function() {
Route::post('apps', 'Api\ApiV1Controller@apps');
Route::get('instance', 'Api\ApiV1Controller@instance');
- Route::get('filters', 'Api\ApiV1Controller@filters');
- Route::get('statuses/{id}', 'Api\ApiV1Controller@statusById');
- Route::get('statuses/{id}/context', 'Api\ApiV1Controller@context');
});
});
diff --git a/routes/web.php b/routes/web.php
index 5f8d730fe..fc9c685d5 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -77,25 +77,67 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::group(['prefix' => 'v1'], function () {
- Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials')->middleware('auth:api');
- Route::get('accounts/relationships', 'PublicApiController@relationships')->middleware('auth:api');
- Route::get('accounts/{id}/statuses', 'PublicApiController@accountStatuses')->middleware('auth:api');
- Route::get('accounts/{id}/following', 'PublicApiController@accountFollowing')->middleware('auth:api');
- Route::get('accounts/{id}/followers', 'PublicApiController@accountFollowers')->middleware('auth:api');
- // Route::get('accounts/{id}', 'PublicApiController@account');
- Route::get('accounts/{id}', 'Api\ApiV1Controller@accountById');
+ Route::get('accounts/verify_credentials', 'Api\ApiV1Controller@verifyCredentials')->middleware('auth:api');
+ Route::patch('accounts/update_credentials', 'Api\ApiV1Controller@accountUpdateCredentials')->middleware('auth:api');
+ Route::get('accounts/relationships', 'Api\ApiV1Controller@accountRelationshipsById')->middleware('auth:api');
+ Route::get('accounts/search', 'Api\ApiV1Controller@accountSearch')->middleware('auth:api');
+ Route::get('accounts/{id}/statuses', 'Api\ApiV1Controller@accountStatusesById')->middleware('auth:api');
+ Route::get('accounts/{id}/following', 'Api\ApiV1Controller@accountFollowingById')->middleware('auth:api');
+ Route::get('accounts/{id}/followers', 'Api\ApiV1Controller@accountFollowersById')->middleware('auth:api');
+ Route::post('accounts/{id}/follow', 'Api\ApiV1Controller@accountFollowById')->middleware('auth:api');
+ Route::post('accounts/{id}/unfollow', 'Api\ApiV1Controller@accountUnfollowById')->middleware('auth:api');
+ Route::post('accounts/{id}/block', 'Api\ApiV1Controller@accountBlockById')->middleware('auth:api');
+ Route::post('accounts/{id}/unblock', 'Api\ApiV1Controller@accountUnblockById')->middleware('auth:api');
+ Route::post('accounts/{id}/pin', 'Api\ApiV1Controller@accountEndorsements')->middleware('auth:api');
+ Route::post('accounts/{id}/unpin', 'Api\ApiV1Controller@accountEndorsements')->middleware('auth:api');
+ Route::post('accounts/{id}/mute', 'Api\ApiV1Controller@accountMuteById')->middleware('auth:api');
+ Route::post('accounts/{id}/unmute', 'Api\ApiV1Controller@accountUnmuteById')->middleware('auth:api');
+ Route::get('accounts/{id}/lists', 'Api\ApiV1Controller@accountListsById')->middleware('auth:api');
+ Route::get('lists/{id}/accounts', 'Api\ApiV1Controller@accountListsById')->middleware('auth:api');
+ Route::get('accounts/{id}', 'Api\ApiV1Controller@accountById')->middleware('auth:api');
+
Route::post('avatar/update', 'ApiController@avatarUpdate')->middleware('auth:api');
- Route::get('likes', 'ApiController@hydrateLikes');
- Route::post('media', 'ApiController@uploadMedia')->middleware('auth:api');
- Route::delete('media', 'ApiController@deleteMedia')->middleware('auth:api');
- Route::get('notifications', 'ApiController@notifications')->middleware('auth:api');
- Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
- Route::get('timelines/home', 'PublicApiController@homeTimelineApi')->middleware('auth:api');
+ Route::get('blocks', 'Api\ApiV1Controller@accountBlocks')->middleware('auth:api');
+ Route::get('conversations', 'Api\ApiV1Controller@conversations')->middleware('auth:api');
+ Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis');
+ Route::get('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware('auth:api');
+ Route::post('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware('auth:api');
+ Route::delete('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware('auth:api');
+ Route::get('endorsements', 'Api\ApiV1Controller@accountEndorsements')->middleware('auth:api');
+ Route::get('favourites', 'Api\ApiV1Controller@accountFavourites')->middleware('auth:api');
+ Route::get('filters', 'Api\ApiV1Controller@accountFilters')->middleware('auth:api');
+ Route::get('follow_requests', 'Api\ApiV1Controller@accountFollowRequests')->middleware('auth:api');
+ Route::post('follow_requests/{id}/authorize', 'Api\ApiV1Controller@accountFollowRequestAccept')->middleware('auth:api');
+ Route::post('follow_requests/{id}/reject', 'Api\ApiV1Controller@accountFollowRequestReject')->middleware('auth:api');
+ Route::get('lists', 'Api\ApiV1Controller@accountLists')->middleware('auth:api');
+ Route::post('media', 'Api\ApiV1Controller@mediaUpload')->middleware('auth:api');
+ Route::put('media/{id}', 'Api\ApiV1Controller@mediaUpdate')->middleware('auth:api');
+ Route::get('mutes', 'Api\ApiV1Controller@accountMutes')->middleware('auth:api');
+ Route::get('notifications', 'Api\ApiV1Controller@accountNotifications')->middleware('auth:api');
+ Route::get('suggestions', 'Api\ApiV1Controller@accountSuggestions')->middleware('auth:api');
+
+ Route::post('statuses/{id}/favourite', 'Api\ApiV1Controller@statusFavouriteById')->middleware('auth:api');
+ Route::post('statuses/{id}/unfavourite', 'Api\ApiV1Controller@statusUnfavouriteById')->middleware('auth:api');
+ Route::get('statuses/{id}/context', 'Api\ApiV1Controller@statusContext')->middleware('auth:api');
+ Route::get('statuses/{id}/card', 'Api\ApiV1Controller@statusCard')->middleware('auth:api');
+ Route::get('statuses/{id}/reblogged_by', 'Api\ApiV1Controller@statusRebloggedBy')->middleware('auth:api');
+ Route::get('statuses/{id}/favourited_by', 'Api\ApiV1Controller@statusFavouritedBy')->middleware('auth:api');
+ Route::post('statuses/{id}/reblog', 'Api\ApiV1Controller@statusShare')->middleware('auth:api');
+ Route::post('statuses/{id}/unreblog', 'Api\ApiV1Controller@statusUnshare')->middleware('auth:api');
+ Route::delete('statuses/{id}', 'Api\ApiV1Controller@statusDelete')->middleware('auth:api');
+ Route::get('statuses/{id}', 'Api\ApiV1Controller@statusById')->middleware('auth:api');
+ Route::post('statuses', 'Api\ApiV1Controller@statusCreate')->middleware('auth:api');
+
+
+ Route::get('timelines/home', 'Api\ApiV1Controller@timelineHome')->middleware('auth:api');
+ Route::get('timelines/public', 'Api\ApiV1Controller@timelinePublic');
+ Route::get('timelines/tag/{hashtag}', 'Api\ApiV1Controller@timelineHashtag')->middleware('auth:api');
+
});
Route::group(['prefix' => 'v2'], function() {
Route::get('config', 'ApiController@siteConfiguration');
Route::get('discover', 'InternalApiController@discover');
- Route::get('discover/posts', 'InternalApiController@discoverPosts');
+ Route::get('discover/posts', 'InternalApiController@discoverPosts')->middleware('auth:api');
Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
@@ -111,12 +153,16 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::group(['prefix' => 'pixelfed'], function() {
Route::group(['prefix' => 'v1'], function() {
Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials');
- Route::get('accounts/relationships', 'PublicApiController@relationships');
+ Route::get('accounts/relationships', 'Api\ApiV1Controller@accountRelationshipsById');
+ Route::get('accounts/search', 'Api\ApiV1Controller@accountSearch');
Route::get('accounts/{id}/statuses', 'PublicApiController@accountStatuses');
Route::get('accounts/{id}/following', 'PublicApiController@accountFollowing');
Route::get('accounts/{id}/followers', 'PublicApiController@accountFollowers');
+ Route::post('accounts/{id}/block', 'Api\ApiV1Controller@accountBlockById');
+ Route::post('accounts/{id}/unblock', 'Api\ApiV1Controller@accountUnblockById');
Route::get('accounts/{id}', 'PublicApiController@account');
Route::post('avatar/update', 'ApiController@avatarUpdate');
+ Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis');
Route::get('likes', 'ApiController@hydrateLikes');
Route::post('media', 'ApiController@uploadMedia');
Route::delete('media', 'ApiController@deleteMedia');
@@ -124,6 +170,23 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
});
+
+ Route::group(['prefix' => 'v2'], function() {
+ Route::get('config', 'ApiController@siteConfiguration');
+ Route::get('discover', 'InternalApiController@discover');
+ Route::get('discover/posts', 'InternalApiController@discoverPosts');
+ Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
+ Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
+ Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
+ Route::get('shares/profile/{username}/status/{id}', 'PublicApiController@statusShares');
+ Route::get('status/{id}/replies', 'InternalApiController@statusReplies');
+ Route::post('moderator/action', 'InternalApiController@modAction');
+ Route::get('discover/categories', 'InternalApiController@discoverCategories');
+ Route::get('loops', 'DiscoverController@loopsApi');
+ Route::post('loops/watch', 'DiscoverController@loopWatch');
+ Route::get('discover/tag', 'DiscoverController@getHashtags');
+ Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
+ });
});
Route::group(['prefix' => 'local'], function () {
// Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials');