GroupController.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Instance;
  4. use App\Models\Group;
  5. use App\Models\GroupBlock;
  6. use App\Models\GroupCategory;
  7. use App\Models\GroupInvitation;
  8. use App\Models\GroupLike;
  9. use App\Models\GroupLimit;
  10. use App\Models\GroupMember;
  11. use App\Models\GroupPost;
  12. use App\Models\GroupReport;
  13. use App\Profile;
  14. use App\Services\AccountService;
  15. use App\Services\GroupService;
  16. use App\Services\HashidService;
  17. use App\Services\StatusService;
  18. use App\Status;
  19. use App\User;
  20. use Illuminate\Http\Request;
  21. use Storage;
  22. class GroupController extends GroupFederationController
  23. {
  24. public function __construct()
  25. {
  26. $this->middleware('auth');
  27. }
  28. public function index(Request $request)
  29. {
  30. abort_unless(config('groups.enabled'), 404);
  31. abort_if(! $request->user(), 404);
  32. return view('layouts.spa');
  33. }
  34. public function home(Request $request)
  35. {
  36. abort_unless(config('groups.enabled'), 404);
  37. abort_if(! $request->user(), 404);
  38. return view('layouts.spa');
  39. }
  40. public function show(Request $request, $id, $path = false)
  41. {
  42. abort_unless(config('groups.enabled'), 404);
  43. $group = Group::find($id);
  44. if (! $group || $group->status) {
  45. return response()->view('groups.unavailable')->setStatusCode(404);
  46. }
  47. if ($request->wantsJson()) {
  48. return $this->showGroupObject($group);
  49. }
  50. return view('layouts.spa', compact('id', 'path'));
  51. }
  52. public function showStatus(Request $request, $gid, $sid)
  53. {
  54. abort_unless(config('groups.enabled'), 404);
  55. $group = Group::find($gid);
  56. $pid = optional($request->user())->profile_id ?? false;
  57. if (! $group || $group->status) {
  58. return response()->view('groups.unavailable')->setStatusCode(404);
  59. }
  60. if ($group->is_private) {
  61. abort_if(! $request->user(), 404);
  62. abort_if(! $group->isMember($pid), 404);
  63. }
  64. $gp = GroupPost::whereGroupId($gid)
  65. ->findOrFail($sid);
  66. return view('layouts.spa', compact('group', 'gp'));
  67. }
  68. public function getGroup(Request $request, $id)
  69. {
  70. abort_unless(config('groups.enabled'), 404);
  71. $group = Group::whereNull('status')->findOrFail($id);
  72. $pid = optional($request->user())->profile_id ?? false;
  73. $group = $this->toJson($group, $pid);
  74. return response()->json($group, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  75. }
  76. public function showStatusLikes(Request $request, $id, $sid)
  77. {
  78. abort_unless(config('groups.enabled'), 404);
  79. $group = Group::findOrFail($id);
  80. $user = $request->user();
  81. $pid = $user->profile_id;
  82. abort_if(! $group->isMember($pid), 404);
  83. $status = GroupPost::whereGroupId($id)->findOrFail($sid);
  84. $likes = GroupLike::whereStatusId($sid)
  85. ->cursorPaginate(10)
  86. ->map(function ($l) use ($group) {
  87. $account = AccountService::get($l->profile_id);
  88. $account['url'] = "/groups/{$group->id}/user/{$account['id']}";
  89. return $account;
  90. })
  91. ->filter(function ($l) {
  92. return $l && isset($l['id']);
  93. })
  94. ->values();
  95. return $likes;
  96. }
  97. public function groupSettings(Request $request, $id)
  98. {
  99. abort_unless(config('groups.enabled'), 404);
  100. abort_if(! $request->user(), 404);
  101. $group = Group::findOrFail($id);
  102. $pid = $request->user()->profile_id;
  103. abort_if(! $group->isMember($pid), 404);
  104. abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
  105. return view('groups.settings', compact('group'));
  106. }
  107. public function joinGroup(Request $request, $id)
  108. {
  109. abort_unless(config('groups.enabled'), 404);
  110. $group = Group::findOrFail($id);
  111. $pid = $request->user()->profile_id;
  112. abort_if($group->isMember($pid), 404);
  113. if (! $request->user()->is_admin) {
  114. abort_if(GroupService::getRejoinTimeout($group->id, $pid), 422, 'Cannot re-join this group for 24 hours after leaving or cancelling a request to join');
  115. }
  116. $member = new GroupMember;
  117. $member->group_id = $group->id;
  118. $member->profile_id = $pid;
  119. $member->role = 'member';
  120. $member->local_group = true;
  121. $member->local_profile = true;
  122. $member->join_request = $group->is_private;
  123. $member->save();
  124. GroupService::delSelf($group->id, $pid);
  125. GroupService::log(
  126. $group->id,
  127. $pid,
  128. 'group:joined',
  129. null,
  130. GroupMember::class,
  131. $member->id
  132. );
  133. $group = $this->toJson($group, $pid);
  134. return $group;
  135. }
  136. public function updateGroup(Request $request, $id)
  137. {
  138. abort_unless(config('groups.enabled'), 404);
  139. $this->validate($request, [
  140. 'description' => 'nullable|max:500',
  141. 'membership' => 'required|in:all,local,private',
  142. 'avatar' => 'nullable',
  143. 'header' => 'nullable',
  144. 'discoverable' => 'required',
  145. 'activitypub' => 'required',
  146. 'is_nsfw' => 'required',
  147. 'category' => 'required|string|in:'.implode(',', GroupService::categories()),
  148. ]);
  149. $pid = $request->user()->profile_id;
  150. $group = Group::whereProfileId($pid)->findOrFail($id);
  151. $member = GroupMember::whereGroupId($group->id)->whereProfileId($pid)->firstOrFail();
  152. abort_if($member->role != 'founder', 403, 'Invalid group permission');
  153. $metadata = $group->metadata;
  154. $len = $group->is_private ? 12 : 4;
  155. if ($request->hasFile('avatar')) {
  156. $avatar = $request->file('avatar');
  157. if ($avatar) {
  158. if (isset($metadata['avatar']) &&
  159. isset($metadata['avatar']['path']) &&
  160. Storage::exists($metadata['avatar']['path'])
  161. ) {
  162. Storage::delete($metadata['avatar']['path']);
  163. }
  164. $fileName = 'avatar_'.strtolower(str_random($len)).'.'.$avatar->extension();
  165. $path = $avatar->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
  166. $url = url(Storage::url($path));
  167. $metadata['avatar'] = [
  168. 'path' => $path,
  169. 'url' => $url,
  170. 'updated_at' => now(),
  171. ];
  172. }
  173. }
  174. if ($request->hasFile('header')) {
  175. $header = $request->file('header');
  176. if ($header) {
  177. if (isset($metadata['header']) &&
  178. isset($metadata['header']['path']) &&
  179. Storage::exists($metadata['header']['path'])
  180. ) {
  181. Storage::delete($metadata['header']['path']);
  182. }
  183. $fileName = 'header_'.strtolower(str_random($len)).'.'.$header->extension();
  184. $path = $header->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
  185. $url = url(Storage::url($path));
  186. $metadata['header'] = [
  187. 'path' => $path,
  188. 'url' => $url,
  189. 'updated_at' => now(),
  190. ];
  191. }
  192. }
  193. $cat = GroupService::categoryById($group->category_id);
  194. if ($request->category !== $cat['name']) {
  195. $group->category_id = GroupCategory::whereName($request->category)->first()->id;
  196. }
  197. $changes = null;
  198. $group->description = e($request->input('description', null));
  199. $group->is_private = $request->input('membership') == 'private';
  200. $group->local_only = $request->input('membership') == 'local';
  201. $group->activitypub = $request->input('activitypub') == 'true';
  202. $group->discoverable = $request->input('discoverable') == 'true';
  203. $group->is_nsfw = $request->input('is_nsfw') == 'true';
  204. $group->metadata = $metadata;
  205. if ($group->isDirty()) {
  206. $changes = $group->getDirty();
  207. }
  208. $group->save();
  209. GroupService::log(
  210. $group->id,
  211. $pid,
  212. 'group:settings:updated',
  213. $changes
  214. );
  215. GroupService::del($group->id);
  216. $res = $this->toJson($group, $pid);
  217. return $res;
  218. }
  219. protected function toJson($group, $pid = false)
  220. {
  221. abort_unless(config('groups.enabled'), 404);
  222. return GroupService::get($group->id, $pid);
  223. }
  224. public function groupLeave(Request $request, $id)
  225. {
  226. abort_unless(config('groups.enabled'), 404);
  227. abort_if(! $request->user(), 404);
  228. $pid = $request->user()->profile_id;
  229. $group = Group::findOrFail($id);
  230. abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
  231. abort_if(! $group->isMember($pid), 403, 'Not a member of group.');
  232. GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
  233. GroupService::del($group->id);
  234. GroupService::delSelf($group->id, $pid);
  235. GroupService::setRejoinTimeout($group->id, $pid);
  236. return [200];
  237. }
  238. public function cancelJoinRequest(Request $request, $id)
  239. {
  240. abort_unless(config('groups.enabled'), 404);
  241. abort_if(! $request->user(), 404);
  242. $pid = $request->user()->profile_id;
  243. $group = Group::findOrFail($id);
  244. abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
  245. abort_if($group->isMember($pid), 422, 'Cannot cancel approved join request, please leave group instead.');
  246. GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
  247. GroupService::del($group->id);
  248. GroupService::delSelf($group->id, $pid);
  249. GroupService::setRejoinTimeout($group->id, $pid);
  250. return [200];
  251. }
  252. public function metaBlockSearch(Request $request, $id)
  253. {
  254. abort_unless(config('groups.enabled'), 404);
  255. abort_if(! $request->user(), 404);
  256. $group = Group::findOrFail($id);
  257. $pid = $request->user()->profile_id;
  258. abort_if(! $group->isMember($pid), 404);
  259. abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
  260. $type = $request->input('type');
  261. $item = $request->input('item');
  262. switch ($type) {
  263. case 'instance':
  264. $res = Instance::whereDomain($item)->first();
  265. if ($res) {
  266. abort_if(GroupBlock::whereGroupId($group->id)->whereInstanceId($res->id)->exists(), 400);
  267. }
  268. break;
  269. case 'user':
  270. $res = Profile::whereUsername($item)->first();
  271. if ($res) {
  272. abort_if(GroupBlock::whereGroupId($group->id)->whereProfileId($res->id)->exists(), 400);
  273. }
  274. if ($res->user_id != null) {
  275. abort_if(User::whereIsAdmin(true)->whereId($res->user_id)->exists(), 400);
  276. }
  277. break;
  278. }
  279. return response()->json((bool) $res, ($res ? 200 : 404));
  280. }
  281. public function reportCreate(Request $request, $id)
  282. {
  283. abort_unless(config('groups.enabled'), 404);
  284. abort_if(! $request->user(), 404);
  285. $group = Group::findOrFail($id);
  286. $pid = $request->user()->profile_id;
  287. abort_if(! $group->isMember($pid), 404);
  288. $id = $request->input('id');
  289. $type = $request->input('type');
  290. $types = [
  291. // original 3
  292. 'spam',
  293. 'sensitive',
  294. 'abusive',
  295. // new
  296. 'underage',
  297. 'violence',
  298. 'copyright',
  299. 'impersonation',
  300. 'scam',
  301. 'terrorism',
  302. ];
  303. $gp = GroupPost::whereGroupId($group->id)->find($id);
  304. abort_if(! $gp, 422, 'Cannot report an invalid or deleted post');
  305. abort_if(! in_array($type, $types), 422, 'Invalid report type');
  306. abort_if($gp->profile_id === $pid, 422, 'Cannot report your own post');
  307. abort_if(
  308. GroupReport::whereGroupId($group->id)
  309. ->whereProfileId($pid)
  310. ->whereItemType(GroupPost::class)
  311. ->whereItemId($id)
  312. ->exists(),
  313. 422,
  314. 'You already reported this'
  315. );
  316. $report = new GroupReport;
  317. $report->group_id = $group->id;
  318. $report->profile_id = $pid;
  319. $report->type = $type;
  320. $report->item_type = GroupPost::class;
  321. $report->item_id = $id;
  322. $report->open = true;
  323. $report->save();
  324. GroupService::log(
  325. $group->id,
  326. $pid,
  327. 'group:report:create',
  328. [
  329. 'type' => $type,
  330. 'report_id' => $report->id,
  331. 'status_id' => $gp->status_id,
  332. 'profile_id' => $gp->profile_id,
  333. 'username' => optional(AccountService::get($gp->profile_id))['acct'],
  334. 'gpid' => $gp->id,
  335. 'url' => $gp->url(),
  336. ],
  337. GroupReport::class,
  338. $report->id
  339. );
  340. return response([200]);
  341. }
  342. public function reportAction(Request $request, $id)
  343. {
  344. abort_unless(config('groups.enabled'), 404);
  345. abort_if(! $request->user(), 404);
  346. $group = Group::findOrFail($id);
  347. $pid = $request->user()->profile_id;
  348. abort_if(! $group->isMember($pid), 404);
  349. abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
  350. $this->validate($request, [
  351. 'action' => 'required|in:cw,delete,ignore',
  352. 'id' => 'required|string',
  353. ]);
  354. $action = $request->input('action');
  355. $id = $request->input('id');
  356. $report = GroupReport::whereGroupId($group->id)
  357. ->findOrFail($id);
  358. $status = Status::findOrFail($report->item_id);
  359. $gp = GroupPost::whereGroupId($group->id)
  360. ->whereStatusId($status->id)
  361. ->firstOrFail();
  362. switch ($action) {
  363. case 'cw':
  364. $status->is_nsfw = true;
  365. $status->save();
  366. StatusService::del($status->id);
  367. GroupReport::whereGroupId($group->id)
  368. ->whereItemType($report->item_type)
  369. ->whereItemId($report->item_id)
  370. ->update(['open' => false]);
  371. GroupService::log(
  372. $group->id,
  373. $pid,
  374. 'group:moderation:action',
  375. [
  376. 'type' => 'cw',
  377. 'report_id' => $report->id,
  378. 'status_id' => $status->id,
  379. 'profile_id' => $status->profile_id,
  380. 'status_url' => $gp->url(),
  381. ],
  382. GroupReport::class,
  383. $report->id
  384. );
  385. return response()->json([200]);
  386. break;
  387. case 'ignore':
  388. GroupReport::whereGroupId($group->id)
  389. ->whereItemType($report->item_type)
  390. ->whereItemId($report->item_id)
  391. ->update(['open' => false]);
  392. GroupService::log(
  393. $group->id,
  394. $pid,
  395. 'group:moderation:action',
  396. [
  397. 'type' => 'ignore',
  398. 'report_id' => $report->id,
  399. 'status_id' => $status->id,
  400. 'profile_id' => $status->profile_id,
  401. 'status_url' => $gp->url(),
  402. ],
  403. GroupReport::class,
  404. $report->id
  405. );
  406. return response()->json([200]);
  407. break;
  408. }
  409. }
  410. public function getMemberInteractionLimits(Request $request, $id)
  411. {
  412. abort_unless(config('groups.enabled'), 404);
  413. abort_if(! $request->user(), 404);
  414. $group = Group::findOrFail($id);
  415. $pid = $request->user()->profile_id;
  416. abort_if(! $group->isMember($pid), 404);
  417. abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
  418. $profile_id = $request->input('profile_id');
  419. abort_if(! $group->isMember($profile_id), 404);
  420. $limits = GroupService::getInteractionLimits($group->id, $profile_id);
  421. return response()->json($limits);
  422. }
  423. public function updateMemberInteractionLimits(Request $request, $id)
  424. {
  425. abort_unless(config('groups.enabled'), 404);
  426. abort_if(! $request->user(), 404);
  427. $group = Group::findOrFail($id);
  428. $pid = $request->user()->profile_id;
  429. abort_if(! $group->isMember($pid), 404);
  430. abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
  431. $this->validate($request, [
  432. 'profile_id' => 'required|exists:profiles,id',
  433. 'can_post' => 'required',
  434. 'can_comment' => 'required',
  435. 'can_like' => 'required',
  436. ]);
  437. $member = $request->input('profile_id');
  438. $can_post = $request->input('can_post');
  439. $can_comment = $request->input('can_comment');
  440. $can_like = $request->input('can_like');
  441. $account = AccountService::get($member);
  442. abort_if(! $account, 422, 'Invalid profile');
  443. abort_if(! $group->isMember($member), 422, 'Invalid profile');
  444. $limit = GroupLimit::firstOrCreate([
  445. 'profile_id' => $member,
  446. 'group_id' => $group->id,
  447. ]);
  448. if ($limit->wasRecentlyCreated) {
  449. abort_if(GroupLimit::whereGroupId($group->id)->count() >= 25, 422, 'limit_reached');
  450. }
  451. $previousLimits = $limit->limits;
  452. $limit->limits = [
  453. 'can_post' => $can_post,
  454. 'can_comment' => $can_comment,
  455. 'can_like' => $can_like,
  456. ];
  457. $limit->save();
  458. GroupService::clearInteractionLimits($group->id, $member);
  459. GroupService::log(
  460. $group->id,
  461. $pid,
  462. 'group:member-limits:updated',
  463. [
  464. 'profile_id' => $account['id'],
  465. 'username' => $account['username'],
  466. 'previousLimits' => $previousLimits,
  467. 'newLimits' => $limit->limits,
  468. ],
  469. GroupLimit::class,
  470. $limit->id
  471. );
  472. return $request->all();
  473. }
  474. public function showProfile(Request $request, $id, $pid)
  475. {
  476. abort_unless(config('groups.enabled'), 404);
  477. $group = Group::find($id);
  478. if (! $group || $group->status) {
  479. return response()->view('groups.unavailable')->setStatusCode(404);
  480. }
  481. return view('layouts.spa');
  482. }
  483. public function showProfileByUsername(Request $request, $id, $pid)
  484. {
  485. abort_unless(config('groups.enabled'), 404);
  486. abort_if(! $request->user(), 404);
  487. if (! $request->user()) {
  488. return redirect("/{$pid}");
  489. }
  490. $group = Group::find($id);
  491. $cid = $request->user()->profile_id;
  492. if (! $group || $group->status) {
  493. return response()->view('groups.unavailable')->setStatusCode(404);
  494. }
  495. if (! $group->isMember($cid)) {
  496. return redirect("/{$pid}");
  497. }
  498. $profile = Profile::whereUsername($pid)->first();
  499. if (! $group->isMember($profile->id)) {
  500. return redirect("/{$pid}");
  501. }
  502. if ($profile) {
  503. $url = url("/groups/{$id}/user/{$profile->id}");
  504. return redirect($url);
  505. }
  506. abort(404, 'Invalid username');
  507. }
  508. public function groupInviteLanding(Request $request, $id)
  509. {
  510. abort_unless(config('groups.enabled'), 404);
  511. abort(404, 'Not yet implemented');
  512. $group = Group::findOrFail($id);
  513. return view('groups.invite', compact('group'));
  514. }
  515. public function groupShortLinkRedirect(Request $request, $hid)
  516. {
  517. abort_unless(config('groups.enabled'), 404);
  518. $gid = HashidService::decode($hid);
  519. $group = Group::findOrFail($gid);
  520. return redirect($group->url());
  521. }
  522. public function groupInviteClaim(Request $request, $id)
  523. {
  524. abort_unless(config('groups.enabled'), 404);
  525. $group = GroupService::get($id);
  526. abort_if(! $group || empty($group), 404);
  527. return view('groups.invite-claim', compact('group'));
  528. }
  529. public function groupMemberInviteCheck(Request $request, $id)
  530. {
  531. abort_unless(config('groups.enabled'), 404);
  532. abort_if(! $request->user(), 404);
  533. $pid = $request->user()->profile_id;
  534. $group = Group::findOrFail($id);
  535. abort_if($group->isMember($pid), 422, 'Already a member');
  536. $exists = GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists();
  537. return response()->json([
  538. 'gid' => $id,
  539. 'can_join' => (bool) $exists,
  540. ]);
  541. }
  542. public function groupMemberInviteAccept(Request $request, $id)
  543. {
  544. abort_unless(config('groups.enabled'), 404);
  545. abort_if(! $request->user(), 404);
  546. $pid = $request->user()->profile_id;
  547. $group = Group::findOrFail($id);
  548. abort_if($group->isMember($pid), 422, 'Already a member');
  549. abort_if(! GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists(), 422);
  550. $gm = new GroupMember;
  551. $gm->group_id = $id;
  552. $gm->profile_id = $pid;
  553. $gm->role = 'member';
  554. $gm->local_group = $group->local;
  555. $gm->local_profile = true;
  556. $gm->join_request = false;
  557. $gm->save();
  558. GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->delete();
  559. GroupService::del($id);
  560. GroupService::delSelf($id, $pid);
  561. return ['next_url' => $group->url()];
  562. }
  563. public function groupMemberInviteDecline(Request $request, $id)
  564. {
  565. abort_unless(config('groups.enabled'), 404);
  566. abort_if(! $request->user(), 404);
  567. $pid = $request->user()->profile_id;
  568. $group = Group::findOrFail($id);
  569. abort_if($group->isMember($pid), 422, 'Already a member');
  570. return ['next_url' => '/'];
  571. }
  572. }