ApiV1Controller.php 62 KB

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