ApiV1Controller.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use Illuminate\Http\Request;
  4. use App\Http\Controllers\Controller;
  5. use Illuminate\Support\Str;
  6. use App\Jobs\StatusPipeline\StatusDelete;
  7. use Laravel\Passport\Passport;
  8. use Auth, Cache, DB;
  9. use App\{
  10. Follower,
  11. Like,
  12. Media,
  13. Profile,
  14. Status
  15. };
  16. use League\Fractal;
  17. use App\Transformer\Api\{
  18. AccountTransformer,
  19. RelationshipTransformer,
  20. StatusTransformer,
  21. };
  22. use League\Fractal\Serializer\ArraySerializer;
  23. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  24. use App\Services\NotificationService;
  25. class ApiV1Controller extends Controller
  26. {
  27. protected $fractal;
  28. public function __construct()
  29. {
  30. $this->fractal = new Fractal\Manager();
  31. $this->fractal->setSerializer(new ArraySerializer());
  32. }
  33. public function apps(Request $request)
  34. {
  35. abort_if(!config('pixelfed.oauth_enabled'), 404);
  36. $this->validate($request, [
  37. 'client_name' => 'required',
  38. 'redirect_uris' => 'required',
  39. 'scopes' => 'nullable',
  40. 'website' => 'nullable'
  41. ]);
  42. $client = Passport::client()->forceFill([
  43. 'user_id' => null,
  44. 'name' => e($request->client_name),
  45. 'secret' => Str::random(40),
  46. 'redirect' => $request->redirect_uris,
  47. 'personal_access_client' => false,
  48. 'password_client' => false,
  49. 'revoked' => false,
  50. ]);
  51. $client->save();
  52. $res = [
  53. 'id' => $client->id,
  54. 'name' => $client->name,
  55. 'website' => null,
  56. 'redirect_uri' => $client->redirect,
  57. 'client_id' => $client->id,
  58. 'client_secret' => $client->secret,
  59. 'vapid_key' => null
  60. ];
  61. return $res;
  62. }
  63. /**
  64. * GET /api/v1/accounts/{id}
  65. *
  66. * @param integer $id
  67. *
  68. * @return \App\Transformer\Api\AccountTransformer
  69. */
  70. public function accountById(Request $request, $id)
  71. {
  72. $profile = Profile::whereNull('status')->findOrFail($id);
  73. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  74. $res = $this->fractal->createData($resource)->toArray();
  75. return response()->json($res);
  76. }
  77. /**
  78. * PATCH /api/v1/accounts/update_credentials
  79. *
  80. * @return \App\Transformer\Api\AccountTransformer
  81. */
  82. public function accountUpdateCredentials(Request $request)
  83. {
  84. abort_if(!$request->user(), 403);
  85. $this->validate($request, [
  86. 'display_name' => 'nullable|string',
  87. 'note' => 'nullable|string',
  88. 'locked' => 'nullable|boolean',
  89. // 'source.privacy' => 'nullable|in:unlisted,public,private',
  90. // 'source.sensitive' => 'nullable|boolean'
  91. ]);
  92. $user = $request->user();
  93. $profile = $user->profile;
  94. $displayName = $request->input('display_name');
  95. $note = $request->input('note');
  96. $locked = $request->input('locked');
  97. // $privacy = $request->input('source.privacy');
  98. // $sensitive = $request->input('source.sensitive');
  99. $changes = false;
  100. if($displayName !== $user->name) {
  101. $user->name = $displayName;
  102. $profile->name = $displayName;
  103. $changes = true;
  104. }
  105. if($note !== $profile->bio) {
  106. $profile->bio = e($note);
  107. $changes = true;
  108. }
  109. if(!is_null($locked)) {
  110. $profile->is_private = $locked;
  111. $changes = true;
  112. }
  113. if($changes) {
  114. $user->save();
  115. $profile->save();
  116. }
  117. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  118. $res = $this->fractal->createData($resource)->toArray();
  119. return response()->json($res);
  120. }
  121. /**
  122. * GET /api/v1/accounts/{id}/followers
  123. *
  124. * @param integer $id
  125. *
  126. * @return \App\Transformer\Api\AccountTransformer
  127. */
  128. public function accountFollowersById(Request $request, $id)
  129. {
  130. abort_if(!$request->user(), 403);
  131. $profile = Profile::whereNull('status')->findOrFail($id);
  132. $settings = $profile->user->settings;
  133. if($settings->show_profile_followers == true) {
  134. $limit = $request->input('limit') ?? 40;
  135. $followers = $profile->followers()->paginate($limit);
  136. $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
  137. $res = $this->fractal->createData($resource)->toArray();
  138. } else {
  139. $res = [];
  140. }
  141. return response()->json($res);
  142. }
  143. /**
  144. * GET /api/v1/accounts/{id}/following
  145. *
  146. * @param integer $id
  147. *
  148. * @return \App\Transformer\Api\AccountTransformer
  149. */
  150. public function accountFollowingById(Request $request, $id)
  151. {
  152. abort_if(!$request->user(), 403);
  153. $profile = Profile::whereNull('status')->findOrFail($id);
  154. $settings = $profile->user->settings;
  155. if($settings->show_profile_following == true) {
  156. $limit = $request->input('limit') ?? 40;
  157. $following = $profile->following()->paginate($limit);
  158. $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
  159. $res = $this->fractal->createData($resource)->toArray();
  160. } else {
  161. $res = [];
  162. }
  163. return response()->json($res);
  164. }
  165. /**
  166. * GET /api/v1/accounts/{id}/statuses
  167. *
  168. * @param integer $id
  169. *
  170. * @return \App\Transformer\Api\StatusTransformer
  171. */
  172. public function accountStatusesById(Request $request, $id)
  173. {
  174. abort_if(!$request->user(), 403);
  175. $this->validate($request, [
  176. 'only_media' => 'nullable',
  177. 'pinned' => 'nullable',
  178. 'exclude_replies' => 'nullable',
  179. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  180. 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  181. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  182. 'limit' => 'nullable|integer|min:1|max:40'
  183. ]);
  184. $profile = Profile::whereNull('status')->findOrFail($id);
  185. $limit = $request->limit ?? 20;
  186. $max_id = $request->max_id;
  187. $min_id = $request->min_id;
  188. $pid = $request->user()->profile_id;
  189. $scope = $request->only_media == true ?
  190. ['photo', 'photo:album', 'video', 'video:album'] :
  191. ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
  192. if($pid == $profile->id) {
  193. $visibility = ['public', 'unlisted', 'private'];
  194. } else if($profile->is_private) {
  195. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  196. $following = Follower::whereProfileId($pid)->pluck('following_id');
  197. return $following->push($pid)->toArray();
  198. });
  199. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
  200. } else {
  201. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  202. $following = Follower::whereProfileId($pid)->pluck('following_id');
  203. return $following->push($pid)->toArray();
  204. });
  205. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  206. }
  207. $dir = $min_id ? '>' : '<';
  208. $id = $min_id ?? $max_id;
  209. $timeline = Status::select(
  210. 'id',
  211. 'uri',
  212. 'caption',
  213. 'rendered',
  214. 'profile_id',
  215. 'type',
  216. 'in_reply_to_id',
  217. 'reblog_of_id',
  218. 'is_nsfw',
  219. 'scope',
  220. 'local',
  221. 'place_id',
  222. 'created_at',
  223. 'updated_at'
  224. )->whereProfileId($profile->id)
  225. ->whereIn('type', $scope)
  226. ->whereLocal(true)
  227. ->whereNull('uri')
  228. ->where('id', $dir, $id)
  229. ->whereIn('visibility', $visibility)
  230. ->latest()
  231. ->limit($limit)
  232. ->get();
  233. $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  234. $res = $this->fractal->createData($resource)->toArray();
  235. return response()->json($res);
  236. }
  237. public function statusById(Request $request, $id)
  238. {
  239. $status = Status::whereVisibility('public')->findOrFail($id);
  240. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  241. $res = $this->fractal->createData($resource)->toArray();
  242. return response()->json($res);
  243. }
  244. public function instance(Request $request)
  245. {
  246. $res = [
  247. 'description' => 'Pixelfed - Photo sharing for everyone',
  248. 'email' => config('instance.email'),
  249. 'languages' => ['en'],
  250. 'max_toot_chars' => config('pixelfed.max_caption_length'),
  251. 'registrations' => config('pixelfed.open_registration'),
  252. 'stats' => [
  253. 'user_count' => 0,
  254. 'status_count' => 0,
  255. 'domain_count' => 0
  256. ],
  257. 'thumbnail' => config('app.url') . '/img/pixelfed-icon-color.png',
  258. 'title' => 'Pixelfed (' . config('pixelfed.domain.app') . ')',
  259. 'uri' => config('app.url'),
  260. 'urls' => [],
  261. 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') . ')'
  262. ];
  263. return response()->json($res, 200, [], JSON_PRETTY_PRINT);
  264. }
  265. public function filters(Request $request)
  266. {
  267. // Pixelfed does not yet support keyword filters
  268. return response()->json([]);
  269. }
  270. public function context(Request $request)
  271. {
  272. // todo
  273. $res = [
  274. 'ancestors' => [],
  275. 'descendants' => []
  276. ];
  277. return response()->json($res);
  278. }
  279. public function createStatus(Request $request)
  280. {
  281. abort_if(!$request->user(), 403);
  282. $this->validate($request, [
  283. 'status' => 'string',
  284. 'media_ids' => 'array',
  285. 'media_ids.*' => 'integer|min:1',
  286. 'sensitive' => 'nullable|boolean',
  287. 'visibility' => 'string|in:private,unlisted,public',
  288. 'in_reply_to_id' => 'integer'
  289. ]);
  290. if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) {
  291. abort(403, 'Empty statuses are not allowed');
  292. }
  293. }
  294. }