diff --git a/README.md b/README.md index 82df13e7..6426cc9e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ _[Русский](README_RU.md)_ **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. +VKontakte belongs to VK (formerly Mail.ru Group). 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://ovk.to/support?act=new) (you will need an OpenVK account for this). @@ -36,7 +36,7 @@ Here is our minimum hardware recommendation: ### Installation procedure -1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) +1. Install PHP 7.4, web-server, Composer, Node.js, NPM and [Chandler](https://github.com/openvk/chandler) * PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed. @@ -65,7 +65,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions 7. Copy `openvk-example.yml` to `openvk.yml` and change options to your liking 8. Run `composer install` in OpenVK directory 9. Run `composer install` in commitcaptcha directory -10. Move to `Web/static/js` and execute `yarn install` +10. Move to `Web/static/js` and execute `npm install` 11. 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): diff --git a/README_RU.md b/README_RU.md index fa09cabe..d59cef44 100644 --- a/README_RU.md +++ b/README_RU.md @@ -4,7 +4,7 @@ _[English](README.md)_ **OpenVK** — это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент, представленный здесь исходный код проекта пока не является стабильным. -ВКонтакте принадлежит Павлу Дурову и VK Group. +ВКонтакте принадлежит VK (в прошлом Mail.ru Group). Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://ovk.to/support?act=new) (для этого вам понадобится учетная запись OpenVK). @@ -28,7 +28,7 @@ _[English](README.md)_ ### Процедура установки -1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) +1. Установите PHP 7.4, веб-сервер, Composer, Node.js, NPM и [Chandler](https://github.com/openvk/chandler) * PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется. @@ -57,7 +57,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions 7. Скопируйте `openvk-example.yml` в `openvk.yml` и измените параметры под свои нужды 8. Запустите `composer install` в директории OpenVK 9. Запустите `composer install` в директории commitcaptcha -10. Перейдите в `Web/static/js` и выполните `yarn install` +10. Перейдите в `Web/static/js` и выполните `npm install` 11. Установите `openvk` в качестве корневого приложения в файле `chandler.yml` После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется): diff --git a/VKAPI/Handlers/Newsfeed.php b/VKAPI/Handlers/Newsfeed.php index 64ee1862..bf36f2b5 100644 --- a/VKAPI/Handlers/Newsfeed.php +++ b/VKAPI/Handlers/Newsfeed.php @@ -32,6 +32,7 @@ final class Newsfeed extends VKAPIRequestHandler ->select("id") ->where("wall IN (?)", $ids) ->where("deleted", 0) + ->where("suggested", 0) ->where("id < (?)", empty($start_from) ? PHP_INT_MAX : $start_from) ->where("? <= created", empty($start_time) ? 0 : $start_time) ->where("? >= created", empty($end_time) ? PHP_INT_MAX : $end_time) @@ -56,6 +57,15 @@ final class Newsfeed extends VKAPIRequestHandler if($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT) $queryBase .= " AND `nsfw` = 0"; + + if($return_banned == 0) { + $ignored_sources_ids = $this->getUser()->getIgnoredSources(0, OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50, true); + + if(sizeof($ignored_sources_ids) > 0) { + $imploded_ids = implode("', '", $ignored_sources_ids); + $queryBase .= " AND `posts`.`wall` NOT IN ('$imploded_ids')"; + } + } $start_from = empty($start_from) ? PHP_INT_MAX : $start_from; $start_time = empty($start_time) ? 0 : $start_time; @@ -74,4 +84,152 @@ final class Newsfeed extends VKAPIRequestHandler return $response; } + + function getByType(string $feed_type = 'top', string $fields = "", int $start_from = 0, int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 0, int $return_banned = 0) + { + $this->requireUser(); + + switch($feed_type) { + case 'top': + return $this->getGlobal($fields, $start_from, $start_time, $end_time, $offset, $count, $extended, $return_banned); + break; + default: + return $this->get($fields, $start_from, $start_time, $end_time, $offset, $count, $extended); + break; + } + } + + function getBanned(int $extended = 0, string $fields = "", string $name_case = "nom", int $merge = 0): object + { + $this->requireUser(); + + $offset = 0; + $count = OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50; + $banned = $this->getUser()->getIgnoredSources($offset, $count, ($extended != 1)); + $return_object = (object) [ + 'groups' => [], + 'members' => [], + ]; + + if($extended == 0) { + foreach($banned as $ban) { + if($ban > 0) + $return_object->members[] = $ban; + else + $return_object->groups[] = $ban; + } + } else { + if($merge == 1) { + $return_object = (object) [ + 'count' => sizeof($banned), + 'items' => [], + ]; + + foreach($banned as $ban) { + $return_object->items[] = $ban->toVkApiStruct($this->getUser(), $fields); + } + } else { + $return_object = (object) [ + 'groups' => [], + 'profiles' => [], + ]; + + foreach($banned as $ban) { + if($ban->getRealId() > 0) + $return_object->profiles[] = $ban->toVkApiStruct($this->getUser(), $fields); + else + $return_object->groups[] = $ban->toVkApiStruct($this->getUser(), $fields); + } + } + } + + return $return_object; + } + + function addBan(string $user_ids = "", string $group_ids = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + # Formatting input ids + if(!empty($user_ids)) { + $user_ids = array_map(function($el) { + return (int)$el; + }, explode(',', $user_ids)); + $user_ids = array_unique($user_ids); + } else + $user_ids = []; + + if(!empty($group_ids)) { + $group_ids = array_map(function($el) { + return abs((int)$el) * -1; + }, explode(',', $group_ids)); + $group_ids = array_unique($group_ids); + } else + $group_ids = []; + + $ids = array_merge($user_ids, $group_ids); + if(sizeof($ids) < 1) + return 0; + + if(sizeof($ids) > 10) + $this->fail(-10, "Limit of 'ids' is 10"); + + $config_limit = OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50; + $user_ignores = $this->getUser()->getIgnoredSourcesCount(); + if(($user_ignores + sizeof($ids)) > $config_limit) { + $this->fail(-50, "Ignoring limit exceeded"); + } + + $entities = get_entities($ids); + $successes = 0; + foreach($entities as $entity) { + if(!$entity || $entity->getRealId() === $this->getUser()->getRealId() || $entity->isHideFromGlobalFeedEnabled() || $entity->isIgnoredBy($this->getUser())) continue; + + $entity->addIgnore($this->getUser()); + $successes += 1; + } + + return 1; + } + + function deleteBan(string $user_ids = "", string $group_ids = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if(!empty($user_ids)) { + $user_ids = array_map(function($el) { + return (int)$el; + }, explode(',', $user_ids)); + $user_ids = array_unique($user_ids); + } else + $user_ids = []; + + if(!empty($group_ids)) { + $group_ids = array_map(function($el) { + return abs((int)$el) * -1; + }, explode(',', $group_ids)); + $group_ids = array_unique($group_ids); + } else + $group_ids = []; + + $ids = array_merge($user_ids, $group_ids); + if(sizeof($ids) < 1) + return 0; + + if(sizeof($ids) > 10) + $this->fail(-10, "Limit of ids is 10"); + + $entities = get_entities($ids); + $successes = 0; + foreach($entities as $entity) { + if(!$entity || $entity->getRealId() === $this->getUser()->getRealId() || !$entity->isIgnoredBy($this->getUser())) continue; + + $entity->removeIgnore($this->getUser()); + $successes += 1; + } + + return 1; + } } diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index c919b059..6121b69d 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -42,10 +42,11 @@ class Club extends RowModel return iterator_to_array($avPhotos)[0] ?? NULL; } - function getAvatarUrl(string $size = "miniscule"): string + function getAvatarUrl(string $size = "miniscule", $avPhoto = NULL): string { $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; - $avPhoto = $this->getAvatarPhoto(); + if(!$avPhoto) + $avPhoto = $this->getAvatarPhoto(); return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size); } @@ -443,30 +444,57 @@ class Club extends RowModel $res->id = $this->getId(); $res->name = $this->getName(); - $res->screen_name = $this->getShortCode(); - $res->is_closed = 0; + $res->screen_name = $this->getShortCode() ?? "club".$this->getId(); + $res->is_closed = false; + $res->type = 'group'; + $res->is_member = $user ? (int)$this->getSubscriptionStatus($user) : 0; $res->deactivated = NULL; - $res->is_admin = $user && $this->canBeModifiedBy($user); + $res->can_access_closed = true; - if($user && $this->canBeModifiedBy($user)) { - $res->admin_level = 3; + if(!is_array($fields)) + $fields = explode(',', $fields); + + $avatar_photo = $this->getAvatarPhoto(); + foreach($fields as $field) { + switch($field) { + case 'verified': + $res->verified = (int)$this->isVerified(); + break; + case 'site': + $res->site = $this->getWebsite(); + break; + case 'description': + $res->description = $this->getDescription(); + break; + case 'background': + $res->background = $this->getBackDropPictureURLs(); + break; + case 'photo_50': + $res->photo_50 = $this->getAvatarUrl('miniscule', $avatar_photo); + break; + case 'photo_100': + $res->photo_100 = $this->getAvatarUrl('tiny', $avatar_photo); + break; + case 'photo_200': + $res->photo_200 = $this->getAvatarUrl('normal', $avatar_photo); + break; + case 'photo_max': + $res->photo_max = $this->getAvatarUrl('original', $avatar_photo); + break; + case 'members_count': + $res->members_count = $this->getFollowersCount(); + break; + case 'real_id': + $res->real_id = $this->getRealId(); + break; + } } - $res->is_member = $user && $this->getSubscriptionStatus($user) ? 1 : 0; - - $res->type = "group"; - $res->photo_50 = $this->getAvatarUrl("miniscule"); - $res->photo_100 = $this->getAvatarUrl("tiny"); - $res->photo_200 = $this->getAvatarUrl("normal"); - - $res->can_create_topic = $user && $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); - - $res->can_post = $user && $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); - return $res; } use Traits\TBackDrops; use Traits\TSubscribable; use Traits\TAudioStatuses; + use Traits\TIgnorable; } diff --git a/Web/Models/Entities/Traits/TIgnorable.php b/Web/Models/Entities/Traits/TIgnorable.php new file mode 100644 index 00000000..b55ea926 --- /dev/null +++ b/Web/Models/Entities/Traits/TIgnorable.php @@ -0,0 +1,52 @@ +getContext(); + $data = [ + "owner" => $user->getId(), + "source" => $this->getRealId(), + ]; + + $sub = $ctx->table("ignored_sources")->where($data); + return $sub->count() > 0; + } + + function addIgnore(User $for_user): bool + { + DatabaseConnection::i()->getContext()->table("ignored_sources")->insert([ + "owner" => $for_user->getId(), + "source" => $this->getRealId(), + ]); + + return true; + } + + function removeIgnore(User $for_user): bool + { + DatabaseConnection::i()->getContext()->table("ignored_sources")->where([ + "owner" => $for_user->getId(), + "source" => $this->getRealId(), + ])->delete(); + + return true; + } + + function toggleIgnore(User $for_user): bool + { + if($this->isIgnoredBy($for_user)) { + $this->removeIgnore($for_user); + + return false; + } else { + $this->addIgnore($for_user); + + return true; + } + } +} diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index 18d7fea9..1e17dd82 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -112,7 +112,7 @@ class User extends RowModel return "/id" . $this->getId(); } - function getAvatarUrl(string $size = "miniscule"): string + function getAvatarUrl(string $size = "miniscule", $avPhoto = NULL): string { $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; @@ -121,7 +121,9 @@ class User extends RowModel else if($this->isBanned()) return "$serverUrl/assets/packages/static/openvk/img/banned.jpg"; - $avPhoto = $this->getAvatarPhoto(); + if(!$avPhoto) + $avPhoto = $this->getAvatarPhoto(); + if(is_null($avPhoto)) return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; else @@ -1344,11 +1346,6 @@ class User extends RowModel $res->first_name = $this->getFirstName(); $res->last_name = $this->getLastName(); $res->deactivated = $this->isDeactivated(); - $res->photo_50 = $this->getAvatarURL(); - $res->photo_100 = $this->getAvatarURL("tiny"); - $res->photo_200 = $this->getAvatarURL("normal"); - $res->photo_id = !is_null($this->getAvatarPhoto()) ? $this->getAvatarPhoto()->getPrettyId() : NULL; - $res->is_closed = $this->isClosed(); if(!is_null($user)) @@ -1357,17 +1354,60 @@ class User extends RowModel if(!is_array($fields)) $fields = explode(',', $fields); + $avatar_photo = $this->getAvatarPhoto(); foreach($fields as $field) { switch($field) { case 'is_dead': - $res->is_dead = $user->isDead(); + $res->is_dead = $this->isDead(); + break; + case 'verified': + $res->verified = (int)$this->isVerified(); + break; + case 'sex': + $res->sex = $this->isFemale() ? 1 : ($this->isNeutral() ? 0 : 2); + break; + case 'photo_50': + $res->photo_50 = $this->getAvatarUrl('miniscule', $avatar_photo); + break; + case 'photo_100': + $res->photo_100 = $this->getAvatarUrl('tiny', $avatar_photo); + break; + case 'photo_200': + $res->photo_200 = $this->getAvatarUrl('normal', $avatar_photo); + break; + case 'photo_max': + $res->photo_max = $this->getAvatarUrl('original', $avatar_photo); + break; + case 'photo_id': + $res->photo_id = $avatar_photo ? $avatar_photo->getPrettyId() : NULL; + break; + case 'background': + $res->background = $this->getBackDropPictureURLs(); + break; + case 'reg_date': + $res->reg_date = $this->getRegistrationTime()->timestamp(); + break; + case 'nickname': + $res->nickname = $this->getPseudo(); + break; + case 'rating': + $res->rating = $this->getRating(); + break; + case 'status': + $res->status = $this->getStatus(); + break; + case 'screen_name': + $res->screen_name = $this->getShortCode() ?? "id".$this->getId(); + break; + case 'real_id': + $res->real_id = $this->getRealId(); break; } } return $res; } - + function getAudiosCollectionSize() { return (new \openvk\Web\Models\Repositories\Audios)->getUserCollectionSize($this); @@ -1407,7 +1447,40 @@ class User extends RowModel return $returnArr; } + function getIgnoredSources(int $offset = 0, int $limit = 10, bool $onlyIds = false) + { + $sources = DatabaseConnection::i()->getContext()->table("ignored_sources")->where("owner", $this->getId())->limit($limit, $offset)->order('id DESC'); + $output_array = []; + + foreach($sources as $source) { + if($onlyIds) { + $output_array[] = (int)$source->source; + } else { + $ignored_source_model = NULL; + $ignored_source_id = (int)$source->source; + + if($ignored_source_id > 0) + $ignored_source_model = (new Users)->get($ignored_source_id); + else + $ignored_source_model = (new Clubs)->get(abs($ignored_source_id)); + + if(!$ignored_source_model) + continue; + + $output_array[] = $ignored_source_model; + } + } + + return $output_array; + } + + function getIgnoredSourcesCount() + { + return DatabaseConnection::i()->getContext()->table("ignored_sources")->where("owner", $this->getId())->count(); + } + use Traits\TBackDrops; use Traits\TSubscribable; use Traits\TAudioStatuses; + use Traits\TIgnorable; } diff --git a/Web/Models/Repositories/Clubs.php b/Web/Models/Repositories/Clubs.php index b393952f..de5d9269 100644 --- a/Web/Models/Repositories/Clubs.php +++ b/Web/Models/Repositories/Clubs.php @@ -43,6 +43,18 @@ class Clubs return $this->toClub($this->clubs->get($id)); } + function getByIds(array $ids = []): array + { + $clubs = $this->clubs->select('*')->where('id IN (?)', $ids); + $clubs_array = []; + + foreach($clubs as $club) { + $clubs_array[] = $this->toClub($club); + } + + return $clubs_array; + } + function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false], int $page = 1, ?int $perPage = NULL): \Traversable { $query = "%$query%"; diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php index cc75b50f..ed5a87a5 100644 --- a/Web/Models/Repositories/Users.php +++ b/Web/Models/Repositories/Users.php @@ -28,6 +28,18 @@ class Users { return $this->toUser($this->users->get($id)); } + + function getByIds(array $ids = []): array + { + $users = $this->users->select('*')->where('id IN (?)', $ids); + $users_array = []; + + foreach($users as $user) { + $users_array[] = $this->toUser($user); + } + + return $users_array; + } function getByShortURL(string $url): ?User { diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index 88844fbe..61b6d67c 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -43,6 +43,7 @@ final class GroupPresenter extends OpenVKPresenter } $this->template->club = $club; + $this->template->ignore_status = $club->isIgnoredBy($this->user->identity); } } diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index baf8f046..b64823fb 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -54,6 +54,10 @@ final class UserPresenter extends OpenVKPresenter $this->template->audioStatus = $user->getCurrentAudioStatus(); $this->template->user = $user; + + if($id !== $this->user->id) { + $this->template->ignore_status = $user->isIgnoredBy($this->user->identity); + } } } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 8cc28efe..ca4c3ece 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -197,6 +197,16 @@ final class WallPresenter extends OpenVKPresenter if($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT) $queryBase .= " AND `nsfw` = 0"; + if(((int)$this->queryParam('return_banned')) == 0) { + $ignored_sources_ids = $this->user->identity->getIgnoredSources(0, OPENVK_ROOT_CONF['openvk']['preferences']['newsfeed']['ignoredSourcesLimit'] ?? 50, true); + + if(sizeof($ignored_sources_ids) > 0) { + $imploded_ids = implode("', '", $ignored_sources_ids); + + $queryBase .= " AND `posts`.`wall` NOT IN ('$imploded_ids')"; + } + } + $posts = DatabaseConnection::i()->getConnection()->query("SELECT `posts`.`id` " . $queryBase . " ORDER BY `created` DESC LIMIT " . $pPage . " OFFSET " . ($page - 1) * $pPage); $count = DatabaseConnection::i()->getConnection()->query("SELECT COUNT(*) " . $queryBase)->fetch()->{"COUNT(*)"}; diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index b54ef44c..42a8787d 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -238,9 +238,9 @@