From 31a684820de49ff15807c982e775687ff1b93f9c Mon Sep 17 00:00:00 2001 From: Roardom Date: Wed, 5 Jul 2023 23:32:19 +0000 Subject: [PATCH] add: livewire/alpinejs chatbox --- app/Bots/NerdBot.php | 150 +-- app/Bots/SystemBot.php | 265 ++-- app/Events/{Chatter.php => EchoCreated.php} | 29 +- app/Events/MessageCreated.php | 65 + app/Events/MessageDeleted.php | 18 +- app/Events/MessageSent.php | 68 -- app/Events/UserEdited.php | 67 ++ app/Http/Controllers/API/ChatController.php | 513 -------- .../Controllers/Staff/ChatRoomController.php | 6 +- app/Http/Livewire/Chatbox.php | 457 +++++++ .../Requests/Staff/UpdateChatBotRequest.php | 1 - app/Models/Message.php | 2 + app/Providers/RouteServiceProvider.php | 4 - app/Repositories/ChatRepository.php | 348 +----- ...3_11_17_092722_remove_bot_transactions.php | 59 + database/seeders/BotsTableSeeder.php | 4 +- package.json | 5 +- resources/js/components/Trans.vue | 32 - resources/js/components/Version.vue | 57 - resources/js/components/chat/ChatForm.vue | 71 -- resources/js/components/chat/ChatMessages.vue | 152 --- resources/js/components/chat/ChatUserList.vue | 58 - resources/js/components/chat/Chatbox.vue | 1068 ----------------- .../js/components/chat/ChatroomsDropdown.vue | 44 - .../components/chat/ChatstatusesDropdown.vue | 47 - resources/js/unit3d/chat.js | 7 - resources/views/blocks/chat.blade.php | 4 +- resources/views/components/user_tag.blade.php | 26 +- resources/views/home/index.blade.php | 5 +- resources/views/livewire/chatbox.blade.php | 408 +++++++ routes/channels.php | 58 +- routes/vue.php | 66 - .../Http/Controllers/HomeControllerTest.php | 5 + .../Staff/UpdateChatBotRequestTest.php | 1 - 34 files changed, 1251 insertions(+), 2919 deletions(-) rename app/Events/{Chatter.php => EchoCreated.php} (56%) create mode 100644 app/Events/MessageCreated.php delete mode 100644 app/Events/MessageSent.php create mode 100644 app/Events/UserEdited.php delete mode 100644 app/Http/Controllers/API/ChatController.php create mode 100644 app/Http/Livewire/Chatbox.php create mode 100644 database/migrations/2023_11_17_092722_remove_bot_transactions.php delete mode 100644 resources/js/components/Trans.vue delete mode 100644 resources/js/components/Version.vue delete mode 100644 resources/js/components/chat/ChatForm.vue delete mode 100644 resources/js/components/chat/ChatMessages.vue delete mode 100644 resources/js/components/chat/ChatUserList.vue delete mode 100644 resources/js/components/chat/Chatbox.vue delete mode 100644 resources/js/components/chat/ChatroomsDropdown.vue delete mode 100644 resources/js/components/chat/ChatstatusesDropdown.vue create mode 100644 resources/views/livewire/chatbox.blade.php delete mode 100644 routes/vue.php diff --git a/app/Bots/NerdBot.php b/app/Bots/NerdBot.php index d5804e5969..797215f8e2 100644 --- a/app/Bots/NerdBot.php +++ b/app/Bots/NerdBot.php @@ -16,39 +16,27 @@ namespace App\Bots; -use App\Events\Chatter; -use App\Http\Resources\UserAudibleResource; -use App\Http\Resources\UserEchoResource; +use App\Events\MessageCreated; use App\Models\Ban; use App\Models\Bot; +use App\Models\Message; use App\Models\Peer; use App\Models\Torrent; use App\Models\User; -use App\Models\UserAudible; -use App\Models\UserEcho; use App\Models\Warning; -use App\Repositories\ChatRepository; use Illuminate\Support\Carbon; class NerdBot { private Bot $bot; - private User $target; - - private string $type; - - private string $message; - - private string $log; - private Carbon $expiresAt; private Carbon $current; private string $site; - public function __construct(private readonly ChatRepository $chatRepository) + public function __construct() { $this->bot = Bot::findOrFail(2); $this->expiresAt = Carbon::now()->addMinutes(60); @@ -219,44 +207,21 @@ public function getKing(): string } /** - * Process Message. + * Handle command. */ - public function process(string $type, User $user, string $message = '', int $targeted = 0): true|\Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory + public function handle(string $message, ?int $roomId = null, ?int $receiverId = null): void { - $this->target = $user; - - if ($type === 'message') { - $x = 0; - $y = 1; - } else { - $x = 1; - $y = 2; - } + [, $command] = preg_split('/ +/', $message, 2) + [null, null]; - if ($message === '') { - $log = ''; - } else { - $log = 'All '.$this->bot->name.' commands must be a private message or begin with /'.$this->bot->command.' or !'.$this->bot->command.'. Need help? Type /'.$this->bot->command.' help and you shall be helped.'; - } - - $command = @explode(' ', $message); - - $params = $command[$y] ?? null; - - if ($params) { - $clone = $command; - array_shift($clone); - array_shift($clone); - array_shift($clone); - } - - if (\array_key_exists($x, $command)) { - $log = match($command[$x]) { + MessageCreated::dispatch(Message::create([ + 'user_id' => User::SYSTEM_USER_ID, + 'chatroom_id' => $roomId, + 'receiver_id' => $receiverId, + 'message' => match($command) { 'banker' => $this->getBanker(), 'bans' => $this->getBans(), 'doubleupload' => $this->getDoubleUpload(), 'freeleech' => $this->getFreeleech(), - 'help' => $this->getHelp(), 'king' => $this->getKing(), 'logins' => $this->getLogins(), 'peers' => $this->getPeers(), @@ -266,97 +231,8 @@ public function process(string $type, User $user, string $message = '', int $tar 'seeded' => $this->getSeeded(), 'leeched' => $this->getLeeched(), 'snatched' => $this->getSnatched(), - default => '', - }; - } - - $this->type = $type; - $this->message = $message; - $this->log = $log; - - return $this->pm(); - } - - /** - * Output Message. - */ - public function pm(): true|\Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory - { - $type = $this->type; - $target = $this->target; - $txt = $this->log; - $message = $this->message; - - if ($type === 'message' || $type === 'private') { - // Create echo for user if missing - $echoes = cache()->remember( - 'user-echoes'.$target->id, - 3600, - fn () => UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get() - ); - - if ($echoes->doesntContain(fn ($echo) => $echo->bot_id == $this->bot->id)) { - UserEcho::create([ - 'user_id' => $target->id, - 'target_id' => $this->bot->id, - ]); - - $echoes = UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get(); - - cache()->put('user-echoes'.$target->id, $echoes, 3600); - - Chatter::dispatch('echo', $target->id, UserEchoResource::collection($echoes)); + default => $this->getHelp(), } - - // Create audible for user if missing - $audibles = cache()->remember( - 'user-audibles'.$target->id, - 3600, - fn () => UserAudible::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get() - ); - - if ($audibles->doesntContain(fn ($audible) => $audible->bot_id == $this->bot->id)) { - UserAudible::create([ - 'user_id' => $target->id, - 'target_id' => $this->bot->id, - 'status' => false, - ]); - - $audibles = UserAudible::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get(); - - cache()->put('user-audibles'.$target->id, $audibles, 3600); - - Chatter::dispatch('audible', $target->id, UserAudibleResource::collection($audibles)); - } - - // Create message - if ($txt !== '') { - $roomId = 0; - $this->chatRepository->privateMessage($target->id, $roomId, $message, 1, $this->bot->id); - $this->chatRepository->privateMessage(1, $roomId, $txt, $target->id, $this->bot->id); - } - - return response('success'); - } - - if ($type === 'echo') { - if ($txt !== '') { - $roomId = 0; - $this->chatRepository->botMessage($this->bot->id, $roomId, $txt, $target->id); - } - - return response('success'); - } - - if ($type === 'public') { - if ($txt !== '') { - $this->chatRepository->message($target->id, $target->chatroom->id, $message, null, null); - $this->chatRepository->message(1, $target->chatroom->id, $txt, null, $this->bot->id); - } - - return response('success'); - } - - return true; + ]), User::SYSTEM_USER_ID, $roomId, $receiverId); } } diff --git a/app/Bots/SystemBot.php b/app/Bots/SystemBot.php index c036863d75..7dab9cfe3b 100644 --- a/app/Bots/SystemBot.php +++ b/app/Bots/SystemBot.php @@ -16,32 +16,25 @@ namespace App\Bots; -use App\Events\Chatter; -use App\Http\Resources\UserAudibleResource; -use App\Http\Resources\UserEchoResource; +use App\Events\MessageCreated; use App\Models\Bot; +use App\Models\Chatroom; use App\Models\Gift; +use App\Models\Message; use App\Models\User; -use App\Models\UserAudible; -use App\Models\UserEcho; use App\Notifications\NewBon; -use App\Repositories\ChatRepository; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; +use Closure; class SystemBot { private Bot $bot; - private User $target; - - private string $type; - - private string $message; - - private string $log; - - public function __construct(private readonly ChatRepository $chatRepository) + public function __construct() { - $this->bot = Bot::where('is_systembot', '=', '1')->sole(); + $this->bot = Bot::where('is_systembot', '=', true)->sole(); } public function replaceVars(string $output): string @@ -62,190 +55,98 @@ public function replaceVars(string $output): string return $output; } - /** - * Get Help. - */ - public function getHelp(): string - { - return $this->replaceVars($this->bot->help ?? ''); - } - - /** - * Send Gift. - * - * @param array $note - */ - public function putGift(string $receiver = '', float $amount = 0, array $note = ['']): string + public function gift(?string $username, ?string $amount, ?string $message): Message { - $output = implode(' ', $note); - $v = validator(['receiver' => $receiver, 'amount' => $amount, 'note' => $output], [ - 'receiver' => 'required|string|exists:users,username', - 'amount' => sprintf('required|numeric|min:1|max:%s', $this->target->seedbonus), - 'note' => 'required|string', + Validator::make([ + 'username' => $username, + 'amount' => $amount, + 'message' => $message, + ], [ + 'username' => [ + 'required', + Rule::exists('users', 'username')->whereNot('id', auth()->id()), + ], + 'amount' => [ + 'required', + 'integer', + 'gte:1', + function (string $attribute, mixed $value, Closure $fail): void { + if ($value > auth()->user()->seedbonus) { + $fail('You do not have enough BON to send this gift.'); + } + } + ], + 'message' => [ + 'required', + ], + ])->validate(); + + $recipient = User::where('username', '=', $username)->sole(); + + DB::transaction(function () use ($recipient, $amount): void { + $recipient->increment('seedbonus', (float) $amount); + auth()->user()->decrement('seedbonus', (float) $amount); + }); + + $gift = Gift::create([ + 'sender_id' => auth()->id(), + 'recipient_id' => $recipient->id, + 'bon' => $amount, + 'message' => $message, ]); - if ($v->passes()) { - $recipient = User::where('username', 'LIKE', $receiver)->first(); - - if (!$recipient || $recipient->id === $this->target->id) { - return 'Your BON gift could not be sent.'; - } - - $value = $amount; - $recipient->increment('seedbonus', $value); - $this->target->decrement('seedbonus', $value); - - $gift = Gift::create([ - 'sender_id' => $this->target->id, - 'recipient_id' => $recipient->id, - 'bon' => $value, - 'message' => $output, - ]); - - if ($this->target->id !== $recipient->id && $recipient->acceptsNotification($this->target, $recipient, 'bon', 'show_bon_gift')) { - $recipient->notify(new NewBon($gift)); - } - - $profileUrl = href_profile($this->target); - $recipientUrl = href_profile($recipient); - - $this->chatRepository->systemMessage( - sprintf('[url=%s]%s[/url] has gifted %s BON to [url=%s]%s[/url]', $profileUrl, $this->target->username, $value, $recipientUrl, $recipient->username) - ); - - return 'Your gift to '.$recipient->username.' for '.$amount.' BON has been sent!'; + if ($recipient->acceptsNotification(auth()->user(), $recipient, 'bon', 'show_bon_gift')) { + $recipient->notify(new NewBon($gift)); } - return 'Your BON gift could not be sent.'; + return Message::create([ + 'user_id' => User::SYSTEM_USER_ID, + 'chatroom_id' => Chatroom::query() + ->where(\is_int(config('chat.system_chatroom')) ? 'id' : 'name', '=', config('chat.system_chatroom')) + ->soleValue('id'), + 'message' => sprintf( + '[url=%s]%s[/url] has gifted %s BON to [url=%s]%s[/url]', + href_profile(auth()->user()), + auth()->user()->username, + $amount, + href_profile($recipient), + $recipient->username + ), + 'receiver_id' => null, + 'bot_id' => $this->bot->id, + ]); } /** - * Process Message. + * Get Help. */ - public function process(string $type, User $user, string $message = ''): \Illuminate\Http\Response|bool|\Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory + public function getHelp(?int $roomId = null, ?int $receiverId = null): Message { - $this->target = $user; - - if ($type === 'message') { - $x = 0; - $y = 1; - $z = 2; - } else { - $x = 1; - $y = 2; - $z = 3; - } - - if ($message === '') { - $log = ''; - } else { - $log = 'All '.$this->bot->name.' commands must be a private message or begin with /'.$this->bot->command.' or !'.$this->bot->command.'. Need help? Type /'.$this->bot->command.' help and you shall be helped.'; - } - - $command = @explode(' ', $message); - - if (\array_key_exists($x, $command)) { - if ($command[$x] === 'gift' && \array_key_exists($y, $command) && \array_key_exists($z, $command) && \array_key_exists($z + 1, $command)) { - $clone = $command; - array_shift($clone); - array_shift($clone); - array_shift($clone); - array_shift($clone); - $log = $this->putGift($command[$y], (float) $command[$z], $clone); - } - - if ($command[$x] === 'help') { - $log = $this->getHelp(); - } - } - - $this->type = $type; - $this->message = $message; - $this->log = $log; - - return $this->pm(); + return Message::create([ + 'user_id' => User::SYSTEM_USER_ID, + 'chatroom_id' => $roomId, + 'receiver_id' => $receiverId ?? null, + 'message' => $this->replaceVars($this->bot->help), + ]); } /** - * Output Message. + * Process Message. */ - public function pm(): \Illuminate\Http\Response|bool|\Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory + public function handle(?string $message, ?int $roomId = null, ?int $receiverId = null): void { - $type = $this->type; - $target = $this->target; - $txt = $this->log; - $message = $this->message; - - if ($type === 'message' || $type === 'private') { - // Create echo for user if missing - $echoes = cache()->remember( - 'user-echoes'.$target->id, - 3600, - fn () => UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get() - ); - - if ($echoes->doesntContain(fn ($echo) => $echo->bot_id == $this->bot->id)) { - UserEcho::create([ - 'user_id' => $target->id, - 'target_id' => $this->bot->id, - ]); - - $echoes = UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get(); - - cache()->put('user-echoes'.$target->id, $echoes, 3600); - - Chatter::dispatch('echo', $target->id, UserEchoResource::collection($echoes)); - } - - // Create audible for user if missing - $audibles = cache()->remember( - 'user-audibles'.$target->id, - 3600, - fn () => UserAudible::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get() - ); - - if ($audibles->doesntContain(fn ($audible) => $audible->bot_id == $this->bot->id)) { - UserAudible::create([ - 'user_id' => $target->id, - 'target_id' => $this->bot->id, - 'status' => false, - ]); + [, $command, $arguments] = preg_split('/ +/', $message, 3) + [null, null, null]; - $audibles = UserAudible::with(['room', 'target', 'bot'])->where('user_id', '=', $target->id)->get(); - - cache()->put('user-audibles'.$target->id, $audibles, 3600); - - Chatter::dispatch('audible', $target->id, UserAudibleResource::collection($audibles)); - } - - // Create message - if ($txt !== '') { - $roomId = 0; - $this->chatRepository->privateMessage($target->id, $roomId, $message, 1, $this->bot->id); - $this->chatRepository->privateMessage(1, $roomId, $txt, $target->id, $this->bot->id); - } - - return response('success'); - } - - if ($type === 'echo') { - if ($txt !== '') { - $roomId = 0; - $this->chatRepository->botMessage($this->bot->id, $roomId, $txt, $target->id); - } - - return response('success'); - } - - if ($type === 'public') { - if ($txt !== '') { - $this->chatRepository->message($target->id, $target->chatroom->id, $message, null, null); - $this->chatRepository->message(1, $target->chatroom->id, $txt, null, $this->bot->id); - } + switch ($command) { + case 'gift': + [$username, $amount, $message] = preg_split('/ +/', $arguments, 3) + [null, null, null]; + $message = $this->gift($username, $amount, $message); - return response('success'); + break; + default: + $message = $this->getHelp($roomId, $receiverId); } - return true; + MessageCreated::dispatch($message, User::SYSTEM_USER_ID, $roomId, $receiverId); } } diff --git a/app/Events/Chatter.php b/app/Events/EchoCreated.php similarity index 56% rename from app/Events/Chatter.php rename to app/Events/EchoCreated.php index 8e022e7141..595507730f 100644 --- a/app/Events/Chatter.php +++ b/app/Events/EchoCreated.php @@ -16,42 +16,21 @@ namespace App\Events; +use App\Models\UserEcho; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class Chatter implements ShouldBroadcastNow +class EchoCreated implements ShouldBroadcastNow { use Dispatchable; use InteractsWithSockets; use SerializesModels; - public $echoes; - - public $message; - - public $ping; - - public $audibles; - - /** - * Chatter Constructor. - */ - public function __construct(public string $type, public $target, $payload) + public function __construct(public UserEcho $echo) { - if ($type == 'echo') { - $this->echoes = $payload; - } elseif ($type == 'audible') { - $this->audibles = $payload; - } elseif ($type == 'new.message') { - $this->message = $payload; - } elseif ($type == 'new.bot') { - $this->message = $payload; - } elseif ($type == 'new.ping') { - $this->ping = $payload; - } } /** @@ -59,6 +38,6 @@ public function __construct(public string $type, public $target, $payload) */ public function broadcastOn(): PrivateChannel { - return new PrivateChannel('chatter.'.$this->target); + return new PrivateChannel('echo.created.'.$this->echo->user_id); } } diff --git a/app/Events/MessageCreated.php b/app/Events/MessageCreated.php new file mode 100644 index 0000000000..87b8b4050e --- /dev/null +++ b/app/Events/MessageCreated.php @@ -0,0 +1,65 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +namespace App\Events; + +use App\Models\Chatroom; +use App\Models\Message; +use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; +use Illuminate\Foundation\Events\Dispatchable; +use Illuminate\Queue\SerializesModels; + +class MessageCreated implements ShouldBroadcastNow +{ + use Dispatchable; + use InteractsWithSockets; + use SerializesModels; + + /** + * Create a new event instance. + */ + public function __construct( + public Message $message, + private readonly ?int $senderId = null, + private readonly ?int $roomId = null, + private readonly ?int $receiverId = null, + ) { + } + + /** + * Get the channels the event should broadcast on. + */ + public function broadcastOn(): PresenceChannel + { + switch (true) { + case $this->senderId !== null && $this->receiverId !== null: // Send to PM + $ids = [$this->senderId, $this->receiverId]; + asort($ids); + + return new PresenceChannel('messages.pm.'.implode('-', $ids)); + case $this->roomId !== null: // Send to chatroom + return new PresenceChannel('messages.room.'.$this->roomId); + default: // Use system chatroom if no parameters are isn't provided + $id = Chatroom::query() + ->where(\is_int(config('chat.system_chatroom')) ? 'id' : 'name', '=', config('chat.system_chatroom')) + ->soleValue('id'); + + return new PresenceChannel('messages.room.'.$id); + } + } +} diff --git a/app/Events/MessageDeleted.php b/app/Events/MessageDeleted.php index 31715a8f09..6cd31c1d79 100644 --- a/app/Events/MessageDeleted.php +++ b/app/Events/MessageDeleted.php @@ -39,15 +39,19 @@ public function __construct(public Message $message) /** * Get the channels the event should broadcast on. */ - public function broadcastOn(): PresenceChannel + public function broadcastOn(): ?PresenceChannel { - // $this->dontBroadcastToCurrentUser(); + if ($this->message->user_id !== null && $this->message->receiver_id !== null) { + $ids = [$this->message->user_id, $this->message->receiver_id]; + asort($ids); - return new PresenceChannel('chatroom.'.$this->message->chatroom_id); - } + return new PresenceChannel('messages.pm.'.implode('-', $ids)); + } - public function broadcastAs(): string - { - return 'delete.message'; + if ($this->message->chatroom_id !== null) { + return new PresenceChannel('messages.room.'.$this->message->chatroom_id); + } + + return null; } } diff --git a/app/Events/MessageSent.php b/app/Events/MessageSent.php deleted file mode 100644 index 246265dbb0..0000000000 --- a/app/Events/MessageSent.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 - */ - -namespace App\Events; - -use App\Http\Resources\ChatMessageResource; -use App\Models\Message; -use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; -use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; -use Illuminate\Foundation\Events\Dispatchable; -use Illuminate\Queue\SerializesModels; - -class MessageSent implements ShouldBroadcastNow -{ - use Dispatchable; - use InteractsWithSockets; - use SerializesModels; - - /** - * Message details. - */ - public ChatMessageResource $message; - - /** - * Create a new event instance. - */ - public function __construct(Message $message) - { - $message = Message::with([ - 'bot', - 'user.group', - 'user.chatStatus', - 'receiver.group', - 'receiver.chatStatus', - ])->find($message->id); - - $this->message = new ChatMessageResource($message); - } - - /** - * Get the channels the event should broadcast on. - */ - public function broadcastOn(): PresenceChannel - { - // $this->dontBroadcastToCurrentUser(); - - return new PresenceChannel('chatroom.'.$this->message->chatroom_id); - } - - public function broadcastAs(): string - { - return 'new.message'; - } -} diff --git a/app/Events/UserEdited.php b/app/Events/UserEdited.php new file mode 100644 index 0000000000..095c190d2a --- /dev/null +++ b/app/Events/UserEdited.php @@ -0,0 +1,67 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +namespace App\Events; + +use App\Models\Chatroom; +use App\Models\UserEcho; +use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PresenceChannel; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; +use Illuminate\Foundation\Events\Dispatchable; +use Illuminate\Queue\SerializesModels; + +class UserEdited implements ShouldBroadcastNow +{ + use Dispatchable; + use InteractsWithSockets; + use SerializesModels; + + /** + * Create a new event instance. + */ + public function __construct( + public int $userId, + private UserEcho $echo, + public ?int $chatStatusId = null, + public ?string $image = null, + public ?string $username = null, + public ?int $groupId = null, + ) { + } + + /** + * Get the channels the event should broadcast on. + */ + public function broadcastOn(): PresenceChannel + { + switch (true) { + case $this->echo->user_id !== null && $this->echo->target_id !== null: // Send to PM + $ids = [$this->echo->user_id, $this->echo->target_id]; + asort($ids); + + return new PresenceChannel('messages.pm.'.implode('-', $ids)); + case $this->echo->room_id !== null: // Send to chatroom + return new PresenceChannel('messages.room.'.$this->echo->room_id); + default: // Use system chatroom if no parameters are isn't provided + $id = Chatroom::query() + ->where(\is_int(config('chat.system_chatroom')) ? 'id' : 'name', '=', config('chat.system_chatroom')) + ->soleValue('id'); + + return new PresenceChannel('messages.room.'.$id); + } + } +} diff --git a/app/Http/Controllers/API/ChatController.php b/app/Http/Controllers/API/ChatController.php deleted file mode 100644 index 1c708539fa..0000000000 --- a/app/Http/Controllers/API/ChatController.php +++ /dev/null @@ -1,513 +0,0 @@ - - * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 - */ - -namespace App\Http\Controllers\API; - -use App\Bots\NerdBot; -use App\Bots\SystemBot; -use App\Events\Chatter; -use App\Http\Controllers\Controller; -use App\Http\Resources\BotResource; -use App\Http\Resources\ChatMessageResource; -use App\Http\Resources\ChatRoomResource; -use App\Http\Resources\UserAudibleResource; -use App\Http\Resources\UserEchoResource; -use App\Models\Bot; -use App\Models\Message; -use App\Models\User; -use App\Models\UserAudible; -use App\Models\UserEcho; -use App\Repositories\ChatRepository; -use Illuminate\Http\Request; -use Illuminate\Support\Carbon; - -/** - * @see \Tests\Feature\Http\Controllers\API\ChatControllerTest - */ -class ChatController extends Controller -{ - /** - * ChatController Constructor. - */ - public function __construct(private readonly ChatRepository $chatRepository) - { - } - - /* STATUSES */ - public function statuses(): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - return response($this->chatRepository->statuses()); - } - - /* ECHOES */ - public function echoes(Request $request): \Illuminate\Http\Resources\Json\AnonymousResourceCollection - { - $user = $request->user()->load(['echoes']); - - if ($user->echoes->isEmpty()) { - $userEcho = new UserEcho(); - $userEcho->user_id = $request->user()->id; - $userEcho->room_id = 1; - $userEcho->save(); - } - - return UserEchoResource::collection($this->chatRepository->echoes($request->user()->id)); - } - - /* AUDIBLES */ - public function audibles(Request $request): \Illuminate\Http\Resources\Json\AnonymousResourceCollection - { - $user = $request->user()->load(['audibles']); - - if ($user->audibles->isEmpty()) { - $userAudible = new UserAudible(); - $userAudible->user_id = $request->user()->id; - $userAudible->room_id = 1; - $userAudible->status = true; - $userAudible->save(); - } - - return UserAudibleResource::collection($this->chatRepository->audibles($request->user()->id)); - } - - /* BOTS */ - public function bots(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection - { - return BotResource::collection($this->chatRepository->bots()); - } - - /* ROOMS */ - public function rooms(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection - { - return ChatRoomResource::collection($this->chatRepository->rooms()); - } - - public function config(): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - return response($this->chatRepository->config()); - } - - /* MESSAGES */ - public function messages($roomId): \Illuminate\Http\Resources\Json\AnonymousResourceCollection - { - return ChatMessageResource::collection($this->chatRepository->messages($roomId)); - } - - /* MESSAGES */ - public function privateMessages(Request $request, $targetId): \Illuminate\Http\Resources\Json\AnonymousResourceCollection - { - return ChatMessageResource::collection($this->chatRepository->privateMessages($request->user()->id, $targetId)); - } - - /* MESSAGES */ - public function botMessages(Request $request, $botId): \Illuminate\Http\Resources\Json\AnonymousResourceCollection - { - $runbot = null; - $bot = Bot::findOrFail($botId); - - if ($bot->is_systembot) { - $runbot = new SystemBot($this->chatRepository); - } elseif ($bot->is_nerdbot) { - $runbot = new NerdBot($this->chatRepository); - } - - $runbot->process('message', $request->user(), '', 0); - - return ChatMessageResource::collection($this->chatRepository->botMessages($request->user()->id, $bot->id)); - } - - public function createMessage(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|bool|ChatMessageResource - { - $bot = null; - $user = $request->user(); - - $userId = $user->id; - $receiverId = $request->input('receiver_id'); - $roomId = $request->input('chatroom_id'); - $botId = $request->input('bot_id'); - $message = $request->input('message'); - $targeted = $request->input('targeted'); - $save = $request->get('save'); - - if ($user->can_chat === false) { - return response('error', 401); - } - - // Temp Fix For HTMLPurifier - if ($message === '<') { - return response('error', 401); - } - - $bots = cache()->remember('bots', 3600, fn () => Bot::where('active', '=', 1)->orderByDesc('position')->get()); - - $which = null; - $target = null; - $runbot = null; - $trip = 'msg'; - - if ($message && str_starts_with((string) $message, '/'.$trip)) { - $which = 'skip'; - $command = @explode(' ', (string) $message); - - if (\array_key_exists(1, $command)) { - $receiverId = User::where('username', 'like', $command[1])->sole()->id; - $clone = $command; - array_shift($clone); - array_shift($clone); - $message = trim(implode(' ', $clone)); - } - - $botId = 1; - } - - $trip = 'gift'; - - if ($message && str_starts_with((string) $message, '/'.$trip)) { - $which = 'echo'; - $target = 'system'; - $message = '/bot gift'.substr((string) $message, \strlen($trip) + 1, \strlen((string) $message)); - } - - if ($target == 'system') { - $runbot = new SystemBot($this->chatRepository); - } - - if ($which == null) { - foreach ($bots as $bot) { - if ($message && str_starts_with((string) $message, '/'.$bot->command)) { - $which = 'echo'; - } elseif ($message && str_starts_with((string) $message, '!'.$bot->command)) { - $which = 'public'; - } elseif ($message && str_starts_with((string) $message, '@'.$bot->command)) { - $message = substr((string) $message, 1 + \strlen((string) $bot->command), \strlen((string) $message)); - $which = 'private'; - } elseif ($message && $receiverId == 1 && $bot->id == $botId) { - if (str_starts_with((string) $message, '/'.$bot->command)) { - $message = substr((string) $message, 1 + \strlen((string) $bot->command), \strlen((string) $message)); - } - - if ($message && str_starts_with((string) $message, '!'.$bot->command)) { - $message = substr((string) $message, 1 + \strlen((string) $bot->command), \strlen((string) $message)); - } - - if ($message && str_starts_with((string) $message, '@'.$bot->command)) { - $message = substr((string) $message, 1 + \strlen((string) $bot->command), \strlen((string) $message)); - } - - $which = 'message'; - } - - if ($which != null) { - break; - } - } - } - - if ($which != null && $which != 'skip' && !$runbot) { - if ($bot->is_systembot) { - $runbot = new SystemBot($this->chatRepository); - } elseif ($bot->is_nerdbot) { - $runbot = new NerdBot($this->chatRepository); - } - } - - if ($runbot !== null) { - return $runbot->process($which ?? '', $request->user(), $message, 0); - } - - $echo = false; - - if ($receiverId && $receiverId > 0) { - // Create echo for both users if missing - foreach ([[$userId, $receiverId], [$receiverId, $userId]] as [$user1Id, $user2Id]) { - $echoes = cache()->remember( - 'user-echoes'.$user1Id, - 3600, - fn () => UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $user1Id)->get() - ); - - if ($echoes->doesntContain(fn ($echo) => $echo->target_id == $user2Id)) { - UserEcho::create([ - 'user_id' => $user1Id, - 'target_id' => $user2Id, - ]); - - $echoes = UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $user1Id)->get(); - - cache()->put('user-echoes'.$user1Id, $echoes, 3600); - - Chatter::dispatch('echo', $user1Id, UserEchoResource::collection($echoes)); - } - } - - // Create audible for both users if missing - foreach ([[$userId, $receiverId], [$receiverId, $userId]] as [$user1Id, $user2Id]) { - $audibles = cache()->remember( - 'user-audibles'.$user1Id, - 3600, - fn () => UserAudible::with(['room', 'target', 'bot'])->where('user_id', '=', $user1Id)->get() - ); - - if ($audibles->doesntContain(fn ($audible) => $audible->target_id == $user2Id)) { - UserAudible::create([ - 'user_id' => $user1Id, - 'target_id' => $user2Id, - 'status' => false, - ]); - - $audibles = UserAudible::with(['room', 'target', 'bot'])->where('user_id', '=', $user1Id)->get(); - - cache()->put('user-audibles'.$user1Id, $audibles, 3600); - - Chatter::dispatch('audible', $user1Id, UserAudibleResource::collection($audibles)); - } - } - - $roomId = 0; - $ignore = $botId > 0 && $receiverId == 1 ? true : null; - $save = true; - $echo = true; - $message = $this->chatRepository->privateMessage($userId, $roomId, $message, $receiverId, null, $ignore); - } else { - $receiverId = null; - $botId = null; - $message = $this->chatRepository->message($userId, $roomId, $message, $receiverId, $botId); - } - - if (!$save) { - $message->delete(); - } - - if ($save && $echo) { - return new ChatMessageResource($message); - } - - return response('success'); - } - - public function deleteMessage(Request $request, $id): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $message = Message::find($id); - - abort_unless($request->user()->id === $message->user_id || $request->user()->group->is_modo, 403); - - $changedByStaff = $request->user()->id !== $message->user_id; - - abort_if($changedByStaff && !$request->user()->group->is_owner && $request->user()->group->level <= $message->user->group->level, 403); - - $this->chatRepository->deleteMessage($id); - - return response('success'); - } - - public function deleteRoomEcho(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user(); - UserEcho::where('user_id', '=', $user->id)->where('room_id', '=', $request->input('room_id'))->delete(); - - $user->load(['chatStatus', 'chatroom', 'group', 'echoes']); - $room = $this->chatRepository->roomFindOrFail($request->input('room_id')); - - $user->chatroom()->dissociate(); - $user->chatroom()->associate($room); - - $user->save(); - - $senderEchoes = UserEcho::with(['room', 'target', 'bot'])->where('user_id', $user->id)->get(); - - $expiresAt = Carbon::now()->addMinutes(60); - cache()->put('user-echoes'.$user->id, $senderEchoes, $expiresAt); - event(new Chatter('echo', $user->id, UserEchoResource::collection($senderEchoes))); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - public function deleteTargetEcho(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user(); - UserEcho::where('user_id', '=', $user->id)->where('target_id', '=', $request->input('target_id'))->delete(); - - $user->load(['chatStatus', 'chatroom', 'group', 'echoes']); - $senderEchoes = UserEcho::with(['room', 'target', 'bot'])->where('user_id', $user->id)->get(); - - $expiresAt = Carbon::now()->addMinutes(60); - cache()->put('user-echoes'.$user->id, $senderEchoes, $expiresAt); - event(new Chatter('echo', $user->id, UserEchoResource::collection($senderEchoes))); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - public function deleteBotEcho(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user(); - UserEcho::where('user_id', '=', $user->id)->where('bot_id', '=', $request->input('bot_id'))->delete(); - - $user->load(['chatStatus', 'chatroom', 'group', 'echoes']); - $senderEchoes = UserEcho::with(['room', 'target', 'bot'])->where('user_id', $user->id)->get(); - - $expiresAt = Carbon::now()->addMinutes(60); - cache()->put('user-echoes'.$user->id, $senderEchoes, $expiresAt); - event(new Chatter('echo', $user->id, UserEchoResource::collection($senderEchoes))); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - public function toggleRoomAudible(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user(); - $echo = UserAudible::where('user_id', '=', $user->id)->where('room_id', '=', $request->input('room_id'))->sole(); - $echo->status = !$echo->status; - $echo->save(); - - $user->load(['chatStatus', 'chatroom', 'group', 'audibles', 'audibles']); - $senderAudibles = UserAudible::with(['room', 'target', 'bot'])->where('user_id', $user->id)->get(); - - $expiresAt = Carbon::now()->addMinutes(60); - cache()->put('user-audibles'.$user->id, $senderAudibles, $expiresAt); - event(new Chatter('audible', $user->id, UserAudibleResource::collection($senderAudibles))); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - public function toggleTargetAudible(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user(); - $echo = UserAudible::where('user_id', '=', $user->id)->where('target_id', '=', $request->input('target_id'))->sole(); - $echo->status = !$echo->status; - $echo->save(); - - $user->load(['chatStatus', 'chatroom', 'group', 'audibles', 'audibles']); - $senderAudibles = UserAudible::with(['target', 'room', 'bot'])->where('user_id', $user->id)->get(); - - $expiresAt = Carbon::now()->addMinutes(60); - cache()->put('user-audibles'.$user->id, $senderAudibles, $expiresAt); - event(new Chatter('audible', $user->id, UserAudibleResource::collection($senderAudibles))); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - public function toggleBotAudible(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user(); - $echo = UserAudible::where('user_id', '=', $user->id)->where('bot_id', '=', $request->input('bot_id'))->sole(); - $echo->status = !$echo->status; - $echo->save(); - - $user->load(['chatStatus', 'chatroom', 'group', 'audibles', 'audibles'])->findOrFail($user->id); - $senderAudibles = UserAudible::with(['bot', 'room', 'bot'])->where('user_id', $user->id)->get(); - - $expiresAt = Carbon::now()->addMinutes(60); - cache()->put('user-audibles'.$user->id, $senderAudibles, $expiresAt); - event(new Chatter('audible', $user->id, UserAudibleResource::collection($senderAudibles))); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - /* USERS */ - public function updateUserChatStatus(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $systemUser = User::where('username', 'System')->sole(); - - $user = $request->user(); - $user->load(['chatStatus', 'chatroom', 'group', 'echoes']); - $status = $this->chatRepository->statusFindOrFail($request->input('status_id')); - - $this->chatRepository->systemMessage('[url=/users/'.$user->username.']'.$user->username.'[/url] has updated their status to [b]'.$status->name.'[/b]'); - - $user->chatStatus()->dissociate(); - $user->chatStatus()->associate($status); - $user->save(); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - public function updateUserRoom(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user(); - $user->load(['chatStatus', 'chatroom', 'group', 'echoes']); - $room = $this->chatRepository->roomFindOrFail($request->input('room_id')); - - $user->chatroom()->dissociate(); - $user->chatroom()->associate($room); - - $user->save(); - - // Create echo for user if missing - $echoes = cache()->remember( - 'user-echoes'.$user->id, - 3600, - fn () => UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $user->id)->get(), - ); - - if ($echoes->doesntContain(fn ($echo) => $echo->room_id == $room->id)) { - UserEcho::create([ - 'user_id' => $user->id, - 'room_id' => $room->id, - ]); - - $echoes = UserEcho::with(['room', 'target', 'bot'])->where('user_id', '=', $user->id)->get(); - - cache()->put('user-echoes'.$user->id, $echoes, 3600); - - Chatter::dispatch('echo', $user->id, UserEchoResource::collection($echoes)); - } - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } - - public function updateUserTarget(Request $request): \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response - { - $user = $request->user()->load(['chatStatus', 'chatroom', 'group', 'echoes']); - - /** - * @phpstan-ignore-next-line Laravel automatically converts models to json - * @see https://github.com/laravel/framework/blob/48246da2320c95a17bfae922d36264105a917906/src/Illuminate/Http/Response.php#L56 - */ - return response($user); - } -} diff --git a/app/Http/Controllers/Staff/ChatRoomController.php b/app/Http/Controllers/Staff/ChatRoomController.php index 53112f52d8..4d42884d13 100644 --- a/app/Http/Controllers/Staff/ChatRoomController.php +++ b/app/Http/Controllers/Staff/ChatRoomController.php @@ -86,11 +86,7 @@ public function update(UpdateChatRoomRequest $request, Chatroom $chatroom): \Ill public function destroy(Chatroom $chatroom): \Illuminate\Http\RedirectResponse { $default = Chatroom::query() - ->when( - \is_int(config('chat.system_chatroom')), - fn ($query) => $query->where('id', '=', config('chat.system_chatroom')), - fn ($query) => $query->where('name', '=', config('chat.system_chatroom')), - ) + ->where(\is_int(config('chat.system_chatroom')) ? 'id' : 'name', '=', config('chat.system_chatroom')) ->soleValue('id'); User::whereBelongsTo($chatroom)->update(['chatroom_id' => $default]); diff --git a/app/Http/Livewire/Chatbox.php b/app/Http/Livewire/Chatbox.php new file mode 100644 index 0000000000..5ba6d8a03c --- /dev/null +++ b/app/Http/Livewire/Chatbox.php @@ -0,0 +1,457 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +namespace App\Http\Livewire; + +use App\Bots\NerdBot; +use App\Bots\SystemBot; +use App\Events\EchoCreated; +use App\Events\MessageDeleted; +use App\Events\MessageCreated; +use App\Events\UserEdited; +use App\Models\Bot; +use App\Models\Chatroom; +use App\Models\ChatStatus; +use App\Models\Group; +use App\Models\Message; +use App\Models\User; +use App\Models\UserEcho; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; +use Livewire\Attributes\Computed; +use Livewire\Component; + +/** + * @phpstan-type AudibleResource array{ + * id: int, + * user_id: int, + * target_id: ?int, + * room_id: ?int, + * } + * @phpstan-type EchoResource array{ + * id: int, + * user_id: int, + * target_id: ?int, + * room_id: ?int, + * } + * @phpstan-type GroupResource array{ + * id: int, + * name: string, + * color: string, + * effect: string, + * icon: string, + * } + * @phpstan-type StatusResource array{ + * id: int, + * name: string, + * color: string, + * } + * @phpstan-type RoomResource array{ + * id: int, + * name: string, + * } + * @phpstan-type UserResource array{ + * id: int, + * username: string, + * group_id: int, + * image: string, + * chatroom_id: int, + * chat_status_id: int, + * } + * @phpstan-type MessageResource array{ + * id: int, + * user_id: int, + * receiver_id: ?int, + * message: string, + * created_at: string, + * } + */ +class Chatbox extends Component +{ + /** @var ?EchoResource Current selected chat tab */ + public ?array $echo = null; + + /** @var User Authenticated user */ + public User $user; + + /** @var string Message to send in chatbox */ + public string $message = ''; + + /** + * @return array + */ + final protected function rules(): array + { + return [ + 'user.chatroom_id' => [ + 'required', + 'exists:chatrooms,id', + ], + 'user.chat_status_id' => [ + 'required', + 'exists:chat_statuses,id', + ], + 'message' => [ + 'required', + 'max:65535', + ], + 'echo.id' => [ + 'integer', + ], + 'echo.user_id' => [ + 'in:'.auth()->id(), + ], + 'echo.room_id' => [ + 'exists:chatrooms,id', + 'prohibits:echo.target_id', + 'required_without:echo.target_id', + ], + 'echo.target_id' => [ + 'exists:users,id', + 'prohibits:echo.room_id', + 'required_without:echo.room_id', + ], + ]; + } + + /* + * Lifecycle hooks + */ + + final public function mount(): void + { + $this->user = auth()->user(); + + if ($this->user->chatStatus()->doesntExist()) { + $this->user->chatStatus()->associate(ChatStatus::first()); + } + + if ($this->user->chatroom()->doesntExist()) { + $this->user->chatroom()->associate($this->systemChatroomId); + } + + $this->echo = $this->defaultEcho; + } + + final public function updated(string $propertyName): void + { + $this->validateOnly($propertyName); + } + + final public function updatedUserChatroomId(int $value): void + { + $this->echo = UserEcho::firstOrCreate([ + 'user_id' => auth()->id(), + 'room_id' => $value, + ]) + ->only(['id', 'user_id', 'room_id', 'target_id']); + } + + /** @param EchoResource $userEcho */ + final public function updatedEcho(array $userEcho): void + { + abort_unless($userEcho['user_id'] === auth()->id(), 403); + + if ($userEcho['room_id'] !== null) { + auth()->user()->chatroom()->associate($userEcho['room_id']); + } + } + + final public function updatedUserChatStatusId(int $value): void + { + $this->user->save(); + + $message = Message::create([ + 'user_id' => User::SYSTEM_USER_ID, + 'chatroom_id' => $this->systemChatroomId, + 'message' => '[url=/users/'.auth()->user()->username.']'.auth()->user()->username.'[/url] has updated their status to [b]'.$this->statuses[$value]['name'].'[/b]', + ]); + + MessageCreated::dispatch($message); + + auth()->user()->echoes()->has('room')->each(function ($echo) use ($value): void { + UserEdited::dispatch(auth()->id(), $echo, $value); + }); + } + + /* + * Actions + */ + + final public function store(): void + { + abort_unless(auth()->user()->can_chat, 403); + + $this->message = trim($this->message); + + if ($this->message === '') { + return; + } + + $firstWord = strstr($this->message, ' ', true); + + switch ($firstWord) { + case '/msg': + [, $username, $message] = preg_split('/ +/', $this->message, 3) + [null, null, '']; + + $this->createEcho($username, $message); + + $this->reset('message'); + + break; + case '/gift': + [, $username, $amount, $message] = preg_split('/ +/', trim($this->message), 4) + [null, null, null, null]; + + (new SystemBot())->handle("command gift {$username} {$amount} {$message}"); + + break; + default: + if ($this->echo['target_id'] !== null) { + $message = Message::create([ + 'user_id' => auth()->id(), + 'receiver_id' => $this->echo['target_id'], + 'message' => trim($this->message), + ]); + + $echo = UserEcho::firstOrCreate([ + 'user_id' => $this->echo['target_id'], + 'target_id' => auth()->id(), + ]); + + if ($echo->wasRecentlyCreated) { + EchoCreated::dispatch($echo); + + sleep(1); + } + + MessageCreated::dispatch($message, auth()->id(), null, $this->echo['target_id']); + } elseif ($this->echo['room_id'] !== null) { + $message = Message::create([ + 'user_id' => auth()->id(), + 'chatroom_id' => $this->echo['room_id'], + 'message' => trim($this->message), + ]); + + MessageCreated::dispatch($message, auth()->id(), $this->echo['room_id']); + } else { + abort(500); + } + + $bots = cache()->remember('bots', 3600, fn () => Bot::select(['id', 'command'])->where('active', '=', true)->get()); + + foreach ($bots as $bot) { + if ($bot->command === $firstWord) { + match ($bot->id) { + 1 => (new SystemBot())->handle($this->message, $this->echo['room_id'], $this->echo['user_id']), + 2 => (new NerdBot())->handle($this->message, $this->echo['room_id'], $this->echo['target_id']), + default => null, + }; + } + } + + $this->reset('message'); + } + } + + final public function destroy(Message $message): void + { + abort_unless($message->user_id === auth()->id() || auth()->user()->group->is_modo, 403); + + $message->delete(); + + MessageDeleted::dispatch($message); + } + + final public function destroyEcho(UserEcho $echo): void + { + abort_unless($echo->user_id === auth()->id(), 403); + + $echo->delete(); + + $this->echo = $this->defaultEcho; + } + + /** @return EchoResource */ + final public function createEcho(?string $username, string $message = ''): array + { + Validator::make([ + 'username' => $username, + ], [ + 'username' => [ + 'required', + Rule::exists('users', 'username')->whereNot('id', auth()->id()), + ], + ])->validate(); + + $receiver = User::where('username', '=', $username)->sole(); + + $echo = UserEcho::firstOrCreate([ + 'user_id' => auth()->id(), + 'target_id' => $receiver->id, + ]); + + if ($echo->wasRecentlyCreated) { + EchoCreated::dispatch($echo); + } + + $this->echo = $echo->only(['id', 'user_id', 'target_id', 'room_id']); + + if ($message !== '') { + $echo = UserEcho::firstOrCreate([ + 'user_id' => $receiver->id, + 'target_id' => auth()->id(), + ]); + + $message = Message::create([ + 'user_id' => auth()->id(), + 'receiver_id' => $receiver->id, + 'message' => trim($message), + ]); + + // Send it twice for more instant feedback for the end user + MessageCreated::dispatch($message, auth()->id(), null, $receiver->id); + + if ($echo->wasRecentlyCreated) { + EchoCreated::dispatch($echo); + + sleep(1); + } + + $this->reset('message'); + + MessageCreated::dispatch($message, auth()->id(), null, $receiver->id); + } + + return $this->echo; + } + + // Custom methods + + final public function getWsChannelName(UserEcho $echo): string + { + return match (true) { + $echo->target_id !== null => 'messages.pm.'.min($echo->target_id, $echo->user_id).'-'.max($echo->target_id, $echo->user_id), + $echo->room_id !== null => 'messages.room.'.$echo->room_id, + default => 'messages.room.'.$this->systemChatroomId, + }; + } + + // Computed Properties + + /** @return array}> */ + #[Computed] + final public function msgs(): array + { + $channels = []; + + foreach (auth()->user()->echoes()->get() as $echo) { + $channels[$this->getWsChannelName($echo)]['messages'] = Message::query() + ->when( + $echo->room_id !== null, + fn ($query) => $query->where('chatroom_id', '=', $echo->room_id), + fn ($query) => $query->where( + fn ($query) => $query + ->where(fn ($query) => $query->where('user_id', '=', auth()->id())->where('receiver_id', '=', $echo->target_id)) + ->orWhere(fn ($query) => $query->where('receiver_id', '=', auth()->id())->where('user_id', '=', $echo->target_id)), + ) + ) + ->select(['id', 'user_id', 'receiver_id', 'message', 'created_at']) + ->orderByDesc('id') + ->limit(config('chat.message_limit')) + ->get() + ->keyBy('id') + ->toArray(); + } + + return $channels; + } + + /** @return EchoResource */ + #[Computed] + final public function defaultEcho(): array + { + return UserEcho::query() + ->where('user_id', '=', auth()->id()) + ->whereIn('room_id', [auth()->user()->chatroom_id, $this->systemChatroomId]) + ->firstOrCreate([ + 'user_id' => auth()->id(), + 'room_id' => $this->systemChatroomId, + 'target_id' => null, + ]) + ->only(['id', 'user_id', 'room_id', 'target_id']); + } + + /** @return array */ + #[Computed] + final public function statuses(): array + { + return ChatStatus::all(['id', 'name', 'color'])->keyBy('id')->toArray(); + } + + /** @return array */ + #[Computed] + final public function rooms(): array + { + return Chatroom::all(['id', 'name'])->keyBy('id')->toArray(); + } + + /** @return array */ + #[Computed] + final public function echoes(): array + { + return auth()->user()->echoes()->get(['id', 'user_id', 'target_id', 'room_id'])->keyBy('id')->toArray(); + } + + /** @return array */ + #[Computed] + final public function audibles(): array + { + return auth()->user()->audibles()->get(['id', 'user_id', 'target_id', 'room_id'])->keyBy('id')->toArray(); + } + + /** @return array */ + #[Computed] + final public function groups(): array + { + return Group::all(['id', 'name', 'color', 'effect', 'icon'])->keyBy('id')->toArray(); + } + + #[Computed] + final public function systemChatroomId(): int + { + $config = config('chat.system_chatroom'); + + return Chatroom::query()->where(\is_int($config) ? 'id' : 'name', '=', $config)->soleValue('id'); + } + + /** @return array */ + #[Computed] + final public function users(): array + { + $messageUserIds = collect($this->msgs)->pluck('messages.*.user_id')->flatten()->toArray(); + + $echoUserIds = auth()->user()->echoes()->has('target')->pluck('target_id')->toArray(); + + $userIds = array_merge($messageUserIds, $echoUserIds, [auth()->id()]); + + return User::query() + ->select(['id', 'username', 'group_id', 'image', 'chatroom_id', 'chat_status_id']) + ->whereIntegerInRaw('id', array_unique($userIds)) + ->get() + ->keyBy('id') + ->toArray(); + } +} diff --git a/app/Http/Requests/Staff/UpdateChatBotRequest.php b/app/Http/Requests/Staff/UpdateChatBotRequest.php index d55ccd7b9c..f8a859e6a9 100644 --- a/app/Http/Requests/Staff/UpdateChatBotRequest.php +++ b/app/Http/Requests/Staff/UpdateChatBotRequest.php @@ -43,7 +43,6 @@ public function rules(): array ], 'command' => [ 'required', - 'alpha_dash', 'min:1', 'max:255', ], diff --git a/app/Models/Message.php b/app/Models/Message.php index 6ea989cbc0..e6e0553d86 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -43,11 +43,13 @@ class Message extends Model * @var array */ protected $fillable = [ + 'id', 'message', 'user_id', 'chatroom_id', 'receiver_id', 'bot_id', + 'created_at', ]; /** diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 684acb7b47..5e1981b69a 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -43,10 +43,6 @@ public function boot(): void $this->removeIndexPhpFromUrl(); $this->routes(function (): void { - Route::prefix('api') - ->middleware(['chat']) - ->group(base_path('routes/vue.php')); - Route::middleware('web') ->group(base_path('routes/web.php')); diff --git a/app/Repositories/ChatRepository.php b/app/Repositories/ChatRepository.php index 70411bd0ad..3aea262d64 100644 --- a/app/Repositories/ChatRepository.php +++ b/app/Repositories/ChatRepository.php @@ -16,348 +16,22 @@ namespace App\Repositories; -use App\Events\Chatter; -use App\Events\MessageDeleted; -use App\Events\MessageSent; -use App\Events\Ping; -use App\Http\Resources\ChatMessageResource; -use App\Models\Bot; +use App\Events\MessageCreated; use App\Models\Chatroom; -use App\Models\ChatStatus; use App\Models\Message; use App\Models\User; -use App\Models\UserAudible; -use App\Models\UserEcho; -use Illuminate\Support\Str; class ChatRepository { - /** - * ChatRepository Constructor. - */ - public function __construct(private readonly Message $message, private readonly Chatroom $chatroom, private readonly ChatStatus $chatStatus, private readonly User $user, private readonly Bot $bot, private readonly UserEcho $userEcho, private readonly UserAudible $userAudible) - { - } - - public function config() - { - return config('chat'); - } - - public function bots() - { - return $this->bot->all(); - } - - public function echoes($userId): \Illuminate\Support\Collection - { - return $this->userEcho->with([ - 'bot', - 'user', - 'target', - 'room', - ])->where(function ($query) use ($userId): void { - $query->where('user_id', '=', $userId); - }) - ->oldest('id') - ->get(); - } - - public function audibles($userId) - { - return $this->userAudible->with([ - 'bot', - 'user', - 'target', - 'room', - ])->where(function ($query) use ($userId): void { - $query->where('user_id', '=', $userId); - }) - ->latest() - ->get(); - } - - public function rooms() - { - return $this->chatroom->all(); - } - - public function roomFindOrFail($id) - { - return $this->chatroom->findOrFail($id); - } - - public function ping($type, $id): bool - { - if ($type == 'room') { - foreach (Chatroom::where('id', '>', 0)->get() as $room) { - broadcast(new Ping($room->id, $id)); - } - } - - return true; - } - - public function message($userId, $roomId, $message, $receiver = null, $bot = null) - { - if ($this->user->find($userId)->settings?->censor) { - $message = $this->censorMessage($message); - } - - $message = $this->htmlifyMessage($message); - - $message = $this->message->create([ - 'user_id' => $userId, - 'chatroom_id' => $roomId, - 'message' => $message, - 'receiver_id' => $receiver, - 'bot_id' => $bot, - ]); - - $this->checkMessageLimits($roomId); - - broadcast(new MessageSent($message)); - - return $message; - } - - public function botMessage($botId, $roomId, $message, $receiver = null): void - { - $user = $this->user->find($receiver); - - if ($user->settings?->censor) { - $message = $this->censorMessage($message); - } - - $message = $this->htmlifyMessage($message); - $save = $this->message->create([ - 'bot_id' => $botId, - 'user_id' => 1, - 'chatroom_id' => 0, - 'message' => $message, - 'receiver_id' => $receiver, - ]); - - $message = Message::with([ - 'bot', - 'user.group', - 'user.chatStatus', - 'receiver.group', - 'receiver.chatStatus', - ])->find($save->id); - - event(new Chatter('new.bot', $receiver, new ChatMessageResource($message))); - event(new Chatter('new.ping', $receiver, ['type' => 'bot', 'id' => $botId])); - $message->delete(); - } - - public function privateMessage($userId, $roomId, $message, $receiver = null, $bot = null, $ignore = null) - { - if ($this->user->find($userId)->settings?->censor) { - $message = $this->censorMessage($message); - } - - $message = $this->htmlifyMessage($message); - - $save = $this->message->create([ - 'user_id' => $userId, - 'chatroom_id' => 0, - 'message' => $message, - 'receiver_id' => $receiver, - 'bot_id' => $bot, - ]); - - $message = Message::with([ - 'bot', - 'user.group', - 'user.chatStatus', - 'receiver.group', - 'receiver.chatStatus', - ])->find($save->id); - - if ($ignore != null) { - event(new Chatter('new.message', $userId, new ChatMessageResource($message))); - } - - event(new Chatter('new.message', $receiver, new ChatMessageResource($message))); - - if ($receiver != 1) { - event(new Chatter('new.ping', $receiver, ['type' => 'target', 'id' => $userId])); - } - - return $message; - } - - public function deleteMessage($id) - { - $message = $this->message->find($id); - - if ($message) { - broadcast(new MessageDeleted($message)); - - return $message->delete(); - } - } - - public function messages($roomId): \Illuminate\Support\Collection - { - return $this->message->with([ - 'bot', - 'user.group', - 'chatroom', - 'user.chatStatus', - 'receiver.group', - 'receiver.chatStatus', - ])->where(function ($query) use ($roomId): void { - $query->where('chatroom_id', '=', $roomId); - $query->where('chatroom_id', '!=', 0); - }) - ->latest('id') - ->limit(config('chat.message_limit')) - ->get(); - } - - public function botMessages($senderId, $botId): \Illuminate\Support\Collection - { - $systemUserId = User::where('username', 'System')->sole()->id; - - return $this->message->with([ - 'bot', - 'user.group', - 'chatroom', - 'user.chatStatus', - 'receiver.group', - 'receiver.chatStatus', - ])->where(function ($query) use ($senderId, $systemUserId): void { - $query->whereRaw('(user_id = ? and receiver_id = ?)', [$senderId, $systemUserId])->orWhereRaw('(user_id = ? and receiver_id = ?)', [$systemUserId, $senderId]); - })->where('bot_id', '=', $botId) - ->where('chatroom_id', '=', 0) - ->latest('id') - ->limit(config('chat.message_limit')) - ->get(); - } - - public function privateMessages($senderId, $targetId): \Illuminate\Support\Collection - { - return $this->message->with([ - 'bot', - 'user.group', - 'chatroom', - 'user.chatStatus', - 'receiver.group', - 'receiver.chatStatus', - ])->where(function ($query) use ($senderId, $targetId): void { - $query->whereRaw('(user_id = ? and receiver_id = ?)', [$senderId, $targetId])->orWhereRaw('(user_id = ? and receiver_id = ?)', [$targetId, $senderId]); - }) - ->where('chatroom_id', '=', 0) - ->latest('id') - ->limit(config('chat.message_limit')) - ->get(); - } - - public function checkMessageLimits($roomId): void - { - $messages = $this->messages($roomId)->toArray(); - $limit = config('chat.message_limit'); - $count = is_countable($messages) ? \count($messages) : 0; - - // Lets purge all old messages and keep the database to the limit settings - if ($count > $limit) { - for ($x = 1; $x <= $count - $limit; $x++) { - $message = array_pop($messages); - echo $message['id']."\n"; - - $message = $this->message->find($message['id']); - - if ($message->receiver_id === null) { - $message->delete(); - } - } - } - } - - public function systemMessage($message, $bot = null): static - { - $systemUserId = User::where('username', 'System')->first()->id; - - if ($bot) { - $this->message($systemUserId, $this->systemChatroom(), $message, null, $bot); - } else { - $systemBotId = Bot::where('command', 'systembot')->first()->id; - - $this->message($systemUserId, $this->systemChatroom(), $message, null, $systemBotId); - } - - return $this; - } - - public function systemChatroom($room = null) - { - $config = config('chat.system_chatroom'); - - if ($room !== null) { - if ($room instanceof Chatroom) { - $room = $room->id; - } elseif (\is_int($room)) { - $room = $this->chatroom->findOrFail($room)->id; - } else { - $room = $this->chatroom->whereName($room)->first()->id; - } - } elseif (\is_int($config)) { - $room = $this->chatroom->findOrFail($config)->id; - } elseif ($config instanceof Chatroom) { - $room = $config->id; - } else { - $room = $this->chatroom->whereName($config)->first()->id; - } - - return $room; - } - - public function statuses() - { - return $this->chatStatus->all(); - } - - public function status($user) - { - $status = null; - - if ($user instanceof User) { - $status = $this->chatStatus->where('user_id', '=', $user->id)->first(); - } - - if (\is_int($user)) { - $status = $this->chatStatus->where('user_id', '=', $user)->first(); - } - - return $status; - } - - public function statusFindOrFail($id) - { - return $this->chatStatus->findOrFail($id); - } - - protected function censorMessage($message): string - { - foreach (config('censor.redact') as $word) { - if (preg_match(sprintf('/\b%s(?=[.,]|$|\s)/mi', $word), (string) $message)) { - $message = str_replace($word, sprintf("%s", $word), (string) $message); - } - } - - foreach (config('censor.replace') as $word => $rword) { - if (Str::contains($message, $word)) { - $message = str_replace($word, $rword, (string) $message); - } - } - - return $message; - } - - protected function htmlifyMessage($message) - { - return $message; + public function systemMessage(?string $message): void + { + MessageCreated::dispatch(Message::create([ + 'user_id' => User::SYSTEM_USER_ID, + 'chatroom_id' => Chatroom::query() + ->where(\is_int(config('chat.system_chatroom')) ? 'id' : 'name', '=', config('chat.system_chatroom')) + ->soleValue('id'), + 'message' => $message ?? '', + 'receiver_id' => null, + ])); } } diff --git a/database/migrations/2023_11_17_092722_remove_bot_transactions.php b/database/migrations/2023_11_17_092722_remove_bot_transactions.php new file mode 100644 index 0000000000..a57ed2a733 --- /dev/null +++ b/database/migrations/2023_11_17_092722_remove_bot_transactions.php @@ -0,0 +1,59 @@ +dropColumn('bot_id'); + $table->unsignedInteger('chatroom_id')->nullable()->change(); + }); + + DB::table('messages') + ->where('chatroom_id', '=', 0) + ->update(['chatroom_id' => null]); + + DB::table('messages') + ->whereNotIn('chatroom_id', DB::table('chatrooms')->select('id')) + ->delete(); + + // Users + + Schema::table('users', function (Blueprint $table): void { + $table->unsignedInteger('chatroom_id')->nullable()->change(); + }); + + DB::table('users') + ->whereNotIn('chatroom_id', DB::table('chatrooms')->select('id')) + ->update(['chatroom_id' => null]); + + Schema::table('users', function (Blueprint $table): void { + $table->unsignedInteger('chatroom_id')->nullable()->change(); + + $table->foreign('chatroom_id')->references('id')->on('chatrooms')->nullOnDelete(); + }); + + // Echoes + + DB::table('user_echoes')->whereNotNull('bot_id')->update(['target_id' => 1]); + + Schema::table('user_echoes', function (Blueprint $table): void { + $table->dropColumn('bot_id'); + }); + + // Audibles + + DB::table('user_audibles')->whereNotNull('bot_id')->update(['target_id' => 1]); + + Schema::table('user_audibles', function (Blueprint $table): void { + $table->dropColumn('bot_id'); + }); + } +}; diff --git a/database/seeders/BotsTableSeeder.php b/database/seeders/BotsTableSeeder.php index d0c21ce203..8c751af2c9 100644 --- a/database/seeders/BotsTableSeeder.php +++ b/database/seeders/BotsTableSeeder.php @@ -28,7 +28,7 @@ public function run(): void [ 'name' => 'SystemBot', 'emoji' => '1f916', - 'command' => 'systembot', + 'command' => '!systembot', 'position' => 1, 'color' => '#f1c40f', 'icon' => 'fab fa-android', @@ -50,7 +50,7 @@ public function run(): void [ 'name' => 'NerdBot', 'emoji' => '1f913', - 'command' => 'nerdbot', + 'command' => '!nerdbot', 'position' => 2, 'color' => '#f1c40f', 'icon' => 'fab fa-android', diff --git a/package.json b/package.json index 5801aa667b..851c19f374 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,7 @@ "socket.io": "2.4.0", "socket.io-client": "2.3.1", "sweetalert2": "^11.4.6", - "virtual-select-plugin": "^1.0.26", - "vue": "^2.6.14", - "vue-loader": "^15.9.8", - "vue-template-compiler": "^2.6.14" + "virtual-select-plugin": "^1.0.26" }, "devDependencies": { "@vitejs/plugin-vue2": "^2.3.1", diff --git a/resources/js/components/Trans.vue b/resources/js/components/Trans.vue deleted file mode 100644 index e51179633d..0000000000 --- a/resources/js/components/Trans.vue +++ /dev/null @@ -1,32 +0,0 @@ - - diff --git a/resources/js/components/Version.vue b/resources/js/components/Version.vue deleted file mode 100644 index 6fecb02ba5..0000000000 --- a/resources/js/components/Version.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/resources/js/components/chat/ChatForm.vue b/resources/js/components/chat/ChatForm.vue deleted file mode 100644 index 2ce50a8587..0000000000 --- a/resources/js/components/chat/ChatForm.vue +++ /dev/null @@ -1,71 +0,0 @@ - - \ No newline at end of file diff --git a/resources/js/components/chat/ChatMessages.vue b/resources/js/components/chat/ChatMessages.vue deleted file mode 100644 index ad2eb2b8f1..0000000000 --- a/resources/js/components/chat/ChatMessages.vue +++ /dev/null @@ -1,152 +0,0 @@ - - \ No newline at end of file diff --git a/resources/js/components/chat/ChatUserList.vue b/resources/js/components/chat/ChatUserList.vue deleted file mode 100644 index 7ece0e06cf..0000000000 --- a/resources/js/components/chat/ChatUserList.vue +++ /dev/null @@ -1,58 +0,0 @@ - - \ No newline at end of file diff --git a/resources/js/components/chat/Chatbox.vue b/resources/js/components/chat/Chatbox.vue deleted file mode 100644 index 6008c6ac03..0000000000 --- a/resources/js/components/chat/Chatbox.vue +++ /dev/null @@ -1,1068 +0,0 @@ - - \ No newline at end of file diff --git a/resources/js/components/chat/ChatroomsDropdown.vue b/resources/js/components/chat/ChatroomsDropdown.vue deleted file mode 100644 index 17216933e8..0000000000 --- a/resources/js/components/chat/ChatroomsDropdown.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/js/components/chat/ChatstatusesDropdown.vue b/resources/js/components/chat/ChatstatusesDropdown.vue deleted file mode 100644 index 528e985362..0000000000 --- a/resources/js/components/chat/ChatstatusesDropdown.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/resources/js/unit3d/chat.js b/resources/js/unit3d/chat.js index 0ec81ec885..70ff383cf0 100644 --- a/resources/js/unit3d/chat.js +++ b/resources/js/unit3d/chat.js @@ -4,8 +4,6 @@ * allows your team to easily build robust real-time web applications. */ import Echo from 'laravel-echo'; -import Vue from 'vue'; -import chatbox from '../components/chat/Chatbox.vue'; import client from 'socket.io-client'; @@ -20,8 +18,3 @@ window.Echo = new Echo({ enabledTransports: ['wss'], }); -new Vue({ - el: '#vue', - components: { chatbox: chatbox }, -}); - diff --git a/resources/views/blocks/chat.blade.php b/resources/views/blocks/chat.blade.php index b67b17df8e..835f9778ac 100644 --- a/resources/views/blocks/chat.blade.php +++ b/resources/views/blocks/chat.blade.php @@ -1,3 +1 @@ - + diff --git a/resources/views/components/user_tag.blade.php b/resources/views/components/user_tag.blade.php index a3f5979724..85d8eb8c1e 100644 --- a/resources/views/components/user_tag.blade.php +++ b/resources/views/components/user_tag.blade.php @@ -6,19 +6,19 @@ ]) @if ($anon) - @if (auth()->user()->is($user) || auth()->user()->group->is_modo) + @if (auth()->id() === $user['id'] || auth()->user()->group->is_modo) class('user-tag fas fa-eye-slash') }} - {{ $attributes->merge(['style' => 'background-image: ' . $user->group->effect . ';' . ($style ?? '')]) }} + {{ $attributes->merge(['style' => 'background-image: ' . $user['group']['effect'] . ';' . ($style ?? '')]) }} > ( - {{ $user->username }} + {{ $user['username'] }} {{ $appendedIcons ?? '' }} ) @@ -31,15 +31,15 @@ class="user-tag__link user-tag__link--anonymous {{ $user->group->icon }}" @else class('user-tag') }} - {{ $attributes->merge(['style' => 'background-image: ' . $user->group->effect . ';' . ($style ?? '')]) }} + {{ $attributes->merge(['style' => 'background-image: ' . $user['group']['effect'] . ';' . ($style ?? '')]) }} > - {{ $user->username }} + {{ $user['username'] }} {{ $appendedIcons ?? '' }} diff --git a/resources/views/home/index.blade.php b/resources/views/home/index.blade.php index 9c774ffad5..24edab161f 100644 --- a/resources/views/home/index.blade.php +++ b/resources/views/home/index.blade.php @@ -4,11 +4,10 @@ @section('main') @include('blocks.news') + @if (! auth()->user()->settings?->chat_hidden) -
- @include('blocks.chat') -
@vite('resources/js/unit3d/chat.js') + @endif @include('blocks.featured') diff --git a/resources/views/livewire/chatbox.blade.php b/resources/views/livewire/chatbox.blade.php new file mode 100644 index 0000000000..9d31a5c309 --- /dev/null +++ b/resources/views/livewire/chatbox.blade.php @@ -0,0 +1,408 @@ +
+
+

Chatbox

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + + +
+
+
    + + + +
+
+ +
+

+ + +

+
+
+ +
diff --git a/routes/channels.php b/routes/channels.php index 18df29f3dd..5fd1f5eaf8 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -15,6 +15,7 @@ */ use App\Models\User; +use App\Models\UserEcho; use Illuminate\Support\Facades\Broadcast; /* @@ -28,16 +29,49 @@ * | */ -Broadcast::channel('chatroom.{id}', function ($user, $id) { - return User::select([ - 'id', - 'username', - 'group_id', - 'image', - 'chatroom_id', - 'chat_status_id' - ]) - ->with(['chatStatus:id,color', 'chatroom:id,name', 'group:id,color,effect,icon']) - ->find($user->id); +Broadcast::channel('messages.room.{chatroomId}', function (User $user, int $chatroomId) { + if ( + UserEcho::query() + ->where('user_id', '=', $user->id) + ->where('room_id', '=', $chatroomId) + ->exists() + ) { + return User::select([ + 'id', + 'username', + 'group_id', + 'image', + 'chatroom_id', + 'chat_status_id' + ]) + ->find($user->id); + } + + return false; }); -Broadcast::channel('chatter.{id}', fn ($user, $id) => $user->id == $id); +Broadcast::channel('messages.pm.{user1Id}-{user2Id}', function (User $user, int $user1Id, int $user2Id) { + if (!($user->id === $user1Id || $user->id === $user2Id)) { + return false; + } + + if ( + UserEcho::query() + ->where(fn ($query) => $query->where('user_id', '=', $user1Id)->where('target_id', '=', $user2Id)) + ->orWhere(fn ($query) => $query->where('user_id', '=', $user2Id)->where('target_id', '=', $user1Id)) + ->exists() + ) { + return User::select([ + 'id', + 'username', + 'group_id', + 'image', + 'chatroom_id', + 'chat_status_id' + ]) + ->find($user->id); + } + + return false; +}); + +Broadcast::channel('echo.created.{userId}', fn (User $user, int $userId) => $user->id === $userId); diff --git a/routes/vue.php b/routes/vue.php deleted file mode 100644 index ecefa9c44f..0000000000 --- a/routes/vue.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 - */ - -/* -|-------------------------------------------------------------------------- -| API Routes For Vue Components -|-------------------------------------------------------------------------- -*/ - -Route::middleware(['auth', 'banned'])->group(function (): void { - Route::prefix('chat')->group(function (): void { - Route::get('/config', [App\Http\Controllers\API\ChatController::class, 'config']); - - /* Statuses */ - Route::get('/statuses', [App\Http\Controllers\API\ChatController::class, 'statuses']); - - /* Rooms */ - Route::get('/rooms', [App\Http\Controllers\API\ChatController::class, 'rooms']); - - /* Bots */ - Route::get('/bots', [App\Http\Controllers\API\ChatController::class, 'bots']); - - /* Audibles */ - Route::get('/audibles', [App\Http\Controllers\API\ChatController::class, 'audibles']); - Route::post('/audibles/toggle/chatroom', [App\Http\Controllers\API\ChatController::class, 'toggleRoomAudible']); - Route::post('/audibles/toggle/target', [App\Http\Controllers\API\ChatController::class, 'toggleTargetAudible']); - Route::post('/audibles/toggle/bot', [App\Http\Controllers\API\ChatController::class, 'toggleBotAudible']); - - /* Echoes */ - Route::get('/echoes', [App\Http\Controllers\API\ChatController::class, 'echoes']); - Route::post('/echoes/delete/chatroom', [App\Http\Controllers\API\ChatController::class, 'deleteRoomEcho']); - Route::post('/echoes/delete/target', [App\Http\Controllers\API\ChatController::class, 'deleteTargetEcho']); - Route::post('/echoes/delete/bot', [App\Http\Controllers\API\ChatController::class, 'deleteBotEcho']); - - /* Messages */ - Route::post('/messages', [App\Http\Controllers\API\ChatController::class, 'createMessage']); - Route::post('/message/{id}/delete', [App\Http\Controllers\API\ChatController::class, 'deleteMessage']); - Route::get('/messages/{room_id}', [App\Http\Controllers\API\ChatController::class, 'messages']); - - /* Private Stuff */ - Route::get('/private/messages/{target_id}', [App\Http\Controllers\API\ChatController::class, 'privateMessages']); - - /* Bot Stuff */ - Route::get('/bot/{bot_id}', [App\Http\Controllers\API\ChatController::class, 'botMessages']); - - /* Users */ - Route::post('/user/target', [App\Http\Controllers\API\ChatController::class, 'updateUserTarget']); - Route::post('/user/chatroom', [App\Http\Controllers\API\ChatController::class, 'updateUserRoom']); - Route::post('/user/status', [App\Http\Controllers\API\ChatController::class, 'updateUserChatStatus']); - }); -}); diff --git a/tests/Feature/Http/Controllers/HomeControllerTest.php b/tests/Feature/Http/Controllers/HomeControllerTest.php index 2ec594053e..02f340c2d6 100644 --- a/tests/Feature/Http/Controllers/HomeControllerTest.php +++ b/tests/Feature/Http/Controllers/HomeControllerTest.php @@ -15,8 +15,13 @@ */ use App\Models\User; +use Database\Seeders\ChatroomTableSeeder; +use Database\Seeders\UsersTableSeeder; test('index returns an ok response', function (): void { + $this->seed(UsersTableSeeder::class); + $this->seed(ChatroomTableSeeder::class); + $user = User::factory()->create(); $response = $this->actingAs($user)->get(route('home.index')); diff --git a/tests/Unit/Http/Requests/Staff/UpdateChatBotRequestTest.php b/tests/Unit/Http/Requests/Staff/UpdateChatBotRequestTest.php index ce0976afc5..8c8ff475d3 100644 --- a/tests/Unit/Http/Requests/Staff/UpdateChatBotRequestTest.php +++ b/tests/Unit/Http/Requests/Staff/UpdateChatBotRequestTest.php @@ -37,7 +37,6 @@ ], 'command' => [ 'required', - 'alpha_dash', 'min:1', 'max:255', ],