1
0

ApiV1Controller.php 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124
  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\Util\ActivityPub\Helpers;
  7. use App\Util\Media\Filter;
  8. use Laravel\Passport\Passport;
  9. use Auth, Cache, DB, URL;
  10. use App\{
  11. Bookmark,
  12. Follower,
  13. FollowRequest,
  14. Hashtag,
  15. Like,
  16. Media,
  17. Notification,
  18. Profile,
  19. Status,
  20. StatusHashtag,
  21. User,
  22. UserSetting,
  23. UserFilter,
  24. };
  25. use League\Fractal;
  26. use App\Transformer\Api\Mastodon\v1\{
  27. AccountTransformer,
  28. MediaTransformer,
  29. NotificationTransformer,
  30. StatusTransformer,
  31. };
  32. use App\Transformer\Api\{
  33. RelationshipTransformer,
  34. };
  35. use App\Http\Controllers\FollowerController;
  36. use League\Fractal\Serializer\ArraySerializer;
  37. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  38. use App\Http\Controllers\StatusController;
  39. use App\Jobs\LikePipeline\LikePipeline;
  40. use App\Jobs\SharePipeline\SharePipeline;
  41. use App\Jobs\StatusPipeline\NewStatusPipeline;
  42. use App\Jobs\StatusPipeline\StatusDelete;
  43. use App\Jobs\FollowPipeline\FollowPipeline;
  44. use App\Jobs\ImageOptimizePipeline\ImageOptimize;
  45. use App\Jobs\VideoPipeline\{
  46. VideoOptimize,
  47. VideoPostProcess,
  48. VideoThumbnail
  49. };
  50. use App\Services\{
  51. LikeService,
  52. NotificationService,
  53. MediaPathService,
  54. PublicTimelineService,
  55. ProfileService,
  56. SearchApiV2Service,
  57. StatusService,
  58. MediaBlocklistService
  59. };
  60. use App\Util\Lexer\Autolink;
  61. class ApiV1Controller extends Controller
  62. {
  63. protected $fractal;
  64. public function __construct()
  65. {
  66. $this->fractal = new Fractal\Manager();
  67. $this->fractal->setSerializer(new ArraySerializer());
  68. }
  69. public function apps(Request $request)
  70. {
  71. abort_if(!config_cache('pixelfed.oauth_enabled'), 404);
  72. $this->validate($request, [
  73. 'client_name' => 'required',
  74. 'redirect_uris' => 'required',
  75. 'scopes' => 'nullable',
  76. 'website' => 'nullable'
  77. ]);
  78. $uris = implode(',', explode('\n', $request->redirect_uris));
  79. $client = Passport::client()->forceFill([
  80. 'user_id' => null,
  81. 'name' => e($request->client_name),
  82. 'secret' => Str::random(40),
  83. 'redirect' => $uris,
  84. 'personal_access_client' => false,
  85. 'password_client' => false,
  86. 'revoked' => false,
  87. ]);
  88. $client->save();
  89. $res = [
  90. 'id' => $client->id,
  91. 'name' => $client->name,
  92. 'website' => null,
  93. 'redirect_uri' => $client->redirect,
  94. 'client_id' => $client->id,
  95. 'client_secret' => $client->secret,
  96. 'vapid_key' => null
  97. ];
  98. return response()->json($res, 200, [
  99. 'Access-Control-Allow-Origin' => '*'
  100. ]);
  101. }
  102. /**
  103. * GET /api/v1/accounts/verify_credentials
  104. *
  105. *
  106. * @return \App\Transformer\Api\AccountTransformer
  107. */
  108. public function verifyCredentials(Request $request)
  109. {
  110. abort_if(!$request->user(), 403);
  111. $id = $request->user()->profile_id;
  112. $res = ProfileService::get($id);
  113. $res['source'] = [
  114. 'privacy' => $res['locked'] ? 'private' : 'public',
  115. 'sensitive' => false,
  116. 'language' => null,
  117. 'note' => '',
  118. 'fields' => []
  119. ];
  120. return response()->json($res);
  121. }
  122. /**
  123. * GET /api/v1/accounts/{id}
  124. *
  125. * @param integer $id
  126. *
  127. * @return \App\Transformer\Api\AccountTransformer
  128. */
  129. public function accountById(Request $request, $id)
  130. {
  131. $profile = Profile::whereNull('status')->findOrFail($id);
  132. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  133. $res = $this->fractal->createData($resource)->toArray();
  134. return response()->json($res);
  135. }
  136. /**
  137. * PATCH /api/v1/accounts/update_credentials
  138. *
  139. * @return \App\Transformer\Api\AccountTransformer
  140. */
  141. public function accountUpdateCredentials(Request $request)
  142. {
  143. abort_if(!$request->user(), 403);
  144. $this->validate($request, [
  145. 'display_name' => 'nullable|string',
  146. 'note' => 'nullable|string',
  147. 'locked' => 'nullable',
  148. // 'source.privacy' => 'nullable|in:unlisted,public,private',
  149. // 'source.sensitive' => 'nullable|boolean'
  150. ]);
  151. $user = $request->user();
  152. $profile = $user->profile;
  153. $displayName = $request->input('display_name');
  154. $note = $request->input('note');
  155. $locked = $request->input('locked');
  156. // $privacy = $request->input('source.privacy');
  157. // $sensitive = $request->input('source.sensitive');
  158. $changes = false;
  159. if($displayName !== $user->name) {
  160. $user->name = $displayName;
  161. $profile->name = $displayName;
  162. $changes = true;
  163. }
  164. if($note !== strip_tags($profile->bio)) {
  165. $profile->bio = Autolink::create()->autolink(strip_tags($note));
  166. $changes = true;
  167. }
  168. if(!is_null($locked)) {
  169. $profile->is_private = $locked;
  170. $changes = true;
  171. }
  172. if($changes) {
  173. $user->save();
  174. $profile->save();
  175. }
  176. $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
  177. $res = $this->fractal->createData($resource)->toArray();
  178. return response()->json($res);
  179. }
  180. /**
  181. * GET /api/v1/accounts/{id}/followers
  182. *
  183. * @param integer $id
  184. *
  185. * @return \App\Transformer\Api\AccountTransformer
  186. */
  187. public function accountFollowersById(Request $request, $id)
  188. {
  189. abort_if(!$request->user(), 403);
  190. $user = $request->user();
  191. $profile = Profile::whereNull('status')->findOrFail($id);
  192. $limit = $request->input('limit') ?? 40;
  193. if($profile->domain) {
  194. $res = [];
  195. } else {
  196. if($profile->id == $user->profile_id) {
  197. $followers = $profile->followers()->paginate($limit);
  198. $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
  199. $res = $this->fractal->createData($resource)->toArray();
  200. } else {
  201. if($profile->is_private) {
  202. abort_if(!$profile->followedBy($user->profile), 403);
  203. }
  204. $settings = $profile->user->settings;
  205. if( in_array($user->profile_id, $profile->blockedIds()->toArray()) ||
  206. $settings->show_profile_followers == false
  207. ) {
  208. $res = [];
  209. } else {
  210. $followers = $profile->followers()->paginate($limit);
  211. $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
  212. $res = $this->fractal->createData($resource)->toArray();
  213. }
  214. }
  215. }
  216. return response()->json($res);
  217. }
  218. /**
  219. * GET /api/v1/accounts/{id}/following
  220. *
  221. * @param integer $id
  222. *
  223. * @return \App\Transformer\Api\AccountTransformer
  224. */
  225. public function accountFollowingById(Request $request, $id)
  226. {
  227. abort_if(!$request->user(), 403);
  228. $user = $request->user();
  229. $profile = Profile::whereNull('status')->findOrFail($id);
  230. $limit = $request->input('limit') ?? 40;
  231. if($profile->domain) {
  232. $res = [];
  233. } else {
  234. if($profile->id == $user->profile_id) {
  235. $following = $profile->following()->paginate($limit);
  236. $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
  237. $res = $this->fractal->createData($resource)->toArray();
  238. } else {
  239. if($profile->is_private) {
  240. abort_if(!$profile->followedBy($user->profile), 403);
  241. }
  242. $settings = $profile->user->settings;
  243. if( in_array($user->profile_id, $profile->blockedIds()->toArray()) ||
  244. $settings->show_profile_following == false
  245. ) {
  246. $res = [];
  247. } else {
  248. $following = $profile->following()->paginate($limit);
  249. $resource = new Fractal\Resource\Collection($following, new AccountTransformer());
  250. $res = $this->fractal->createData($resource)->toArray();
  251. }
  252. }
  253. }
  254. return response()->json($res);
  255. }
  256. /**
  257. * GET /api/v1/accounts/{id}/statuses
  258. *
  259. * @param integer $id
  260. *
  261. * @return \App\Transformer\Api\StatusTransformer
  262. */
  263. public function accountStatusesById(Request $request, $id)
  264. {
  265. abort_if(!$request->user(), 403);
  266. $this->validate($request, [
  267. 'only_media' => 'nullable',
  268. 'pinned' => 'nullable',
  269. 'exclude_replies' => 'nullable',
  270. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  271. 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  272. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  273. 'limit' => 'nullable|integer|min:1|max:80'
  274. ]);
  275. $profile = Profile::whereNull('status')->findOrFail($id);
  276. $limit = $request->limit ?? 20;
  277. $max_id = $request->max_id;
  278. $min_id = $request->min_id;
  279. $pid = $request->user()->profile_id;
  280. $scope = $request->only_media == true ?
  281. ['photo', 'photo:album', 'video', 'video:album'] :
  282. ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
  283. if($pid == $profile->id) {
  284. $visibility = ['public', 'unlisted', 'private'];
  285. } else if($profile->is_private) {
  286. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  287. $following = Follower::whereProfileId($pid)->pluck('following_id');
  288. return $following->push($pid)->toArray();
  289. });
  290. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
  291. } else {
  292. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  293. $following = Follower::whereProfileId($pid)->pluck('following_id');
  294. return $following->push($pid)->toArray();
  295. });
  296. $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  297. }
  298. if($min_id || $max_id) {
  299. $dir = $min_id ? '>' : '<';
  300. $id = $min_id ?? $max_id;
  301. $timeline = Status::select(
  302. 'id',
  303. 'uri',
  304. 'caption',
  305. 'rendered',
  306. 'profile_id',
  307. 'type',
  308. 'in_reply_to_id',
  309. 'reblog_of_id',
  310. 'is_nsfw',
  311. 'scope',
  312. 'local',
  313. 'place_id',
  314. 'likes_count',
  315. 'reblogs_count',
  316. 'created_at',
  317. 'updated_at'
  318. )->whereProfileId($profile->id)
  319. ->whereIn('type', $scope)
  320. ->where('id', $dir, $id)
  321. ->whereIn('visibility', $visibility)
  322. ->latest()
  323. ->limit($limit)
  324. ->get();
  325. } else {
  326. $timeline = Status::select(
  327. 'id',
  328. 'uri',
  329. 'caption',
  330. 'rendered',
  331. 'profile_id',
  332. 'type',
  333. 'in_reply_to_id',
  334. 'reblog_of_id',
  335. 'is_nsfw',
  336. 'scope',
  337. 'local',
  338. 'place_id',
  339. 'likes_count',
  340. 'reblogs_count',
  341. 'created_at',
  342. 'updated_at'
  343. )->whereProfileId($profile->id)
  344. ->whereIn('type', $scope)
  345. ->whereIn('visibility', $visibility)
  346. ->latest()
  347. ->limit($limit)
  348. ->get();
  349. }
  350. $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  351. $res = $this->fractal->createData($resource)->toArray();
  352. return response()->json($res);
  353. }
  354. /**
  355. * POST /api/v1/accounts/{id}/follow
  356. *
  357. * @param integer $id
  358. *
  359. * @return \App\Transformer\Api\RelationshipTransformer
  360. */
  361. public function accountFollowById(Request $request, $id)
  362. {
  363. abort_if(!$request->user(), 403);
  364. $user = $request->user();
  365. $target = Profile::where('id', '!=', $user->profile_id)
  366. ->whereNull('status')
  367. ->findOrFail($id);
  368. $private = (bool) $target->is_private;
  369. $remote = (bool) $target->domain;
  370. $blocked = UserFilter::whereUserId($target->id)
  371. ->whereFilterType('block')
  372. ->whereFilterableId($user->profile_id)
  373. ->whereFilterableType('App\Profile')
  374. ->exists();
  375. if($blocked == true) {
  376. abort(400, 'You cannot follow this user.');
  377. }
  378. $isFollowing = Follower::whereProfileId($user->profile_id)
  379. ->whereFollowingId($target->id)
  380. ->exists();
  381. // Following already, return empty relationship
  382. if($isFollowing == true) {
  383. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  384. $res = $this->fractal->createData($resource)->toArray();
  385. return response()->json($res);
  386. }
  387. // Rate limits, max 7500 followers per account
  388. if($user->profile->following()->count() >= Follower::MAX_FOLLOWING) {
  389. abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
  390. }
  391. // Rate limits, follow 30 accounts per hour max
  392. if($user->profile->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
  393. abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
  394. }
  395. if($private == true) {
  396. $follow = FollowRequest::firstOrCreate([
  397. 'follower_id' => $user->profile_id,
  398. 'following_id' => $target->id
  399. ]);
  400. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  401. (new FollowerController())->sendFollow($user->profile, $target);
  402. }
  403. } else {
  404. $follower = new Follower();
  405. $follower->profile_id = $user->profile_id;
  406. $follower->following_id = $target->id;
  407. $follower->save();
  408. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  409. (new FollowerController())->sendFollow($user->profile, $target);
  410. }
  411. FollowPipeline::dispatch($follower);
  412. }
  413. Cache::forget('profile:following:'.$target->id);
  414. Cache::forget('profile:followers:'.$target->id);
  415. Cache::forget('profile:following:'.$user->profile_id);
  416. Cache::forget('profile:followers:'.$user->profile_id);
  417. Cache::forget('api:local:exp:rec:'.$user->profile_id);
  418. Cache::forget('user:account:id:'.$target->user_id);
  419. Cache::forget('user:account:id:'.$user->id);
  420. Cache::forget('profile:follower_count:'.$target->id);
  421. Cache::forget('profile:follower_count:'.$user->profile_id);
  422. Cache::forget('profile:following_count:'.$target->id);
  423. Cache::forget('profile:following_count:'.$user->profile_id);
  424. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  425. $res = $this->fractal->createData($resource)->toArray();
  426. return response()->json($res);
  427. }
  428. /**
  429. * POST /api/v1/accounts/{id}/unfollow
  430. *
  431. * @param integer $id
  432. *
  433. * @return \App\Transformer\Api\RelationshipTransformer
  434. */
  435. public function accountUnfollowById(Request $request, $id)
  436. {
  437. abort_if(!$request->user(), 403);
  438. $user = $request->user();
  439. $target = Profile::where('id', '!=', $user->profile_id)
  440. ->whereNull('status')
  441. ->findOrFail($id);
  442. $private = (bool) $target->is_private;
  443. $remote = (bool) $target->domain;
  444. $isFollowing = Follower::whereProfileId($user->profile_id)
  445. ->whereFollowingId($target->id)
  446. ->exists();
  447. if($isFollowing == false) {
  448. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  449. $res = $this->fractal->createData($resource)->toArray();
  450. return response()->json($res);
  451. }
  452. // Rate limits, follow 30 accounts per hour max
  453. if($user->profile->following()->where('followers.updated_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
  454. abort(400, 'You can only follow or unfollow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
  455. }
  456. FollowRequest::whereFollowerId($user->profile_id)
  457. ->whereFollowingId($target->id)
  458. ->delete();
  459. Follower::whereProfileId($user->profile_id)
  460. ->whereFollowingId($target->id)
  461. ->delete();
  462. if($remote == true && config('federation.activitypub.remoteFollow') == true) {
  463. (new FollowerController())->sendUndoFollow($user->profile, $target);
  464. }
  465. Cache::forget('profile:following:'.$target->id);
  466. Cache::forget('profile:followers:'.$target->id);
  467. Cache::forget('profile:following:'.$user->profile_id);
  468. Cache::forget('profile:followers:'.$user->profile_id);
  469. Cache::forget('api:local:exp:rec:'.$user->profile_id);
  470. Cache::forget('user:account:id:'.$target->user_id);
  471. Cache::forget('user:account:id:'.$user->id);
  472. $resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
  473. $res = $this->fractal->createData($resource)->toArray();
  474. return response()->json($res);
  475. }
  476. /**
  477. * GET /api/v1/accounts/relationships
  478. *
  479. * @param array|integer $id
  480. *
  481. * @return \App\Transformer\Api\RelationshipTransformer
  482. */
  483. public function accountRelationshipsById(Request $request)
  484. {
  485. abort_if(!$request->user(), 403);
  486. $this->validate($request, [
  487. 'id' => 'required|array|min:1|max:20',
  488. 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX
  489. ]);
  490. $pid = $request->user()->profile_id ?? $request->user()->profile->id;
  491. $ids = collect($request->input('id'));
  492. $filtered = $ids->filter(function($v) use($pid) {
  493. return $v != $pid;
  494. });
  495. $relations = Profile::whereNull('status')->findOrFail($filtered->values());
  496. $fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
  497. $res = $this->fractal->createData($fractal)->toArray();
  498. return response()->json($res);
  499. }
  500. /**
  501. * GET /api/v1/accounts/search
  502. *
  503. *
  504. *
  505. * @return \App\Transformer\Api\AccountTransformer
  506. */
  507. public function accountSearch(Request $request)
  508. {
  509. abort_if(!$request->user(), 403);
  510. $this->validate($request, [
  511. 'q' => 'required|string|min:1|max:255',
  512. 'limit' => 'nullable|integer|min:1|max:40',
  513. 'resolve' => 'nullable'
  514. ]);
  515. $user = $request->user();
  516. $query = $request->input('q');
  517. $limit = $request->input('limit') ?? 20;
  518. $resolve = (bool) $request->input('resolve', false);
  519. $q = '%' . $query . '%';
  520. $profiles = Profile::whereNull('status')
  521. ->where('username', 'like', $q)
  522. ->orWhere('name', 'like', $q)
  523. ->limit($limit)
  524. ->get();
  525. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  526. $res = $this->fractal->createData($resource)->toArray();
  527. return response()->json($res);
  528. }
  529. /**
  530. * GET /api/v1/blocks
  531. *
  532. *
  533. *
  534. * @return \App\Transformer\Api\AccountTransformer
  535. */
  536. public function accountBlocks(Request $request)
  537. {
  538. abort_if(!$request->user(), 403);
  539. $this->validate($request, [
  540. 'limit' => 'nullable|integer|min:1|max:40',
  541. 'page' => 'nullable|integer|min:1|max:10'
  542. ]);
  543. $user = $request->user();
  544. $limit = $request->input('limit') ?? 40;
  545. $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
  546. ->whereUserId($user->profile_id)
  547. ->whereFilterableType('App\Profile')
  548. ->whereFilterType('block')
  549. ->simplePaginate($limit)
  550. ->pluck('filterable_id');
  551. $profiles = Profile::findOrFail($blocked);
  552. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  553. $res = $this->fractal->createData($resource)->toArray();
  554. return response()->json($res);
  555. }
  556. /**
  557. * POST /api/v1/accounts/{id}/block
  558. *
  559. * @param integer $id
  560. *
  561. * @return \App\Transformer\Api\RelationshipTransformer
  562. */
  563. public function accountBlockById(Request $request, $id)
  564. {
  565. abort_if(!$request->user(), 403);
  566. $user = $request->user();
  567. $pid = $user->profile_id ?? $user->profile->id;
  568. if($id == $pid) {
  569. abort(400, 'You cannot block yourself');
  570. }
  571. $profile = Profile::findOrFail($id);
  572. if($profile->user->is_admin == true) {
  573. abort(400, 'You cannot block an admin');
  574. }
  575. Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
  576. Follower::whereProfileId($pid)->whereFollowingId($profile->id)->delete();
  577. Notification::whereProfileId($pid)->whereActorId($profile->id)->delete();
  578. $filter = UserFilter::firstOrCreate([
  579. 'user_id' => $pid,
  580. 'filterable_id' => $profile->id,
  581. 'filterable_type' => 'App\Profile',
  582. 'filter_type' => 'block',
  583. ]);
  584. Cache::forget("user:filter:list:$pid");
  585. Cache::forget("api:local:exp:rec:$pid");
  586. $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
  587. $res = $this->fractal->createData($resource)->toArray();
  588. return response()->json($res);
  589. }
  590. /**
  591. * POST /api/v1/accounts/{id}/unblock
  592. *
  593. * @param integer $id
  594. *
  595. * @return \App\Transformer\Api\RelationshipTransformer
  596. */
  597. public function accountUnblockById(Request $request, $id)
  598. {
  599. abort_if(!$request->user(), 403);
  600. $user = $request->user();
  601. $pid = $user->profile_id ?? $user->profile->id;
  602. if($id == $pid) {
  603. abort(400, 'You cannot unblock yourself');
  604. }
  605. $profile = Profile::findOrFail($id);
  606. UserFilter::whereUserId($pid)
  607. ->whereFilterableId($profile->id)
  608. ->whereFilterableType('App\Profile')
  609. ->whereFilterType('block')
  610. ->delete();
  611. Cache::forget("user:filter:list:$pid");
  612. Cache::forget("api:local:exp:rec:$pid");
  613. $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer());
  614. $res = $this->fractal->createData($resource)->toArray();
  615. return response()->json($res);
  616. }
  617. /**
  618. * GET /api/v1/custom_emojis
  619. *
  620. * Return empty array, we don't support custom emoji
  621. *
  622. * @return array
  623. */
  624. public function customEmojis()
  625. {
  626. return response()->json([]);
  627. }
  628. /**
  629. * GET /api/v1/domain_blocks
  630. *
  631. * Return empty array
  632. *
  633. * @return array
  634. */
  635. public function accountDomainBlocks(Request $request)
  636. {
  637. abort_if(!$request->user(), 403);
  638. return response()->json([]);
  639. }
  640. /**
  641. * GET /api/v1/endorsements
  642. *
  643. * Return empty array
  644. *
  645. * @return array
  646. */
  647. public function accountEndorsements(Request $request)
  648. {
  649. abort_if(!$request->user(), 403);
  650. return response()->json([]);
  651. }
  652. /**
  653. * GET /api/v1/favourites
  654. *
  655. * Returns collection of liked statuses
  656. *
  657. * @return \App\Transformer\Api\StatusTransformer
  658. */
  659. public function accountFavourites(Request $request)
  660. {
  661. abort_if(!$request->user(), 403);
  662. $user = $request->user();
  663. $limit = $request->input('limit') ?? 20;
  664. $favourites = Like::whereProfileId($user->profile_id)
  665. ->latest()
  666. ->simplePaginate($limit)
  667. ->pluck('status_id');
  668. $statuses = Status::findOrFail($favourites);
  669. $resource = new Fractal\Resource\Collection($statuses, new StatusTransformer());
  670. $res = $this->fractal->createData($resource)->toArray();
  671. return response()->json($res);
  672. }
  673. /**
  674. * POST /api/v1/statuses/{id}/favourite
  675. *
  676. * @param integer $id
  677. *
  678. * @return \App\Transformer\Api\StatusTransformer
  679. */
  680. public function statusFavouriteById(Request $request, $id)
  681. {
  682. abort_if(!$request->user(), 403);
  683. $user = $request->user();
  684. $status = Status::findOrFail($id);
  685. if($status->profile_id !== $user->profile_id) {
  686. if($status->scope == 'private') {
  687. abort_if(!$status->profile->followedBy($user->profile), 403);
  688. } else {
  689. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  690. }
  691. }
  692. $like = Like::firstOrCreate([
  693. 'profile_id' => $user->profile_id,
  694. 'status_id' => $status->id
  695. ]);
  696. if($like->wasRecentlyCreated == true) {
  697. $like->status_profile_id = $status->profile_id;
  698. $like->is_comment = !empty($status->in_reply_to_id);
  699. $like->save();
  700. $status->likes_count = $status->likes()->count();
  701. $status->save();
  702. LikePipeline::dispatch($like);
  703. }
  704. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  705. $res = $this->fractal->createData($resource)->toArray();
  706. return response()->json($res);
  707. }
  708. /**
  709. * POST /api/v1/statuses/{id}/unfavourite
  710. *
  711. * @param integer $id
  712. *
  713. * @return \App\Transformer\Api\StatusTransformer
  714. */
  715. public function statusUnfavouriteById(Request $request, $id)
  716. {
  717. abort_if(!$request->user(), 403);
  718. $user = $request->user();
  719. $status = Status::findOrFail($id);
  720. if($status->profile_id !== $user->profile_id) {
  721. if($status->scope == 'private') {
  722. abort_if(!$status->profile->followedBy($user->profile), 403);
  723. } else {
  724. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  725. }
  726. }
  727. $like = Like::whereProfileId($user->profile_id)
  728. ->whereStatusId($status->id)
  729. ->first();
  730. if($like) {
  731. $like->forceDelete();
  732. $status->likes_count = $status->likes()->count();
  733. $status->save();
  734. }
  735. StatusService::del($status->id);
  736. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  737. $res = $this->fractal->createData($resource)->toArray();
  738. return response()->json($res);
  739. }
  740. /**
  741. * GET /api/v1/filters
  742. *
  743. * Return empty response since we filter server side
  744. *
  745. * @return array
  746. */
  747. public function accountFilters(Request $request)
  748. {
  749. abort_if(!$request->user(), 403);
  750. return response()->json([]);
  751. }
  752. /**
  753. * GET /api/v1/follow_requests
  754. *
  755. * Return array of Accounts that have sent follow requests
  756. *
  757. * @return \App\Transformer\Api\AccountTransformer
  758. */
  759. public function accountFollowRequests(Request $request)
  760. {
  761. abort_if(!$request->user(), 403);
  762. $user = $request->user();
  763. $followRequests = FollowRequest::whereFollowingId($user->profile->id)->pluck('follower_id');
  764. $profiles = Profile::find($followRequests);
  765. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  766. $res = $this->fractal->createData($resource)->toArray();
  767. return response()->json($res);
  768. }
  769. /**
  770. * POST /api/v1/follow_requests/{id}/authorize
  771. *
  772. * @param integer $id
  773. *
  774. * @return null
  775. */
  776. public function accountFollowRequestAccept(Request $request, $id)
  777. {
  778. abort_if(!$request->user(), 403);
  779. // todo
  780. return response()->json([]);
  781. }
  782. /**
  783. * POST /api/v1/follow_requests/{id}/reject
  784. *
  785. * @param integer $id
  786. *
  787. * @return null
  788. */
  789. public function accountFollowRequestReject(Request $request, $id)
  790. {
  791. abort_if(!$request->user(), 403);
  792. // todo
  793. return response()->json([]);
  794. }
  795. /**
  796. * GET /api/v1/suggestions
  797. *
  798. * Return empty array as we don't support suggestions
  799. *
  800. * @return null
  801. */
  802. public function accountSuggestions(Request $request)
  803. {
  804. abort_if(!$request->user(), 403);
  805. // todo
  806. return response()->json([]);
  807. }
  808. /**
  809. * GET /api/v1/instance
  810. *
  811. * Information about the server.
  812. *
  813. * @return Instance
  814. */
  815. public function instance(Request $request)
  816. {
  817. $res = Cache::remember('api:v1:instance-data', now()->addMinutes(15), function () {
  818. $rules = config_cache('app.rules') ? collect(json_decode(config_cache('app.rules'), true))
  819. ->map(function($rule, $key) {
  820. $id = $key + 1;
  821. return [
  822. 'id' => "{$id}",
  823. 'text' => $rule
  824. ];
  825. })
  826. ->toArray() : [];
  827. $res = [
  828. 'approval_required' => false,
  829. 'contact_account' => null,
  830. 'description' => config_cache('app.description'),
  831. 'email' => config('instance.email'),
  832. 'invites_enabled' => false,
  833. 'rules' => $rules,
  834. 'short_description' => 'Pixelfed - Photo sharing for everyone',
  835. 'languages' => ['en'],
  836. 'max_toot_chars' => (int) config('pixelfed.max_caption_length'),
  837. 'registrations' => (bool) config_cache('pixelfed.open_registration'),
  838. 'stats' => [
  839. 'user_count' => 0,
  840. 'status_count' => 0,
  841. 'domain_count' => 0
  842. ],
  843. 'thumbnail' => config('app.url') . '/img/pixelfed-icon-color.png',
  844. 'title' => config_cache('app.name'),
  845. 'uri' => config('pixelfed.domain.app'),
  846. 'urls' => [],
  847. 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') . ')',
  848. 'environment' => [
  849. 'max_photo_size' => (int) config_cache('pixelfed.max_photo_size'),
  850. 'max_avatar_size' => (int) config('pixelfed.max_avatar_size'),
  851. 'max_caption_length' => (int) config('pixelfed.max_caption_length'),
  852. 'max_bio_length' => (int) config('pixelfed.max_bio_length'),
  853. 'max_album_length' => (int) config_cache('pixelfed.max_album_length'),
  854. 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled')
  855. ]
  856. ];
  857. return $res;
  858. });
  859. return response()->json($res);
  860. }
  861. /**
  862. * GET /api/v1/lists
  863. *
  864. * Return empty array as we don't support lists
  865. *
  866. * @return null
  867. */
  868. public function accountLists(Request $request)
  869. {
  870. abort_if(!$request->user(), 403);
  871. return response()->json([]);
  872. }
  873. /**
  874. * GET /api/v1/accounts/{id}/lists
  875. *
  876. * @param integer $id
  877. *
  878. * @return null
  879. */
  880. public function accountListsById(Request $request, $id)
  881. {
  882. abort_if(!$request->user(), 403);
  883. return response()->json([]);
  884. }
  885. /**
  886. * POST /api/v1/media
  887. *
  888. *
  889. * @return MediaTransformer
  890. */
  891. public function mediaUpload(Request $request)
  892. {
  893. abort_if(!$request->user(), 403);
  894. $this->validate($request, [
  895. 'file.*' => function() {
  896. return [
  897. 'required',
  898. 'mimes:' . config_cache('pixelfed.media_types'),
  899. 'max:' . config_cache('pixelfed.max_photo_size'),
  900. ];
  901. },
  902. 'filter_name' => 'nullable|string|max:24',
  903. 'filter_class' => 'nullable|alpha_dash|max:24',
  904. 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length')
  905. ]);
  906. $user = $request->user();
  907. if($user->last_active_at == null) {
  908. return [];
  909. }
  910. $limitKey = 'compose:rate-limit:media-upload:' . $user->id;
  911. $limitTtl = now()->addMinutes(15);
  912. $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
  913. $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
  914. return $dailyLimit >= 250;
  915. });
  916. abort_if($limitReached == true, 429);
  917. $profile = $user->profile;
  918. if(config_cache('pixelfed.enforce_account_limit') == true) {
  919. $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
  920. return Media::whereUserId($user->id)->sum('size') / 1000;
  921. });
  922. $limit = (int) config_cache('pixelfed.max_account_size');
  923. if ($size >= $limit) {
  924. abort(403, 'Account size limit reached.');
  925. }
  926. }
  927. $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
  928. $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
  929. $photo = $request->file('file');
  930. $mimes = explode(',', config_cache('pixelfed.media_types'));
  931. if(in_array($photo->getMimeType(), $mimes) == false) {
  932. abort(403, 'Invalid or unsupported mime type.');
  933. }
  934. $storagePath = MediaPathService::get($user, 2);
  935. $path = $photo->store($storagePath);
  936. $hash = \hash_file('sha256', $photo);
  937. $license = null;
  938. $settings = UserSetting::whereUserId($user->id)->first();
  939. if($settings && !empty($settings->compose_settings)) {
  940. $compose = json_decode($settings->compose_settings, true);
  941. if(isset($compose['default_license']) && $compose['default_license'] != 1) {
  942. $license = $compose['default_license'];
  943. }
  944. }
  945. abort_if(MediaBlocklistService::exists($hash) == true, 451);
  946. $media = new Media();
  947. $media->status_id = null;
  948. $media->profile_id = $profile->id;
  949. $media->user_id = $user->id;
  950. $media->media_path = $path;
  951. $media->original_sha256 = $hash;
  952. $media->size = $photo->getSize();
  953. $media->mime = $photo->getMimeType();
  954. $media->caption = $request->input('description');
  955. $media->filter_class = $filterClass;
  956. $media->filter_name = $filterName;
  957. if($license) {
  958. $media->license = $license;
  959. }
  960. $media->save();
  961. switch ($media->mime) {
  962. case 'image/jpeg':
  963. case 'image/png':
  964. ImageOptimize::dispatch($media);
  965. break;
  966. case 'video/mp4':
  967. VideoThumbnail::dispatch($media);
  968. $preview_url = '/storage/no-preview.png';
  969. $url = '/storage/no-preview.png';
  970. break;
  971. }
  972. Cache::forget($limitKey);
  973. $resource = new Fractal\Resource\Item($media, new MediaTransformer());
  974. $res = $this->fractal->createData($resource)->toArray();
  975. $res['preview_url'] = $media->url(). '?cb=1&_v=' . time();
  976. $res['url'] = $media->url(). '?cb=1&_v=' . time();
  977. return response()->json($res);
  978. }
  979. /**
  980. * PUT /api/v1/media/{id}
  981. *
  982. * @param integer $id
  983. *
  984. * @return MediaTransformer
  985. */
  986. public function mediaUpdate(Request $request, $id)
  987. {
  988. abort_if(!$request->user(), 403);
  989. $this->validate($request, [
  990. 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length')
  991. ]);
  992. $user = $request->user();
  993. $media = Media::whereUserId($user->id)
  994. ->whereNull('status_id')
  995. ->findOrFail($id);
  996. $media->caption = $request->input('description');
  997. $media->save();
  998. $resource = new Fractal\Resource\Item($media, new MediaTransformer());
  999. $res = $this->fractal->createData($resource)->toArray();
  1000. $res['preview_url'] = url('/storage/no-preview.png');
  1001. $res['url'] = url('/storage/no-preview.png');
  1002. return response()->json($res);
  1003. }
  1004. /**
  1005. * GET /api/v1/mutes
  1006. *
  1007. *
  1008. * @return AccountTransformer
  1009. */
  1010. public function accountMutes(Request $request)
  1011. {
  1012. abort_if(!$request->user(), 403);
  1013. $this->validate($request, [
  1014. 'limit' => 'nullable|integer|min:1|max:40'
  1015. ]);
  1016. $user = $request->user();
  1017. $limit = $request->input('limit') ?? 40;
  1018. $mutes = UserFilter::whereUserId($user->profile_id)
  1019. ->whereFilterableType('App\Profile')
  1020. ->whereFilterType('mute')
  1021. ->simplePaginate($limit)
  1022. ->pluck('filterable_id');
  1023. $accounts = Profile::find($mutes);
  1024. $resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
  1025. $res = $this->fractal->createData($resource)->toArray();
  1026. return response()->json($res);
  1027. }
  1028. /**
  1029. * POST /api/v1/accounts/{id}/mute
  1030. *
  1031. * @param integer $id
  1032. *
  1033. * @return RelationshipTransformer
  1034. */
  1035. public function accountMuteById(Request $request, $id)
  1036. {
  1037. abort_if(!$request->user(), 403);
  1038. $user = $request->user();
  1039. $pid = $user->profile_id;
  1040. $account = Profile::findOrFail($id);
  1041. $filter = UserFilter::firstOrCreate([
  1042. 'user_id' => $pid,
  1043. 'filterable_id' => $account->id,
  1044. 'filterable_type' => 'App\Profile',
  1045. 'filter_type' => 'mute',
  1046. ]);
  1047. Cache::forget("user:filter:list:$pid");
  1048. Cache::forget("feature:discover:posts:$pid");
  1049. Cache::forget("api:local:exp:rec:$pid");
  1050. $resource = new Fractal\Resource\Item($account, new RelationshipTransformer());
  1051. $res = $this->fractal->createData($resource)->toArray();
  1052. return response()->json($res);
  1053. }
  1054. /**
  1055. * POST /api/v1/accounts/{id}/unmute
  1056. *
  1057. * @param integer $id
  1058. *
  1059. * @return RelationshipTransformer
  1060. */
  1061. public function accountUnmuteById(Request $request, $id)
  1062. {
  1063. abort_if(!$request->user(), 403);
  1064. $user = $request->user();
  1065. $pid = $user->profile_id;
  1066. $account = Profile::findOrFail($id);
  1067. $filter = UserFilter::whereUserId($pid)
  1068. ->whereFilterableId($account->id)
  1069. ->whereFilterableType('App\Profile')
  1070. ->whereFilterType('mute')
  1071. ->first();
  1072. if($filter) {
  1073. $filter->delete();
  1074. Cache::forget("user:filter:list:$pid");
  1075. Cache::forget("feature:discover:posts:$pid");
  1076. Cache::forget("api:local:exp:rec:$pid");
  1077. }
  1078. $resource = new Fractal\Resource\Item($account, new RelationshipTransformer());
  1079. $res = $this->fractal->createData($resource)->toArray();
  1080. return response()->json($res);
  1081. }
  1082. /**
  1083. * GET /api/v1/notifications
  1084. *
  1085. *
  1086. * @return NotificationTransformer
  1087. */
  1088. public function accountNotifications(Request $request)
  1089. {
  1090. abort_if(!$request->user(), 403);
  1091. $this->validate($request, [
  1092. 'limit' => 'nullable|integer|min:1|max:80',
  1093. 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
  1094. 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
  1095. 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
  1096. ]);
  1097. $pid = $request->user()->profile_id;
  1098. $limit = $request->input('limit', 20);
  1099. $since = $request->input('since_id');
  1100. $min = $request->input('min_id');
  1101. $max = $request->input('max_id');
  1102. if(!$since && !$min && !$max) {
  1103. $min = 1;
  1104. }
  1105. $maxId = null;
  1106. $minId = null;
  1107. if($max) {
  1108. $res = NotificationService::getMax($pid, $max, $limit);
  1109. $ids = NotificationService::getRankedMaxId($pid, $max, $limit);
  1110. if(!empty($ids)) {
  1111. $maxId = max($ids);
  1112. $minId = min($ids);
  1113. }
  1114. } else {
  1115. $res = NotificationService::getMin($pid, $min ?? $since, $limit);
  1116. $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
  1117. if(!empty($ids)) {
  1118. $maxId = max($ids);
  1119. $minId = min($ids);
  1120. }
  1121. }
  1122. if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
  1123. Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
  1124. NotificationService::warmCache($pid, 400, true);
  1125. }
  1126. $baseUrl = config('app.url') . '/api/v1/notifications?';
  1127. if($minId == $maxId) {
  1128. $minId = null;
  1129. }
  1130. if($maxId) {
  1131. $link = '<'.$baseUrl.'max_id='.$maxId.'>; rel="next"';
  1132. }
  1133. if($minId) {
  1134. $link = '<'.$baseUrl.'min_id='.$minId.'>; rel="prev"';
  1135. }
  1136. if($maxId && $minId) {
  1137. $link = '<'.$baseUrl.'max_id='.$maxId.'>; rel="next",<'.$baseUrl.'min_id='.$minId.'>; rel="prev"';
  1138. }
  1139. $res = response()->json($res);
  1140. if(isset($link)) {
  1141. $res->withHeaders([
  1142. 'Link' => $link,
  1143. ]);
  1144. }
  1145. return $res;
  1146. }
  1147. /**
  1148. * GET /api/v1/timelines/home
  1149. *
  1150. *
  1151. * @return StatusTransformer
  1152. */
  1153. public function timelineHome(Request $request)
  1154. {
  1155. abort_if(!$request->user(), 403);
  1156. $this->validate($request,[
  1157. 'page' => 'nullable|integer|max:40',
  1158. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  1159. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  1160. 'limit' => 'nullable|integer|max:80'
  1161. ]);
  1162. $page = $request->input('page');
  1163. $min = $request->input('min_id');
  1164. $max = $request->input('max_id');
  1165. $limit = $request->input('limit') ?? 3;
  1166. $user = $request->user();
  1167. if($user->last_active_at) {
  1168. $key = 'user:last_active_at:id:'.$user->id;
  1169. $ttl = now()->addMinutes(5);
  1170. Cache::remember($key, $ttl, function() use($user) {
  1171. $user->last_active_at = now();
  1172. $user->save();
  1173. return;
  1174. });
  1175. }
  1176. $pid = $request->user()->profile_id;
  1177. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
  1178. $following = Follower::whereProfileId($pid)->pluck('following_id');
  1179. return $following->push($pid)->toArray();
  1180. });
  1181. if($min || $max) {
  1182. $dir = $min ? '>' : '<';
  1183. $id = $min ?? $max;
  1184. $timeline = Status::select(
  1185. 'id',
  1186. 'uri',
  1187. 'caption',
  1188. 'rendered',
  1189. 'profile_id',
  1190. 'type',
  1191. 'in_reply_to_id',
  1192. 'reblog_of_id',
  1193. 'is_nsfw',
  1194. 'scope',
  1195. 'local',
  1196. 'reply_count',
  1197. 'likes_count',
  1198. 'reblogs_count',
  1199. 'comments_disabled',
  1200. 'place_id',
  1201. 'created_at',
  1202. 'updated_at'
  1203. )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
  1204. ->with('profile', 'hashtags', 'mentions')
  1205. ->where('id', $dir, $id)
  1206. ->whereIn('profile_id', $following)
  1207. ->whereIn('visibility',['public', 'unlisted', 'private'])
  1208. ->latest()
  1209. ->limit($limit)
  1210. ->get();
  1211. } else {
  1212. $timeline = Status::select(
  1213. 'id',
  1214. 'uri',
  1215. 'caption',
  1216. 'rendered',
  1217. 'profile_id',
  1218. 'type',
  1219. 'in_reply_to_id',
  1220. 'reblog_of_id',
  1221. 'is_nsfw',
  1222. 'scope',
  1223. 'local',
  1224. 'reply_count',
  1225. 'comments_disabled',
  1226. 'likes_count',
  1227. 'reblogs_count',
  1228. 'place_id',
  1229. 'created_at',
  1230. 'updated_at'
  1231. )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
  1232. ->with('profile', 'hashtags', 'mentions')
  1233. ->whereIn('profile_id', $following)
  1234. ->whereIn('visibility',['public', 'unlisted', 'private'])
  1235. ->latest()
  1236. ->simplePaginate($limit);
  1237. }
  1238. $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
  1239. $res = $this->fractal->createData($fractal)->toArray();
  1240. return response()->json($res);
  1241. }
  1242. /**
  1243. * GET /api/v1/conversations
  1244. *
  1245. * Not implemented
  1246. *
  1247. * @return array
  1248. */
  1249. public function conversations(Request $request)
  1250. {
  1251. abort_if(!$request->user(), 403);
  1252. return response()->json([]);
  1253. }
  1254. /**
  1255. * GET /api/v1/timelines/public
  1256. *
  1257. *
  1258. * @return StatusTransformer
  1259. */
  1260. public function timelinePublic(Request $request)
  1261. {
  1262. $this->validate($request,[
  1263. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  1264. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  1265. 'limit' => 'nullable|integer|max:80'
  1266. ]);
  1267. $min = $request->input('min_id');
  1268. $max = $request->input('max_id');
  1269. $limit = $request->input('limit') ?? 3;
  1270. $user = $request->user();
  1271. Cache::remember('api:v1:timelines:public:cache_check', 3600, function() {
  1272. if(PublicTimelineService::count() == 0) {
  1273. PublicTimelineService::warmCache(true, 400);
  1274. }
  1275. });
  1276. if ($max) {
  1277. $feed = PublicTimelineService::getRankedMaxId($max, $limit);
  1278. } else if ($min) {
  1279. $feed = PublicTimelineService::getRankedMinId($min, $limit);
  1280. } else {
  1281. $feed = PublicTimelineService::get(0, $limit);
  1282. }
  1283. $res = collect($feed)
  1284. ->map(function($k) use($user) {
  1285. $status = StatusService::get($k);
  1286. if($user) {
  1287. $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
  1288. }
  1289. return $status;
  1290. })
  1291. ->toArray();
  1292. return response()->json($res);
  1293. }
  1294. /**
  1295. * GET /api/v1/statuses/{id}
  1296. *
  1297. * @param integer $id
  1298. *
  1299. * @return StatusTransformer
  1300. */
  1301. public function statusById(Request $request, $id)
  1302. {
  1303. abort_if(!$request->user(), 403);
  1304. $user = $request->user();
  1305. $status = Status::findOrFail($id);
  1306. if($status->profile_id !== $user->profile_id) {
  1307. if($status->scope == 'private') {
  1308. abort_if(!$status->profile->followedBy($user->profile), 403);
  1309. } else {
  1310. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  1311. }
  1312. }
  1313. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1314. $res = $this->fractal->createData($resource)->toArray();
  1315. return response()->json($res);
  1316. }
  1317. /**
  1318. * GET /api/v1/statuses/{id}/context
  1319. *
  1320. * @param integer $id
  1321. *
  1322. * @return StatusTransformer
  1323. */
  1324. public function statusContext(Request $request, $id)
  1325. {
  1326. abort_if(!$request->user(), 403);
  1327. $user = $request->user();
  1328. $status = Status::findOrFail($id);
  1329. if($status->profile_id !== $user->profile_id) {
  1330. if($status->scope == 'private') {
  1331. abort_if(!$status->profile->followedBy($user->profile), 403);
  1332. } else {
  1333. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  1334. }
  1335. }
  1336. if($status->comments_disabled) {
  1337. $res = [
  1338. 'ancestors' => [],
  1339. 'descendants' => []
  1340. ];
  1341. } else {
  1342. $ancestors = $status->parent();
  1343. if($ancestors) {
  1344. $ares = new Fractal\Resource\Item($ancestors, new StatusTransformer());
  1345. $ancestors = [
  1346. $this->fractal->createData($ares)->toArray()
  1347. ];
  1348. } else {
  1349. $ancestors = [];
  1350. }
  1351. $descendants = Status::whereInReplyToId($id)->latest()->limit(20)->get();
  1352. $dres = new Fractal\Resource\Collection($descendants, new StatusTransformer());
  1353. $descendants = $this->fractal->createData($dres)->toArray();
  1354. $res = [
  1355. 'ancestors' => $ancestors,
  1356. 'descendants' => $descendants
  1357. ];
  1358. }
  1359. return response()->json($res);
  1360. }
  1361. /**
  1362. * GET /api/v1/statuses/{id}/card
  1363. *
  1364. * @param integer $id
  1365. *
  1366. * @return StatusTransformer
  1367. */
  1368. public function statusCard(Request $request, $id)
  1369. {
  1370. abort_if(!$request->user(), 403);
  1371. $user = $request->user();
  1372. $status = Status::findOrFail($id);
  1373. if($status->profile_id !== $user->profile_id) {
  1374. if($status->scope == 'private') {
  1375. abort_if(!$status->profile->followedBy($user->profile), 403);
  1376. } else {
  1377. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  1378. }
  1379. }
  1380. // Return empty response since we don't handle support cards
  1381. $res = [];
  1382. return response()->json($res);
  1383. }
  1384. /**
  1385. * GET /api/v1/statuses/{id}/reblogged_by
  1386. *
  1387. * @param integer $id
  1388. *
  1389. * @return AccountTransformer
  1390. */
  1391. public function statusRebloggedBy(Request $request, $id)
  1392. {
  1393. abort_if(!$request->user(), 403);
  1394. $this->validate($request, [
  1395. 'page' => 'nullable|integer|min:1|max:40',
  1396. 'limit' => 'nullable|integer|min:1|max:80'
  1397. ]);
  1398. $limit = $request->input('limit') ?? 40;
  1399. $user = $request->user();
  1400. $status = Status::findOrFail($id);
  1401. if($status->profile_id !== $user->profile_id) {
  1402. if($status->scope == 'private') {
  1403. abort_if(!$status->profile->followedBy($user->profile), 403);
  1404. } else {
  1405. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  1406. }
  1407. }
  1408. $shared = $status->sharedBy()->latest()->simplePaginate($limit);
  1409. $resource = new Fractal\Resource\Collection($shared, new AccountTransformer());
  1410. $res = $this->fractal->createData($resource)->toArray();
  1411. $url = $request->url();
  1412. $page = $request->input('page', 1);
  1413. $next = $page < 40 ? $page + 1 : 40;
  1414. $prev = $page > 1 ? $page - 1 : 1;
  1415. $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
  1416. return response()->json($res, 200, ['Link' => $links]);
  1417. }
  1418. /**
  1419. * GET /api/v1/statuses/{id}/favourited_by
  1420. *
  1421. * @param integer $id
  1422. *
  1423. * @return AccountTransformer
  1424. */
  1425. public function statusFavouritedBy(Request $request, $id)
  1426. {
  1427. abort_if(!$request->user(), 403);
  1428. $this->validate($request, [
  1429. 'page' => 'nullable|integer|min:1|max:40',
  1430. 'limit' => 'nullable|integer|min:1|max:80'
  1431. ]);
  1432. $limit = $request->input('limit') ?? 40;
  1433. $user = $request->user();
  1434. $status = Status::findOrFail($id);
  1435. if($status->profile_id !== $user->profile_id) {
  1436. if($status->scope == 'private') {
  1437. abort_if(!$status->profile->followedBy($user->profile), 403);
  1438. } else {
  1439. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  1440. }
  1441. }
  1442. $liked = $status->likedBy()->latest()->simplePaginate($limit);
  1443. $resource = new Fractal\Resource\Collection($liked, new AccountTransformer());
  1444. $res = $this->fractal->createData($resource)->toArray();
  1445. $url = $request->url();
  1446. $page = $request->input('page', 1);
  1447. $next = $page < 40 ? $page + 1 : 40;
  1448. $prev = $page > 1 ? $page - 1 : 1;
  1449. $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
  1450. return response()->json($res, 200, ['Link' => $links]);
  1451. }
  1452. /**
  1453. * POST /api/v1/statuses
  1454. *
  1455. *
  1456. * @return StatusTransformer
  1457. */
  1458. public function statusCreate(Request $request)
  1459. {
  1460. abort_if(!$request->user(), 403);
  1461. $this->validate($request, [
  1462. 'status' => 'nullable|string',
  1463. 'in_reply_to_id' => 'nullable|integer',
  1464. 'media_ids' => 'array|max:' . config_cache('pixelfed.max_album_length'),
  1465. 'media_ids.*' => 'integer|min:1',
  1466. 'sensitive' => 'nullable|boolean',
  1467. 'visibility' => 'string|in:private,unlisted,public',
  1468. ]);
  1469. if(config('costar.enabled') == true) {
  1470. $blockedKeywords = config('costar.keyword.block');
  1471. if($blockedKeywords !== null && $request->status) {
  1472. $keywords = config('costar.keyword.block');
  1473. foreach($keywords as $kw) {
  1474. if(Str::contains($request->status, $kw) == true) {
  1475. abort(400, 'Invalid object. Contains banned keyword.');
  1476. }
  1477. }
  1478. }
  1479. }
  1480. if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) {
  1481. abort(403, 'Empty statuses are not allowed');
  1482. }
  1483. $ids = $request->input('media_ids');
  1484. $in_reply_to_id = $request->input('in_reply_to_id');
  1485. $user = $request->user();
  1486. $profile = $user->profile;
  1487. $limitKey = 'compose:rate-limit:store:' . $user->id;
  1488. $limitTtl = now()->addMinutes(15);
  1489. $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
  1490. $dailyLimit = Status::whereProfileId($user->profile_id)
  1491. ->whereNull('in_reply_to_id')
  1492. ->whereNull('reblog_of_id')
  1493. ->where('created_at', '>', now()->subDays(1))
  1494. ->count();
  1495. return $dailyLimit >= 100;
  1496. });
  1497. abort_if($limitReached == true, 429);
  1498. $visibility = $profile->is_private ? 'private' : (
  1499. $profile->unlisted == true &&
  1500. $request->input('visibility', 'public') == 'public' ?
  1501. 'unlisted' :
  1502. $request->input('visibility', 'public'));
  1503. if($user->last_active_at == null) {
  1504. return [];
  1505. }
  1506. if($in_reply_to_id) {
  1507. $parent = Status::findOrFail($in_reply_to_id);
  1508. $status = new Status;
  1509. $status->caption = strip_tags($request->input('status'));
  1510. $status->scope = $visibility;
  1511. $status->visibility = $visibility;
  1512. $status->profile_id = $user->profile_id;
  1513. $status->is_nsfw = $user->profile->cw == true ? true : $request->input('sensitive', false);
  1514. $status->in_reply_to_id = $parent->id;
  1515. $status->in_reply_to_profile_id = $parent->profile_id;
  1516. $status->save();
  1517. StatusService::del($parent->id);
  1518. } else if($ids) {
  1519. if(Media::whereUserId($user->id)
  1520. ->whereNull('status_id')
  1521. ->find($ids)
  1522. ->count() == 0
  1523. ) {
  1524. abort(400, 'Invalid media_ids');
  1525. }
  1526. $status = new Status;
  1527. $status->caption = strip_tags($request->input('status'));
  1528. $status->profile_id = $user->profile_id;
  1529. $status->scope = 'draft';
  1530. $status->is_nsfw = $user->profile->cw == true ? true : $request->input('sensitive', false);
  1531. $status->save();
  1532. $mimes = [];
  1533. foreach($ids as $k => $v) {
  1534. if($k + 1 > config_cache('pixelfed.max_album_length')) {
  1535. continue;
  1536. }
  1537. $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v);
  1538. if($m->profile_id !== $user->profile_id || $m->status_id) {
  1539. abort(403, 'Invalid media id');
  1540. }
  1541. $m->status_id = $status->id;
  1542. $m->save();
  1543. array_push($mimes, $m->mime);
  1544. }
  1545. if(empty($mimes)) {
  1546. $status->delete();
  1547. abort(400, 'Invalid media ids');
  1548. }
  1549. $status->scope = $visibility;
  1550. $status->visibility = $visibility;
  1551. $status->type = StatusController::mimeTypeCheck($mimes);
  1552. $status->save();
  1553. }
  1554. if(!$status) {
  1555. abort(500, 'An error occured.');
  1556. }
  1557. NewStatusPipeline::dispatch($status);
  1558. Cache::forget('user:account:id:'.$user->id);
  1559. Cache::forget('_api:statuses:recent_9:'.$user->profile_id);
  1560. Cache::forget('profile:status_count:'.$user->profile_id);
  1561. Cache::forget($user->storageUsedKey());
  1562. Cache::forget('profile:embed:' . $status->profile_id);
  1563. Cache::forget($limitKey);
  1564. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1565. $res = $this->fractal->createData($resource)->toArray();
  1566. return response()->json($res);
  1567. }
  1568. /**
  1569. * DELETE /api/v1/statuses
  1570. *
  1571. * @param integer $id
  1572. *
  1573. * @return null
  1574. */
  1575. public function statusDelete(Request $request, $id)
  1576. {
  1577. abort_if(!$request->user(), 403);
  1578. $status = Status::whereProfileId($request->user()->profile->id)
  1579. ->findOrFail($id);
  1580. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1581. Cache::forget('profile:status_count:'.$status->profile_id);
  1582. StatusDelete::dispatch($status);
  1583. $res = $this->fractal->createData($resource)->toArray();
  1584. $res['text'] = $res['content'];
  1585. unset($res['content']);
  1586. return response()->json($res);
  1587. }
  1588. /**
  1589. * POST /api/v1/statuses/{id}/reblog
  1590. *
  1591. * @param integer $id
  1592. *
  1593. * @return StatusTransformer
  1594. */
  1595. public function statusShare(Request $request, $id)
  1596. {
  1597. abort_if(!$request->user(), 403);
  1598. $user = $request->user();
  1599. $status = Status::findOrFail($id);
  1600. if($status->profile_id !== $user->profile_id) {
  1601. if($status->scope == 'private') {
  1602. abort_if(!$status->profile->followedBy($user->profile), 403);
  1603. } else {
  1604. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  1605. }
  1606. }
  1607. $share = Status::firstOrCreate([
  1608. 'profile_id' => $user->profile_id,
  1609. 'reblog_of_id' => $status->id,
  1610. 'in_reply_to_profile_id' => $status->profile_id,
  1611. 'scope' => 'public',
  1612. 'visibility' => 'public'
  1613. ]);
  1614. if($share->wasRecentlyCreated == true) {
  1615. SharePipeline::dispatch($share);
  1616. }
  1617. StatusService::del($status->id);
  1618. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1619. $res = $this->fractal->createData($resource)->toArray();
  1620. return response()->json($res);
  1621. }
  1622. /**
  1623. * POST /api/v1/statuses/{id}/unreblog
  1624. *
  1625. * @param integer $id
  1626. *
  1627. * @return StatusTransformer
  1628. */
  1629. public function statusUnshare(Request $request, $id)
  1630. {
  1631. abort_if(!$request->user(), 403);
  1632. $user = $request->user();
  1633. $status = Status::findOrFail($id);
  1634. if($status->profile_id !== $user->profile_id) {
  1635. if($status->scope == 'private') {
  1636. abort_if(!$status->profile->followedBy($user->profile), 403);
  1637. } else {
  1638. abort_if(!in_array($status->scope, ['public','unlisted']), 403);
  1639. }
  1640. }
  1641. $reblog = Status::whereProfileId($user->profile_id)
  1642. ->whereReblogOfId($status->id)
  1643. ->first();
  1644. if(!$reblog) {
  1645. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1646. $res = $this->fractal->createData($resource)->toArray();
  1647. return response()->json($res);
  1648. }
  1649. UndoSharePipeline::dispatch($reblog);
  1650. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1651. $res = $this->fractal->createData($resource)->toArray();
  1652. return response()->json($res);
  1653. }
  1654. /**
  1655. * GET /api/v1/timelines/tag/{hashtag}
  1656. *
  1657. * @param string $hashtag
  1658. *
  1659. * @return StatusTransformer
  1660. */
  1661. public function timelineHashtag(Request $request, $hashtag)
  1662. {
  1663. $this->validate($request,[
  1664. 'page' => 'nullable|integer|max:40',
  1665. 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  1666. 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
  1667. 'limit' => 'nullable|integer|max:40'
  1668. ]);
  1669. $tag = Hashtag::whereName($hashtag)
  1670. ->orWhere('slug', $hashtag)
  1671. ->first();
  1672. if(!$tag) {
  1673. return response()->json([]);
  1674. }
  1675. $min = $request->input('min_id');
  1676. $max = $request->input('max_id');
  1677. $limit = $request->input('limit', 20);
  1678. if(!$min && !$max) {
  1679. $id = 1;
  1680. $dir = '>';
  1681. } else {
  1682. $dir = $min ? '>' : '<';
  1683. $id = $min ?? $max;
  1684. }
  1685. $res = StatusHashtag::whereHashtagId($tag->id)
  1686. ->whereStatusVisibility('public')
  1687. ->whereHas('media')
  1688. ->where('status_id', $dir, $id)
  1689. ->latest()
  1690. ->limit($limit)
  1691. ->pluck('status_id')
  1692. ->filter(function($i) {
  1693. return StatusService::get($i);
  1694. })
  1695. ->map(function ($i) {
  1696. return StatusService::get($i);
  1697. })
  1698. ->filter()
  1699. ->values()
  1700. ->toArray();
  1701. return response()->json($res, 200, [], JSON_PRETTY_PRINT);
  1702. }
  1703. /**
  1704. * GET /api/v1/bookmarks
  1705. *
  1706. *
  1707. *
  1708. * @return StatusTransformer
  1709. */
  1710. public function bookmarks(Request $request)
  1711. {
  1712. abort_if(!$request->user(), 403);
  1713. $this->validate($request, [
  1714. 'limit' => 'nullable|integer|min:1|max:40',
  1715. 'max_id' => 'nullable|integer|min:0',
  1716. 'since_id' => 'nullable|integer|min:0',
  1717. 'min_id' => 'nullable|integer|min:0'
  1718. ]);
  1719. $pid = $request->user()->profile_id;
  1720. $limit = $request->input('limit') ?? 20;
  1721. $max_id = $request->input('max_id');
  1722. $since_id = $request->input('since_id');
  1723. $min_id = $request->input('min_id');
  1724. $dir = $min_id ? '>' : '<';
  1725. $id = $min_id ?? $max_id;
  1726. if($id) {
  1727. $bookmarks = Bookmark::whereProfileId($pid)
  1728. ->where('status_id', $dir, $id)
  1729. ->limit($limit)
  1730. ->pluck('status_id');
  1731. } else {
  1732. $bookmarks = Bookmark::whereProfileId($pid)
  1733. ->latest()
  1734. ->limit($limit)
  1735. ->pluck('status_id');
  1736. }
  1737. $res = [];
  1738. foreach($bookmarks as $id) {
  1739. $res[] = \App\Services\StatusService::get($id);
  1740. }
  1741. return $res;
  1742. }
  1743. /**
  1744. * POST /api/v1/statuses/{id}/bookmark
  1745. *
  1746. *
  1747. *
  1748. * @return StatusTransformer
  1749. */
  1750. public function bookmarkStatus(Request $request, $id)
  1751. {
  1752. abort_if(!$request->user(), 403);
  1753. $status = Status::whereNull('uri')
  1754. ->whereScope('public')
  1755. ->findOrFail($id);
  1756. Bookmark::firstOrCreate([
  1757. 'status_id' => $status->id,
  1758. 'profile_id' => $request->user()->profile_id
  1759. ]);
  1760. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1761. $res = $this->fractal->createData($resource)->toArray();
  1762. return response()->json($res);
  1763. }
  1764. /**
  1765. * POST /api/v1/statuses/{id}/unbookmark
  1766. *
  1767. *
  1768. *
  1769. * @return StatusTransformer
  1770. */
  1771. public function unbookmarkStatus(Request $request, $id)
  1772. {
  1773. abort_if(!$request->user(), 403);
  1774. $status = Status::whereNull('uri')
  1775. ->whereScope('public')
  1776. ->findOrFail($id);
  1777. Bookmark::firstOrCreate([
  1778. 'status_id' => $status->id,
  1779. 'profile_id' => $request->user()->profile_id
  1780. ]);
  1781. $bookmark = Bookmark::whereStatusId($status->id)
  1782. ->whereProfileId($request->user()->profile_id)
  1783. ->firstOrFail();
  1784. $bookmark->delete();
  1785. $resource = new Fractal\Resource\Item($status, new StatusTransformer());
  1786. $res = $this->fractal->createData($resource)->toArray();
  1787. return response()->json($res);
  1788. }
  1789. /**
  1790. * GET /api/v2/search
  1791. *
  1792. *
  1793. * @return array
  1794. */
  1795. public function searchV2(Request $request)
  1796. {
  1797. abort_if(!$request->user(), 403);
  1798. $this->validate($request, [
  1799. 'q' => 'required|string|min:1|max:80',
  1800. 'account_id' => 'nullable|string',
  1801. 'max_id' => 'nullable|string',
  1802. 'min_id' => 'nullable|string',
  1803. 'type' => 'nullable|in:accounts,hashtags,statuses',
  1804. 'exclude_unreviewed' => 'nullable',
  1805. 'resolve' => 'nullable',
  1806. 'limit' => 'nullable|integer|max:40',
  1807. 'offset' => 'nullable|integer',
  1808. 'following' => 'nullable'
  1809. ]);
  1810. return SearchApiV2Service::query($request);
  1811. }
  1812. }