AdminApiController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use Illuminate\Http\Request;
  4. use App\Http\Controllers\Controller;
  5. use App\Jobs\StatusPipeline\StatusDelete;
  6. use Auth, Cache, DB;
  7. use Carbon\Carbon;
  8. use App\{
  9. AccountInterstitial,
  10. Like,
  11. Media,
  12. Profile,
  13. Report,
  14. Status,
  15. User
  16. };
  17. use App\Services\AccountService;
  18. use App\Services\AdminStatsService;
  19. use App\Services\ConfigCacheService;
  20. use App\Services\ModLogService;
  21. use App\Services\StatusService;
  22. use App\Services\NotificationService;
  23. use App\Http\Resources\AdminUser;
  24. class AdminApiController extends Controller
  25. {
  26. public function supported(Request $request)
  27. {
  28. abort_if(!$request->user(), 404);
  29. abort_unless($request->user()->is_admin === 1, 404);
  30. return response()->json(['supported' => true]);
  31. }
  32. public function getStats(Request $request)
  33. {
  34. abort_if(!$request->user(), 404);
  35. abort_unless($request->user()->is_admin === 1, 404);
  36. $res = AdminStatsService::summary();
  37. $res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
  38. ->whereNull('appeal_handled_at')
  39. ->count();
  40. return $res;
  41. }
  42. public function autospam(Request $request)
  43. {
  44. abort_if(!$request->user(), 404);
  45. abort_unless($request->user()->is_admin === 1, 404);
  46. $appeals = AccountInterstitial::whereType('post.autospam')
  47. ->whereNull('appeal_handled_at')
  48. ->latest()
  49. ->simplePaginate(6)
  50. ->map(function($report) {
  51. $r = [
  52. 'id' => $report->id,
  53. 'type' => $report->type,
  54. 'item_id' => $report->item_id,
  55. 'item_type' => $report->item_type,
  56. 'created_at' => $report->created_at
  57. ];
  58. if($report->item_type === 'App\\Status') {
  59. $status = StatusService::get($report->item_id, false);
  60. if(!$status) {
  61. return;
  62. }
  63. $r['status'] = $status;
  64. if($status['in_reply_to_id']) {
  65. $r['parent'] = StatusService::get($status['in_reply_to_id'], false);
  66. }
  67. }
  68. return $r;
  69. });
  70. return $appeals;
  71. }
  72. public function autospamHandle(Request $request)
  73. {
  74. abort_if(!$request->user(), 404);
  75. abort_unless($request->user()->is_admin === 1, 404);
  76. $this->validate($request, [
  77. 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all',
  78. 'id' => 'required'
  79. ]);
  80. $action = $request->input('action');
  81. $id = $request->input('id');
  82. $appeal = AccountInterstitial::whereType('post.autospam')
  83. ->whereNull('appeal_handled_at')
  84. ->findOrFail($id);
  85. $now = now();
  86. $res = ['status' => 'success'];
  87. $meta = json_decode($appeal->meta);
  88. if($action == 'dismiss') {
  89. $appeal->is_spam = true;
  90. $appeal->appeal_handled_at = $now;
  91. $appeal->save();
  92. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  93. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  94. Cache::forget('admin-dash:reports:spam-count');
  95. return $res;
  96. }
  97. if($action == 'dismiss-all') {
  98. AccountInterstitial::whereType('post.autospam')
  99. ->whereItemType('App\Status')
  100. ->whereNull('appeal_handled_at')
  101. ->whereUserId($appeal->user_id)
  102. ->update(['appeal_handled_at' => $now, 'is_spam' => true]);
  103. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  104. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  105. Cache::forget('admin-dash:reports:spam-count');
  106. return $res;
  107. }
  108. if($action == 'approve') {
  109. $status = $appeal->status;
  110. $status->is_nsfw = $meta->is_nsfw;
  111. $status->scope = 'public';
  112. $status->visibility = 'public';
  113. $status->save();
  114. $appeal->is_spam = false;
  115. $appeal->appeal_handled_at = now();
  116. $appeal->save();
  117. StatusService::del($status->id);
  118. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  119. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  120. Cache::forget('admin-dash:reports:spam-count');
  121. return $res;
  122. }
  123. if($action == 'approve-all') {
  124. AccountInterstitial::whereType('post.autospam')
  125. ->whereItemType('App\Status')
  126. ->whereNull('appeal_handled_at')
  127. ->whereUserId($appeal->user_id)
  128. ->get()
  129. ->each(function($report) use($meta) {
  130. $report->is_spam = false;
  131. $report->appeal_handled_at = now();
  132. $report->save();
  133. $status = Status::find($report->item_id);
  134. if($status) {
  135. $status->is_nsfw = $meta->is_nsfw;
  136. $status->scope = 'public';
  137. $status->visibility = 'public';
  138. $status->save();
  139. StatusService::del($status->id, true);
  140. }
  141. });
  142. Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
  143. Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
  144. Cache::forget('admin-dash:reports:spam-count');
  145. return $res;
  146. }
  147. return $res;
  148. }
  149. public function modReports(Request $request)
  150. {
  151. abort_if(!$request->user(), 404);
  152. abort_unless($request->user()->is_admin === 1, 404);
  153. $reports = Report::whereNull('admin_seen')
  154. ->orderBy('created_at','desc')
  155. ->paginate(6)
  156. ->map(function($report) {
  157. $r = [
  158. 'id' => $report->id,
  159. 'type' => $report->type,
  160. 'message' => $report->message,
  161. 'object_id' => $report->object_id,
  162. 'object_type' => $report->object_type,
  163. 'created_at' => $report->created_at
  164. ];
  165. if($report->profile_id) {
  166. $r['reported_by_account'] = AccountService::get($report->profile_id, true);
  167. }
  168. if($report->object_type === 'App\\Status') {
  169. $status = StatusService::get($report->object_id, false);
  170. if(!$status) {
  171. return;
  172. }
  173. $r['status'] = $status;
  174. if($status['in_reply_to_id']) {
  175. $r['parent'] = StatusService::get($status['in_reply_to_id'], false);
  176. }
  177. }
  178. if($report->object_type === 'App\\Profile') {
  179. $r['account'] = AccountService::get($report->object_id, false);
  180. }
  181. return $r;
  182. })
  183. ->filter()
  184. ->values();
  185. return $reports;
  186. }
  187. public function modReportHandle(Request $request)
  188. {
  189. abort_if(!$request->user(), 404);
  190. abort_unless($request->user()->is_admin === 1, 404);
  191. $this->validate($request, [
  192. 'action' => 'required|string',
  193. 'id' => 'required'
  194. ]);
  195. $action = $request->input('action');
  196. $id = $request->input('id');
  197. $actions = [
  198. 'ignore',
  199. 'cw',
  200. 'unlist'
  201. ];
  202. if (!in_array($action, $actions)) {
  203. return abort(403);
  204. }
  205. $report = Report::findOrFail($id);
  206. $item = $report->reported();
  207. $report->admin_seen = now();
  208. switch ($action) {
  209. case 'ignore':
  210. $report->not_interested = true;
  211. break;
  212. case 'cw':
  213. Cache::forget('status:thumb:'.$item->id);
  214. $item->is_nsfw = true;
  215. $item->save();
  216. $report->nsfw = true;
  217. StatusService::del($item->id, true);
  218. break;
  219. case 'unlist':
  220. $item->visibility = 'unlisted';
  221. $item->save();
  222. StatusService::del($item->id, true);
  223. break;
  224. default:
  225. $report->admin_seen = null;
  226. break;
  227. }
  228. $report->save();
  229. Cache::forget('admin-dash:reports:list-cache');
  230. Cache::forget('admin:dashboard:home:data:v0:15min');
  231. return ['success' => true];
  232. }
  233. public function getConfiguration(Request $request)
  234. {
  235. abort_if(!$request->user(), 404);
  236. abort_unless($request->user()->is_admin === 1, 404);
  237. abort_unless(config('instance.enable_cc'), 400);
  238. return collect([
  239. [
  240. 'name' => 'ActivityPub Federation',
  241. 'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
  242. 'key' => 'federation.activitypub.enabled'
  243. ],
  244. [
  245. 'name' => 'Open Registration',
  246. 'description' => 'Allow new account registrations.',
  247. 'key' => 'pixelfed.open_registration'
  248. ],
  249. [
  250. 'name' => 'Stories',
  251. 'description' => 'Enable the ephemeral Stories feature.',
  252. 'key' => 'instance.stories.enabled'
  253. ],
  254. [
  255. 'name' => 'Require Email Verification',
  256. 'description' => 'Require new accounts to verify their email address.',
  257. 'key' => 'pixelfed.enforce_email_verification'
  258. ],
  259. [
  260. 'name' => 'AutoSpam Detection',
  261. 'description' => 'Detect and remove spam from public timelines.',
  262. 'key' => 'pixelfed.bouncer.enabled'
  263. ],
  264. ])
  265. ->map(function($s) {
  266. $s['state'] = (bool) config_cache($s['key']);
  267. return $s;
  268. });
  269. }
  270. public function updateConfiguration(Request $request)
  271. {
  272. abort_if(!$request->user(), 404);
  273. abort_unless($request->user()->is_admin === 1, 404);
  274. abort_unless(config('instance.enable_cc'), 400);
  275. $this->validate($request, [
  276. 'key' => 'required',
  277. 'value' => 'required'
  278. ]);
  279. $allowedKeys = [
  280. 'federation.activitypub.enabled',
  281. 'pixelfed.open_registration',
  282. 'instance.stories.enabled',
  283. 'pixelfed.enforce_email_verification',
  284. 'pixelfed.bouncer.enabled',
  285. ];
  286. $key = $request->input('key');
  287. $value = (bool) filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
  288. abort_if(!in_array($key, $allowedKeys), 400, 'Invalid cache key.');
  289. ConfigCacheService::put($key, $value);
  290. return collect([
  291. [
  292. 'name' => 'ActivityPub Federation',
  293. 'description' => 'Enable activitypub federation support, compatible with Pixelfed, Mastodon and other platforms.',
  294. 'key' => 'federation.activitypub.enabled'
  295. ],
  296. [
  297. 'name' => 'Open Registration',
  298. 'description' => 'Allow new account registrations.',
  299. 'key' => 'pixelfed.open_registration'
  300. ],
  301. [
  302. 'name' => 'Stories',
  303. 'description' => 'Enable the ephemeral Stories feature.',
  304. 'key' => 'instance.stories.enabled'
  305. ],
  306. [
  307. 'name' => 'Require Email Verification',
  308. 'description' => 'Require new accounts to verify their email address.',
  309. 'key' => 'pixelfed.enforce_email_verification'
  310. ],
  311. [
  312. 'name' => 'AutoSpam Detection',
  313. 'description' => 'Detect and remove spam from public timelines.',
  314. 'key' => 'pixelfed.bouncer.enabled'
  315. ],
  316. ])
  317. ->map(function($s) {
  318. $s['state'] = (bool) config_cache($s['key']);
  319. return $s;
  320. });
  321. }
  322. public function getUsers(Request $request)
  323. {
  324. abort_if(!$request->user(), 404);
  325. abort_unless($request->user()->is_admin === 1, 404);
  326. $q = $request->input('q');
  327. $sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
  328. $res = User::whereNull('status')
  329. ->when($q, function($query, $q) {
  330. return $query->where('username', 'like', '%' . $q . '%');
  331. })
  332. ->orderBy('id', $sort)
  333. ->cursorPaginate(10);
  334. return AdminUser::collection($res);
  335. }
  336. public function getUser(Request $request)
  337. {
  338. abort_if(!$request->user(), 404);
  339. abort_unless($request->user()->is_admin === 1, 404);
  340. $id = $request->input('user_id');
  341. $user = User::findOrFail($id);
  342. $profile = $user->profile;
  343. $account = AccountService::get($user->profile_id, true);
  344. return (new AdminUser($user))->additional(['meta' => [
  345. 'account' => $account,
  346. 'moderation' => [
  347. 'unlisted' => (bool) $profile->unlisted,
  348. 'cw' => (bool) $profile->cw,
  349. 'no_autolink' => (bool) $profile->no_autolink
  350. ]
  351. ]]);
  352. }
  353. public function userAdminAction(Request $request)
  354. {
  355. abort_if(!$request->user(), 404);
  356. abort_unless($request->user()->is_admin === 1, 404);
  357. $this->validate($request, [
  358. 'id' => 'required',
  359. 'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email',
  360. 'value' => 'sometimes'
  361. ]);
  362. $id = $request->input('id');
  363. $user = User::findOrFail($id);
  364. $profile = Profile::findOrFail($user->profile_id);
  365. $action = $request->input('action');
  366. abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
  367. if($action === 'refresh_stats') {
  368. $profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count();
  369. $profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
  370. $statusCount = Status::whereProfileId($user->profile_id)
  371. ->whereNull('in_reply_to_id')
  372. ->whereNull('reblog_of_id')
  373. ->whereIn('scope', ['public', 'unlisted', 'private'])
  374. ->count();
  375. $profile->status_count = $statusCount;
  376. $profile->save();
  377. } else if($action === 'verify_email') {
  378. $user->email_verified_at = now();
  379. $user->save();
  380. ModLogService::boot()
  381. ->objectUid($user->id)
  382. ->objectId($user->id)
  383. ->objectType('App\User::class')
  384. ->user($request->user())
  385. ->action('admin.user.moderate')
  386. ->metadata([
  387. 'action' => 'Manually verified email address',
  388. 'message' => 'Success!'
  389. ])
  390. ->accessLevel('admin')
  391. ->save();
  392. } else {
  393. $profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
  394. $profile->save();
  395. ModLogService::boot()
  396. ->objectUid($user->id)
  397. ->objectId($user->id)
  398. ->objectType('App\User::class')
  399. ->user($request->user())
  400. ->action('admin.user.moderate')
  401. ->metadata([
  402. 'action' => $action,
  403. 'message' => 'Success!'
  404. ])
  405. ->accessLevel('admin')
  406. ->save();
  407. }
  408. AccountService::del($user->profile_id);
  409. $account = AccountService::get($user->profile_id, true);
  410. return (new AdminUser($user))->additional(['meta' => [
  411. 'account' => $account,
  412. 'moderation' => [
  413. 'unlisted' => (bool) $profile->unlisted,
  414. 'cw' => (bool) $profile->cw,
  415. 'no_autolink' => (bool) $profile->no_autolink
  416. ]
  417. ]]);
  418. }
  419. }