1
0

InternalApiController.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\AccountInterstitial;
  4. use App\Bookmark;
  5. use App\DirectMessage;
  6. use App\DiscoverCategory;
  7. use App\Follower;
  8. use App\Jobs\ModPipeline\HandleSpammerPipeline;
  9. use App\Profile;
  10. use App\Services\BookmarkService;
  11. use App\Services\DiscoverService;
  12. use App\Services\ModLogService;
  13. use App\Services\PublicTimelineService;
  14. use App\Services\StatusService;
  15. use App\Services\UserFilterService;
  16. use App\Status; // StatusMediaContainerTransformer,
  17. use App\Transformer\Api\StatusTransformer;
  18. use App\User;
  19. use Auth;
  20. use Cache;
  21. use Illuminate\Http\Request;
  22. use Illuminate\Support\Facades\Redis;
  23. use Illuminate\Validation\Rule;
  24. use League\Fractal;
  25. use League\Fractal\Serializer\ArraySerializer;
  26. class InternalApiController extends Controller
  27. {
  28. protected $fractal;
  29. public function __construct()
  30. {
  31. $this->middleware('auth');
  32. $this->fractal = new Fractal\Manager;
  33. $this->fractal->setSerializer(new ArraySerializer);
  34. }
  35. // deprecated v2 compose api
  36. public function compose(Request $request)
  37. {
  38. return redirect('/');
  39. }
  40. // deprecated
  41. public function discover(Request $request) {}
  42. public function discoverPosts(Request $request)
  43. {
  44. $pid = $request->user()->profile_id;
  45. $filters = UserFilterService::filters($pid);
  46. $forYou = DiscoverService::getForYou();
  47. $posts = $forYou->take(50)->map(function ($post) {
  48. return StatusService::get($post);
  49. })
  50. ->filter(function ($post) use ($filters) {
  51. return $post &&
  52. isset($post['account']) &&
  53. isset($post['account']['id']) &&
  54. ! in_array($post['account']['id'], $filters);
  55. })
  56. ->take(12)
  57. ->values();
  58. return response()->json(compact('posts'));
  59. }
  60. public function directMessage(Request $request, $profileId, $threadId)
  61. {
  62. $profile = Auth::user()->profile;
  63. if ($profileId != $profile->id) {
  64. abort(403);
  65. }
  66. $msg = DirectMessage::whereToId($profile->id)
  67. ->orWhere('from_id', $profile->id)
  68. ->findOrFail($threadId);
  69. $thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
  70. ->whereIn('from_id', [$profile->id, $msg->from_id])
  71. ->orderBy('created_at', 'asc')
  72. ->paginate(30);
  73. return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
  74. }
  75. public function statusReplies(Request $request, int $id)
  76. {
  77. $this->validate($request, [
  78. 'limit' => 'nullable|int|min:1|max:6',
  79. ]);
  80. $parent = Status::whereScope('public')->findOrFail($id);
  81. $limit = $request->input('limit') ?? 3;
  82. $children = Status::whereInReplyToId($parent->id)
  83. ->orderBy('created_at', 'desc')
  84. ->take($limit)
  85. ->get();
  86. $resource = new Fractal\Resource\Collection($children, new StatusTransformer);
  87. $res = $this->fractal->createData($resource)->toArray();
  88. return response()->json($res);
  89. }
  90. public function stories(Request $request) {}
  91. public function discoverCategories(Request $request)
  92. {
  93. $categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
  94. $res = $categories->map(function ($item) {
  95. return [
  96. 'name' => $item->name,
  97. 'url' => $item->url(),
  98. 'thumb' => $item->thumb(),
  99. ];
  100. });
  101. return response()->json($res);
  102. }
  103. public function modAction(Request $request)
  104. {
  105. abort_unless(Auth::user()->is_admin, 400);
  106. $this->validate($request, [
  107. 'action' => [
  108. 'required',
  109. 'string',
  110. Rule::in([
  111. 'addcw',
  112. 'remcw',
  113. 'unlist',
  114. 'spammer',
  115. ]),
  116. ],
  117. 'item_id' => 'required|integer|min:1',
  118. 'item_type' => [
  119. 'required',
  120. 'string',
  121. Rule::in(['profile', 'status']),
  122. ],
  123. ]);
  124. $action = $request->input('action');
  125. $item_id = $request->input('item_id');
  126. $item_type = $request->input('item_type');
  127. $status = Status::findOrFail($item_id);
  128. $author = User::whereProfileId($status->profile_id)->first();
  129. abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
  130. switch ($action) {
  131. case 'addcw':
  132. $status->is_nsfw = true;
  133. $status->save();
  134. ModLogService::boot()
  135. ->user(Auth::user())
  136. ->objectUid($status->profile->user_id)
  137. ->objectId($status->id)
  138. ->objectType('App\Status::class')
  139. ->action('admin.status.moderate')
  140. ->metadata([
  141. 'action' => 'cw',
  142. 'message' => 'Success!',
  143. ])
  144. ->accessLevel('admin')
  145. ->save();
  146. if ($status->uri == null) {
  147. $media = $status->media;
  148. $ai = new AccountInterstitial;
  149. $ai->user_id = $status->profile->user_id;
  150. $ai->type = 'post.cw';
  151. $ai->view = 'account.moderation.post.cw';
  152. $ai->item_type = 'App\Status';
  153. $ai->item_id = $status->id;
  154. $ai->has_media = (bool) $media->count();
  155. $ai->blurhash = $media->count() ? $media->first()->blurhash : null;
  156. $ai->meta = json_encode([
  157. 'caption' => $status->caption,
  158. 'created_at' => $status->created_at,
  159. 'type' => $status->type,
  160. 'url' => $status->url(),
  161. 'is_nsfw' => $status->is_nsfw,
  162. 'scope' => $status->scope,
  163. 'reblog' => $status->reblog_of_id,
  164. 'likes_count' => $status->likes_count,
  165. 'reblogs_count' => $status->reblogs_count,
  166. ]);
  167. $ai->save();
  168. $u = $status->profile->user;
  169. $u->has_interstitial = true;
  170. $u->save();
  171. }
  172. break;
  173. case 'remcw':
  174. $status->is_nsfw = false;
  175. $status->save();
  176. ModLogService::boot()
  177. ->user(Auth::user())
  178. ->objectUid($status->profile->user_id)
  179. ->objectId($status->id)
  180. ->objectType('App\Status::class')
  181. ->action('admin.status.moderate')
  182. ->metadata([
  183. 'action' => 'remove_cw',
  184. 'message' => 'Success!',
  185. ])
  186. ->accessLevel('admin')
  187. ->save();
  188. if ($status->uri == null) {
  189. $ai = AccountInterstitial::whereUserId($status->profile->user_id)
  190. ->whereType('post.cw')
  191. ->whereItemId($status->id)
  192. ->whereItemType('App\Status')
  193. ->first();
  194. $ai->delete();
  195. }
  196. break;
  197. case 'unlist':
  198. $status->scope = $status->visibility = 'unlisted';
  199. $status->save();
  200. PublicTimelineService::del($status->id);
  201. ModLogService::boot()
  202. ->user(Auth::user())
  203. ->objectUid($status->profile->user_id)
  204. ->objectId($status->id)
  205. ->objectType('App\Status::class')
  206. ->action('admin.status.moderate')
  207. ->metadata([
  208. 'action' => 'unlist',
  209. 'message' => 'Success!',
  210. ])
  211. ->accessLevel('admin')
  212. ->save();
  213. if ($status->uri == null) {
  214. $media = $status->media;
  215. $ai = new AccountInterstitial;
  216. $ai->user_id = $status->profile->user_id;
  217. $ai->type = 'post.unlist';
  218. $ai->view = 'account.moderation.post.unlist';
  219. $ai->item_type = 'App\Status';
  220. $ai->item_id = $status->id;
  221. $ai->has_media = (bool) $media->count();
  222. $ai->blurhash = $media->count() ? $media->first()->blurhash : null;
  223. $ai->meta = json_encode([
  224. 'caption' => $status->caption,
  225. 'created_at' => $status->created_at,
  226. 'type' => $status->type,
  227. 'url' => $status->url(),
  228. 'is_nsfw' => $status->is_nsfw,
  229. 'scope' => $status->scope,
  230. 'reblog' => $status->reblog_of_id,
  231. 'likes_count' => $status->likes_count,
  232. 'reblogs_count' => $status->reblogs_count,
  233. ]);
  234. $ai->save();
  235. $u = $status->profile->user;
  236. $u->has_interstitial = true;
  237. $u->save();
  238. }
  239. break;
  240. case 'spammer':
  241. HandleSpammerPipeline::dispatch($status->profile);
  242. ModLogService::boot()
  243. ->user(Auth::user())
  244. ->objectUid($status->profile->user_id)
  245. ->objectId($status->id)
  246. ->objectType('App\User::class')
  247. ->action('admin.status.moderate')
  248. ->metadata([
  249. 'action' => 'spammer',
  250. 'message' => 'Success!',
  251. ])
  252. ->accessLevel('admin')
  253. ->save();
  254. break;
  255. }
  256. StatusService::del($status->id, true);
  257. return ['msg' => 200];
  258. }
  259. public function composePost(Request $request)
  260. {
  261. abort(400, 'Endpoint deprecated');
  262. }
  263. public function bookmarks(Request $request)
  264. {
  265. $pid = $request->user()->profile_id;
  266. $res = Bookmark::whereProfileId($pid)
  267. ->orderByDesc('created_at')
  268. ->simplePaginate(10)
  269. ->map(function ($bookmark) use ($pid) {
  270. $status = StatusService::get($bookmark->status_id, false);
  271. if (! $status) {
  272. return false;
  273. }
  274. $status['bookmarked_at'] = str_replace('+00:00', 'Z', $bookmark->created_at->format(DATE_RFC3339_EXTENDED));
  275. if ($status) {
  276. BookmarkService::add($pid, $status['id']);
  277. }
  278. return $status;
  279. })
  280. ->filter(function ($bookmark) {
  281. return $bookmark && isset($bookmark['id']);
  282. })
  283. ->values();
  284. return response()->json($res);
  285. }
  286. public function accountStatuses(Request $request, $id)
  287. {
  288. $this->validate($request, [
  289. 'only_media' => 'nullable',
  290. 'pinned' => 'nullable',
  291. 'exclude_replies' => 'nullable',
  292. 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  293. 'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  294. 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
  295. 'limit' => 'nullable|integer|min:1|max:24',
  296. ]);
  297. $profile = Profile::whereNull('status')->findOrFail($id);
  298. $limit = $request->limit ?? 9;
  299. $max_id = $request->max_id;
  300. $min_id = $request->min_id;
  301. $scope = $request->only_media == true ?
  302. ['photo', 'photo:album', 'video', 'video:album'] :
  303. ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
  304. if ($profile->is_private) {
  305. if (! Auth::check()) {
  306. return response()->json([]);
  307. }
  308. $pid = Auth::user()->profile->id;
  309. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
  310. $following = Follower::whereProfileId($pid)->pluck('following_id');
  311. return $following->push($pid)->toArray();
  312. });
  313. $visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : [];
  314. } else {
  315. if (Auth::check()) {
  316. $pid = Auth::user()->profile->id;
  317. $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
  318. $following = Follower::whereProfileId($pid)->pluck('following_id');
  319. return $following->push($pid)->toArray();
  320. });
  321. $visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
  322. } else {
  323. $visibility = ['public', 'unlisted'];
  324. }
  325. }
  326. $dir = $min_id ? '>' : '<';
  327. $id = $min_id ?? $max_id;
  328. $timeline = Status::select(
  329. 'id',
  330. 'uri',
  331. 'caption',
  332. 'profile_id',
  333. 'type',
  334. 'in_reply_to_id',
  335. 'reblog_of_id',
  336. 'is_nsfw',
  337. 'likes_count',
  338. 'reblogs_count',
  339. 'scope',
  340. 'local',
  341. 'created_at',
  342. 'updated_at'
  343. )->whereProfileId($profile->id)
  344. ->whereIn('type', $scope)
  345. ->where('id', $dir, $id)
  346. ->whereIn('visibility', $visibility)
  347. ->latest()
  348. ->limit($limit)
  349. ->get();
  350. $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer);
  351. $res = $this->fractal->createData($resource)->toArray();
  352. return response()->json($res);
  353. }
  354. public function remoteProfile(Request $request, $id)
  355. {
  356. return redirect('/i/web/profile/'.$id);
  357. }
  358. public function remoteStatus(Request $request, $profileId, $statusId)
  359. {
  360. return redirect('/i/web/post/'.$statusId);
  361. }
  362. public function requestEmailVerification(Request $request)
  363. {
  364. $pid = $request->user()->profile_id;
  365. $exists = Redis::sismember('email:manual', $pid);
  366. return view('account.email.request_verification', compact('exists'));
  367. }
  368. public function requestEmailVerificationStore(Request $request)
  369. {
  370. $pid = $request->user()->profile_id;
  371. Redis::sadd('email:manual', $pid);
  372. return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']);
  373. }
  374. }