ApiV1Controller.php 54 KB

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