diff --git a/.github/workflows/codeberg-mirror.yml b/.github/workflows/codeberg-mirror.yml new file mode 100644 index 00000000..7d4049dc --- /dev/null +++ b/.github/workflows/codeberg-mirror.yml @@ -0,0 +1,15 @@ +name: Codeberg Mirroring + +on: push + +jobs: + to_codeberg: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: pixta-dev/repository-mirroring-action@v1 + with: + target_repo_url: "git@codeberg.org:openvk/openvk.git" + ssh_private_key: ${{ secrets.CODEBERG_MIRRORSSH }} diff --git a/.gitignore b/.gitignore index 41e799c7..78f62184 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ themepacks/* !themepacks/midnight storage/* !storage/.gitkeep + +.idea \ No newline at end of file diff --git a/README.md b/README.md index ccaed853..1937c38a 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,30 @@ _[Русский](README_RU.md)_ -**OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VK. Code provided here is not stable yet. +**OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VKontakte. Code provided here is not stable yet. VKontakte belongs to Pavel Durov and VK Group. -To be honest, we don't know whether it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug-tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OVK account for this). +To be honest, we don't know whether if it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OpenVK account for this). ## When's the release? -We will release OpenVK as soon as it's ready. As for now you can: +We will release OpenVK as soon as it's ready. As for now, you can: * `git clone` this repo's master branch (use `git pull` to update) * Grab a prebuilt OpenVK distro from [GitHub artifacts](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip) ## Instances * **[openvk.su](https://openvk.su/)** -* **[openvk.uk](https://openvk.uk)** - official mirror of openvk.su () -* **[openvk.co](http://openvk.co)** - yet another official mirror of openvk.su without TLS () + * **[openvk.uk](https://openvk.uk)** ([mirror](https://t.me/openvk/1609)) + * **[openvk.co](http://openvk.co)** (mirror [without TLS](https://t.me/openvk/1654)) * [social.fetbuk.ru](http://social.fetbuk.ru/) * [vepurovk.xyz](http://vepurovk.xyz/) + * [vepurovk.fun](http://vepurovk.fun/) (mirror without TLS) ## Can I create my own OpenVK instance? -Yes! And you're very welcome to. +Yes! And you are very welcome to. However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. these extensions are available on most of ISPManager hostings). @@ -34,12 +35,12 @@ If you want, you can add your instance to the list above so that people can regi 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) -* PHP 8.1 is supported, but it was not tested carefully, be aware of that. +* PHP 8.1 is supported too, however it was not tested carefully, so be aware. 2. Install MySQL-compatible database. -* We recommend using Percona Server, but any MySQL-compatible server should work -* Server should be compatible with at least MySQL 5.6, MySQL 8.0+ recommended. +* We recommend using Percona Server, but any MySQL-compatible server should work too. +* Server should be compatible with at least MySQL 5.6, MySQL 8.0+ is recommended. * Support for MySQL 4.1+ is WIP, replace `utf8mb4` and `utf8mb4_unicode_520_ci` with `utf8` and `utf8_unicode_ci` in SQLs. 3. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this: @@ -77,20 +78,20 @@ See `install/automated/docker/README.md` and `install/automated/kubernetes/READM ### If my website uses OpenVK, should I release it's sources? -It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you're planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc). +It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you are planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc). ## Where can I get assistance? You may reach out to us via: -* [Bug-tracker](https://github.com/openvk/openvk/projects/1) -* [Ticketing system](https://openvk.su/support?act=new) -* Telegram chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu. +* [Bug Tracker](https://github.com/openvk/openvk/projects/1) +* [Ticketing System](https://openvk.su/support?act=new) +* Telegram Chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu. * [Reddit](https://www.reddit.com/r/openvk/) -* [Discussions](https://github.com/openvk/openvk/discussions) -* Matrix chat: #openvk:matrix.org +* [GitHub Discussions](https://github.com/openvk/openvk/discussions) +* Matrix Chat: #openvk:matrix.org -**Attention**: bug tracker, board, telegram and matrix chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), please use contact us directly at this email: **openvk [at] tutanota [dot] com** +**Attention**: bug tracker, board, Telegram and Matrix chat are public places, ticketing system is being served by volunteers. If you need to report something that should not be immediately disclosed to general public (for instance, a vulnerability), please contact us directly via this email: **openvk [at] tutanota [dot] com** Get it on Codeberg diff --git a/README_RU.md b/README_RU.md index c30a6cf7..6b03dda3 100644 --- a/README_RU.md +++ b/README_RU.md @@ -2,11 +2,11 @@ _[English](README.md)_ -**OpenVK** - это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент представленный здесь исходный код проекта пока не является стабильным. +**OpenVK** — это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент, представленный здесь исходный код проекта пока не является стабильным. ВКонтакте принадлежит Павлу Дурову и VK Group. -Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OVK). +Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OpenVK). ## Когда выйдет релизная версия? @@ -17,18 +17,19 @@ _[English](README.md)_ ## Инстанции * **[openvk.su](https://openvk.su/)** -* **[openvk.uk](https://openvk.uk)** - официальное зеркало openvk.su () -* **[openvk.co](http://openvk.co)** - ещё одно официальное зеркало openvk.su без TLS () + * **[openvk.uk](https://openvk.uk)** ([зеркало]()) + * **[openvk.co](http://openvk.co)** (зеркало [без TLS]()) * [social.fetbuk.ru](http://social.fetbuk.ru/) * [vepurovk.xyz](http://vepurovk.xyz/) + * **[vepurovk.fun](http://vepurovk.fun)** (зеркало без TLS) ## Могу ли я создать свою собственную инстанцию OpenVK? Да! И всегда пожалуйста. -Однако, OVK использует Chandler Application Server. Это программное обеспечение требует расширений, которые могут быть не предоставлены вашим хостинг-провайдером (а именно, sodium и yaml. эти расширения доступны на большинстве хостингов ISPManager). +Однако, OpenVK использует Chandler Application Server. Это программное обеспечение требует расширений, которые могут быть не предоставлены вашим хостинг-провайдером (а именно, sodium и yaml. Эти расширения доступны на большинстве хостингов ISPManager). -Если вы хотите, вы можете добавить вашу инстанцию в список выше, чтобы люди могли зарегистрироваться там. +Если хотите, вы можете добавить вашу инстанцию в список выше, чтобы люди могли зарегистрироваться там. ### Процедура установки @@ -38,7 +39,7 @@ _[English](README.md)_ 2. Установите MySQL-совместимую базу данных. -* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать +* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать. * Сервер должен поддерживать хотя бы MySQL 5.6, рекомендуется использовать MySQL 8.0+. * Поддержка для MySQL 4.1+ находится в процессе, а пока замените `utf8mb4` и `utf8mb4_unicode_520_ci` на `utf8` и `utf8_unicode_ci` в SQL-файлах, соответственно. @@ -87,10 +88,10 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions * [Помощь в OVK](https://openvk.su/support?act=new) * Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала. * [Reddit](https://www.reddit.com/r/openvk/) -* [Обсуждения](https://github.com/openvk/openvk/discussions) +* [GitHub Discussions](https://github.com/openvk/openvk/discussions) * Чат в Matrix: #ovk:matrix.org -**Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**. +**Внимание**: баг-трекер, форум, Telegram- и Matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собачка] tutanota [точка] com**. Get it on Codeberg diff --git a/VKAPI/Handlers/Account.php b/VKAPI/Handlers/Account.php index 2b9f88a3..42bfb38e 100644 --- a/VKAPI/Handlers/Account.php +++ b/VKAPI/Handlers/Account.php @@ -46,6 +46,7 @@ final class Account extends VKAPIRequestHandler $this->requireUser(); $this->getUser()->setOnline(time()); + $this->getUser()->setClient_name($this->getPlatform()); $this->getUser()->save(); return 1; @@ -81,6 +82,8 @@ final class Account extends VKAPIRequestHandler function saveProfileInfo(string $first_name = "", string $last_name = "", string $screen_name = "", int $sex = -1, int $relation = -1, string $bdate = "", int $bdate_visibility = -1, string $home_town = "", string $status = ""): object { $this->requireUser(); + $this->willExecuteWriteAction(); + $user = $this->getUser(); $output = [ diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php index 5040674c..f7873520 100644 --- a/VKAPI/Handlers/Friends.php +++ b/VKAPI/Handlers/Friends.php @@ -66,6 +66,7 @@ final class Friends extends VKAPIRequestHandler function add(string $user_id): int { $this->requireUser(); + $this->willExecuteWriteAction(); $users = new UsersRepo; $user = $users->get(intval($user_id)); @@ -96,6 +97,7 @@ final class Friends extends VKAPIRequestHandler function delete(string $user_id): int { $this->requireUser(); + $this->willExecuteWriteAction(); $users = new UsersRepo; @@ -152,10 +154,7 @@ final class Friends extends VKAPIRequestHandler $response = $followers; $usersApi = new Users($this->getUser()); - if($extended == 1) - $response = $usersApi->get(implode(',', $followers), $fields, 0, $count); - else - $response = $usersApi->get(implode(',', $followers), "", 0, $count); + $response = $usersApi->get(implode(',', $followers), $fields, 0, $count); foreach($response as $user) $user->user_id = $user->id; diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 42a2d265..071ded81 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -185,7 +185,7 @@ final class Groups extends VKAPIRequestHandler $response[$i]->site = $clb->getWebsite(); break; case "description": - $response[$i]->desctiption = $clb->getDescription(); + $response[$i]->description = $clb->getDescription(); break; case "contacts": $contacts; @@ -237,6 +237,7 @@ final class Groups extends VKAPIRequestHandler function join(int $group_id) { $this->requireUser(); + $this->willExecuteWriteAction(); $club = (new ClubsRepo)->get($group_id); @@ -251,6 +252,7 @@ final class Groups extends VKAPIRequestHandler function leave(int $group_id) { $this->requireUser(); + $this->willExecuteWriteAction(); $club = (new ClubsRepo)->get($group_id); diff --git a/VKAPI/Handlers/Likes.php b/VKAPI/Handlers/Likes.php index 5caa2171..9501b433 100644 --- a/VKAPI/Handlers/Likes.php +++ b/VKAPI/Handlers/Likes.php @@ -8,6 +8,7 @@ final class Likes extends VKAPIRequestHandler function add(string $type, int $owner_id, int $item_id): object { $this->requireUser(); + $this->willExecuteWriteAction(); switch($type) { case "post": @@ -28,6 +29,7 @@ final class Likes extends VKAPIRequestHandler function delete(string $type, int $owner_id, int $item_id): object { $this->requireUser(); + $this->willExecuteWriteAction(); switch($type) { case "post": diff --git a/VKAPI/Handlers/Messages.php b/VKAPI/Handlers/Messages.php index af7dcdcd..ead8e273 100644 --- a/VKAPI/Handlers/Messages.php +++ b/VKAPI/Handlers/Messages.php @@ -68,6 +68,7 @@ final class Messages extends VKAPIRequestHandler function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1) { $this->requireUser(); + $this->willExecuteWriteAction(); if($chat_id !== -1) $this->fail(946, "Chats are not implemented"); @@ -117,6 +118,7 @@ final class Messages extends VKAPIRequestHandler function delete(string $message_ids, int $spam = 0, int $delete_for_all = 0): object { $this->requireUser(); + $this->willExecuteWriteAction(); $msgs = new MSGRepo; $ids = preg_split("%, ?%", $message_ids); @@ -136,6 +138,7 @@ final class Messages extends VKAPIRequestHandler function restore(int $message_id): int { $this->requireUser(); + $this->willExecuteWriteAction(); $msg = (new MSGRepo)->get($message_id); if(!$msg) diff --git a/VKAPI/Handlers/Polls.php b/VKAPI/Handlers/Polls.php index c3b7da33..be947a44 100755 --- a/VKAPI/Handlers/Polls.php +++ b/VKAPI/Handlers/Polls.php @@ -66,6 +66,7 @@ final class Polls extends VKAPIRequestHandler function addVote(int $poll_id, string $answers_ids) { $this->requireUser(); + $this->willExecuteWriteAction(); $poll = (new PollsRepo)->get($poll_id); @@ -87,6 +88,7 @@ final class Polls extends VKAPIRequestHandler function deleteVote(int $poll_id) { $this->requireUser(); + $this->willExecuteWriteAction(); $poll = (new PollsRepo)->get($poll_id); diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index 6185a250..acb29469 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -108,11 +108,31 @@ final class Users extends VKAPIRequestHandler } break; case "last_seen": - if ($usr->onlineStatus() == 0) + if ($usr->onlineStatus() == 0) { + $platform = $usr->getOnlinePlatform(true); + switch ($platform) { + case 'iphone': + $platform = 2; + break; + + case 'android': + $platform = 4; + break; + + case NULL: + $platform = 7; + break; + + default: + $platform = 1; + break; + } + $response[$i]->last_seen = (object) [ - "platform" => 1, + "platform" => $platform, "time" => $usr->getOnline()->timestamp() ]; + } case "music": $response[$i]->music = $usr->getFavoriteMusic(); break; diff --git a/VKAPI/Handlers/VKAPIRequestHandler.php b/VKAPI/Handlers/VKAPIRequestHandler.php index 79cfb41b..d2fcfc74 100644 --- a/VKAPI/Handlers/VKAPIRequestHandler.php +++ b/VKAPI/Handlers/VKAPIRequestHandler.php @@ -1,15 +1,19 @@ user = $user; + $this->user = $user; + $this->platform = $platform; } protected function fail(int $code, string $message): void @@ -22,6 +26,11 @@ abstract class VKAPIRequestHandler return $this->user; } + protected function getPlatform(): ?string + { + return $this->platform; + } + protected function userAuthorized(): bool { return !is_null($this->getUser()); @@ -32,4 +41,19 @@ abstract class VKAPIRequestHandler if(!$this->userAuthorized()) $this->fail(5, "User authorization failed: no access_token passed."); } + + protected function willExecuteWriteAction(): void + { + $ip = (new IPs)->get(CONNECTING_IP); + $res = $ip->rateLimit(); + + if(!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) { + if($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) { + $this->user->ban("User account has been suspended for breaking API terms of service", false); + $this->fail(18, "User account has been suspended due to repeated violation of API rate limits."); + } + + $this->fail(29, "You have been rate limited."); + } + } } diff --git a/VKAPI/Handlers/Video.php b/VKAPI/Handlers/Video.php new file mode 100755 index 00000000..7198f766 --- /dev/null +++ b/VKAPI/Handlers/Video.php @@ -0,0 +1,37 @@ +requireUser(); + + $vids = explode(',', $videos); + + foreach($vids as $vid) + { + $id = explode("_", $vid); + + $items = []; + + $video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1])); + if($video) { + $items[] = $video->getApiStructure(); + } + } + + return (object) [ + "count" => count($items), + "items" => $items + ]; + } +} diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 47f48762..26b927ea 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -14,6 +14,8 @@ final class Wall extends VKAPIRequestHandler { function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 30, int $extended = 0): object { + $this->requireUser(); + $posts = new PostsRepo; $items = []; @@ -46,6 +48,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $this->getApiPhoto($attachment); } else if($attachment instanceof \openvk\Web\Models\Entities\Poll) { $attachments[] = $this->getApiPoll($attachment, $this->getUser()); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { + $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -64,6 +68,17 @@ final class Wall extends VKAPIRequestHandler else $profiles[] = $attachment->getOwner()->getId(); + $post_source = []; + + if($attachment->getPlatform(true) === NULL) { + $post_source = (object)["type" => "vk"]; + } else { + $post_source = (object)[ + "type" => "api", + "platform" => $attachment->getPlatform(true) + ]; + } + $repost[] = [ "id" => $attachment->getVirtualId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), @@ -72,13 +87,22 @@ final class Wall extends VKAPIRequestHandler "post_type" => "post", "text" => $attachment->getText(false), "attachments" => $repostAttachments, - "post_source" => [ - "type" => "vk" - ], + "post_source" => $post_source, ]; } } + $post_source = []; + + if($post->getPlatform(true) === NULL) { + $post_source = (object)["type" => "vk"]; + } else { + $post_source = (object)[ + "type" => "api", + "platform" => $post->getPlatform(true) + ]; + } + $items[] = (object)[ "id" => $post->getVirtualId(), "from_id" => $from_id, @@ -94,7 +118,7 @@ final class Wall extends VKAPIRequestHandler "is_archived" => false, "is_pinned" => $post->isPinned(), "attachments" => $attachments, - "post_source" => (object)["type" => "vk"], + "post_source" => $post_source, "comments" => (object)[ "count" => $post->getCommentsCount(), "can_post" => 1 @@ -194,6 +218,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $this->getApiPhoto($attachment); } else if($attachment instanceof \openvk\Web\Models\Entities\Poll) { $attachments[] = $this->getApiPoll($attachment, $user); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { + $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -212,6 +238,17 @@ final class Wall extends VKAPIRequestHandler else $profiles[] = $attachment->getOwner()->getId(); + $post_source = []; + + if($attachment->getPlatform(true) === NULL) { + $post_source = (object)["type" => "vk"]; + } else { + $post_source = (object)[ + "type" => "api", + "platform" => $attachment->getPlatform(true) + ]; + } + $repost[] = [ "id" => $attachment->getVirtualId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), @@ -220,13 +257,22 @@ final class Wall extends VKAPIRequestHandler "post_type" => "post", "text" => $attachment->getText(false), "attachments" => $repostAttachments, - "post_source" => [ - "type" => "vk" - ], + "post_source" => $post_source, ]; } } + $post_source = []; + + if($post->getPlatform(true) === NULL) { + $post_source = (object)["type" => "vk"]; + } else { + $post_source = (object)[ + "type" => "api", + "platform" => $post->getPlatform(true) + ]; + } + $items[] = (object)[ "id" => $post->getVirtualId(), "from_id" => $from_id, @@ -241,7 +287,7 @@ final class Wall extends VKAPIRequestHandler "can_archive" => false, # TODO MAYBE "is_archived" => false, "is_pinned" => $post->isPinned(), - "post_source" => (object)["type" => "vk"], + "post_source" => $post_source, "attachments" => $attachments, "comments" => (object)[ "count" => $post->getCommentsCount(), @@ -320,6 +366,7 @@ final class Wall extends VKAPIRequestHandler function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0): object { $this->requireUser(); + $this->willExecuteWriteAction(); $owner_id = intval($owner_id); @@ -384,6 +431,7 @@ final class Wall extends VKAPIRequestHandler $post->setCreated(time()); $post->setContent($message); $post->setFlags($flags); + $post->setApi_Source_Name($this->getPlatform()); $post->save(); } catch(\LogicException $ex) { $this->fail(100, "One of the parameters specified was missing or invalid"); @@ -403,6 +451,7 @@ final class Wall extends VKAPIRequestHandler function repost(string $object, string $message = "") { $this->requireUser(); + $this->willExecuteWriteAction(); $postArray; if(preg_match('/wall((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0) @@ -415,6 +464,7 @@ final class Wall extends VKAPIRequestHandler $nPost->setOwner($this->user->getId()); $nPost->setWall($this->user->getId()); $nPost->setContent($message); + $nPost->setApi_Source_Name($this->getPlatform()); $nPost->save(); $nPost->attach($post); @@ -445,6 +495,14 @@ final class Wall extends VKAPIRequestHandler $oid = $owner->getId(); if($owner instanceof Club) $oid *= -1; + + $attachments = []; + + foreach($comment->getChildren() as $attachment) { + if($attachment instanceof \openvk\Web\Models\Entities\Photo) { + $attachments[] = $this->getApiPhoto($attachment); + } + } $item = [ "id" => $comment->getId(), @@ -454,6 +512,7 @@ final class Wall extends VKAPIRequestHandler "post_id" => $post->getVirtualId(), "owner_id" => $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(), "parents_stack" => [], + "attachments" => $attachments, "thread" => [ "count" => 0, "items" => [], @@ -474,6 +533,9 @@ final class Wall extends VKAPIRequestHandler $items[] = $item; if($extended == true) $profiles[] = $comment->getOwner()->getId(); + + $attachments = null; + // Reset $attachments to not duplicate prikols } $response = [ @@ -500,6 +562,14 @@ final class Wall extends VKAPIRequestHandler $profiles = []; + $attachments = []; + + foreach($comment->getChildren() as $attachment) { + if($attachment instanceof \openvk\Web\Models\Entities\Photo) { + $attachments[] = $this->getApiPhoto($attachment); + } + } + $item = [ "id" => $comment->getId(), "from_id" => $comment->getOwner()->getId(), @@ -508,6 +578,7 @@ final class Wall extends VKAPIRequestHandler "post_id" => $comment->getTarget()->getVirtualId(), "owner_id" => $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(), "parents_stack" => [], + "attachments" => $attachments, "likes" => [ "can_like" => 1, "count" => $comment->getLikesCount(), @@ -538,11 +609,14 @@ final class Wall extends VKAPIRequestHandler $response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []); } + + return $response; } function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0) { $this->requireUser(); + $this->willExecuteWriteAction(); $post = (new PostsRepo)->getPostById($owner_id, $post_id); if(!$post || $post->isDeleted()) $this->fail(100, "One of the parameters specified was missing or invalid"); @@ -579,6 +653,7 @@ final class Wall extends VKAPIRequestHandler function deleteComment(int $comment_id) { $this->requireUser(); + $this->willExecuteWriteAction(); $comment = (new CommentsRepo)->get($comment_id); if(!$comment) $this->fail(100, "One of the parameters specified was missing or invalid");; @@ -598,7 +673,7 @@ final class Wall extends VKAPIRequestHandler "date" => $attachment->getPublicationTime()->timestamp(), "id" => $attachment->getVirtualId(), "owner_id" => $attachment->getOwner()->getId(), - "sizes" => array_values($attachment->getVkApiSizes()), + "sizes" => !is_null($attachment->getVkApiSizes()) ? array_values($attachment->getVkApiSizes()) : NULL, "text" => "", "has_tags" => false ] diff --git a/Web/Models/Entities/APIToken.php b/Web/Models/Entities/APIToken.php index 7257c6a8..f5744ec3 100644 --- a/Web/Models/Entities/APIToken.php +++ b/Web/Models/Entities/APIToken.php @@ -22,6 +22,11 @@ class APIToken extends RowModel { return $this->getId() . "-" . chunk_split($this->getSecret(), 8, "-") . "jill"; } + + function getPlatform(): ?string + { + return $this->getRecord()->platform; + } function isRevoked(): bool { diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index a5a3027b..db5baa88 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -360,5 +360,6 @@ class Club extends RowModel return $this->getRecord()->alert; } + use Traits\TBackDrops; use Traits\TSubscribable; } diff --git a/Web/Models/Entities/Notifications/CommentNotification.php b/Web/Models/Entities/Notifications/CommentNotification.php index 68af586b..95fd13cd 100644 --- a/Web/Models/Entities/Notifications/CommentNotification.php +++ b/Web/Models/Entities/Notifications/CommentNotification.php @@ -8,6 +8,6 @@ final class CommentNotification extends Notification function __construct(User $recipient, Comment $comment, $postable, User $commenter) { - parent::__construct($recipient, $postable, $commenter, time(), ovk_proc_strtr($comment->getText(), 10)); + parent::__construct($recipient, $postable, $commenter, time(), ovk_proc_strtr(strip_tags($comment->getText()), 400)); } } diff --git a/Web/Models/Entities/Notifications/MentionNotification.php b/Web/Models/Entities/Notifications/MentionNotification.php index 4c744733..25680f57 100644 --- a/Web/Models/Entities/Notifications/MentionNotification.php +++ b/Web/Models/Entities/Notifications/MentionNotification.php @@ -1,13 +1,14 @@ actionCode; diff --git a/Web/Models/Entities/Photo.php b/Web/Models/Entities/Photo.php index 628589db..3c4db886 100644 --- a/Web/Models/Entities/Photo.php +++ b/Web/Models/Entities/Photo.php @@ -1,6 +1,8 @@ getWidth() / $image->getHeight()) > ($px / $py)) { - # For some weird reason using resize with EXACT flag causes system to consume an unholy amount of RAM - $image->crop(0, 0, "100%", (int) ceil(($px * $image->getWidth()) / $py)); + if(($image->getImageWidth() / $image->getImageHeight()) > ($px / $py)) { + $height = (int) ceil(($px * $image->getImageWidth()) / $py); + $image->cropImage($image->getImageWidth(), $height, 0, 0); $res[0] = true; } } - + if(isset($size["maxSize"])) { $maxSize = (int) $size["maxSize"]; - $image->resize($maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT); + $sizes = Image::calculateSize($image->getImageWidth(), $image->getImageHeight(), $maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); } else if(isset($size["maxResolution"])) { $resolution = explode("x", (string) $size["maxResolution"]); - $image->resize((int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT); + $sizes = Image::calculateSize( + $image->getImageWidth(), $image->getImageHeight(), (int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT + ); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); } else { throw new \RuntimeException("Malformed size description: " . (string) $size["id"]); } - - $res[1] = $image->getWidth(); - $res[2] = $image->getHeight(); + + $res[1] = $image->getImageWidth(); + $res[2] = $image->getImageHeight(); if($res[1] <= 300 || $res[2] <= 300) - $image->save("$outputDir/" . (string) $size["id"] . ".gif"); + $image->writeImage("$outputDir/$size[id].gif"); else - $image->save("$outputDir/" . (string) $size["id"] . ".jpeg"); - - imagedestroy($image->getImageResource()); + $image->writeImage("$outputDir/$size[id].jpeg"); + + $res[3] = true; + $image->destroy(); unset($image); - + return $res; } - private function saveImageResizedCopies(string $filename, string $hash): void + private function saveImageResizedCopies(?\Imagick $image, string $filename, string $hash): void { + if(!$image) { + $image = new \Imagick; + $image->readImage($filename); + } + $dir = dirname($this->pathFromHash($hash)); $dir = "$dir/$hash" . "_cropped"; if(!is_dir($dir)) { @@ -67,8 +83,13 @@ class Photo extends Media throw new \RuntimeException("Could not load photosizes.xml!"); $sizesMeta = []; - foreach($sizes->Size as $size) - $sizesMeta[(string) $size["id"]] = $this->resizeImage($filename, $dir, $size); + if(OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["photoSaving"] === "quick") { + foreach($sizes->Size as $size) + $sizesMeta[(string)$size["id"]] = [false, false, false, false]; + } else { + foreach($sizes->Size as $size) + $sizesMeta[(string)$size["id"]] = $this->resizeImage(clone $image, $dir, $size); + } $sizesMeta = MessagePack::pack($sizesMeta); $this->stateChanges("sizes", $sizesMeta); @@ -76,13 +97,19 @@ class Photo extends Media protected function saveFile(string $filename, string $hash): bool { - $image = Image::fromFile($filename); - if(($image->height >= ($image->width * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($image->width >= ($image->height * Photo::ALLOWED_SIDE_MULTIPLIER))) + $image = new \Imagick; + $image->readImage($filename); + $h = $image->getImageHeight(); + $w = $image->getImageWidth(); + if(($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER))) throw new ISE("Invalid layout: image is too wide/short"); - - $image->resize(8192, 4320, Image::SHRINK_ONLY | Image::FIT); - $image->save($this->pathFromHash($hash), 92, Image::JPEG); - $this->saveImageResizedCopies($filename, $hash); + + $sizes = Image::calculateSize( + $image->getImageWidth(), $image->getImageHeight(), 8192, 4320, Image::SHRINK_ONLY | Image::FIT + ); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); + $image->writeImage($this->pathFromHash($hash)); + $this->saveImageResizedCopies($image, $filename, $hash); return true; } @@ -114,8 +141,8 @@ class Photo extends Media $sizes = $this->getRecord()->sizes; if(!$sizes || $forceUpdate) { if($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) { - $hash = $this->getRecord()->hash; - $this->saveImageResizedCopies($this->pathFromHash($hash), $hash); + $hash = $this->getRecord()->hash; + $this->saveImageResizedCopies(NULL, $this->pathFromHash($hash), $hash); $this->save(); return $this->getSizes(); @@ -127,6 +154,16 @@ class Photo extends Media $res = []; $sizes = MessagePack::unpack($sizes); foreach($sizes as $id => $meta) { + if(isset($meta[3]) && !$meta[3]) { + $res[$id] = (object) [ + "url" => ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/photos/thumbnails/" . $this->getId() . "_$id.jpeg", + "width" => NULL, + "height" => NULL, + "crop" => NULL + ]; + continue; + } + $url = $this->getURL(); $url = str_replace(".$this->fileExtension", "_cropped/$id.", $url); $url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg"; @@ -149,6 +186,47 @@ class Photo extends Media return $res; } + + function forceSize(string $sizeName): bool + { + $hash = $this->getRecord()->hash; + $sizes = MessagePack::unpack($this->getRecord()->sizes); + $size = $sizes[$sizeName] ?? false; + if(!$size) + return $size; + + if(!isset($size[3]) || $size[3] === true) + return true; + + $path = $this->pathFromHash($hash); + $dir = dirname($this->pathFromHash($hash)); + $dir = "$dir/$hash" . "_cropped"; + if(!is_dir($dir)) { + @unlink($dir); + mkdir($dir); + } + + $sizeMetas = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml"); + if(!$sizeMetas) + throw new \RuntimeException("Could not load photosizes.xml!"); + + $sizeInfo = NULL; + foreach($sizeMetas->Size as $size) + if($size["id"] == $sizeName) + $sizeInfo = $size; + + if(!$sizeInfo) + return false; + + $pic = new \Imagick; + $pic->readImage($path); + $sizes[$sizeName] = $this->resizeImage($pic, $dir, $sizeInfo); + + $this->stateChanges("sizes", MessagePack::pack($sizes)); + $this->save(); + + return $sizes[$sizeName][3]; + } function getVkApiSizes(): ?array { @@ -205,6 +283,14 @@ class Photo extends Media return [$x, $y]; } + function getPageURL(): string + { + if($this->isAnonymous()) + return "/photos/" . base_convert((string) $this->getId(), 10, 32); + + return "/photo" . $this->getPrettyId(); + } + function getAlbum(): ?Album { return (new Albums)->getAlbumByPhotoId($this); diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 40769292..8c1c6a62 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -113,6 +113,63 @@ class Post extends Postable { return $this->getOwner(false)->getId(); } + + function getPlatform(bool $forAPI = false): ?string + { + $platform = $this->getRecord()->api_source_name; + if($forAPI) { + switch ($platform) { + case 'openvk_refresh_android': + case 'openvk_legacy_android': + return 'android'; + break; + + case 'openvk_ios': + case 'openvk_legacy_ios': + return 'iphone'; + break; + + case 'vika_touch': // кика хохотач ахахахаххахахахахах + case 'vk4me': + return 'mobile'; + break; + + case NULL: + return NULL; + break; + + default: + return 'api'; + break; + } + } else { + return $platform; + } + } + + function getPlatformDetails(): array + { + $clients = simplexml_load_file(OPENVK_ROOT . "/data/clients.xml"); + + foreach($clients as $client) { + if($client['tag'] == $this->getPlatform()) { + return [ + "tag" => $client['tag'], + "name" => $client['name'], + "url" => $client['url'], + "img" => $client['img'] + ]; + break; + } + } + + return [ + "tag" => $this->getPlatform(), + "name" => NULL, + "url" => NULL, + "img" => NULL + ]; + } function pin(): void { diff --git a/Web/Models/Entities/Traits/TBackDrops.php b/Web/Models/Entities/Traits/TBackDrops.php new file mode 100644 index 00000000..cab67138 --- /dev/null +++ b/Web/Models/Entities/Traits/TBackDrops.php @@ -0,0 +1,44 @@ +getRecord()->backdrop_1; + $photo2 = $this->getRecord()->backdrop_2; + if(is_null($photo1) && is_null($photo2)) + return NULL; + + $photo1obj = $photo2obj = NULL; + if(!is_null($photo1)) + $photo1obj = (new Photos)->get($photo1); + if(!is_null($photo2)) + $photo2obj = (new Photos)->get($photo2); + + if(is_null($photo1obj) && is_null($photo2obj)) + return NULL; + + return [ + is_null($photo1obj) ? "" : $photo1obj->getURL(), + is_null($photo2obj) ? "" : $photo2obj->getURL(), + ]; + } + + function setBackDropPictures(?Photo $first, ?Photo $second): void + { + if(!is_null($first)) + $this->stateChanges("backdrop_1", $first->getId()); + + if(!is_null($second)) + $this->stateChanges("backdrop_2", $second->getId()); + } + + function unsetBackDropPictures(): void + { + $this->stateChanges("backdrop_1", NULL); + $this->stateChanges("backdrop_2", NULL); + } +} \ No newline at end of file diff --git a/Web/Models/Entities/Traits/TRichText.php b/Web/Models/Entities/Traits/TRichText.php index f2229250..dc78a034 100644 --- a/Web/Models/Entities/Traits/TRichText.php +++ b/Web/Models/Entities/Traits/TRichText.php @@ -1,5 +1,6 @@ isAd() ? "sponsored" : "ugc"; return "$link" . htmlentities($matches[4]); @@ -48,7 +49,63 @@ trait TRichText private function removeZalgo(string $text): string { - return preg_replace("%[\x{0300}-\x{036F}]{3,}%Xu", "�", $text); + return preg_replace("%\p{M}{3,}%Xu", "", $text); + } + + function resolveMentions(array $skipUsers = []): \Traversable + { + $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content"; + $text = $this->getRecord()->{$contentColumn}; + $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%Xu", "[$1|$2]", $text); + $text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text); + + $resolvedUsers = $skipUsers; + $resolvedClubs = []; + preg_match_all("%\[([A-Za-z0-9]++)\|((?:[\p{L&}\p{Lo} 0-9@]\p{Mn}?)++)\]%Xu", $text, $links, PREG_PATTERN_ORDER); + foreach($links[1] as $link) { + if(preg_match("%^id([0-9]++)$%", $link, $match)) { + $uid = (int) $match[1]; + if(in_array($uid, $resolvedUsers)) + continue; + + $resolvedUsers[] = $uid; + $maybeUser = (new Users)->get($uid); + if($maybeUser) + yield $maybeUser; + } else if(preg_match("%^(?:club|public|event)([0-9]++)$%", $link, $match)) { + $cid = (int) $match[1]; + if(in_array($cid, $resolvedClubs)) + continue; + + $resolvedClubs[] = $cid; + $maybeClub = (new Clubs)->get($cid); + if($maybeClub) + yield $maybeClub; + } else { + $maybeUser = (new Users)->getByShortURL($link); + if($maybeUser) { + $uid = $maybeUser->getId(); + if(in_array($uid, $resolvedUsers)) + continue; + else + $resolvedUsers[] = $uid; + + yield $maybeUser; + continue; + } + + $maybeClub = (new Clubs)->getByShortURL($link); + if($maybeClub) { + $cid = $maybeClub->getId(); + if(in_array($cid, $resolvedClubs)) + continue; + else + $resolvedClubs[] = $cid; + + yield $maybeClub; + } + } + } } function getText(bool $html = true): string @@ -59,7 +116,6 @@ trait TRichText $proc = iconv_strlen($this->getRecord()->{$contentColumn}) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"]; if($html) { if($proc) { - $rel = $this->isAd() ? "sponsored" : "ugc"; $text = $this->formatLinks($text); $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%Xu", "[$1|$2]", $text); $text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text); diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 59cb33ad..ace69699 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -5,7 +5,7 @@ use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Gifts, Notifications}; +use openvk\Web\Models\Repositories\{Photos, Users, Clubs, Albums, Gifts, Notifications}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; use Chandler\Database\DatabaseConnection; @@ -751,6 +751,63 @@ class User extends RowModel return time() - $this->getRecord()->online <= 300; } + function getOnlinePlatform(bool $forAPI = false): ?string + { + $platform = $this->getRecord()->client_name; + if($forAPI) { + switch ($platform) { + case 'openvk_refresh_android': + case 'openvk_legacy_android': + return 'android'; + break; + + case 'openvk_ios': + case 'openvk_legacy_ios': + return 'iphone'; + break; + + case 'vika_touch': // кика хохотач ахахахаххахахахахах + case 'vk4me': + return 'mobile'; + break; + + case NULL: + return NULL; + break; + + default: + return 'api'; + break; + } + } else { + return $platform; + } + } + + function getOnlinePlatformDetails(): array + { + $clients = simplexml_load_file(OPENVK_ROOT . "/data/clients.xml"); + + foreach($clients as $client) { + if($client['tag'] == $this->getOnlinePlatform()) { + return [ + "tag" => $client['tag'], + "name" => $client['name'], + "url" => $client['url'], + "img" => $client['img'] + ]; + break; + } + } + + return [ + "tag" => $this->getOnlinePlatform(), + "name" => NULL, + "url" => NULL, + "img" => NULL + ]; + } + function prefersNotToSeeRating(): bool { return !((bool) $this->getRecord()->show_rating); @@ -1044,5 +1101,6 @@ class User extends RowModel return true; } + use Traits\TBackDrops; use Traits\TSubscribable; } diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index b45072b9..ee53b378 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -13,7 +13,7 @@ class Video extends Media const TYPE_EMBED = 1; protected $tableName = "videos"; - protected $fileExtension = "ogv"; + protected $fileExtension = "mp4"; protected $processingPlaceholder = "video/rendering"; @@ -30,7 +30,7 @@ class Video extends Media throw new \DomainException("$filename does not contain any video streams"); $durations = []; - preg_match('%duration=([0-9\.]++)%', $streams, $durations); + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); if(sizeof($durations[1]) === 0) throw new \DomainException("$filename does not contain any meaningful video streams"); @@ -104,7 +104,7 @@ class Video extends Media if(!$this->isProcessed()) return "/assets/packages/static/openvk/video/rendering.apng"; - return preg_replace("%\.[A-z]++$%", ".gif", $this->getURL()); + return preg_replace("%\.[A-z0-9]++$%", ".gif", $this->getURL()); } else { return $this->getVideoDriver()->getThumbnailURL(); } @@ -114,6 +114,56 @@ class Video extends Media { return $this->getRecord()->owner; } + + function getApiStructure(): object + { + return (object)[ + "type" => "video", + "video" => [ + "can_comment" => 1, + "can_like" => 0, // we don't h-have wikes in videos + "can_repost" => 0, + "can_subscribe" => 1, + "can_add_to_faves" => 0, + "can_add" => 0, + "comments" => $this->getCommentsCount(), + "date" => $this->getPublicationTime()->timestamp(), + "description" => $this->getDescription(), + "duration" => 0, // я хуй знает как получить длину видео + "image" => [ + [ + "url" => $this->getThumbnailURL(), + "width" => 320, + "height" => 240, + "with_padding" => 1 + ] + ], + "width" => 640, + "height" => 480, + "id" => $this->getVirtualId(), + "owner_id" => $this->getOwner()->getId(), + "user_id" => $this->getOwner()->getId(), + "title" => $this->getName(), + "is_favorite" => false, + "player" => $this->getURL(), + "files" => [ + "mp4_480" => $this->getURL() + ], + "added" => 0, + "repeat" => 0, + "type" => "video", + "views" => 0, + "likes" => [ + "count" => 0, + "user_likes" => 0 + ], + "reposts" => [ + "count" => 0, + "user_reposted" => 0 + ] + ] + ]; + } function setLink(string $link): string { diff --git a/Web/Models/VideoDrivers/VideoDriver.php b/Web/Models/VideoDrivers/VideoDriver.php index 141e738b..ca9fb74b 100644 --- a/Web/Models/VideoDrivers/VideoDriver.php +++ b/Web/Models/VideoDrivers/VideoDriver.php @@ -14,5 +14,5 @@ abstract class VideoDriver abstract function getURL(): string; - abstract function getEmbed(): string; + abstract function getEmbed(string $w = "600", string $h = "340"): string; } diff --git a/Web/Models/VideoDrivers/YouTubeVideoDriver.php b/Web/Models/VideoDrivers/YouTubeVideoDriver.php index 93aee0e5..1b9940e6 100644 --- a/Web/Models/VideoDrivers/YouTubeVideoDriver.php +++ b/Web/Models/VideoDrivers/YouTubeVideoDriver.php @@ -13,12 +13,12 @@ final class YouTubeVideoDriver extends VideoDriver return "https://youtu.be/$this->id"; } - function getEmbed(): string + function getEmbed(string $w = "600", string $h = "340"): string { return <<getOwner()->getId() !== $this->user->identity->getId()) if(($owner = $entity->getOwner()) instanceof User) (new CommentNotification($owner, $comment, $entity, $this->user->identity))->emit(); + + $excludeMentions = [$this->user->identity->getId()]; + if(($owner = $entity->getOwner()) instanceof User) + $excludeMentions[] = $owner->getId(); + + $mentions = iterator_to_array($comment->resolveMentions($excludeMentions)); + foreach($mentions as $mentionee) + if($mentionee instanceof User) + (new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit(); $this->flashFail("succ", "Комментарий добавлен", "Ваш комментарий появится на странице."); } diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index 00d74c2e..a0b83a59 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -1,6 +1,7 @@ willExecuteWriteAction(); $club = $this->clubs->get($id); - if(!$club->canBeModifiedBy($this->user->identity)) + if(!$club || !$club->canBeModifiedBy($this->user->identity)) $this->notFound(); else $this->template->club = $club; @@ -250,6 +251,45 @@ final class GroupPresenter extends OpenVKPresenter } } + function renderEditBackdrop(int $id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $club = $this->clubs->get($id); + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->notFound(); + else + $this->template->club = $club; + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + if($this->postParam("subact") === "remove") { + $club->unsetBackDropPictures(); + $club->save(); + $this->flashFail("succ", tr("backdrop_succ_rem"), tr("backdrop_succ_desc")); # will exit + } + + $pic1 = $pic2 = NULL; + try { + if($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE) + $pic1 = Photo::fastMake($this->user->id, "Profile backdrop (system)", $_FILES["backdrop1"]); + + if($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE) + $pic2 = Photo::fastMake($this->user->id, "Profile backdrop (system)", $_FILES["backdrop2"]); + } catch(InvalidStateException $e) { + $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); + } + + if($pic1 == $pic2 && is_null($pic1)) + $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); + + $club->setBackDropPictures($pic1, $pic2); + $club->save(); + $this->flashFail("succ", tr("backdrop_succ"), tr("backdrop_succ_desc")); + } + function renderStatistics(int $id): void { $this->assertUserLoggedIn(); diff --git a/Web/Presenters/MessengerPresenter.php b/Web/Presenters/MessengerPresenter.php index 46094c43..d5ffb988 100644 --- a/Web/Presenters/MessengerPresenter.php +++ b/Web/Presenters/MessengerPresenter.php @@ -57,6 +57,11 @@ final class MessengerPresenter extends OpenVKPresenter $correspondent = $this->getCorrespondent($sel); if(!$correspondent) $this->notFound(); + + if(!$this->user->identity->getPrivacyPermission('messages.write', $correspondent)) + { + $this->flash("err", tr("warning"), tr("user_may_not_reply")); + } $this->template->selId = $sel; $this->template->correspondent = $correspondent; diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index 1505df92..710713e5 100755 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -254,6 +254,7 @@ abstract class OpenVKPresenter extends SimplePresenter $cacheTime = 0; # Force no cache if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { $this->user->identity->setOnline(time()); + $this->user->identity->setClient_name(NULL); $this->user->identity->save(); } diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 3dd2a774..02d6ae46 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -185,6 +185,18 @@ final class PhotosPresenter extends OpenVKPresenter $this->renderPhoto($photo->getOwner(true)->getId(), $photo->getVirtualId()); } + function renderThumbnail($id, $size): void + { + $photo = $this->photos->get($id); + if(!$photo || $photo->isDeleted()) + $this->notFound(); + + if(!$photo->forceSize($size)) + chandler_http_panic(588, "Gone", "This thumbnail cannot be generated due to server misconfiguration"); + + $this->redirect($photo->getURLBySizeId($size), 8); + } + function renderEditPhoto(int $ownerId, int $photoId): void { $this->assertUserLoggedIn(); diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 3acabc84..3cf68757 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -1,5 +1,6 @@ setFav_Books(empty($this->postParam("fav_books")) ? NULL : ovk_proc_strtr($this->postParam("fav_books"), 300)); $user->setFav_Quote(empty($this->postParam("fav_quote")) ? NULL : ovk_proc_strtr($this->postParam("fav_quote"), 300)); $user->setAbout(empty($this->postParam("about")) ? NULL : ovk_proc_strtr($this->postParam("about"), 300)); + } elseif($_GET["act"] === "backdrop") { + if($this->postParam("subact") === "remove") { + $user->unsetBackDropPictures(); + $user->save(); + $this->flashFail("succ", tr("backdrop_succ_rem"), tr("backdrop_succ_desc")); # will exit + } + + $pic1 = $pic2 = NULL; + try { + if($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE) + $pic1 = Photo::fastMake($user->getId(), "Profile backdrop (system)", $_FILES["backdrop1"]); + + if($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE) + $pic2 = Photo::fastMake($user->getId(), "Profile backdrop (system)", $_FILES["backdrop2"]); + } catch(InvalidStateException $e) { + $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); + } + + if($pic1 == $pic2 && is_null($pic1)) + $this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media")); + + $user->setBackDropPictures($pic1, $pic2); + $user->save(); + $this->flashFail("succ", tr("backdrop_succ"), tr("backdrop_succ_desc")); } elseif($_GET['act'] === "status") { if(mb_strlen($this->postParam("status")) > 255) { $statusLength = (string) mb_strlen($this->postParam("status")); @@ -235,7 +260,7 @@ final class UserPresenter extends OpenVKPresenter } $this->template->mode = in_array($this->queryParam("act"), [ - "main", "contacts", "interests", "avatar" + "main", "contacts", "interests", "avatar", "backdrop" ]) ? $this->queryParam("act") : "main"; diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index a26a25b9..4cf6e050 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -195,19 +195,24 @@ final class VKAPIPresenter extends OpenVKPresenter $identity = NULL; } else { $token = (new APITokens)->getByCode($this->requestParam("access_token")); - if(!$token) + if(!$token) { $identity = NULL; - else + } else { $identity = $token->getUser(); + $platform = $token->getPlatform(); + } } } + if(!is_null($identity) && $identity->isBanned()) + $this->fail(18, "User account is deactivated", $object, $method); + $object = ucfirst(strtolower($object)); $handlerClass = "openvk\\VKAPI\\Handlers\\$object"; if(!class_exists($handlerClass)) $this->badMethod($object, $method); - $handler = new $handlerClass($identity); + $handler = new $handlerClass($identity, $platform); if(!is_callable([$handler, $method])) $this->badMethod($object, $method); @@ -274,8 +279,11 @@ final class VKAPIPresenter extends OpenVKPresenter $this->fail(28, "Invalid 2FA code", "internal", "acquireToken"); } + $platform = $this->requestParam("client_name"); + $token = new APIToken; $token->setUser($user); + $token->setPlatform(is_null($platform) ? "api" : $platform); $token->save(); $payload = json_encode([ diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index cade97ca..ffcec2a7 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; -use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification}; +use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification}; use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; @@ -305,6 +305,15 @@ final class WallPresenter extends OpenVKPresenter if($wall > 0 && $wall !== $this->user->identity->getId()) (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); + $excludeMentions = [$this->user->identity->getId()]; + if($wall > 0) + $excludeMentions[] = $wall; + + $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); + foreach($mentions as $mentionee) + if($mentionee instanceof User) + (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); + $this->redirect($wallOwner->getURL()); } diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 9b5b9de1..bb0d172e 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -49,6 +49,12 @@
+ {if isset($backdrops) && !is_null($backdrops)} +
+
+
+ {/if} +
⬆ {_to_top}
@@ -163,7 +169,7 @@
- +
+{$thisUser->getFollowersCount()} @@ -171,10 +177,10 @@
- + - +
+{$thisUser->getUnreadMessagesCount()} @@ -182,10 +188,10 @@
- + - +
+{$thisUser->getNotificationsCount()} @@ -288,6 +294,7 @@ {script "js/messagebox.js"} {script "js/notifications.js"} {script "js/scroll.js"} + {script "js/player.js"} {script "js/al_wall.js"} {script "js/al_api.js"} {script "js/al_mentions.js"} @@ -304,6 +311,8 @@ {/if} + +