diff --git a/.github/workflows/build-base.yaml b/.github/workflows/build-base.yaml new file mode 100644 index 00000000..0a98503f --- /dev/null +++ b/.github/workflows/build-base.yaml @@ -0,0 +1,58 @@ +name: Build base images + +on: + schedule: + - cron: '0 0 * * *' + +env: + BASE_IMAGE_NAME: php + BASE_IMAGE_VERSION: "8.1" + +jobs: + build-cli: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + lfs: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build cli image + run: | + IMAGE_NAME=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-cli + + docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/base-php-cli.Dockerfile --build-arg VERSION=$BASE_IMAGE_VERSION + + build-apache: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + lfs: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build apache image + run: | + IMAGE_NAME=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-apache + + docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/base-php-apache.Dockerfile --build-arg VERSION=$BASE_IMAGE_VERSION diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..d4645520 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,64 @@ +name: Build images + +on: + push: + # Publish `master` as Docker `latest` image. + branches: + - master + + # Publish `v1.2.3` tags as releases. + tags: + - v* + +env: + BASE_IMAGE_NAME: openvk + DB_IMAGE_NAME: mariadb + EVENT_IMAGE_NAME: mariadb + DB_VERSION: "10.9" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + arch: ['x86_64'] + + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v3 + with: + lfs: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build base image + run: | + IMAGE_ID=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + [ "$VERSION" == "master" ] && VERSION=latest + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + + docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_ID:$VERSION . --push -f install/automated/docker/openvk.Dockerfile --build-arg GITREPO=${{ github.repository }} + + - name: Build MariaDB primary image + run: | + IMAGE_NAME=ghcr.io/${{ github.repository }}/$DB_IMAGE_NAME:$DB_VERSION-primary + + docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/mariadb-primary.Dockerfile --build-arg VERSION=$DB_VERSION + + - name: Build MariaDB event image + run: | + IMAGE_NAME=ghcr.io/${{ github.repository }}/$EVENT_IMAGE_NAME:$DB_VERSION-eventdb + + docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/mariadb-eventdb.Dockerfile --build-arg VERSION=$DB_VERSION \ No newline at end of file diff --git a/.gitignore b/.gitignore index b3bb2167..41e799c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ vendor openvk.yml +chandler.yml update.pid update.pid.old Web/static/js/node_modules @@ -10,5 +11,6 @@ tmp/* themepacks/* !themepacks/.gitkeep !themepacks/openvk_modern +!themepacks/midnight storage/* !storage/.gitkeep diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cdaea87a..00000000 --- a/Dockerfile +++ /dev/null @@ -1,82 +0,0 @@ -FROM fedora:33 - -#update and install httpd -RUN dnf -y update && dnf -y autoremove && dnf install -y httpd - -#Let's install Remi repos for PHP 7.4: -RUN dnf -y install https://rpms.remirepo.net/fedora/remi-release-$(rpm -E %fedora).rpm - -#Then enable modules that we need: -RUN dnf -y module enable php:remi-7.4 && \ -dnf -y module enable nodejs:14 - -#And install dependencies: -RUN dnf -y --skip-broken install php php-cli php-common unzip php-zip php-yaml php-gd php-pdo_mysql nodejs git - -#Don't forget about Yarn and Composer: -RUN npm i -g yarn && \ -php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \ -php composer-setup.php --filename=composer2 --install-dir=/bin --snapshot && \ -rm composer-setup.php - -#We will use Mariadb for DB: -RUN dnf -y install mysql mysql-server && \ -systemctl enable mariadb && \ -echo 'skip-grant-tables' >> /etc/my.cnf - -#Additionally, you can install ffmpeg for processing videos. -#You will need to use RPMFusion repo to install it: -RUN dnf -y install --nogpgcheck https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm && \ -dnf -y install --nogpgcheck https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm - -#Then install SDL2 and ffmpeg: -RUN dnf -y install --nogpgcheck SDL2 ffmpeg - -#Install Chandler and OpenVk/Capcha-extention in /opt: -RUN cd /opt && \ -git clone https://github.com/samukhin/chandler.git && \ -cd chandler/ && \ -composer2 install && \ -mv chandler-example.yml chandler.yml && \ -cd extensions/available/ && \ -git clone https://github.com/samukhin/commitcaptcha.git && \ -cd commitcaptcha/ && \ -composer2 install && \ -cd .. && \ -git clone https://github.com/samukhin/openvk.git && \ -cd openvk/ && \ -composer2 install && \ -cd Web/static/js && \ -yarn install && \ -cd ../../../ && \ -mv openvk-example.yml openvk.yml && \ -ln -s /opt/chandler/extensions/available/commitcaptcha/ /opt/chandler/extensions/enabled/commitcaptcha && \ -ln -s /opt/chandler/extensions/available/openvk/ /opt/chandler/extensions/enabled/openvk - -#Create database -RUN cp /opt/chandler/extensions/available/openvk/install/automated/common/create_db.service /etc/systemd/system/ && \ -chmod 644 /etc/systemd/system/create_db.service && \ -chmod 777 /opt/chandler/extensions/available/openvk/install/automated/common/autoexec && \ -systemctl enable create_db - -#Make the user apache owner of the chandler folder: -RUN cd /opt && \ -chown -R apache: chandler/ - -#Now let's create config file /etc/httpd/conf.d/10-openvk.conf and -#Also enable rewrite_module by creating /etc/httpd/conf.modules.d/02-rewrite.conf -RUN cp /opt/chandler/extensions/available/openvk/install/automated/common/10-openvk.conf /etc/httpd/conf.d/ && \ -cp /opt/chandler/extensions/available/openvk/install/automated/common/02-rewrite.conf /etc/httpd/conf.modules.d/ - -#Make directory for OpenVK logs and make the user apache owner of it: -RUN mkdir /var/log/openvk && \ -chown apache: /var/log/openvk/ - -#And start Apache: -#RUN systemctl enable httpd - -#For login -RUN dnf -y install passwd && passwd -d root - -#Start systemd -CMD ["/sbin/init"] diff --git a/README.md b/README.md index 24a893cd..ccaed853 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ Once you are done, you can login as a system administrator on the network itself 💡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)). +### Looking for Docker or Kubernetes deployment? +See `install/automated/docker/README.md` and `install/automated/kubernetes/README.md` for Docker and Kubernetes deployment instructions. + ### 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). diff --git a/README_RU.md b/README_RU.md index 2b06d593..c30a6cf7 100644 --- a/README_RU.md +++ b/README_RU.md @@ -72,6 +72,9 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions 💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)). +# Установка в Docker/Kubernetes +Подробные иструкции можно найти в `install/automated/docker/README.md` и `install/automated/kubernetes/README.md` соответственно. + ### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты? Это зависит от обстоятельств. Вы можете оставить исходные тексты при себе, если не планируете распространять бинарники вашего сайта. Если программное обеспечение вашего сайта должно распространяться, оно может оставаться не-OSS при условии, что OpenVK не используется в качестве основного приложения и не модифицируется. Если вы модифицировали OpenVK для своих нужд или ваша работа основана на нем и вы планируете ее распространять, то вы должны лицензировать ее на условиях любой совместимой с LGPL лицензии (например, OSL, GPL, LGPL и т.д.). diff --git a/ServiceAPI/Apps.php b/ServiceAPI/Apps.php index 521b117e..6504c23e 100644 --- a/ServiceAPI/Apps.php +++ b/ServiceAPI/Apps.php @@ -54,6 +54,11 @@ class Apps implements Handler $reject("No application with this id found"); return; } + + if($amount < 0) { + $reject(552, "Payment amount is invalid"); + return; + } $coinsLeft = $this->user->getCoins() - $amount; if($coinsLeft < 0) { diff --git a/ServiceAPI/Mentions.php b/ServiceAPI/Mentions.php new file mode 100644 index 00000000..33c1fc01 --- /dev/null +++ b/ServiceAPI/Mentions.php @@ -0,0 +1,50 @@ +user = $user; + } + + function resolve(int $id, callable $resolve, callable $reject): void + { + if($id > 0) { + $user = (new Users)->get($id); + if(!$user) { + $reject("Not found"); + return; + } + + $resolve([ + "url" => $user->getURL(), + "name" => $user->getFullName(), + "ava" => $user->getAvatarURL("miniscule"), + "about" => $user->getStatus() ?? "", + "online" => ($user->isFemale() ? tr("was_online_f") : tr("was_online_m")) . " " . $user->getOnline(), + "verif" => $user->isVerified(), + ]); + return; + } + + $club = (new Clubs)->get(abs($id)); + if(!$club) { + $reject("Not found"); + return; + } + + $resolve([ + "url" => $club->getURL(), + "name" => $club->getName(), + "ava" => $club->getAvatarURL("miniscule"), + "about" => $club->getDescription() ?? "", + "online" => tr("participants", $club->getFollowersCount()), + "verif" => $club->isVerified(), + ]); + } +} diff --git a/ServiceAPI/Polls.php b/ServiceAPI/Polls.php new file mode 100644 index 00000000..9d3e2e7f --- /dev/null +++ b/ServiceAPI/Polls.php @@ -0,0 +1,70 @@ +user = $user; + $this->polls = new PollRepo; + } + + private function getPollHtml(int $poll): string + { + return Router::i()->execute("/poll$poll", "SAPI"); + } + + function vote(int $pollId, string $options, callable $resolve, callable $reject): void + { + $poll = $this->polls->get($pollId); + if(!$poll) { + $reject("Poll not found"); + return; + } + + try { + $options = explode(",", $options); + $poll->vote($this->user, $options); + } catch(AlreadyVotedException $ex) { + $reject("Poll state changed: user has already voted."); + return; + } catch(PollLockedException $ex) { + $reject("Poll state changed: poll has ended."); + return; + } catch(InvalidOptionException $ex) { + $reject("Foreign options passed."); + return; + } catch(UnexpectedValueException $ex) { + $reject("Too much options passed."); + return; + } + + $resolve(["html" => $this->getPollHtml($pollId)]); + } + + function unvote(int $pollId, callable $resolve, callable $reject): void + { + $poll = $this->polls->get($pollId); + if(!$poll) { + $reject("Poll not found"); + return; + } + + try { + $poll->revokeVote($this->user); + } catch(PollLockedException $ex) { + $reject("Votes can't be revoked from this poll."); + return; + } + + $resolve(["html" => $this->getPollHtml($pollId)]); + } +} \ No newline at end of file diff --git a/VKAPI/Handlers/Account.php b/VKAPI/Handlers/Account.php index 6f4ad0e6..b05e01bd 100644 --- a/VKAPI/Handlers/Account.php +++ b/VKAPI/Handlers/Account.php @@ -1,5 +1,6 @@ $this->getUser()->getLastName(), "home_town" => $this->getUser()->getHometown(), "status" => $this->getUser()->getStatus(), - "bdate" => $this->getUser()->getBirthday()->format('%e.%m.%Y'), + "bdate" => is_null($this->getUser()->getBirthday()) ? '01.01.1970' : $this->getUser()->getBirthday()->format('%e.%m.%Y'), "bdate_visibility" => $this->getUser()->getBirthdayPrivacy(), "phone" => "+420 ** *** 228", # TODO "relation" => $this->getUser()->getMaritalStatus(), @@ -74,4 +75,77 @@ final class Account extends VKAPIRequestHandler # TODO: Filter } + + 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(); + $user = $this->getUser(); + + $output = [ + "changed" => 0, + ]; + + if(!empty($first_name) || !empty($last_name)) { + $output["name_request"] = [ + "id" => random_int(1, 2048), # For compatibility with original VK API + "status" => "success", + "first_name" => !empty($first_name) ? $first_name : $user->getFirstName(), + "last_name" => !empty($last_name) ? $last_name : $user->getLastName(), + ]; + + try { + if(!empty($first_name)) + $user->setFirst_name($first_name); + if(!empty($last_name)) + $user->setLast_Name($last_name); + } catch (InvalidUserNameException $e) { + $output["name_request"]["status"] = "declined"; + return (object) $output; + } + } + + if(!empty($screen_name)) + if (!$user->setShortCode($screen_name)) + $this->fail(1260, "Invalid screen name"); + + # For compatibility with original VK API + if($sex > 0) + $user->setSex($sex == 1 ? 1 : 0); + + if($relation > -1) + $user->setMarital_Status($relation); + + if(!empty($bdate)) { + $birthday = strtotime($bdate); + if (!is_int($birthday)) + $this->fail(100, "invalid value of bdate."); + + $user->setBirthday($birthday); + } + + # For compatibility with original VK API + switch($bdate_visibility) { + case 0: + $this->fail(946, "Hiding date of birth is not implemented."); + break; + case 1: + $user->setBirthday_privacy(0); + break; + case 2: + $user->setBirthday_privacy(1); + } + + if(!empty($home_town)) + $user->setHometown($home_town); + + if(!empty($status)) + $user->setStatus($status); + + if($sex > 0 || $relation > -1 || $bdate_visibility > 1 || !empty("$first_name$last_name$screen_name$bdate$home_town$status")) { + $output["changed"] = 1; + $user->save(); + } + + return (object) $output; + } } diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php index ca9b7573..5040674c 100644 --- a/VKAPI/Handlers/Friends.php +++ b/VKAPI/Handlers/Friends.php @@ -107,7 +107,7 @@ final class Friends extends VKAPIRequestHandler return 1; default: - fail(15, "Access denied: No friend or friend request found."); + $this->fail(15, "Access denied: No friend or friend request found."); } } @@ -133,15 +133,18 @@ final class Friends extends VKAPIRequestHandler return $response; } - function getRequests(string $fields = "", int $offset = 0, int $count = 100): object + function getRequests(string $fields = "", int $offset = 0, int $count = 100, int $extended = 0): object { + if ($count >= 1000) + $this->fail(100, "One of the required parameters was not passed or is invalid."); + $this->requireUser(); $i = 0; $offset++; $followers = []; - foreach($this->getUser()->getFollowers() as $follower) { + foreach($this->getUser()->getFollowers($offset, $count) as $follower) { $followers[$i] = $follower->getId(); $i++; } @@ -149,8 +152,10 @@ final class Friends extends VKAPIRequestHandler $response = $followers; $usersApi = new Users($this->getUser()); - if(!is_null($fields)) - $response = $usersApi->get(implode(',', $followers), $fields, 0, $count); # FIXME + if($extended == 1) + $response = $usersApi->get(implode(',', $followers), $fields, 0, $count); + else + $response = $usersApi->get(implode(',', $followers), "", 0, $count); foreach($response as $user) $user->user_id = $user->id; diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 59b19fa4..42a2d265 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -10,7 +10,7 @@ final class Groups extends VKAPIRequestHandler $this->requireUser(); if($user_id == 0) { - foreach($this->getUser()->getClubs($offset+1) as $club) + foreach($this->getUser()->getClubs($offset, false, $count, true) as $club) $clbs[] = $club; $clbsCount = $this->getUser()->getClubCount(); } else { @@ -20,7 +20,7 @@ final class Groups extends VKAPIRequestHandler if(is_null($user)) $this->fail(15, "Access denied"); - foreach($user->getClubs($offset+1) as $club) + foreach($user->getClubs($offset, false, $count, true) as $club) $clbs[] = $club; $clbsCount = $user->getClubCount(); @@ -33,17 +33,9 @@ final class Groups extends VKAPIRequestHandler $ic = $count; if(!empty($clbs)) { - $clbs = array_slice($clbs, $offset * $count); - for($i=0; $i < $ic; $i++) { $usr = $clbs[$i]; - if(is_null($usr)) { - $rClubs[$i] = (object)[ - "id" => $clbs[$i], - "name" => "DELETED", - "deactivated" => "deleted" - ]; - } else if($clbs[$i] == NULL) { + if(is_null($usr)) { } else { $rClubs[$i] = (object) [ @@ -102,23 +94,32 @@ final class Groups extends VKAPIRequestHandler ]; } - function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array + function getById(string $group_ids = "", string $group_id = "", string $fields = "", int $offset = 0, int $count = 500): ?array { + /* Both offset and count SHOULD be used only in OpenVK code, + not in your app or script, since it's not oficially documented by VK */ + $clubs = new ClubsRepo; - if($group_ids == NULL && $group_id != NULL) + if(empty($group_ids) && !empty($group_id)) $group_ids = $group_id; - if($group_ids == NULL && $group_id == NULL) + if(empty($group_ids) && empty($group_id)) $this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined"); $clbs = explode(',', $group_ids); - $response; + $response = array(); $ic = sizeof($clbs); + if(sizeof($clbs) > $count) + $ic = $count; + + $clbs = array_slice($clbs, $offset * $count); + + for($i=0; $i < $ic; $i++) { - if($i > 500) + if($i > 500 || $clbs[$i] == 0) break; if($clbs[$i] < 0) @@ -142,6 +143,7 @@ final class Groups extends VKAPIRequestHandler "screen_name" => $clb->getShortCode() ?? "club".$clb->getId(), "is_closed" => false, "type" => "group", + "is_member" => !is_null($this->getUser()) ? (int) $clb->getSubscriptionStatus($this->getUser()) : 0, "can_access_closed" => true, ]; @@ -204,10 +206,6 @@ final class Groups extends VKAPIRequestHandler else $response[$i]->can_post = $clb->canPost(); break; - case "is_member": - if(!is_null($this->getUser())) - $response[$i]->is_member = (int) $clb->getSubscriptionStatus($this->getUser()); - break; } } } @@ -215,4 +213,52 @@ final class Groups extends VKAPIRequestHandler return $response; } + + function search(string $q, int $offset = 0, int $count = 100) + { + $clubs = new ClubsRepo; + + $array = []; + $find = $clubs->find($q); + + foreach ($find as $group) + $array[] = $group->getId(); + + return (object) [ + "count" => $find->size(), + "items" => $this->getById(implode(',', $array), "", "is_admin,is_member,is_advertiser,photo_50,photo_100,photo_200", $offset, $count) + /* + * As there is no thing as "fields" by the original documentation + * i'll just bake this param by the example shown here: https://dev.vk.com/method/groups.search + */ + ]; + } + + function join(int $group_id) + { + $this->requireUser(); + + $club = (new ClubsRepo)->get($group_id); + + $isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0; + + if($isMember == 0) + $club->toggleSubscription($this->getUser()); + + return 1; + } + + function leave(int $group_id) + { + $this->requireUser(); + + $club = (new ClubsRepo)->get($group_id); + + $isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0; + + if($isMember == 1) + $club->toggleSubscription($this->getUser()); + + return 1; + } } diff --git a/VKAPI/Handlers/Messages.php b/VKAPI/Handlers/Messages.php index c1a85a0a..af7dcdcd 100644 --- a/VKAPI/Handlers/Messages.php +++ b/VKAPI/Handlers/Messages.php @@ -247,32 +247,34 @@ final class Messages extends VKAPIRequestHandler $user = (new USRRepo)->get((int) $peer); - $dialogue = new Correspondence($this->getUser(), $user); - $iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false); - $msg = $iterator[0]->unwrap(); // шоб удобнее было - $output['items'][] = [ - "peer" => [ - "id" => $user->getId(), - "type" => "user", - "local_id" => $user->getId() - ], - "last_message_id" => $msg->id, - "in_read" => $msg->id, - "out_read" => $msg->id, - "sort_id" => [ - "major_id" => 0, - "minor_id" => $msg->id, // КОНЕЧНО ЖЕ - ], - "last_conversation_message_id" => $user->getId(), - "in_read_cmid" => $user->getId(), - "out_read_cmid" => $user->getId(), - "is_marked_unread" => $iterator[0]->isUnread(), - "important" => false, // целестора когда релиз - "can_write" => [ - "allowed" => ($user->getId() === $this->getUser()->getId() || $user->getPrivacyPermission('messages.write', $this->getUser()) === true) - ] - ]; - $userslist[] = $user->getId(); + if($user) { + $dialogue = new Correspondence($this->getUser(), $user); + $iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false); + $msg = $iterator[0]->unwrap(); // шоб удобнее было + $output['items'][] = [ + "peer" => [ + "id" => $user->getId(), + "type" => "user", + "local_id" => $user->getId() + ], + "last_message_id" => $msg->id, + "in_read" => $msg->id, + "out_read" => $msg->id, + "sort_id" => [ + "major_id" => 0, + "minor_id" => $msg->id, // КОНЕЧНО ЖЕ + ], + "last_conversation_message_id" => $user->getId(), + "in_read_cmid" => $user->getId(), + "out_read_cmid" => $user->getId(), + "is_marked_unread" => $iterator[0]->isUnread(), + "important" => false, // целестора когда релиз + "can_write" => [ + "allowed" => ($user->getId() === $this->getUser()->getId() || $user->getPrivacyPermission('messages.write', $this->getUser()) === true) + ] + ]; + $userslist[] = $user->getId(); + } } if($extended == 1) { diff --git a/VKAPI/Handlers/Newsfeed.php b/VKAPI/Handlers/Newsfeed.php index 06474220..5451f449 100644 --- a/VKAPI/Handlers/Newsfeed.php +++ b/VKAPI/Handlers/Newsfeed.php @@ -2,6 +2,7 @@ namespace openvk\VKAPI\Handlers; use Chandler\Database\DatabaseConnection; use openvk\Web\Models\Repositories\Posts as PostsRepo; +use openvk\Web\Models\Entities\User; use openvk\VKAPI\Handlers\Wall; final class Newsfeed extends VKAPIRequestHandler @@ -26,7 +27,7 @@ final class Newsfeed extends VKAPIRequestHandler ->select("id") ->where("wall IN (?)", $ids) ->where("deleted", 0) - ->where("id < (?)", empty($start_from) ? time()+1 : $start_from) + ->where("id < (?)", empty($start_from) ? PHP_INT_MAX : $start_from) ->order("created DESC"); $rposts = []; @@ -35,6 +36,32 @@ final class Newsfeed extends VKAPIRequestHandler $response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); $response->next_from = end(end($posts->page((int) ($offset + 1), $count))); // ну и костыли пиздец конечно) + + return $response; + } + + function getGlobal(string $fields = "", int $start_from = 0, int $offset = 0, int $count = 30, int $extended = 0) + { + $this->requireUser(); + + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0"; + + if($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT) + $queryBase .= " AND `nsfw` = 0"; + + $start_from = empty($start_from) ? PHP_INT_MAX : $start_from; + $posts = DatabaseConnection::i()->getConnection()->query("SELECT `posts`.`id` " . $queryBase . " AND `posts`.`id` < " . $start_from . " ORDER BY `created` DESC LIMIT " . $count . " OFFSET " . $offset); + + $rposts = []; + $ids = []; + foreach($posts as $post) { + $rposts[] = (new PostsRepo)->get($post->id)->getPrettyId(); + $ids[] = $post->id; + } + + $response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); + $response->next_from = end($ids); + return $response; } } diff --git a/VKAPI/Handlers/Polls.php b/VKAPI/Handlers/Polls.php new file mode 100755 index 00000000..1c20edd4 --- /dev/null +++ b/VKAPI/Handlers/Polls.php @@ -0,0 +1,105 @@ +get($poll_id); + + if (!$poll) + $this->fail(100, "One of the parameters specified was missing or invalid: poll_id is incorrect"); + + $users = array(); + $answers = array(); + foreach($poll->getResults()->options as $answer) { + $answers[] = (object)[ + "id" => $answer->id, + "rate" => $answer->pct, + "text" => $answer->name, + "votes" => $answer->votes + ]; + } + + $userVote = array(); + foreach($poll->getUserVote($this->getUser()) as $vote) + $userVote[] = $vote[0]; + + $response = [ + "multiple" => $poll->isMultipleChoice(), + "end_date" => $poll->endsAt() == NULL ? 0 : $poll->endsAt()->timestamp(), + "closed" => $poll->hasEnded(), + "is_board" => false, + "can_edit" => false, + "can_vote" => $poll->canVote($this->getUser()), + "can_report" => false, + "can_share" => true, + "created" => 0, + "id" => $poll->getId(), + "owner_id" => $poll->getOwner()->getId(), + "question" => $poll->getTitle(), + "votes" => $poll->getVoterCount(), + "disable_unvote" => $poll->isRevotable(), + "anonymous" => $poll->isAnonymous(), + "answer_ids" => $userVote, + "answers" => $answers, + "author_id" => $poll->getOwner()->getId(), + ]; + + if ($extended) { + $response["profiles"] = (new Users)->get(strval($poll->getOwner()->getId()), $fields, 0, 1); + /* Currently there is only one person that can be shown trough "Extended" param. + * As "friends" param will be implemented, "profiles" will show more users + */ + } + + return (object) $response; + } + + function addVote(int $poll_id, string $answers_ids) + { + $this->requireUser(); + + $poll = (new PollsRepo)->get($poll_id); + + if(!$poll) + $this->fail(251, "Invalid poll id"); + + try { + $poll->vote($this->getUser(), explode(",", $answers_ids)); + return 0; + } catch(AlreadyVotedException $ex) { + return 0; + } catch(PollLockedException $ex) { + return 0; + } catch(InvalidOptionException $ex) { + $this->fail(8, "бдсм вибратор купить в киеве"); + } + } + + function deleteVote(int $poll_id) + { + $this->requireUser(); + + $poll = (new PollsRepo)->get($poll_id); + + if(!$poll) + $this->fail(251, "Invalid poll id"); + + try { + $poll->revokeVote($this->getUser()); + return 0; + } catch(PollLockedException $ex) { + $this->fail(15, "Access denied: Poll is locked or isn't revotable"); + } catch(InvalidOptionException $ex) { + $this->fail(8, "how.to. ook.bacon.in.microwova."); + } + } +} diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index c669cd02..9297dcbc 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -21,10 +21,17 @@ final class Wall extends VKAPIRequestHandler $groups = []; $cnt = $posts->getPostCountOnUserWall($owner_id); - $wallOnwer = (new UsersRepo)->get($owner_id); + if ($owner_id > 0) + $wallOnwer = (new UsersRepo)->get($owner_id); + else + $wallOnwer = (new ClubsRepo)->get($owner_id * -1); - if(!$wallOnwer || $wallOnwer->isDeleted() || $wallOnwer->isDeleted()) - $this->fail(18, "User was deleted or banned"); + if ($owner_id > 0) + if(!$wallOnwer || $wallOnwer->isDeleted()) + $this->fail(18, "User was deleted or banned"); + else + if(!$wallOnwer) + $this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls foreach($posts->getPostsFromUsersWall($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(); @@ -37,12 +44,14 @@ final class Wall extends VKAPIRequestHandler continue; $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\Post) { $repostAttachments = []; foreach($attachment->getChildren() as $repostAttachment) { if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { - if($attachment->isDeleted()) + if($repostAttachment->isDeleted()) continue; $repostAttachments[] = $this->getApiPhoto($repostAttachment); @@ -50,6 +59,11 @@ final class Wall extends VKAPIRequestHandler } } + if ($attachment->isPostedOnBehalfOfGroup()) + $groups[] = $attachment->getOwner()->getId(); + else + $profiles[] = $attachment->getOwner()->getId(); + $repost[] = [ "id" => $attachment->getVirtualId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), @@ -178,6 +192,8 @@ final class Wall extends VKAPIRequestHandler foreach($post->getChildren() as $attachment) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) { $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\Post) { $repostAttachments = []; @@ -189,7 +205,12 @@ final class Wall extends VKAPIRequestHandler $repostAttachments[] = $this->getApiPhoto($repostAttachment); /* Рекурсии, сука! Заказывали? */ } - } + } + + if ($attachment->isPostedOnBehalfOfGroup()) + $groups[] = $attachment->getOwner()->getId(); + else + $profiles[] = $attachment->getOwner()->getId(); $repost[] = [ "id" => $attachment->getVirtualId(), @@ -420,9 +441,14 @@ final class Wall extends VKAPIRequestHandler $profiles = []; foreach($comments as $comment) { + $owner = $comment->getOwner(); + $oid = $owner->getId(); + if($owner instanceof Club) + $oid *= -1; + $item = [ "id" => $comment->getId(), - "from_id" => $comment->getOwner()->getId(), + "from_id" => $oid, "date" => $comment->getPublicationTime()->timestamp(), "text" => $comment->getText(false), "post_id" => $post->getVirtualId(), @@ -561,7 +587,7 @@ final class Wall extends VKAPIRequestHandler return 1; } - + private function getApiPhoto($attachment) { return [ "type" => "photo", @@ -576,4 +602,44 @@ final class Wall extends VKAPIRequestHandler ] ]; } + + private function getApiPoll($attachment, $user) { + $answers = array(); + foreach($attachment->getResults()->options as $answer) { + $answers[] = (object)[ + "id" => $answer->id, + "rate" => $answer->pct, + "text" => $answer->name, + "votes" => $answer->votes + ]; + } + + $userVote = array(); + foreach($attachment->getUserVote($user) as $vote) + $userVote[] = $vote[0]; + + return [ + "type" => "poll", + "poll" => [ + "multiple" => $attachment->isMultipleChoice(), + "end_date" => $attachment->endsAt() == NULL ? 0 : $attachment->endsAt()->timestamp(), + "closed" => $attachment->hasEnded(), + "is_board" => false, + "can_edit" => false, + "can_vote" => $attachment->canVote($user), + "can_report" => false, + "can_share" => true, + "created" => 0, + "id" => $attachment->getId(), + "owner_id" => $attachment->getOwner()->getId(), + "question" => $attachment->getTitle(), + "votes" => $attachment->getVoterCount(), + "disable_unvote" => $attachment->isRevotable(), + "anonymous" => $attachment->isAnonymous(), + "answer_ids" => $userVote, + "answers" => $answers, + "author_id" => $attachment->getOwner()->getId(), + ] + ]; + } } diff --git a/VKAPI/README.md b/VKAPI/README.md index b3c85918..d4db764c 100644 --- a/VKAPI/README.md +++ b/VKAPI/README.md @@ -1,10 +1,12 @@ # VK API Compatability layer for OpenVK -This directory contains VK api handlers, structures and relared +This directory contains VK API handlers, structures and relared exceptions. It is still a work-in-progress functionality. -**Note**: requests to api are routed through +**Note**: requests to API are routed through openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers. +[Documentation for API clients](https://docs.openvk.su/openvk_engine/api/description/) + ## Implementing API methods VK API methods have names like this: `example.test`. To implement a diff --git a/Web/Events/NewMessageEvent.php b/Web/Events/NewMessageEvent.php index 49d102b3..3b828e90 100644 --- a/Web/Events/NewMessageEvent.php +++ b/Web/Events/NewMessageEvent.php @@ -27,14 +27,23 @@ class NewMessageEvent implements ILPEmitable if($peer === $userId) $peer = $msg->getRecipient()->getId(); + /* + * Source: + * https://github.com/danyadev/longpoll-doc + */ + return [ 4, # event type + $msg->getId(), # messageId 256, # checked for spam flag $peer, # TODO calculate peer correctly $msg->getSendTime()->timestamp(), # creation time in unix $msg->getText(), # text (formatted) + [], # empty additional info [], # empty attachments $msg->getId() << 2, # id as random_id + $peer, # conversation id + 0 # not edited yet ]; } } diff --git a/Web/Models/Entities/Alias.php b/Web/Models/Entities/Alias.php new file mode 100644 index 00000000..99f7baae --- /dev/null +++ b/Web/Models/Entities/Alias.php @@ -0,0 +1,34 @@ +getRecord()->owner_id; + } + + function getType(): string + { + if ($this->getOwnerId() < 0) + return "club"; + + return "user"; + } + + function getUser(): ?User + { + return (new Users)->get($this->getOwnerId()); + } + + function getClub(): ?Club + { + return (new Clubs)->get($this->getOwnerId() * -1); + } +} 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/Poll.php b/Web/Models/Entities/Poll.php new file mode 100644 index 00000000..6f2885b1 --- /dev/null +++ b/Web/Models/Entities/Poll.php @@ -0,0 +1,295 @@ +getRecord()->title; + } + + function getMetaDescription(): string + { + $props = []; + $props[] = tr($this->isAnonymous() ? "poll_anon" : "poll_public"); + if($this->isMultipleChoice()) $props[] = tr("poll_multi"); + if(!$this->isRevotable()) $props[] = tr("poll_lock"); + if(!is_null($this->endsAt())) $props[] = tr("poll_until", $this->endsAt()); + + return implode(" • ", $props); + } + + function getOwner(): User + { + return (new Users)->get($this->getRecord()->owner); + } + + function getOptions(): array + { + $options = $this->getRecord()->related("poll_options.poll"); + $res = []; + foreach($options as $opt) + $res[$opt->id] = $opt->name; + + return $res; + } + + function getUserVote(User $user): ?array + { + $ctx = DatabaseConnection::i()->getContext(); + $votedOpts = $ctx->table("poll_votes") + ->where(["user" => $user->getId(), "poll" => $this->getId()]); + + if($votedOpts->count() == 0) + return NULL; + + $res = []; + foreach($votedOpts as $votedOpt) { + $option = $ctx->table("poll_options")->get($votedOpt->option); + $res[] = [$option->id, $option->name]; + } + + return $res; + } + + function getVoters(int $optionId, int $page = 1, ?int $perPage = NULL): array + { + $res = []; + $ctx = DatabaseConnection::i()->getContext(); + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $voters = $ctx->table("poll_votes")->where(["poll" => $this->getId(), "option" => $optionId]); + foreach($voters->page($page, $perPage) as $vote) + $res[] = (new Users)->get($vote->user); + + return $res; + } + + function getVoterCount(?int $optionId = NULL): int + { + $votes = DatabaseConnection::i()->getContext()->table("poll_votes"); + if(!$optionId) + return $votes->select("COUNT(DISTINCT user) AS c")->where("poll", $this->getId())->fetch()->c; + + return $votes->where(["poll" => $this->getId(), "option" => $optionId])->count(); + } + + function getResults(?User $user = NULL): object + { + $ctx = DatabaseConnection::i()->getContext(); + $voted = NULL; + if(!is_null($user)) + $voted = $this->getUserVote($user); + + $result = (object) []; + $result->totalVotes = $this->getVoterCount(); + + $unsOptions = []; + foreach($this->getOptions() as $id => $title) { + $option = (object) []; + $option->id = $id; + $option->name = $title; + + $option->votes = $this->getVoterCount($id); + $option->pct = $result->totalVotes == 0 ? 0 : min(100, floor(($option->votes / $result->totalVotes) * 100)); + $option->voters = $this->getVoters($id, 1, 10); + if(!$user || !$voted) + $option->voted = NULL; + else + $option->voted = in_array([$id, $title], $voted); + + $unsOptions[$id] = $option; + } + + $optionsC = sizeof($unsOptions); + $sOptions = $unsOptions; + usort($sOptions, function($a, $b) { return $a->votes <=> $b->votes; }); + for($i = 0; $i < $optionsC; $i++) + $unsOptions[$id]->rate = $optionsC - $i - 1; + + $result->options = array_values($unsOptions); + + return $result; + } + + function isAnonymous(): bool + { + return (bool) $this->getRecord()->is_anonymous; + } + + function isMultipleChoice(): bool + { + return (bool) $this->getRecord()->allows_multiple; + } + + function isRevotable(): bool + { + return (bool) $this->getRecord()->can_revote; + } + + function endsAt(): ?DateTime + { + if(!$this->getRecord()->until) + return NULL; + + return new DateTime($this->getRecord()->until); + } + + function hasEnded(): bool + { + if($this->getRecord()->ended) + return true; + + if(!is_null($this->getRecord()->until)) + return time() >= $this->getRecord()->until; + + return false; + } + + function hasVoted(User $user): bool + { + return !is_null($this->getUserVote($user)); + } + + function canVote(User $user): bool + { + return !$this->hasEnded() && !$this->hasVoted($user); + } + + function vote(User $user, array $optionIds): void + { + if($this->hasEnded()) + throw new PollLockedException; + + if($this->hasVoted($user)) + throw new AlreadyVotedException; + + $optionIds = array_map(function($x) { return (int) $x; }, array_unique($optionIds)); + $validOpts = array_keys($this->getOptions()); + if(empty($optionIds) || (sizeof($optionIds) > 1 && !$this->isMultipleChoice())) + throw new UnexpectedValueException; + + if(sizeof(array_diff($optionIds, $validOpts)) > 0) + throw new InvalidOptionException; + + foreach($optionIds as $opt) { + DatabaseConnection::i()->getContext()->table("poll_votes")->insert([ + "user" => $user->getId(), + "poll" => $this->getId(), + "option" => $opt, + ]); + } + } + + function revokeVote(User $user): void + { + if(!$this->isRevotable()) + throw new PollLockedException; + + $this->getRecord()->related("poll_votes.poll") + ->where("user", $user->getId())->delete(); + } + + function setOwner(User $owner): void + { + $this->stateChanges("owner", $owner->getId()); + } + + function setEndDate(int $timestamp): void + { + if(!is_null($this->getRecord())) + throw new PollLockedException; + + $this->stateChanges("until", $timestamp); + } + + function setEnded(): void + { + $this->stateChanges("ended", 1); + } + + function setOptions(array $options): void + { + if(!is_null($this->getRecord())) + throw new PollLockedException; + + if(sizeof($options) > ovkGetQuirk("polls.max-opts")) + throw new TooMuchOptionsException; + + $this->choicesToPersist = $options; + } + + function setRevotability(bool $canReVote): void + { + if(!is_null($this->getRecord())) + throw new PollLockedException; + + $this->stateChanges("can_revote", $canReVote); + } + + function setAnonymity(bool $anonymous): void + { + $this->stateChanges("is_anonymous", $anonymous); + } + + function setMultipleChoice(bool $mc): void + { + $this->stateChanges("allows_multiple", $mc); + } + + function importXML(User $owner, string $xml): void + { + $xml = simplexml_load_string($xml); + $this->setOwner($owner); + $this->setTitle($xml["title"] ?? "Untitled"); + $this->setMultipleChoice(($xml["multiple"] ?? "no") == "yes"); + $this->setAnonymity(($xml["anonymous"] ?? "no") == "yes"); + $this->setRevotability(($xml["locked"] ?? "no") == "no"); + if(ctype_digit((string) ($xml["duration"] ?? ""))) + $this->setEndDate(time() + ((86400 * (int) $xml["duration"]))); + + $options = []; + foreach($xml->options->option as $opt) + $options[] = (string) $opt; + + if(empty($options)) + throw new UnexpectedValueException; + + $this->setOptions($options); + } + + static function import(User $owner, string $xml): Poll + { + $poll = new Poll; + $poll->importXML($owner, $xml); + $poll->save(); + + return $poll; + } + + function save(): void + { + if(empty($this->choicesToPersist)) + throw new InvalidStateException; + + parent::save(); + foreach($this->choicesToPersist as $option) { + DatabaseConnection::i()->getContext()->table("poll_options")->insert([ + "poll" => $this->getId(), + "name" => $option, + ]); + } + } +} diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 4afb4baa..40769292 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -1,7 +1,7 @@ getRecord()->wall; } + + function getWallOwner() + { + $w = $this->getRecord()->wall; + if($w < 0) + return (new Clubs)->get(abs($w)); + + return (new Users)->get($w); + } function getRepostCount(): int { @@ -87,7 +96,7 @@ class Post extends Postable function isDeactivationMessage(): bool { - return ($this->getRecord()->flags & 0b00100000) > 0; + return (($this->getRecord()->flags & 0b00100000) > 0) && ($this->getRecord()->owner > 0); } function isExplicit(): bool diff --git a/Web/Models/Entities/SupportAgent.php b/Web/Models/Entities/SupportAgent.php new file mode 100644 index 00000000..2f7fc21b --- /dev/null +++ b/Web/Models/Entities/SupportAgent.php @@ -0,0 +1,39 @@ +getRecord()->agent; + } + + function getName(): ?string + { + return $this->getRecord()->name; + } + + function getCanonicalName(): string + { + return $this->getName(); + } + + function getAvatarURL(): ?string + { + return $this->getRecord()->icon; + } + + function isShowNumber(): int + { + return $this->getRecord()->numerate; + } + + function getRealName(): string + { + return (new Users)->get($this->getAgentId())->getCanonicalName(); + } +} \ No newline at end of file diff --git a/Web/Models/Entities/TicketComment.php b/Web/Models/Entities/TicketComment.php index a9fb684a..2f1a5e8f 100644 --- a/Web/Models/Entities/TicketComment.php +++ b/Web/Models/Entities/TicketComment.php @@ -42,7 +42,7 @@ class TicketComment extends RowModel $alias = $this->getSupportAlias(); if(!$alias) - return OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["supportName"] . " №" . $this->getAgentNumber(); + return tr("helpdesk_agent") . " #" . $this->getAgentNumber(); $name = $alias->getName(); if($alias->shouldAppendNumber()) 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..7ea119e6 100644 --- a/Web/Models/Entities/Traits/TRichText.php +++ b/Web/Models/Entities/Traits/TRichText.php @@ -1,5 +1,6 @@ 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 { $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content"; @@ -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 115bf8c2..31d8f4c8 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; @@ -148,8 +148,9 @@ class User extends RowModel function getFirstName(bool $pristine = false): string { $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE)); - if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine) - return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE); + $tsn = tr("__transNames"); + if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine) + return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE); else return $name; } @@ -157,8 +158,9 @@ class User extends RowModel function getLastName(bool $pristine = false): string { $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE)); - if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine) - return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE); + $tsn = tr("__transNames"); + if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine) + return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE); else return $name; } @@ -535,12 +537,15 @@ class User extends RowModel return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1])); } - function getClubs(int $page = 1, bool $admin = false): \Traversable + function getClubs(int $page = 1, bool $admin = false, int $count = OPENVK_DEFAULT_PER_PAGE, bool $offset = false): \Traversable { + if(!$offset) + $page = ($page - 1) * $count; + if($admin) { $id = $this->getId(); $query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?"; - $query .= " LIMIT " . OPENVK_DEFAULT_PER_PAGE . " OFFSET " . ($page - 1) * OPENVK_DEFAULT_PER_PAGE; + $query .= " LIMIT " . $count . " OFFSET " . $page; $sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id); foreach($sel as $target) { @@ -550,7 +555,7 @@ class User extends RowModel yield $target; } } else { - $sel = $this->getRecord()->related("subscriptions.follower")->page($page, OPENVK_DEFAULT_PER_PAGE); + $sel = $this->getRecord()->related("subscriptions.follower")->limit($count, $page); foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) { $target = (new Clubs)->get($target->target); if(!$target) continue; @@ -908,6 +913,10 @@ class User extends RowModel $pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch(); if(!is_null($pClub)) return false; + + $pAlias = DatabaseConnection::i()->getContext()->table("aliases")->where("shortcode", $code)->fetch(); + if(!is_null($pAlias)) + return false; } $this->stateChanges("shortcode", $code); @@ -1035,5 +1044,6 @@ class User extends RowModel return true; } + use Traits\TBackDrops; use Traits\TSubscribable; } diff --git a/Web/Models/Exceptions/AlreadyVotedException.php b/Web/Models/Exceptions/AlreadyVotedException.php new file mode 100644 index 00000000..08363b9a --- /dev/null +++ b/Web/Models/Exceptions/AlreadyVotedException.php @@ -0,0 +1,7 @@ +context = DB::i()->getContext(); + $this->aliases = $this->context->table("aliases"); + } + + private function toAlias(?ActiveRow $ar): ?Alias + { + return is_null($ar) ? NULL : new Alias($ar); + } + + function get(int $id): ?Alias + { + return $this->toAlias($this->aliases->get($id)); + } + + function getByShortcode(string $shortcode): ?Alias + { + return $this->toAlias($this->aliases->where("shortcode", $shortcode)->fetch()); + } +} diff --git a/Web/Models/Repositories/ChandlerGroups.php b/Web/Models/Repositories/ChandlerGroups.php new file mode 100644 index 00000000..45af2a62 --- /dev/null +++ b/Web/Models/Repositories/ChandlerGroups.php @@ -0,0 +1,48 @@ +context = DB::i()->getContext(); + $this->groups = $this->context->table("ChandlerGroups"); + $this->members = $this->context->table("ChandlerACLRelations"); + $this->perms = $this->context->table("ChandlerACLGroupsPermissions"); + } + + function get(string $UUID): ?ActiveRow + { + return $this->groups->where("id", $UUID)->fetch(); + } + + function getList(): \Traversable + { + foreach($this->groups as $group) yield $group; + } + + function getMembersById(string $UUID): \Traversable + { + foreach($this->members->where("group", $UUID) as $member) + yield (new Users)->getByChandlerUser( + new ChandlerUser($this->context->table("ChandlerUsers")->where("id", $member->user)->fetch()) + ); + } + + function getUsersMemberships(string $UUID): \Traversable + { + foreach($this->members->where("user", $UUID) as $member) yield $member; + } + + function getPermissionsById(string $UUID): \Traversable + { + foreach($this->perms->where("group", $UUID) as $perm) yield $perm; + } +} diff --git a/Web/Models/Repositories/ChandlerUsers.php b/Web/Models/Repositories/ChandlerUsers.php new file mode 100644 index 00000000..a827afac --- /dev/null +++ b/Web/Models/Repositories/ChandlerUsers.php @@ -0,0 +1,39 @@ +context = DB::i()->getContext(); + $this->users = $this->context->table("ChandlerUsers"); + } + + private function toUser(?ActiveRow $ar): ?ChandlerUser + { + return is_null($ar) ? NULL : (new User($ar))->getChandlerUser(); + } + + function get(int $id): ?ChandlerUser + { + return (new Users)->get($id)->getChandlerUser(); + } + + function getById(string $UUID): ?ChandlerUser + { + return new ChandlerUser($this->users->where("id", $UUID)->fetch()); + } + + function getList(int $page = 1): \Traversable + { + foreach($this->users as $user) + yield new ChandlerUser($user); + } +} diff --git a/Web/Models/Repositories/Clubs.php b/Web/Models/Repositories/Clubs.php index b7b59251..edbe75c6 100644 --- a/Web/Models/Repositories/Clubs.php +++ b/Web/Models/Repositories/Clubs.php @@ -1,6 +1,7 @@ toClub($this->clubs->where("shortcode", $url)->fetch()); + $shortcode = $this->toClub($this->clubs->where("shortcode", $url)->fetch()); + + if ($shortcode) + return $shortcode; + + $alias = (new Aliases)->getByShortcode($url); + + if (!$alias) return NULL; + if ($alias->getType() !== "club") return NULL; + + return $alias->getClub(); } function get(int $id): ?Club @@ -45,6 +56,9 @@ class Clubs function getPopularClubs(): \Traversable { + // TODO rewrite + + /* $query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 30;"; $entries = DatabaseConnection::i()->getConnection()->query($query); @@ -54,6 +68,7 @@ class Clubs "club" => $this->get($entry["id"]), "subscriptions" => $entry["subscriptions"], ]; + */ } use \Nette\SmartObject; diff --git a/Web/Models/Repositories/Polls.php b/Web/Models/Repositories/Polls.php new file mode 100644 index 00000000..c2ba720b --- /dev/null +++ b/Web/Models/Repositories/Polls.php @@ -0,0 +1,23 @@ +polls = DatabaseConnection::i()->getContext()->table("polls"); + } + + function get(int $id): ?Poll + { + $poll = $this->polls->get($id); + if(!$poll) + return NULL; + + return new Poll($poll); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/SupportAgents.php b/Web/Models/Repositories/SupportAgents.php new file mode 100644 index 00000000..7b3a1e7e --- /dev/null +++ b/Web/Models/Repositories/SupportAgents.php @@ -0,0 +1,32 @@ +context = DatabaseConnection::i()->getContext(); + $this->agents = $this->context->table("support_names"); + } + + private function toAgent(?ActiveRow $ar) + { + return is_null($ar) ? NULL : new SupportAgent($ar); + } + + function get(int $id): ?SupportAgent + { + return $this->toAgent($this->agents->where("agent", $id)->fetch()); + } + + function isExists(int $id): bool + { + return !is_null($this->get($id)); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/TicketComments.php b/Web/Models/Repositories/TicketComments.php index 9277218f..ee05bb55 100644 --- a/Web/Models/Repositories/TicketComments.php +++ b/Web/Models/Repositories/TicketComments.php @@ -27,6 +27,13 @@ class TicketComments else return NULL; } + + function getCountByAgent(int $agent_id, int $mark = NULL): int + { + $filter = ['user_id' => $agent_id, 'user_type' => 1]; + $mark && $filter['mark'] = $mark; + return sizeof($this->comments->where($filter)); + } use \Nette\SmartObject; } diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php index 9cd7d001..63e77df0 100644 --- a/Web/Models/Repositories/Users.php +++ b/Web/Models/Repositories/Users.php @@ -1,6 +1,7 @@ context = DatabaseConnection::i()->getContext(); $this->users = $this->context->table("profiles"); + $this->aliases = $this->context->table("aliases"); } private function toUser(?ActiveRow $ar): ?User @@ -28,7 +31,17 @@ class Users function getByShortURL(string $url): ?User { - return $this->toUser($this->users->where("shortcode", $url)->fetch()); + $shortcode = $this->toUser($this->users->where("shortcode", $url)->fetch()); + + if ($shortcode) + return $shortcode; + + $alias = (new Aliases)->getByShortcode($url); + + if (!$alias) return NULL; + if ($alias->getType() !== "user") return NULL; + + return $alias->getUser(); } function getByChandlerUser(ChandlerUser $user): ?User diff --git a/Web/Models/VideoDrivers/YouTubeVideoDriver.php b/Web/Models/VideoDrivers/YouTubeVideoDriver.php index f517d7fd..93aee0e5 100644 --- a/Web/Models/VideoDrivers/YouTubeVideoDriver.php +++ b/Web/Models/VideoDrivers/YouTubeVideoDriver.php @@ -19,7 +19,7 @@ final class YouTubeVideoDriver extends VideoDriver