GroupController.php 21 KB

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