diff --git a/.gitignore b/.gitignore
index 28edaa42..b3bb2167 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,7 @@ update.pid.old
Web/static/js/node_modules
tmp/*
-!tmp/.gitkeep
+!tmp/api-storage
!tmp/themepack_artifacts/.gitkeep
themepacks/*
!themepacks/.gitkeep
diff --git a/.idea/openvk.iml b/.idea/openvk.iml
index b5f07aa6..3d72b6b1 100644
--- a/.idea/openvk.iml
+++ b/.idea/openvk.iml
@@ -28,6 +28,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/php.xml b/.idea/php.xml
index 22c50b95..5aabe8cd 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -29,6 +29,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/CLI/RebuildImagesCommand.php b/CLI/RebuildImagesCommand.php
new file mode 100644
index 00000000..937978b1
--- /dev/null
+++ b/CLI/RebuildImagesCommand.php
@@ -0,0 +1,86 @@
+images = DatabaseConnection::i()->getContext()->table("photos");
+
+ parent::__construct();
+ }
+
+ protected function configure(): void
+ {
+ $this->setDescription("Create resized versions of images")
+ ->setHelp("This command allows you to resize all your images after configuration change")
+ ->addOption("upgrade-only", "U", InputOption::VALUE_NEGATABLE, "Only upgrade images which aren't resized?");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $header = $output->section();
+ $counter = $output->section();
+
+ $header->writeln([
+ "Image Rebuild Utility",
+ "=====================",
+ "",
+ ]);
+
+ $filter = ["deleted" => false];
+ if($input->getOption("upgrade-only"))
+ $filter["sizes"] = NULL;
+
+ $selection = $this->images->select("id")->where($filter);
+ $totalPics = $selection->count();
+ $header->writeln([
+ "Total of $totalPics images found.",
+ "",
+ ]);
+
+ $errors = 0;
+ $count = 0;
+ $avgTime = NULL;
+ $begin = new \DateTimeImmutable("now");
+ foreach($selection as $idHolder) {
+ $start = microtime(true);
+
+ try {
+ $photo = (new Photos)->get($idHolder->id);
+ $photo->getSizes(true, true);
+ $photo->getDimensions();
+ } catch(ImageException $ex) {
+ $errors++;
+ }
+
+ $timeConsumed = microtime(true) - $start;
+ if(!$avgTime)
+ $avgTime = $timeConsumed;
+ else
+ $avgTime = ($avgTime + $timeConsumed) / 2;
+
+ $eta = $begin->getTimestamp() + ceil($totalPics * $avgTime);
+ $int = (new \DateTimeImmutable("now"))->diff(new \DateTimeImmutable("@$eta"));
+ $int = $int->d . "d" . $int->h . "h" . $int->i . "m" . $int->s . "s";
+ $pct = floor(100 * ($count / $totalPics));
+
+ $counter->overwrite("Processed " . ++$count . " images... ($pct% $int left $errors/$count fail)");
+ }
+
+ $counter->overwrite("Processing finished :3");
+
+ return Command::SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index aee95755..f56a7fc0 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,13 @@ _[Русский](README_RU.md)_
VKontakte belongs to Pavel Durov and VK Group.
-To be honest, we don't even 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 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).
## When's the release?
-Please use the master branch, as it has the most changes.
-
-Updating the source code is done with this command: `git pull`
+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://github.com/openvk/archive/actions/workflows/nightly.yml)
## Instances
@@ -24,7 +24,7 @@ Updating the source code is done with this command: `git pull`
Yes! And you're 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. this extensions are available on most of ISPManager hostings).
+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).
If you want, you can add your instance to the list above so that people can register there.
@@ -32,7 +32,7 @@ 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 has **not** yet been tested, so you should not expect it to work.
+* PHP 8 has **not** yet been tested, so you should not expect it to work. (edit: it does not work).
2. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this:
@@ -48,26 +48,25 @@ ln -s /path/to/chandler/extensions/available/commitcaptcha /path/to/chandler/ext
ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/
```
-4. Import `install/init-static-db.sql` to **same database** you installed Chandler to and import all sqls from `install/sqls` to **same database**
-5. Import `install/init-event-db.sql` to **separate database**
-6. Copy `openvk-example.yml` to `openvk.yml` and change options
+4. Import `install/init-static-db.sql` to the **same database** you installed Chandler to and import all sqls from `install/sqls` to the **same database**
+5. Import `install/init-event-db.sql` to a **separate database** (Yandex.Clickhouse can also be used, higly recommended)
+6. Copy `openvk-example.yml` to `openvk.yml` and change options to your liking
7. Run `composer install` in OpenVK directory
-8. Move to `Web/static/js` and execute `yarn install`
-9. Set `openvk` as your root app in `chandler.yml`
+8. Run `composer install` in commitcaptcha directory
+9. Move to `Web/static/js` and execute `yarn install`
+10. Set `openvk` as your root app in `chandler.yml`
Once you are done, you can login as a system administrator on the network itself (no registration required):
* **Login**: `admin@localhost.localdomain6`
* **Password**: `admin`
- * It is recommended to change the password before using the built-in account.
+ * It is recommended to change the password of the built-in account or disable it.
-Full example installation instruction for CentOS 8 is also available [here](https://docs.openvk.su/openvk_engine/centos8_installation/).
+💡Confused? Full installation walkthrough is available [here](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)).
-### If my website uses OpenVK, should I publish it's sources?
+### If my website uses OpenVK, should I release it's sources?
-You are encouraged to do so. We don't enforce this though. You can keep your sources to yourself (unless you distribute your OpenVK distro to other people).
-
-You also not required to publish source texts of your themepacks and plugins.
+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).
## Where can I get assistance?
@@ -80,7 +79,7 @@ You may reach out to us via:
* [Discussions](https://github.com/openvk/openvk/discussions)
* Matrix chat: #openvk:matrix.org
-**Attention**: bug tracker, 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. 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**
diff --git a/README_RU.md b/README_RU.md
index 47a06202..dda9e989 100644
--- a/README_RU.md
+++ b/README_RU.md
@@ -10,9 +10,9 @@ _[English](README.md)_
## Когда релиз?
-Пожалуйста, используйте ветку master, так как в ней больше всего изменений.
-
-Обновление исходного кода выполняется с помощью этой команды: `git pull`.
+Мы выпустим OpenVK, как только он будет готов. На данный момент Вы можете:
+* Сделать `git clone` master ветки этой репозитории (используйте `git pull` для обновления)
+* Взять готовую сборку OpenVK из [GitHub Actions](https://github.com/openvk/archive/actions/workflows/nightly.yml)
## Инстанции
@@ -32,7 +32,7 @@ _[English](README.md)_
1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler)
-* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать.
+* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (обновление: он не работает).
2. Установите [commitcaptcha](https://github.com/openvk/commitcaptcha) и OpenVK в качестве расширений Chandler следующим образом:
@@ -49,11 +49,12 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
```
4. Импортируйте `install/init-static-db.sql` в **ту же базу данных**, в которую вы установили Chandler, и импортируйте все SQL файлы из папки `install/sqls` в **ту же базу данных**
-5. Импортируйте `install/init-event-db.sql` в **отдельную базу данных**
+5. Импортируйте `install/init-event-db.sql` в **отдельную базу данных** (Яндекс.Clickhouse также может быть использован, настоятельно рекомендуется)
6. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры
7. Запустите `composer install` в директории OpenVK
-8. Перейдите в `Web/static/js` и выполните `yarn install`
-9. Установите `openvk` в качестве корневого приложения в файле `chandler.yml`
+8. Запустите `composer install` в директории commitcaptcha
+9. Перейдите в `Web/static/js` и выполните `yarn install`
+10. Установите `openvk` в качестве корневого приложения в файле `chandler.yml`
После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется):
@@ -61,13 +62,11 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* **Пароль**: `admin`
* Перед использованием встроенной учетной записи рекомендуется сменить пароль.
-Полный пример инструкции по установке CentOS 8 также доступен [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/).
+💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)).
### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты?
-Вам рекомендуется это делать. Однако мы не следим за этим. Вы можете держать свои исходные тексты при себе (если только вы не распространяете свой дистрибутив OpenVK среди других людей).
-
-Вы также не обязаны публиковать исходные тексты ваших тематических пакетов и плагинов.
+Это зависит от обстоятельств. Вы можете оставить исходные тексты при себе, если не планируете распространять бинарники вашего сайта. Если программное обеспечение вашего сайта должно распространяться, оно может оставаться не-OSS при условии, что OpenVK не используется в качестве основного приложения и не модифицируется. Если вы модифицировали OpenVK для своих нужд или ваша работа основана на нем и вы планируете ее распространять, то вы должны лицензировать ее на условиях любой совместимой с LGPL лицензии (например, OSL, GPL, LGPL и т.д.).
## Где я могу получить помощь?
@@ -80,7 +79,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* [Обсуждения](https://github.com/openvk/openvk/discussions)
* Чат в Matrix: #ovk:matrix.org
-**Внимание**: баг-трекер, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**.
+**Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**.
diff --git a/VKAPI/Handlers/Account.php b/VKAPI/Handlers/Account.php
index ee55e863..7e707cf3 100644
--- a/VKAPI/Handlers/Account.php
+++ b/VKAPI/Handlers/Account.php
@@ -60,7 +60,7 @@ final class Account extends VKAPIRequestHandler
return 1;
}
- function getAppPermissions(): object
+ function getAppPermissions(): int
{
return 9355263;
}
diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php
index c9125a47..2e917f2d 100644
--- a/VKAPI/Handlers/Friends.php
+++ b/VKAPI/Handlers/Friends.php
@@ -25,7 +25,7 @@ final class Friends extends VKAPIRequestHandler
$usersApi = new Users($this->getUser());
if (!is_null($fields)) {
- $response = $usersApi->get(implode(',', $friends), $fields, 0, $count, true); // FIXME
+ $response = $usersApi->get(implode(',', $friends), $fields, 0, $count); // FIXME
}
return (object) [
diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php
index 21f4c93b..eb5d3230 100644
--- a/VKAPI/Handlers/Groups.php
+++ b/VKAPI/Handlers/Groups.php
@@ -88,4 +88,100 @@ final class Groups extends VKAPIRequestHandler
"items" => $rClubs
];
}
+
+ function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array
+ {
+ $this->requireUser();
+
+ $clubs = new ClubsRepo;
+
+ if ($group_ids == null && $group_id != null)
+ $group_ids = $group_id;
+
+ if ($group_ids == null && $group_id == null)
+ $this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined");
+
+ $clbs = explode(',', $group_ids);
+ $response;
+
+ $ic = sizeof($clbs);
+
+ for ($i=0; $i < $ic; $i++) {
+ if($i > 500)
+ break;
+
+ if($clbs[$i] < 0)
+ $this->fail(100, "ты ошибся чутка, у айди группы убери минус");
+
+ $clb = $clubs->get((int) $clbs[$i]);
+ if(is_null($clb))
+ {
+ $response[$i] = (object)[
+ "id" => intval($clbs[$i]),
+ "name" => "DELETED",
+ "screen_name" => "club".intval($clbs[$i]),
+ "type" => "group",
+ "description" => "This group was deleted or it doesn't exist"
+ ];
+ }else if($clbs[$i] == null){
+
+ }else{
+ $response[$i] = (object)[
+ "id" => $clb->getId(),
+ "name" => $clb->getName(),
+ "screen_name" => $clb->getShortCode() ?? "club".$clb->getId(),
+ "is_closed" => false,
+ "type" => "group",
+ "can_access_closed" => true,
+ ];
+
+ $flds = explode(',', $fields);
+
+ foreach($flds as $field) {
+ switch ($field) {
+ case 'verified':
+ $response[$i]->verified = intval($clb->isVerified());
+ break;
+ case 'has_photo':
+ $response[$i]->has_photo = is_null($clb->getAvatarPhoto()) ? 0 : 1;
+ break;
+ case 'photo_max_orig':
+ $response[$i]->photo_max_orig = $clb->getAvatarURL();
+ break;
+ case 'photo_max':
+ $response[$i]->photo_max = $clb->getAvatarURL();
+ break;
+ case 'members_count':
+ $response[$i]->members_count = $clb->getFollowersCount();
+ break;
+ case 'site':
+ $response[$i]->site = $clb->getWebsite();
+ break;
+ case 'description':
+ $response[$i]->desctiption = $clb->getDescription();
+ break;
+ case 'contacts':
+ $contacts;
+ $contactTmp = $clb->getManagers(1, true);
+ foreach($contactTmp as $contact) {
+ $contacts[] = array(
+ 'user_id' => $contact->getUser()->getId(),
+ 'desc' => $contact->getComment()
+ );
+ }
+ $response[$i]->contacts = $contacts;
+ break;
+ case 'can_post':
+ if($clb->canBeModifiedBy($this->getUser()))
+ $response[$i]->can_post = true;
+ else
+ $response[$i]->can_post = $clb->canPost();
+ break;
+ }
+ }
+ }
+ }
+
+ return $response;
+ }
}
diff --git a/VKAPI/Handlers/Likes.php b/VKAPI/Handlers/Likes.php
new file mode 100644
index 00000000..b002075b
--- /dev/null
+++ b/VKAPI/Handlers/Likes.php
@@ -0,0 +1,74 @@
+requireUser();
+
+ switch ($type) {
+ case 'post':
+ $post = (new PostsRepo)->getPostById($owner_id, $item_id);
+ if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found');
+
+ $post->setLike(true, $this->getUser());
+ return (object)[
+ "likes" => $post->getLikesCount()
+ ];
+ break;
+ default:
+ $this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
+ break;
+ }
+ }
+
+ function remove(string $type, int $owner_id, int $item_id): object
+ {
+ $this->requireUser();
+
+ switch ($type) {
+ case 'post':
+ $post = (new PostsRepo)->getPostById($owner_id, $item_id);
+ if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found');
+
+ $post->setLike(false, $this->getUser());
+ return (object)[
+ "likes" => $post->getLikesCount()
+ ];
+ break;
+ default:
+ $this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
+ break;
+ }
+ }
+
+ function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object
+ {
+ $this->requireUser();
+
+ switch ($type) {
+ case 'post':
+ $user = (new UsersRepo)->get($user_id);
+ if (is_null($user)) return (object)[
+ "liked" => 0,
+ "copied" => 0,
+ "sex" => 0
+ ];
+
+ $post = (new PostsRepo)->getPostById($owner_id, $item_id);
+ if (is_null($post)) $this->fail(100, 'One of the parameters specified was missing or invalid: object not found');
+
+ return (object)[
+ "liked" => (int) $post->hasLikeFrom($user),
+ "copied" => 0 // TODO: handle this
+ ];
+ break;
+ default:
+ $this->fail(100, 'One of the parameters specified was missing or invalid: incorrect type');
+ break;
+ }
+ }
+}
diff --git a/VKAPI/Handlers/Messages.php b/VKAPI/Handlers/Messages.php
index 2d9b575e..456e31df 100644
--- a/VKAPI/Handlers/Messages.php
+++ b/VKAPI/Handlers/Messages.php
@@ -4,6 +4,7 @@ use openvk\Web\Events\NewMessageEvent;
use openvk\Web\Models\Entities\{Correspondence, Message};
use openvk\Web\Models\Repositories\{Messages as MSGRepo, Users as USRRepo};
use openvk\VKAPI\Structures\{Message as APIMsg, Conversation as APIConvo};
+use openvk\VKAPI\Handlers\Users as APIUsers;
use Chandler\Signaling\SignalManager;
final class Messages extends VKAPIRequestHandler
@@ -48,10 +49,12 @@ final class Messages extends VKAPIRequestHandler
$rMsg->read_state = 1;
$rMsg->out = (int) ($message->getSender()->getId() === $this->getUser()->getId());
$rMsg->body = $message->getText(false);
+ $rMsg->text = $message->getText(false);
$rMsg->emoji = true;
if($preview_length > 0)
$rMsg->body = ovk_proc_strtr($rMsg->body, $preview_length);
+ $rMsg->text = ovk_proc_strtr($rMsg->text, $preview_length);
$items[] = $rMsg;
}
@@ -145,12 +148,14 @@ final class Messages extends VKAPIRequestHandler
return 1;
}
- function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 0): object
+ function getConversations(int $offset = 0, int $count = 20, string $filter = "all", int $extended = 0, string $fields = ""): object
{
$this->requireUser();
$convos = (new MSGRepo)->getCorrespondencies($this->getUser(), -1, $count, $offset);
$list = [];
+
+ $users = [];
foreach($convos as $convo) {
$correspondents = $convo->getCorrespondents();
if($correspondents[0]->getId() === $this->getUser()->getId())
@@ -189,7 +194,13 @@ final class Messages extends VKAPIRequestHandler
$lastMessagePreview->read_state = 1;
$lastMessagePreview->out = (int) ($lastMessage->getSender()->getId() === $this->getUser()->getId());
$lastMessagePreview->body = $lastMessage->getText(false);
+ $lastMessagePreview->text = $lastMessage->getText(false);
$lastMessagePreview->emoji = true;
+
+ if($extended == 1) {
+ $users[] = $lastMessage->getSender()->getId();
+ $users[] = $author;
+ }
}
$list[] = [
@@ -198,10 +209,20 @@ final class Messages extends VKAPIRequestHandler
];
}
- return (object) [
- "count" => sizeof($list),
- "items" => $list,
- ];
+ if($extended == 0){
+ return (object) [
+ "count" => sizeof($list),
+ "items" => $list,
+ ];
+ } else {
+ $users = array_unique($users);
+
+ return (object) [
+ "count" => sizeof($list),
+ "items" => $list,
+ "profiles" => (new APIUsers)->get(implode(',', $users), $fields, $offset, $count)
+ ];
+ }
}
function getHistory(int $offset = 0, int $count = 20, int $user_id = -1, int $peer_id = -1, int $start_message_id = 0, int $rev = 0, int $extended = 0): object
@@ -230,6 +251,7 @@ final class Messages extends VKAPIRequestHandler
$rMsg->read_state = 1;
$rMsg->out = (int) ($msgU->sender_id === $this->getUser()->getId());
$rMsg->body = $message->getText(false);
+ $rMsg->text = $message->getText(false);
$rMsg->emoji = true;
$results[] = $rMsg;
diff --git a/VKAPI/Handlers/Photos.php b/VKAPI/Handlers/Photos.php
new file mode 100644
index 00000000..9a02dd8c
--- /dev/null
+++ b/VKAPI/Handlers/Photos.php
@@ -0,0 +1,230 @@
+getUser()->getId(),
+ $group,
+ 0, # this is unused but stays here base64 reasons (X2 doesn't work, so there's dummy value for short)
+ ];
+ $uploadInfo = pack("vZ10v2P3S", ...$uploadInfo);
+ $uploadInfo = base64_encode($uploadInfo);
+ $uploadHash = hash_hmac("sha3-224", $uploadInfo, $secret);
+ $uploadInfo = rawurlencode($uploadInfo);
+
+ return ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/upload/photo/$uploadHash?$uploadInfo";
+ }
+
+ private function getImagePath(string $photo, string $hash, ?string& $up = NULL, ?string& $group = NULL): string
+ {
+ $secret = CHANDLER_ROOT_CONF["security"]["secret"];
+ if(!hash_equals(hash_hmac("sha3-224", $photo, $secret), $hash))
+ $this->fail(121, "Incorrect hash");
+
+ [$up, $image, $group] = explode("|", $photo);
+
+ $imagePath = __DIR__ . "/../../tmp/api-storage/photos/$up" . "_$image.oct";
+ if(!file_exists($imagePath))
+ $this->fail(10, "Invalid image");
+
+ return $imagePath;
+ }
+
+ function getOwnerPhotoUploadServer(int $owner_id = 0): object
+ {
+ $this->requireUser();
+
+ if($owner_id < 0) {
+ $club = (new Clubs)->get(abs($owner_id));
+ if(!$club)
+ $this->fail(0404, "Club not found");
+ else if(!$club->canBeModifiedBy($this->getUser()))
+ $this->fail(200, "Access: Club can't be 'written' by user");
+ }
+
+ return (object) [
+ "upload_url" => $this->getPhotoUploadUrl("photo", isset($club) ? 0 : $club->getId()),
+ ];
+ }
+
+ function saveOwnerPhoto(string $photo, string $hash): object
+ {
+ $imagePath = $this->getImagePath($photo, $hash, $uploader, $group);
+ if($group == 0) {
+ $user = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader);
+ $album = (new Albums)->getUserAvatarAlbum($user);
+ } else {
+ $club = (new Clubs)->get((int) $group);
+ $album = (new Albums)->getClubAvatarAlbum($club);
+ }
+
+ try {
+ $avatar = new Photo;
+ $avatar->setOwner((int) $uploader);
+ $avatar->setDescription("Profile photo");
+ $avatar->setCreated(time());
+ $avatar->setFile([
+ "tmp_name" => $imagePath,
+ "error" => 0,
+ ]);
+ $avatar->save();
+ $album->addPhoto($avatar);
+ unlink($imagePath);
+ } catch(ImageException | InvalidStateException $e) {
+ unlink($imagePath);
+ $this->fail(129, "Invalid image file");
+ }
+
+ return (object) [
+ "photo_hash" => NULL,
+ "photo_src" => $avatar->getURL(),
+ ];
+ }
+
+ function getWallUploadServer(?int $group_id = NULL): object
+ {
+ $this->requireUser();
+
+ $album = NULL;
+ if(!is_null($group_id)) {
+ $club = (new Clubs)->get(abs($group_id));
+ if(!$club)
+ $this->fail(0404, "Club not found");
+ else if(!$club->canBeModifiedBy($this->getUser()))
+ $this->fail(200, "Access: Club can't be 'written' by user");
+ } else {
+ $album = (new Albums)->getUserWallAlbum($this->getUser());
+ }
+
+ return (object) [
+ "upload_url" => $this->getPhotoUploadUrl("photo", $group_id ?? 0),
+ "album_id" => $album,
+ "user_id" => $this->getUser()->getId(),
+ ];
+ }
+
+ function saveWallPhoto(string $photo, string $hash, int $group_id = 0, ?string $caption = NULL): array
+ {
+ $imagePath = $this->getImagePath($photo, $hash, $uploader, $group);
+ if($group_id != $group)
+ $this->fail(8, "group_id doesn't match");
+
+ $album = NULL;
+ if($group_id != 0) {
+ $uploader = (new \openvk\Web\Models\Repositories\Users)->get((int) $uploader);
+ $album = (new Albums)->getUserWallAlbum($uploader);
+ }
+
+ try {
+ $photo = new Photo;
+ $photo->setOwner((int) $uploader);
+ $photo->setCreated(time());
+ $photo->setFile([
+ "tmp_name" => $imagePath,
+ "error" => 0,
+ ]);
+
+ if (!is_null($caption))
+ $photo->setDescription($caption);
+
+ $photo->save();
+ unlink($imagePath);
+ } catch(ImageException | InvalidStateException $e) {
+ unlink($imagePath);
+ $this->fail(129, "Invalid image file");
+ }
+
+ if(!is_null($album))
+ $album->addPhoto($photo);
+
+ return [
+ $photo->toVkApiStruct(),
+ ];
+ }
+
+ function getUploadServer(?int $album_id = NULL): object
+ {
+ $this->requireUser();
+
+ # Not checking rights to album because save() method will do so anyways
+ return (object) [
+ "upload_url" => $this->getPhotoUploadUrl("photo", 0, true),
+ "album_id" => $album_id,
+ "user_id" => $this->getUser()->getId(),
+ ];
+ }
+
+ function save(string $photos_list, string $hash, int $album_id = 0, ?string $caption = NULL): object
+ {
+ $this->requireUser();
+
+ $secret = CHANDLER_ROOT_CONF["security"]["secret"];
+ if(!hash_equals(hash_hmac("sha3-224", $photos_list, $secret), $hash))
+ $this->fail(121, "Incorrect hash");
+
+ $album = NULL;
+ if($album_id != 0) {
+ $album_ = (new Albums)->get($album_id);
+ if(!$album_)
+ $this->fail(0404, "Invalid album");
+ else if(!$album_->canBeModifiedBy($this->getUser()))
+ $this->fail(15, "Access: Album can't be 'written' by user");
+
+ $album = $album_;
+ }
+
+ $pList = json_decode($photos_list);
+ $imagePaths = [];
+ foreach($pList as $pDesc)
+ $imagePaths[] = __DIR__ . "/../../tmp/api-storage/photos/$pDesc->keyholder" . "_$pDesc->resource.oct";
+
+ $images = [];
+ try {
+ foreach($imagePaths as $imagePath) {
+ $photo = new Photo;
+ $photo->setOwner($this->getUser()->getId());
+ $photo->setCreated(time());
+ $photo->setFile([
+ "tmp_name" => $imagePath,
+ "error" => 0,
+ ]);
+
+ if (!is_null($caption))
+ $photo->setDescription($caption);
+
+ $photo->save();
+ unlink($imagePath);
+
+ if(!is_null($album))
+ $album->addPhoto($photo);
+
+ $images[] = $photo->toVkApiStruct();
+ }
+ } catch(ImageException | InvalidStateException $e) {
+ foreach($imagePaths as $imagePath)
+ unlink($imagePath);
+
+ $this->fail(129, "Invalid image file");
+ }
+
+ return (object) [
+ "count" => sizeof($images),
+ "items" => $images,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php
index 705ff3fe..2f968803 100644
--- a/VKAPI/Handlers/Users.php
+++ b/VKAPI/Handlers/Users.php
@@ -5,13 +5,15 @@ use openvk\Web\Models\Repositories\Users as UsersRepo;
final class Users extends VKAPIRequestHandler
{
- function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100): array
+ function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100, User $authuser = null /* костыль(( */): array
{
- $this->requireUser();
+ // $this->requireUser();
+
+ if($authuser == null) $authuser = $this->getUser();
$users = new UsersRepo;
if($user_ids == "0")
- $user_ids = (string) $this->getUser()->getId();
+ $user_ids = (string) $authuser->getId();
$usrs = explode(',', $user_ids);
$response;
@@ -51,7 +53,7 @@ final class Users extends VKAPIRequestHandler
$response[$i]->verified = intval($usr->isVerified());
break;
case 'sex':
- $response[$i]->sex = $this->getUser()->isFemale() ? 1 : 2;
+ $response[$i]->sex = $usr->isFemale() ? 1 : 2;
break;
case 'has_photo':
$response[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1;
@@ -71,10 +73,10 @@ final class Users extends VKAPIRequestHandler
$response[$i]->screen_name = $usr->getShortCode();
break;
case 'friend_status':
- switch($usr->getSubscriptionStatus($this->getUser())) {
+ switch($usr->getSubscriptionStatus($authuser)) {
case 3:
case 0:
- $response[$i]->friend_status = $usr->getSubscriptionStatus($this->getUser());
+ $response[$i]->friend_status = $usr->getSubscriptionStatus($authuser);
break;
case 1:
$response[$i]->friend_status = 2;
@@ -158,13 +160,14 @@ final class Users extends VKAPIRequestHandler
$users = new UsersRepo;
$array = [];
+ $find = $users->find($q);
- foreach ($users->find($q) as $user) {
+ foreach ($find as $user) {
$array[] = $user->getId();
}
return (object)[
- "count" => $users->getFoundCount($q),
+ "count" => $find->size(),
"items" => $this->get(implode(',', $array), $fields, $offset, $count)
];
}
diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php
index ec153695..59f8c751 100644
--- a/VKAPI/Handlers/Wall.php
+++ b/VKAPI/Handlers/Wall.php
@@ -22,6 +22,32 @@ final class Wall extends VKAPIRequestHandler
foreach ($posts->getPostsFromUsersWall((int)$owner_id, 1, $count, $offset) as $post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
+
+ $attachments;
+ foreach($post->getChildren() as $attachment)
+ {
+ if($attachment instanceof \openvk\Web\Models\Entities\Photo)
+ {
+ $attachments[] = [
+ "type" => "photo",
+ "photo" => [
+ "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : null,
+ "date" => $attachment->getPublicationTime()->timestamp(),
+ "id" => $attachment->getVirtualId(),
+ "owner_id" => $attachment->getOwner()->getId(),
+ "sizes" => array([
+ "height" => 500, // Для временного компросима оставляю статическое число. Если каждый раз обращаться к файлу за количеством пикселов, то наступает пuпuська полная с производительностью, так что пока так
+ "url" => $attachment->getURL(),
+ "type" => "m",
+ "width" => 500,
+ ]),
+ "text" => "",
+ "has_tags" => false
+ ]
+ ];
+ }
+ }
+
$items[] = (object)[
"id" => $post->getVirtualId(),
"from_id" => $from_id,
@@ -35,6 +61,7 @@ final class Wall extends VKAPIRequestHandler
"can_archive" => false, // TODO MAYBE
"is_archived" => false,
"is_pinned" => $post->isPinned(),
+ "attachments" => $attachments,
"post_source" => (object)["type" => "vk"],
"comments" => (object)[
"count" => $post->getCommentsCount(),
@@ -56,6 +83,8 @@ final class Wall extends VKAPIRequestHandler
$profiles[] = $from_id;
else
$groups[] = $from_id * -1;
+
+ $attachments = null; // free attachments so it will not clone everythingg
}
if($extended == 1)
@@ -127,6 +156,31 @@ final class Wall extends VKAPIRequestHandler
$post = (new PostsRepo)->getPostById(intval($id[0]), intval($id[1]));
if($post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
+ $attachments;
+ foreach($post->getChildren() as $attachment)
+ {
+ if($attachment instanceof \openvk\Web\Models\Entities\Photo)
+ {
+ $attachments[] = [
+ "type" => "photo",
+ "photo" => [
+ "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : null,
+ "date" => $attachment->getPublicationTime()->timestamp(),
+ "id" => $attachment->getVirtualId(),
+ "owner_id" => $attachment->getOwner()->getId(),
+ "sizes" => array([
+ "height" => 500, // я ещё я заебался вставлять одинаковый код в два разных места
+ "url" => $attachment->getURL(),
+ "type" => "m",
+ "width" => 500,
+ ]),
+ "text" => "",
+ "has_tags" => false
+ ]
+ ];
+ }
+ }
+
$items[] = (object)[
"id" => $post->getVirtualId(),
"from_id" => $from_id,
@@ -141,6 +195,7 @@ final class Wall extends VKAPIRequestHandler
"is_archived" => false,
"is_pinned" => $post->isPinned(),
"post_source" => (object)["type" => "vk"],
+ "attachments" => $attachments,
"comments" => (object)[
"count" => $post->getCommentsCount(),
"can_post" => 1
@@ -161,6 +216,8 @@ final class Wall extends VKAPIRequestHandler
$profiles[] = $from_id;
else
$groups[] = $from_id * -1;
+
+ $attachments = null; // free attachments so it will not clone everythingg
}
}
diff --git a/VKAPI/Structures/Message.php b/VKAPI/Structures/Message.php
index b728925b..ba022f3f 100644
--- a/VKAPI/Structures/Message.php
+++ b/VKAPI/Structures/Message.php
@@ -11,10 +11,11 @@ final class Message
public $out;
public $title = "";
public $body;
+ public $text;
public $attachments = [];
public $fwd_messages = [];
public $emoji;
- public $important = 1;
+ public $important = true;
public $deleted = 0;
public $random_id = NULL;
}
diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php
index 0c91b11e..382332dd 100644
--- a/Web/Models/Entities/Club.php
+++ b/Web/Models/Entities/Club.php
@@ -38,12 +38,12 @@ class Club extends RowModel
return iterator_to_array($avPhotos)[0] ?? NULL;
}
- function getAvatarUrl(): string
+ function getAvatarUrl(string $size = "miniscule"): string
{
$serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"];
$avPhoto = $this->getAvatarPhoto();
- return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURL();
+ return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size);
}
function getAvatarLink(): string
diff --git a/Web/Models/Entities/Note.php b/Web/Models/Entities/Note.php
index b33f1238..762ebef8 100644
--- a/Web/Models/Entities/Note.php
+++ b/Web/Models/Entities/Note.php
@@ -48,6 +48,7 @@ class Note extends Postable
"acronym",
"blockquote",
"cite",
+ "span",
]);
$config->set("HTML.AllowedAttributes", [
"table.summary",
@@ -59,6 +60,8 @@ class Note extends Postable
"img.style",
"div.style",
"div.title",
+ "span.class",
+ "p.class",
]);
$config->set("CSS.AllowedProperties", [
"float",
@@ -68,6 +71,9 @@ class Note extends Postable
"max-width",
"font-weight",
]);
+ $config->set("Attr.AllowedClasses", [
+ "underline",
+ ]);
$purifier = new HTMLPurifier($config);
return $purifier->purify($this->getRecord()->source);
diff --git a/Web/Models/Entities/Photo.php b/Web/Models/Entities/Photo.php
index ecda93f6..0f223679 100644
--- a/Web/Models/Entities/Photo.php
+++ b/Web/Models/Entities/Photo.php
@@ -1,5 +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));
+ $res[0] = true;
+ }
+ }
+
+ if(isset($size["maxSize"])) {
+ $maxSize = (int) $size["maxSize"];
+ $image->resize($maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT);
+ } else if(isset($size["maxResolution"])) {
+ $resolution = explode("x", (string) $size["maxResolution"]);
+ $image->resize((int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT);
+ } else {
+ throw new \RuntimeException("Malformed size description: " . (string) $size["id"]);
+ }
+
+ $res[1] = $image->getWidth();
+ $res[2] = $image->getHeight();
+ if($res[1] <= 300 || $res[2] <= 300)
+ $image->save("$outputDir/" . (string) $size["id"] . ".gif");
+ else
+ $image->save("$outputDir/" . (string) $size["id"] . ".jpeg");
+
+ imagedestroy($image->getImageResource());
+ unset($image);
+
+ return $res;
+ }
+
+ private function saveImageResizedCopies(string $filename, string $hash): void
+ {
+ $dir = dirname($this->pathFromHash($hash));
+ $dir = "$dir/$hash" . "_cropped";
+ if(!is_dir($dir)) {
+ @unlink($dir); # Added to transparently bypass issues with dead pesudofolders summoned by buggy SWIFT impls (selectel)
+ mkdir($dir);
+ }
+
+ $sizes = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml");
+ if(!$sizes)
+ throw new \RuntimeException("Could not load photosizes.xml!");
+
+ $sizesMeta = [];
+ foreach($sizes->Size as $size)
+ $sizesMeta[(string) $size["id"]] = $this->resizeImage($filename, $dir, $size);
+
+ $sizesMeta = MessagePack::pack($sizesMeta);
+ $this->stateChanges("sizes", $sizesMeta);
+ }
+
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)))
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);
return true;
}
- function crop(real $left, real $top, real $width, real $height): bool
+ function crop(real $left, real $top, real $width, real $height): void
{
if(isset($this->changes["hash"]))
$hash = $this->changes["hash"];
@@ -33,7 +98,7 @@ class Photo extends Media
$image = Image::fromFile($this->pathFromHash($hash));
$image->crop($left, $top, $width, $height);
- return $image->save($this->pathFromHash($hash));
+ $image->save($this->pathFromHash($hash));
}
function isolate(): void
@@ -43,7 +108,128 @@ class Photo extends Media
DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete();
}
-
+
+ function getSizes(bool $upgrade = false, bool $forceUpdate = false): ?array
+ {
+ $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);
+ $this->save();
+
+ return $this->getSizes();
+ }
+
+ return NULL;
+ }
+
+ $res = [];
+ $sizes = MessagePack::unpack($sizes);
+ foreach($sizes as $id => $meta) {
+ $url = $this->getURL();
+ $url = str_replace(".$this->fileExtension", "_cropped/$id.", $url);
+ $url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg";
+
+ $res[$id] = (object) [
+ "url" => $url,
+ "width" => $meta[1],
+ "height" => $meta[2],
+ "crop" => $meta[0]
+ ];
+ }
+
+ [$x, $y] = $this->getDimensions();
+ $res["UPLOADED_MAXRES"] = (object) [
+ "url" => $this->getURL(),
+ "width" => $x,
+ "height" => $y,
+ "crop" => false
+ ];
+
+ return $res;
+ }
+
+ function getVkApiSizes(): ?array
+ {
+ $res = [];
+ $sizes = $this->getSizes();
+ if(!$sizes)
+ return NULL;
+
+ $manifest = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml");
+ if(!$manifest)
+ return NULL;
+
+ $mappings = [];
+ foreach($manifest->Size as $size)
+ $mappings[(string) $size["id"]] = (string) $size["vkId"];
+
+ foreach($sizes as $id => $meta)
+ $res[$mappings[$id] ?? $id] = $meta;
+
+ return $res;
+ }
+
+ function getURLBySizeId(string $size): string
+ {
+ $sizes = $this->getSizes();
+ if(!$sizes)
+ return $this->getURL();
+
+ $size = $sizes[$size];
+ if(!$size)
+ return $this->getURL();
+
+ return $size->url;
+ }
+
+ function getDimensions(): array
+ {
+ $x = $this->getRecord()->width;
+ $y = $this->getRecord()->height;
+ if(!$x) { # no sizes in database
+ $hash = $this->getRecord()->hash;
+ $image = Image::fromFile($this->pathFromHash($hash));
+
+ $x = $image->getWidth();
+ $y = $image->getHeight();
+ $this->stateChanges("width", $x);
+ $this->stateChanges("height", $y);
+ $this->save();
+ }
+
+ return [$x, $y];
+ }
+
+ function getAlbum(): ?Album
+ {
+ return (new Albums)->getAlbumByPhotoId($this);
+ }
+
+ function toVkApiStruct(): object
+ {
+ $res = (object) [];
+
+ $res->id = $res->pid = $this->getId();
+ $res->owner_id = $res->user_id = $this->getOwner()->getId()->getId();
+ $res->aid = $res->album_id = NULL;
+ $res->width = $this->getDimensions()[0];
+ $res->height = $this->getDimensions()[1];
+ $res->date = $res->created = $this->getPublicationTime()->timestamp();
+
+ $res->sizes = $this->getVkApiSizes();
+ $res->src_small = $res->photo_75 = $this->getURLBySizeId("miniscule");
+ $res->src = $res->photo_130 = $this->getURLBySizeId("tiny");
+ $res->src_big = $res->photo_604 = $this->getURLBySizeId("normal");
+ $res->src_xbig = $res->photo_807 = $this->getURLBySizeId("large");
+ $res->src_xxbig = $res->photo_1280 = $this->getURLBySizeId("larger");
+ $res->src_xxxbig = $res->photo_2560 = $this->getURLBySizeId("original");
+ $res->src_original = $res->url = $this->getURLBySizeId("UPLOADED_MAXRES");
+
+ return $res;
+ }
+
static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo
{
$photo = new static;
@@ -53,10 +239,10 @@ class Photo extends Media
$photo->setCreated(time());
$photo->setFile($file);
$photo->save();
-
+
if(!is_null($album))
$album->addPhoto($photo);
-
+
return $photo;
}
}
diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php
index 64b689e4..e3307bf9 100644
--- a/Web/Models/Entities/User.php
+++ b/Web/Models/Entities/User.php
@@ -31,11 +31,11 @@ class User extends RowModel
const NSFW_TOLERANT = 1;
const NSFW_FULL_TOLERANT = 2;
- protected function _abstractRelationGenerator(string $filename, int $page = 1): \Traversable
+ protected function _abstractRelationGenerator(string $filename, int $page = 1, int $limit = 6): \Traversable
{
$id = $this->getId();
$query = "SELECT id FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql");
- $query .= "\n LIMIT 6 OFFSET " . ( ($page - 1) * 6 );
+ $query .= "\n LIMIT " . $limit . " OFFSET " . ( ($page - 1) * $limit );
$rels = DatabaseConnection::i()->getConnection()->query($query, $id, $id);
foreach($rels as $rel) {
@@ -102,7 +102,7 @@ class User extends RowModel
return "/id" . $this->getId();
}
- function getAvatarUrl(): string
+ function getAvatarUrl(string $size = "miniscule"): string
{
$serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"];
@@ -115,7 +115,7 @@ class User extends RowModel
if(is_null($avPhoto))
return "$serverUrl/assets/packages/static/openvk/img/camera_200.png";
else
- return $avPhoto->getURL();
+ return $avPhoto->getURLBySizeId($size);
}
function getAvatarLink(): string
@@ -214,6 +214,11 @@ class User extends RowModel
{
return $this->getRecord()->block_reason;
}
+
+ function getBanInSupportReason(): ?string
+ {
+ return $this->getRecord()->block_in_support_reason;
+ }
function getType(): int
{
@@ -438,9 +443,9 @@ class User extends RowModel
];
}
- function getFriends(int $page = 1): \Traversable
+ function getFriends(int $page = 1, int $limit = 6): \Traversable
{
- return $this->_abstractRelationGenerator("get-friends", $page);
+ return $this->_abstractRelationGenerator("get-friends", $page, $limit);
}
function getFriendsCount(): int
@@ -448,9 +453,9 @@ class User extends RowModel
return $this->_abstractRelationCount("get-friends");
}
- function getFollowers(int $page = 1): \Traversable
+ function getFollowers(int $page = 1, int $limit = 6): \Traversable
{
- return $this->_abstractRelationGenerator("get-followers", $page);
+ return $this->_abstractRelationGenerator("get-followers", $page, $limit);
}
function getFollowersCount(): int
@@ -458,9 +463,9 @@ class User extends RowModel
return $this->_abstractRelationCount("get-followers");
}
- function getSubscriptions(int $page = 1): \Traversable
+ function getSubscriptions(int $page = 1, int $limit = 6): \Traversable
{
- return $this->_abstractRelationGenerator("get-subscriptions-user", $page);
+ return $this->_abstractRelationGenerator("get-subscriptions-user", $page, $limit);
}
function getSubscriptionsCount(): int
@@ -673,6 +678,11 @@ class User extends RowModel
{
return !is_null($this->getBanReason());
}
+
+ function isBannedInSupport(): bool
+ {
+ return !is_null($this->getBanInSupportReason());
+ }
function isOnline(): bool
{
diff --git a/Web/Models/Repositories/Albums.php b/Web/Models/Repositories/Albums.php
index e1bcc671..99c6c732 100644
--- a/Web/Models/Repositories/Albums.php
+++ b/Web/Models/Repositories/Albums.php
@@ -1,6 +1,7 @@
context->table("album_relations")->where(["media" => $photo->getId()])->fetch();
+
+ return $dbalbum->collection ? $this->get($dbalbum->collection) : null;
+ }
}
diff --git a/Web/Models/Repositories/Notes.php b/Web/Models/Repositories/Notes.php
index f1b19eaf..a41c6914 100644
--- a/Web/Models/Repositories/Notes.php
+++ b/Web/Models/Repositories/Notes.php
@@ -29,7 +29,7 @@ class Notes
function getUserNotes(User $user, int $page = 1, ?int $perPage = NULL): \Traversable
{
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
- foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->page($page, $perPage) as $album)
+ foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->order("created DESC")->page($page, $perPage) as $album)
yield new Note($album);
}
diff --git a/Web/Models/Repositories/Posts.php b/Web/Models/Repositories/Posts.php
index ba590baf..94ce6482 100644
--- a/Web/Models/Repositories/Posts.php
+++ b/Web/Models/Repositories/Posts.php
@@ -94,7 +94,6 @@ class Posts
{
$post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch();
if(!is_null($post))
-
return new Post($post);
else
return null;
diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php
index 66f9f5ae..9cd7d001 100644
--- a/Web/Models/Repositories/Users.php
+++ b/Web/Models/Repositories/Users.php
@@ -39,7 +39,7 @@ class Users
function find(string $query): Util\EntityStream
{
$query = "%$query%";
- $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo) LIKE ?", $query)->where("deleted", 0);
+ $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo, shortcode) LIKE ?", $query)->where("deleted", 0);
return new Util\EntityStream("User", $result);
}
diff --git a/Web/Models/shell/processVideo.ps1 b/Web/Models/shell/processVideo.ps1
index 8e87442e..4ad382dd 100644
--- a/Web/Models/shell/processVideo.ps1
+++ b/Web/Models/shell/processVideo.ps1
@@ -6,12 +6,15 @@ $hashT = $hash.substring(0, 2)
$temp = [System.IO.Path]::GetTempFileName()
$temp2 = [System.IO.Path]::GetTempFileName()
+$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID"
+$shell.SetPriority(16384)
+
Move-Item $file $temp
# video stub logic was implicitly deprecated, so we start processing at once
ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif"
-start /B /Low /Wait /Shared ffmpeg -i $temp -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf scale=640x360,setsar=1:1 -y $temp2
+ffmpeg -i $temp -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf scale=640x360,setsar=1:1 -y $temp2
Move-Item $temp2 "$dir$hashT/$hash.ogv"
Remove-Item $temp
-Remove-Item $temp2
\ No newline at end of file
+Remove-Item $temp2
diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php
index 9c356d55..39845ae4 100644
--- a/Web/Presenters/AdminPresenter.php
+++ b/Web/Presenters/AdminPresenter.php
@@ -346,7 +346,20 @@ final class AdminPresenter extends OpenVKPresenter
exit(json_encode([ "error" => "User does not exist" ]));
$user->ban($this->queryParam("reason"));
- exit(json_encode([ "reason" => $this->queryParam("reason") ]));
+ exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ]));
+ }
+
+ function renderQuickUnban(int $id): void
+ {
+ $this->assertNoCSRF();
+
+ $user = $this->users->get($id);
+ if(!$user)
+ exit(json_encode([ "error" => "User does not exist" ]));
+
+ $user->setBlock_Reason(null);
+ $user->save();
+ exit(json_encode([ "success" => true ]));
}
function renderQuickWarn(int $id): void
diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php
index a80bbc68..8d15d3e2 100644
--- a/Web/Presenters/AuthPresenter.php
+++ b/Web/Presenters/AuthPresenter.php
@@ -4,6 +4,7 @@ use openvk\Web\Models\Entities\IP;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\PasswordReset;
use openvk\Web\Models\Entities\EmailVerification;
+use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Models\Repositories\IPs;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Restores;
@@ -88,20 +89,25 @@ final class AuthPresenter extends OpenVKPresenter
if (strtotime($this->postParam("birthday")) > time())
$this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment"));
+ try {
+ $user = new User;
+ $user->setFirst_Name($this->postParam("first_name"));
+ $user->setLast_Name($this->postParam("last_name"));
+ $user->setSex((int)($this->postParam("sex") === "female"));
+ $user->setEmail($this->postParam("email"));
+ $user->setSince(date("Y-m-d H:i:s"));
+ $user->setRegistering_Ip(CONNECTING_IP);
+ $user->setBirthday(strtotime($this->postParam("birthday")));
+ $user->setActivated((int)!OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']);
+ } catch(InvalidUserNameException $ex) {
+ $this->flashFail("err", tr("error"), tr("invalid_real_name"));
+ }
+
$chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password"));
if(!$chUser)
$this->flashFail("err", tr("failed_to_register"), tr("user_already_exists"));
-
- $user = new User;
+
$user->setUser($chUser->getId());
- $user->setFirst_Name($this->postParam("first_name"));
- $user->setLast_Name($this->postParam("last_name"));
- $user->setSex((int) ($this->postParam("sex") === "female"));
- $user->setEmail($this->postParam("email"));
- $user->setSince(date("Y-m-d H:i:s"));
- $user->setRegistering_Ip(CONNECTING_IP);
- $user->setBirthday(strtotime($this->postParam("birthday")));
- $user->setActivated((int) !OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']);
$user->save();
if(!is_null($referer)) {
diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php
index e307c134..410f4d5d 100755
--- a/Web/Presenters/OpenVKPresenter.php
+++ b/Web/Presenters/OpenVKPresenter.php
@@ -204,6 +204,8 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false;
$this->template->isTimezoned = Session::i()->get("_timezoneOffset");
+ $userValidated = 0;
+ $cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0;
if(!is_null($user)) {
$this->user = (object) [];
$this->user->raw = $user;
@@ -261,6 +263,8 @@ abstract class OpenVKPresenter extends SimplePresenter
exit;
}
+ $userValidated = 1;
+ $cacheTime = 0; # Force no cache
if ($this->user->identity->onlineStatus() == 0) {
$this->user->identity->setOnline(time());
$this->user->identity->save();
@@ -271,6 +275,8 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0);
}
+ header("X-OpenVK-User-Validated: $userValidated");
+ header("X-Accel-Expires: $cacheTime");
setlocale(LC_TIME, ...(explode(";", tr("__locale"))));
parent::onStartup();
diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php
index b79b7547..01093be7 100644
--- a/Web/Presenters/PhotosPresenter.php
+++ b/Web/Presenters/PhotosPresenter.php
@@ -276,6 +276,8 @@ final class PhotosPresenter extends OpenVKPresenter
$photo->isolate();
$photo->delete();
- exit("Фотография успешно удалена!");
+
+ $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена.");
+ $this->redirect("/id0", static::REDIRECT_TEMPORARY);
}
}
diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php
index 6af05712..0fc4611f 100644
--- a/Web/Presenters/SearchPresenter.php
+++ b/Web/Presenters/SearchPresenter.php
@@ -25,6 +25,10 @@ final class SearchPresenter extends OpenVKPresenter
$type = $this->queryParam("type") ?? "users";
$page = (int) ($this->queryParam("p") ?? 1);
+ $this->willExecuteWriteAction();
+ if($query != "")
+ $this->assertUserLoggedIn();
+
// https://youtu.be/pSAWM5YuXx8
$repos = [ "groups" => "clubs", "users" => "users" ];
diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php
index a96ac465..9c2bcc8e 100644
--- a/Web/Presenters/SupportPresenter.php
+++ b/Web/Presenters/SupportPresenter.php
@@ -1,7 +1,7 @@
template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page);
}
+ if($this->template->mode === "new")
+ $this->template->banReason = $this->user->identity->getBanInSupportReason();
+
if($_SERVER["REQUEST_METHOD"] === "POST") {
+ if($this->user->identity->isBannedInSupport())
+ $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment"));
+
if(!empty($this->postParam("name")) && !empty($this->postParam("text"))) {
$this->willExecuteWriteAction();
@@ -268,4 +274,32 @@ final class SupportPresenter extends OpenVKPresenter
exit(header("HTTP/1.1 200 OK"));
}
-}
\ No newline at end of file
+
+ function renderQuickBanInSupport(int $id): void
+ {
+ $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
+ $this->assertNoCSRF();
+
+ $user = (new Users)->get($id);
+ if(!$user)
+ exit(json_encode([ "error" => "User does not exist" ]));
+
+ $user->setBlock_In_Support_Reason($this->queryParam("reason"));
+ $user->save();
+ $this->returnJson([ "success" => true, "reason" => $this->queryParam("reason") ]);
+ }
+
+ function renderQuickUnbanInSupport(int $id): void
+ {
+ $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
+ $this->assertNoCSRF();
+
+ $user = (new Users)->get($id);
+ if(!$user)
+ exit(json_encode([ "error" => "User does not exist" ]));
+
+ $user->setBlock_In_Support_Reason(null);
+ $user->save();
+ $this->returnJson([ "success" => true ]);
+ }
+}
diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php
index 13766acb..1eca5f57 100644
--- a/Web/Presenters/UserPresenter.php
+++ b/Web/Presenters/UserPresenter.php
@@ -285,7 +285,6 @@ final class UserPresenter extends OpenVKPresenter
$photo->setCreated(time());
$photo->save();
} catch(ISE $ex) {
- $name = $album->getName();
$this->flashFail("err", tr("error"), tr("error_upload_failed"));
}
diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php
index 2add61ea..5e1959e5 100644
--- a/Web/Presenters/VKAPIPresenter.php
+++ b/Web/Presenters/VKAPIPresenter.php
@@ -77,6 +77,92 @@ final class VKAPIPresenter extends OpenVKPresenter
exit; # Terminate request processing as this is definitely a CORS preflight request.
}
}
+
+ function renderPhotoUpload(string $signature): void
+ {
+ $secret = CHANDLER_ROOT_CONF["security"]["secret"];
+ $computedSignature = hash_hmac("sha3-224", $_SERVER["QUERY_STRING"], $secret);
+ if(!(strlen($signature) == 56 && sodium_memcmp($signature, $computedSignature) == 0)) {
+ header("HTTP/1.1 422 Unprocessable Entity");
+ exit("Try harder <3");
+ }
+
+ $data = unpack("vDOMAIN/Z10FIELD/vMF/vMP/PTIME/PUSER/PGROUP", base64_decode($_SERVER["QUERY_STRING"]));
+ if((time() - $data["TIME"]) > 600) {
+ header("HTTP/1.1 422 Unprocessable Entity");
+ exit("Expired");
+ }
+
+ $folder = __DIR__ . "../../tmp/api-storage/photos";
+ $maxSize = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFileSize"];
+ $maxFiles = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["api"]["maxFilesPerDomain"];
+ $usrFiles = sizeof(glob("$folder/$data[USER]_*.oct"));
+ if($usrFiles >= $maxFiles) {
+ header("HTTP/1.1 507 Insufficient Storage");
+ exit("There are $maxFiles pending already. Please save them before uploading more :3");
+ }
+
+ # Not multifile
+ if($data["MF"] === 0) {
+ $file = $_FILES[$data["FIELD"]];
+ if(!$file) {
+ header("HTTP/1.0 400");
+ exit("No file");
+ } else if($file["error"] != UPLOAD_ERR_OK) {
+ header("HTTP/1.0 500");
+ exit("File could not be consumed");
+ } else if($file["size"] > $maxSize) {
+ header("HTTP/1.0 507 Insufficient Storage");
+ exit("File is too big");
+ }
+
+ move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_" . ($usrFiles + 1) . ".oct");
+ header("HTTP/1.0 202 Accepted");
+
+ $photo = $data["USER"] . "|" . ($usrFiles + 1) . "|" . $data["GROUP"];
+ exit(json_encode([
+ "server" => "ephemeral",
+ "photo" => $photo,
+ "hash" => hash_hmac("sha3-224", $photo, $secret),
+ ]));
+ }
+
+ $files = [];
+ for($i = 1; $i <= 5; $i++) {
+ $file = $_FILES[$data["FIELD"] . $i] ?? NULL;
+ if (!$file || $file["error"] != UPLOAD_ERR_OK || $file["size"] > $maxSize) {
+ continue;
+ } else if((sizeof($files) + $usrFiles) > $maxFiles) {
+ # Clear uploaded files since they can't be saved anyway
+ foreach($files as $f)
+ unlink($f);
+
+ header("HTTP/1.1 507 Insufficient Storage");
+ exit("There are $maxFiles pending already. Please save them before uploading more :3");
+ }
+
+ $files[++$usrFiles] = move_uploaded_file($file["tmp_name"], "$folder/$data[USER]_$usrFiles.oct");
+ }
+
+ if(sizeof($files) === 0) {
+ header("HTTP/1.0 400");
+ exit("No file");
+ }
+
+ $filesManifest = [];
+ foreach($files as $id => $file)
+ $filesManifest[] = ["keyholder" => $data["USER"], "resource" => $id, "club" => $data["GROUP"]];
+
+ $filesManifest = json_encode($filesManifest);
+ $manifestHash = hash_hmac("sha3-224", $filesManifest, $secret);
+ header("HTTP/1.0 202 Accepted");
+ exit(json_encode([
+ "server" => "ephemeral",
+ "photos_list" => $filesManifest,
+ "album_id" => "undefined",
+ "hash" => $manifestHash,
+ ]));
+ }
function renderRoute(string $object, string $method): void
{
diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml
index ef2ccfdb..9f5eea9a 100644
--- a/Web/Presenters/templates/@layout.xml
+++ b/Web/Presenters/templates/@layout.xml
@@ -98,12 +98,12 @@