PageRenderTime 26ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/app/Http/Controllers/ChatsController.php

https://gitlab.com/Imangazaliev/laravel-code-example
PHP | 425 lines | 336 code | 79 blank | 10 comment | 26 complexity | 14d28f12cb272affc324b01d0a43ee5c MD5 | raw file
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Http\Controllers;
  4. use App\Events\ChatFinished;
  5. use App\Events\MessageStatusChanged;
  6. use App\Models\Chat;
  7. use App\Models\ChatMessage;
  8. use App\Models\CoinTransaction;
  9. use App\Models\ContactsRequest;
  10. use App\Models\Notification;
  11. use App\Models\User;
  12. use App\Services\Chat\ChatElementsParameters;
  13. use App\Services\Chat\ChatsService;
  14. use App\Services\Chat\ContactsRequestService;
  15. use App\Services\Coins\Coins;
  16. use App\Services\Coins\CoinTransactionDto;
  17. use Carbon\Carbon;
  18. use DB;
  19. use Illuminate\Http\Request;
  20. use Throwable;
  21. class ChatsController extends Controller
  22. {
  23. public function __construct()
  24. {
  25. $this->middleware('auth');
  26. }
  27. public function chats(Request $request): array
  28. {
  29. $this->validate($request, [
  30. 'after_time' => ['string', 'date_format:' . DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT],
  31. ]);
  32. $afterTime = $request->get('after_time');
  33. return ChatsService::getChats(current_user()->id, $afterTime, ChatsService::VIEW_TYPE_USER);
  34. }
  35. public function chatElements(Request $request): array
  36. {
  37. $this->validate($request, [
  38. 'chat_id' => ['required', 'integer'],
  39. 'after_time' => ['string', 'date_format:' . DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT],
  40. 'before_time' => ['string', 'date_format:' . DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT],
  41. ]);
  42. $chatId = (int) $request->get('chat_id');
  43. return ChatsService::getElements(new ChatElementsParameters([
  44. 'chatId' => $chatId,
  45. 'userId' => current_user()->id,
  46. 'respectMessageStatus' => true,
  47. 'afterTime' => $request->get('after_time'),
  48. 'beforeTime' => $request->get('before_time'),
  49. ]));
  50. }
  51. public function sendMessage(Request $request, Coins $coins): void
  52. {
  53. $this->validate($request, [
  54. 'chat_id' => ['required', 'integer'],
  55. 'text' => ['required', 'string', 'min:1', 'max:' . config('chats.max_message_length')],
  56. 'replied_message_id' => 'exists:chat_messages,id',
  57. ]);
  58. // don't allow to send messages if the current user profile is not accepted
  59. if (current_user()->status !== User::STATUS_ACCEPTED) {
  60. abort(403, __('errors.your-profile-is-not-accepted'));
  61. }
  62. $chat = (new Chat())
  63. ->whereActive()
  64. ->find($request->get('chat_id'), [
  65. 'id', 'requestor_id', 'target_user_id', 'last_activity_at',
  66. ])
  67. ;
  68. if ($chat === null) {
  69. abort(403);
  70. }
  71. // forbid sending messages to an arbitrary chat
  72. if ($chat->requestor_id !== current_user()->id && $chat->target_user_id !== current_user()->id) {
  73. abort(403);
  74. }
  75. $peerId = $chat->requestor_id === current_user()->id ? $chat->target_user_id : $chat->requestor_id;
  76. $peer = (new User())->find($peerId, ['status', 'ban_id']);
  77. if ($peer->status === User::STATUS_BANNED) {
  78. $isTemporaryBan = $peer->ban()->value('unban_at') !== null;
  79. abort(403, $isTemporaryBan ? __('profile:errors.user-temporary-banned') : __('profile:errors.user-banned'));
  80. }
  81. if ($peer->status === User::STATUS_DEACTIVATED) {
  82. abort(403, __('profile:errors.user-deactivated'));
  83. }
  84. if ($peer->status === User::STATUS_PENDING_DELETION || $peer->status === User::STATUS_DELETED) {
  85. abort(403, __('profile:errors.user-deleted'));
  86. }
  87. $repliedMessageId = $request->get('replied_message_id');
  88. $repliedMessageText = null;
  89. if ($repliedMessageId !== null) {
  90. $repliedMessageText = (new ChatMessage())->findOrFail($repliedMessageId, ['text'])->text;
  91. }
  92. DB::beginTransaction();
  93. try {
  94. $coinTransactionDto = (new CoinTransactionDto())
  95. ->setType(CoinTransaction::TYPE_CHAT_MESSAGES)
  96. ->setAmount(-1)
  97. ->setUserId(current_user()->id)
  98. ->setDescription(sprintf('Chat messages'))
  99. ;
  100. $transaction = (new CoinTransaction())
  101. ->where('type', CoinTransaction::TYPE_CHAT_MESSAGES)
  102. ->where('user_id', current_user()->id)
  103. ->orderBy('transaction_time', 'desc')
  104. ->first()
  105. ;
  106. if ($transaction === null) {
  107. $transaction = $coins->changeBalance($coinTransactionDto);
  108. } else {
  109. $transactionMessageCount = (new ChatMessage())
  110. ->where('transaction_id', $transaction->id)
  111. ->count()
  112. ;
  113. $messageCountPerCoin = config('chats.message_count_per_coin');
  114. // if the count of messages paid by the last transaction reached the specified number
  115. // the create a new one
  116. if ($transactionMessageCount >= $messageCountPerCoin) {
  117. $transaction = $coins->changeBalance($coinTransactionDto);
  118. }
  119. }
  120. $currentTime = Carbon::now();
  121. $message = (new ChatMessage())->create([
  122. 'chat_id' => $chat->id,
  123. 'user_id' => current_user()->id,
  124. 'peer_id' => $peerId,
  125. 'replied_message_id' => $repliedMessageId,
  126. 'replied_message_text' => $repliedMessageText,
  127. 'text' => $request->get('text'),
  128. 'sent_at' => $currentTime->format(DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT),
  129. 'status' => ChatMessage::STATUS_PENDING_MODERATION,
  130. 'status_updated_at' => $currentTime->format(DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT),
  131. 'transaction_id' => $transaction->id,
  132. ]);
  133. ChatsService::updateChatLastActivityTime($chat->id, $chat->last_activity_at, $currentTime);
  134. DB::commit();
  135. } catch (Throwable $exception) {
  136. DB::rollBack();
  137. throw $exception;
  138. }
  139. }
  140. public function cancelMessage(Request $request): void
  141. {
  142. $this->validate($request, [
  143. 'message_id' => ['required', 'integer'],
  144. ]);
  145. $message = (new ChatMessage())
  146. ->find($request->get('message_id'), [
  147. 'id',
  148. 'chat_id',
  149. 'user_id',
  150. 'status',
  151. 'status_updated_at',
  152. 'seen_at',
  153. ])
  154. ;
  155. if ($message === null) {
  156. abort(403);
  157. }
  158. // forbid cancelling an arbitrary message
  159. if ($message->user_id !== current_user()->id) {
  160. abort(403);
  161. }
  162. // forbid cancelling a rejected message, a message that already canceled
  163. // or if a message already seen by a peer
  164. if (
  165. in_array($message->status, [
  166. ChatMessage::STATUS_REJECTED_BY_MODERATOR,
  167. ChatMessage::STATUS_CANCELED,
  168. ], true) || $message->seen_at !== null
  169. ) {
  170. abort(403);
  171. }
  172. DB::beginTransaction();
  173. try {
  174. $currentTime = Carbon::now();
  175. $updatedMessageCount = (new ChatMessage())
  176. ->where('id', $message->id)
  177. ->where('status_updated_at', $message->status_updated_at->format(DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT))
  178. ->update([
  179. 'status' => ChatMessage::STATUS_CANCELED,
  180. 'status_updated_at' => $currentTime->format(DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT),
  181. ])
  182. ;
  183. // if the status of the message was already changed by someone else
  184. if ($updatedMessageCount === 0) {
  185. abort(403);
  186. }
  187. event(new MessageStatusChanged($message->id, $currentTime));
  188. DB::commit();
  189. } catch (Throwable $exception) {
  190. DB::rollBack();
  191. throw $exception;
  192. }
  193. }
  194. public function markAsRead(Request $request): void
  195. {
  196. $this->validate($request, [
  197. 'chat_id' => ['required', 'integer'],
  198. ]);
  199. $chat = (new Chat())
  200. ->whereActive()
  201. ->find($request->get('chat_id'), [
  202. 'id', 'requestor_id', 'target_user_id',
  203. ])
  204. ;
  205. if ($chat === null) {
  206. abort(403);
  207. }
  208. // forbid access to an arbitrary chat
  209. if ($chat->requestor_id !== current_user()->id && $chat->target_user_id !== current_user()->id) {
  210. abort(403);
  211. }
  212. DB::beginTransaction();
  213. try {
  214. $currentTime = Carbon::now();
  215. $updatedRowCount = (new ChatMessage())
  216. ->where('chat_id', $chat->id)
  217. ->where('user_id', '!=', current_user()->id)
  218. ->whereIn('status', [
  219. ChatMessage::STATUS_ACCEPTED,
  220. ChatMessage::STATUS_REJECTED_BY_MODERATOR,
  221. ])
  222. ->whereNull('seen_at')
  223. ->update([
  224. 'seen_at' => $currentTime->format(DATE_TIME_WITH_TIME_ZONE_FORMAT),
  225. ])
  226. ;
  227. if (current_user()->sex === User::SEX_MALE) {
  228. $updatedRowCount += (new ContactsRequest())
  229. ->where('chat_id', $chat->id)
  230. ->where('requestor_id', current_user()->id)
  231. ->whereIn('status', [ContactsRequest::STATUS_ACCEPTED, ContactsRequest::STATUS_REJECTED])
  232. ->whereNull('answer_seen_at')
  233. ->update([
  234. 'answer_seen_at' => $currentTime->format(DATE_TIME_WITH_TIME_ZONE_FORMAT),
  235. ])
  236. ;
  237. } else {
  238. $updatedRowCount += (new ContactsRequest())
  239. ->where('chat_id', $chat->id)
  240. ->where('target_user_id', current_user()->id)
  241. ->whereNull('seen_at')
  242. ->update([
  243. 'seen_at' => $currentTime->format(DATE_TIME_WITH_TIME_ZONE_FORMAT),
  244. ])
  245. ;
  246. }
  247. if ($updatedRowCount > 0) {
  248. ChatsService::updateChatLastActivityTime($chat->id, $chat->last_activity_at, $currentTime);
  249. cache_info()->newMessageCount(current_user()->id)->forget();
  250. }
  251. DB::commit();
  252. } catch (Throwable $exception) {
  253. DB::rollBack();
  254. throw $exception;
  255. }
  256. }
  257. public function finishChat(Request $request): void
  258. {
  259. $this->validate($request, [
  260. 'chat_id' => ['required', 'integer'],
  261. ]);
  262. $chat = (new Chat())
  263. ->with('chatRequest:id,chat_id,transaction_id')
  264. ->with('chatRequest.transaction:id,amount')
  265. ->whereActive()
  266. ->find($request->get('chat_id'), [
  267. 'id',
  268. 'chat_request_id',
  269. 'requestor_id',
  270. 'target_user_id',
  271. ])
  272. ;
  273. if ($chat === null) {
  274. abort(403);
  275. }
  276. // forbid access to an arbitrary chat
  277. if ($chat->requestor_id !== current_user()->id && $chat->target_user_id !== current_user()->id) {
  278. abort(403);
  279. }
  280. DB::beginTransaction();
  281. try {
  282. $currentTime = Carbon::now();
  283. $chat->update([
  284. 'finished_at' => $currentTime->format(DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT),
  285. 'finished_by' => current_user()->id,
  286. 'is_finished_automatically' => false,
  287. 'moderation_moderator_id' => null,
  288. 'moderation_claimed_at' => null,
  289. 'moderation_forwarded_to' => null,
  290. 'moderation_forwarded_by' => null,
  291. 'moderation_forwarded_at' => null,
  292. ]);
  293. event(new ChatFinished($chat->id));
  294. $peerId = $chat->requestor_id === current_user()->id ? $chat->target_user_id : $chat->requestor_id;
  295. (new Notification())->create([
  296. 'user_id' => $peerId,
  297. 'type' => Notification::TYPE_CHAT_FINISHED,
  298. 'title' => 'notifications:chat-finished.title',
  299. 'body' => 'notifications:chat-finished.body',
  300. 'parameters' => [
  301. 'peerId' => current_user()->id,
  302. 'peerName' => current_user()->name,
  303. ],
  304. 'context' => current_user()->sex === User::SEX_MALE ? 'male' : 'female',
  305. 'additional_data' => [
  306. 'chat_id' => $chat->id,
  307. ],
  308. 'is_hidden' => false,
  309. 'created_at' => $currentTime->format(DATE_TIME_WITH_MICROSECONDS_AND_TIME_ZONE_FORMAT),
  310. ]);
  311. cache_info()->newMessageCount(current_user()->id)->forget();
  312. DB::commit();
  313. } catch (Throwable $exception) {
  314. DB::rollBack();
  315. throw $exception;
  316. }
  317. }
  318. public function sendContactsRequest(Request $request, ContactsRequestService $contactsRequests): void
  319. {
  320. $this->validate($request, [
  321. 'chat_id' => ['required', 'integer'],
  322. ]);
  323. $contactsRequests->sendRequest(current_user(), $request->get('chat_id'));
  324. }
  325. public function cancelContactsRequest(Request $request, ContactsRequestService $contactsRequests): void
  326. {
  327. $this->validate($request, [
  328. 'request_id' => ['required', 'integer'],
  329. ]);
  330. $contactsRequests->cancelRequest($request->get('request_id'));
  331. }
  332. public function acceptContactsRequest(Request $request, ContactsRequestService $contactsRequests): void
  333. {
  334. $this->validate($request, [
  335. 'request_id' => ['required', 'integer'],
  336. ]);
  337. $contactsRequests->answerRequest($request->get('request_id'), true);
  338. }
  339. public function rejectContactsRequest(Request $request, ContactsRequestService $contactsRequests): void
  340. {
  341. $this->validate($request, [
  342. 'request_id' => ['required', 'integer'],
  343. ]);
  344. $contactsRequests->answerRequest($request->get('request_id'), false);
  345. }
  346. }