diff --git a/ServiceAPI/Chats.php b/ServiceAPI/Chats.php new file mode 100644 index 000000000..0d3b6c61a --- /dev/null +++ b/ServiceAPI/Chats.php @@ -0,0 +1,60 @@ +user = $user; + $this->chats = new Chats(); + } + + public function getChat(int $id, callable $resolve, callable $reject): void + { + $chat = $this->chats->get($id); + if (!$chat) { + $reject(53, "No chat with id=$id"); + } + + $userIds = array_map(fn($u) => $u->getId(), $chat->getUsers()); + if (!in_array($this->user->getId(), $userIds)) { + $reject(12, "Access denied"); + } + + $res = (object) []; + $res->id = $chat->getId(); + $res->type = $chat->getRecord()->type; + $res->title = $chat->getRecord()->title; + $res->admin_id = $chat->getRecord()->admin_id; + $res->users = $userIds; + $res->push_settings = $chat->getPushSettings(); + $res->photo_50 = $chat->getRecord()->photo_50; + $res->photo_100 = $chat->getRecord()->photo_100; + $res->photo_200 = $chat->getRecord()->photo_200; + $res->left = $chat->isLeft(); + $res->kicked = $chat->isKicked(); + + $resolve($res); + } + + public function createChat(string $title, array $userIds, callable $resolve, callable $reject): void + { + if (!$this->user) { + $reject(5, "Authorization required"); + } + + $users = array_merge([$this->user->getId()], $userIds); + $chat = $this->chats->create('chat', $title, $this->user->getId(), $users); + + $resolve((object)['chat_id' => $chat->getId()]); + } +} \ No newline at end of file diff --git a/VKAPI/Handlers/Messages.php b/VKAPI/Handlers/Messages.php index fd6745b0e..df6e384df 100644 --- a/VKAPI/Handlers/Messages.php +++ b/VKAPI/Handlers/Messages.php @@ -5,7 +5,7 @@ namespace openvk\VKAPI\Handlers; use openvk\Web\Util\IMBroker; -use openvk\Web\Models\Repositories\{Users as USRRepo, Clubs as ClubRepo}; +use openvk\Web\Models\Repositories\{Users as USRRepo, Clubs as ClubRepo, Chats}; use openvk\Web\Models\Entities\{Club as ClubEnt}; use openvk\VKAPI\Handlers\{Users as APIUsers, Groups as APIClubs}; @@ -813,6 +813,89 @@ public function getMe(int $group_id = 0) return $this->invoke("im.getMe", [], $group_id); } + // May be broken lmao + public function getConversationsById(string $peer_ids, int $extended = 0, string $fields = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $peer_ids = explode(',', $peer_ids); + $result = []; + + foreach ($peer_ids as $peer_id) { + $peer_id = (int) $peer_id; + + if ($peer_id < 0) { + $club = (new ClubRepo())->get(abs($peer_id)); + if (!$club || $club->isBanned()) { + continue; + } + + if (!$club->canBeViewedBy($this->getUser())) { + continue; + } + } else { + $user = (new USRRepo())->get($peer_id); + if (!$user || $user->isDeleted() || $user->isBanned()) { + continue; + } + + if (!$user->canBeViewedBy($this->getUser())) { + continue; + } + } + + $result[] = $this->getConversationById($peer_id, $extended, $fields); + } + + return $result; + } + + // Chat methods + public function getChat(int $chat_id): object + { + $this->requireUser(); + + $chats = new Chats(); + $chat = $chats->get($chat_id); + if (!$chat) { + $this->fail(100, "One of the parameters specified was missing or invalid: chat not found"); + } + + $userIds = array_map(fn($u) => $u->getId(), $chat->getUsers()); + if (!in_array($this->getUser()->getId(), $userIds)) { + $this->fail(15, "Access denied"); + } + + return (object) [ + 'id' => $chat->getId(), + 'type' => $chat->getRecord()->type, + 'title' => $chat->getRecord()->title, + 'admin_id' => $chat->getRecord()->admin_id, + 'users' => $userIds, + 'push_settings' => $chat->getPushSettings(), + 'photo_50' => $chat->getRecord()->photo_50, + 'photo_100' => $chat->getRecord()->photo_100, + 'photo_200' => $chat->getRecord()->photo_200, + 'left' => $chat->isLeft(), + 'kicked' => $chat->isKicked(), + ]; + } + + public function createChat(string $title, string $user_ids): object + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $userIds = array_map('intval', explode(',', $user_ids)); + $users = array_merge([$this->getUser()->getId()], $userIds); + + $chats = new Chats(); + $chat = $chats->create('chat', $title, $this->getUser()->getId(), $users); + + return (object) ['chat_id' => $chat->getId()]; + } + /* public function delete(string $message_ids, int $spam = 0, int $delete_for_all = 0): object { diff --git a/Web/Models/Entities/Chat.php b/Web/Models/Entities/Chat.php new file mode 100644 index 000000000..21678a47d --- /dev/null +++ b/Web/Models/Entities/Chat.php @@ -0,0 +1,107 @@ +get($this->getRecord()->admin_id); + } + + /** + * Get the list of users in the chat. + * + * @return array + */ + public function getUsers(): array + { + $userIds = json_decode($this->getRecord()->users, true) ?? []; + $users = []; + $usersRepo = new Users(); + foreach ($userIds as $id) { + $user = $usersRepo->get($id); + if ($user) { + $users[] = $user; + } + } + return $users; + } + + /** + * Get push settings. + * + * @return object + */ + public function getPushSettings(): object + { + return json_decode($this->getRecord()->push_settings) ?? (object)['sound' => 1, 'disabled_until' => 0]; + } + + /** + * Check if the chat is left by the user. + */ + public function isLeft(): bool + { + return (bool) $this->getRecord()->left; + } + + /** + * Check if the user is kicked from the chat. + */ + public function isKicked(): bool + { + return (bool) $this->getRecord()->kicked; + } + + /** + * Add a user to the chat. + */ + public function addUser(int $userId): bool + { + $users = json_decode($this->getRecord()->users, true) ?? []; + if (!in_array($userId, $users)) { + $users[] = $userId; + $this->stateChanges("users", json_encode($users)); + return true; + } + return false; + } + + /** + * Remove a user from the chat. + */ + public function removeUser(int $userId): bool + { + $users = json_decode($this->getRecord()->users, true) ?? []; + $key = array_search($userId, $users); + if ($key !== false) { + unset($users[$key]); + $this->stateChanges("users", json_encode(array_values($users))); + return true; + } + return false; + } + + /** + * Update push settings. + */ + public function updatePushSettings(int $sound, int $disabledUntil): void + { + $settings = json_encode(['sound' => $sound, 'disabled_until' => $disabledUntil]); + $this->stateChanges("push_settings", $settings); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/Chats.php b/Web/Models/Repositories/Chats.php new file mode 100644 index 000000000..9985037a7 --- /dev/null +++ b/Web/Models/Repositories/Chats.php @@ -0,0 +1,75 @@ +context = DatabaseConnection::i()->getContext(); + $this->chats = $this->context->table("chats"); + } + + public function get(int $id): ?Chat + { + $chat = $this->chats->get($id); + if (!$chat) { + return null; + } + + return new Chat($chat); + } + + public function getByUser(int $userId, int $page = 1, ?int $perPage = null): \Traversable + { + $limit = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $offset = ($page - 1) * $limit; + $chats = $this->chats->where("JSON_CONTAINS(users, ?)", json_encode($userId)) + ->order("id DESC") + ->limit($limit, $offset); + + foreach ($chats as $chat) { + yield new Chat($chat); + } + } + + public function create(string $type, string $title, int $adminId, array $users): Chat + { + $data = [ + 'type' => $type, + 'title' => $title, + 'admin_id' => $adminId, + 'users' => json_encode($users), + 'push_settings' => json_encode(['sound' => 1, 'disabled_until' => 0]), + 'photo_50' => null, + 'photo_100' => null, + 'photo_200' => null, + 'left' => 0, + 'kicked' => 0, + ]; + + $chat = $this->chats->insert($data); + return new Chat($chat); + } + + public function findByUsers(array $userIds): ?Chat + { + // For simplicity, assume chats are unique by users, but in reality, might need better logic + $chats = $this->chats->where("JSON_CONTAINS(users, ?)", json_encode($userIds[0])); + foreach ($chats as $chat) { + $chatUsers = json_decode($chat->users, true); + if (count($chatUsers) == count($userIds) && !array_diff($chatUsers, $userIds)) { + return new Chat($chat); + } + } + return null; + } +} \ No newline at end of file diff --git a/Web/Presenters/MessengerPresenter.php b/Web/Presenters/MessengerPresenter.php index 0c981c7c3..51755547b 100644 --- a/Web/Presenters/MessengerPresenter.php +++ b/Web/Presenters/MessengerPresenter.php @@ -6,18 +6,20 @@ use Chandler\Signaling\SignalManager; use openvk\Web\Events\NewMessageEvent; -use openvk\Web\Models\Repositories\{Users, Clubs, Messages}; -use openvk\Web\Models\Entities\{Message, Correspondence}; +use openvk\Web\Models\Repositories\{Users, Clubs, Messages, Chats}; +use openvk\Web\Models\Entities\{Message, Correspondence, Chat}; final class MessengerPresenter extends OpenVKPresenter { private $messages; + private $chats; private $signaler; protected $presenterName = "messenger"; public function __construct(Messages $messages) { $this->messages = $messages; + $this->chats = new Chats(); $this->signaler = SignalManager::i(); parent::__construct(); @@ -95,4 +97,42 @@ public function renderVKEvents(int $id): void ])); }, $id, $time); } + + public function renderChat(int $id): void + { + $this->assertUserLoggedIn(); + + $chat = $this->chats->get($id); + if (!$chat) { + $this->notFound(); + } + + // Check if user is in chat + $userIds = array_map(fn($u) => $u->getId(), $chat->getUsers()); + if (!in_array($this->user->id, $userIds)) { + $this->notFound(); + } + + $this->template->chat = $chat; + } + + public function renderCreateChat(): void + { + $this->assertUserLoggedIn(); + + if ($this->request->isMethod('POST')) { + $title = $this->postParam('title'); + $userIds = $this->postParam('users'); // array of user ids + + if (!$title || empty($userIds)) { + $this->flashFail("err", tr("error"), tr("chat_create_error")); + return; + } + + $users = array_merge([$this->user->id], $userIds); + $chat = $this->chats->create('chat', $title, $this->user->id, $users); + + $this->redirect("/messenger/chat/" . $chat->getId()); + } + } } diff --git a/install/sqls/00061-chats.sql b/install/sqls/00061-chats.sql new file mode 100644 index 000000000..f9bab8f45 --- /dev/null +++ b/install/sqls/00061-chats.sql @@ -0,0 +1,18 @@ +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +CREATE TABLE IF NOT EXISTS `chats` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `type` varchar(32) NOT NULL DEFAULT 'chat', + `title` varchar(255) NOT NULL, + `admin_id` bigint(20) unsigned NOT NULL, + `users` json NOT NULL, + `push_settings` json NOT NULL, + `photo_50` varchar(255) DEFAULT NULL, + `photo_100` varchar(255) DEFAULT NULL, + `photo_200` varchar(255) DEFAULT NULL, + `left` tinyint(1) NOT NULL DEFAULT '0', + `kicked` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `admin_id` (`admin_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/locales/en.strings b/locales/en.strings index c39b92319..584f34c2c 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -1051,6 +1051,14 @@ "is_x_audio_many" = "Just $1 audios."; "is_x_audio_other" = "Just $1 audios."; +/* Chats */ + +"chat" = "Chat"; +"create_chat" = "Create chat"; +"chat_title" = "Chat title"; +"add_users" = "Add users"; +"chat_create_error" = "Error creating chat"; + /* Notifications */ "feedback" = "Feedback";