diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ddc730d65..04a1b3445 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: "https://openvk.su/donate" \ No newline at end of file +custom: "https://ovk.to/donate" diff --git a/.github/workflows/build-base.yaml b/.github/workflows/build-base.yaml index 0a98503f7..587ce3e7c 100644 --- a/.github/workflows/build-base.yaml +++ b/.github/workflows/build-base.yaml @@ -2,11 +2,12 @@ name: Build base images on: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" + workflow_dispatch: env: BASE_IMAGE_NAME: php - BASE_IMAGE_VERSION: "8.1" + BASE_IMAGE_VERSION: "8.2" jobs: build-cli: @@ -16,23 +17,29 @@ jobs: - 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: Change repository string to lowercase + id: repositorystring + uses: Entepotenz/change-string-case-action-min-dependencies@v1.1.0 + with: + string: ${{ github.repository }} + - 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 + IMAGE_NAME=ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/$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 @@ -40,19 +47,25 @@ jobs: - 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: Change repository string to lowercase + id: repositorystring + uses: Entepotenz/change-string-case-action-min-dependencies@v1.1.0 + with: + string: ${{ github.repository }} + - 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 + IMAGE_NAME=ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/$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 index d4645520d..638ba7cda 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,14 +1,6 @@ name: Build images -on: - push: - # Publish `master` as Docker `latest` image. - branches: - - master - - # Publish `v1.2.3` tags as releases. - tags: - - v* +on: [push, pull_request] env: BASE_IMAGE_NAME: openvk @@ -17,48 +9,136 @@ env: DB_VERSION: "10.9" jobs: - build: - runs-on: ubuntu-latest + buildbase: + name: Build base images strategy: matrix: - arch: ['x86_64'] + platform: [amd64, arm64] + + runs-on: ubuntu-latest - if: github.event_name == 'push' steps: - - uses: actions/checkout@v3 - with: - lfs: false - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 + + - name: Change repository string to lowercase + id: repositorystring + uses: Entepotenz/change-string-case-action-min-dependencies@v1.1.0 + with: + string: ${{ github.repository }} + + - name: Base image meta + id: basemeta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/${{env.BASE_IMAGE_NAME}} + labels: | + org.opencontainers.image.documentation=https://github.com/OpenVK/openvk/blob/master/install/automated/docker/Readme.md + tags: | + type=sha + type=ref,event=branch + type=ref,event=pr + type=ref,event=tag + type=raw,value=latest,enable={{is_default_branch}} - name: Log into registry + if: github.event_name != 'pull_request' 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 }} + uses: docker/build-push-action@v6 + with: + platforms: linux/${{matrix.platform}} + file: install/automated/docker/openvk.Dockerfile + tags: ${{ steps.basemeta.outputs.tags }} + labels: ${{ steps.basemeta.outputs.labels }} + push: ${{ github.event_name != 'pull_request' }} + build-args: | + GITREPO=${{ steps.repositorystring.outputs.lowercase }} + + builddb: + name: Build DB images + strategy: + matrix: + platform: [amd64, arm64] + + runs-on: ubuntu-latest + + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - name: Build MariaDB primary image - run: | - IMAGE_NAME=ghcr.io/${{ github.repository }}/$DB_IMAGE_NAME:$DB_VERSION-primary + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 - 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: Change repository string to lowercase + id: repositorystring + uses: Entepotenz/change-string-case-action-min-dependencies@v1.1.0 + with: + string: ${{ github.repository }} + + - name: MariaDB primary meta + id: db-primarymeta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/${{env.DB_IMAGE_NAME}} + labels: | + org.opencontainers.image.title=OpenVK MariaDB (Primary) + org.opencontainers.image.description=OpenVK's image for MariaDB for primary database. + org.opencontainers.image.documentation=https://github.com/OpenVK/openvk/blob/master/install/automated/docker/Readme.md + tags: | + type=sha,prefix=${{env.DB_VERSION}}-primary-sha- + type=ref,event=branch,prefix=${{env.DB_VERSION}}-primary- + type=ref,event=pr,prefix=${{env.DB_VERSION}}-primary-pr- + type=ref,event=tag,prefix=${{env.DB_VERSION}}-primary- + type=raw,value=${{env.DB_VERSION}}-primary,enable={{is_default_branch}} + + - name: MariaDB event meta + id: db-eventmeta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ steps.repositorystring.outputs.lowercase }}/${{env.DB_IMAGE_NAME}} + labels: | + org.opencontainers.image.title=OpenVK MariaDB (EventDB) + org.opencontainers.image.description=OpenVK's image for MariaDB for event database. + org.opencontainers.image.documentation=https://github.com/OpenVK/openvk/blob/master/install/automated/docker/Readme.md + tags: | + type=sha,prefix=${{env.DB_VERSION}}-eventdb-sha- + type=ref,event=branch,prefix=${{env.DB_VERSION}}-eventdb- + type=ref,event=pr,prefix=${{env.DB_VERSION}}-eventdb-pr- + type=ref,event=tag,prefix=${{env.DB_VERSION}}-eventdb- + type=raw,value=${{env.DB_VERSION}}-eventdb,enable={{is_default_branch}} + + - name: Log into registry + if: github.event_name != 'pull_request' + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build MariaDB primary image + uses: docker/build-push-action@v6 + with: + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/${{matrix.platform}} + file: install/automated/docker/mariadb-primary.Dockerfile + tags: ${{ steps.db-primarymeta.outputs.tags }} + labels: ${{ steps.db-primarymeta.outputs.labels }} + build-args: | + VERSION=${{env.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 + uses: docker/build-push-action@v6 + with: + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/${{matrix.platform}} + file: install/automated/docker/mariadb-eventdb.Dockerfile + tags: ${{ steps.db-eventmeta.outputs.tags }} + labels: ${{ steps.db-eventmeta.outputs.labels }} + build-args: | + VERSION=${{env.DB_VERSION}} \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index ecca00766..000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml \ No newline at end of file diff --git a/.idea/intellij-latte/xmlSources/Latte.dtd b/.idea/intellij-latte/xmlSources/Latte.dtd deleted file mode 100644 index 9b8273f5c..000000000 --- a/.idea/intellij-latte/xmlSources/Latte.dtd +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/intellij-latte/xmlSources/Latte.xml b/.idea/intellij-latte/xmlSources/Latte.xml deleted file mode 100644 index e7bb3f7a7..000000000 --- a/.idea/intellij-latte/xmlSources/Latte.xml +++ /dev/null @@ -1,290 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/intellij-latte/xmlSources/NetteApplication.xml b/.idea/intellij-latte/xmlSources/NetteApplication.xml deleted file mode 100644 index aa4a46853..000000000 --- a/.idea/intellij-latte/xmlSources/NetteApplication.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/intellij-latte/xmlSources/NetteForms.xml b/.idea/intellij-latte/xmlSources/NetteForms.xml deleted file mode 100644 index 036e07f6a..000000000 --- a/.idea/intellij-latte/xmlSources/NetteForms.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index f4364fd52..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index af99c76e3..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/openvk.iml b/.idea/openvk.iml deleted file mode 100644 index 16097db54..000000000 --- a/.idea/openvk.iml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml deleted file mode 100644 index 28cf2b656..000000000 --- a/.idea/php.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7b45e612c..000000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 9db25eeff..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/DBEntity.updated.php b/DBEntity.updated.php new file mode 100644 index 000000000..4c039b54e --- /dev/null +++ b/DBEntity.updated.php @@ -0,0 +1,140 @@ +getTable()->getName(); + if($_table !== $this->tableName) + throw new ISE("Invalid data supplied for model: table $_table is not compatible with table" . $this->tableName); + + $this->record = $row; + } + + function __call(string $fName, array $args) + { + if(substr($fName, 0, 3) === "set") { + $field = mb_strtolower(substr($fName, 3)); + $this->stateChanges($field, $args[0]); + } else { + throw new \Error("Call to undefined method " . get_class($this) . "::$fName"); + } + } + + private function getTable(): Selection + { + return DatabaseConnection::i()->getContext()->table($this->tableName); + } + + protected function getRecord(): ?ActiveRow + { + return $this->record; + } + + protected function stateChanges(string $column, $value): void + { + if(!is_null($this->record)) + $t = $this->record->{$column}; #Test if column exists + + $this->changes[$column] = $value; + } + + function getId() + { + return $this->getRecord()->id; + } + + function isDeleted(): bool + { + return (bool) $this->getRecord()->deleted; + } + + function unwrap(): object + { + return (object) $this->getRecord()->toArray(); + } + + function delete(bool $softly = true): void + { + $user = CurrentUser::i()->getUser(); + $user_id = is_null($user) ? (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getId(); + + if(is_null($this->record)) + throw new ISE("Can't delete a model, that hasn't been flushed to DB. Have you forgotten to call save() first?"); + + (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 2, $this->record->toArray(), $this->changes); + + if($softly) { + $this->record = $this->getTable()->where("id", $this->record->id)->update(["deleted" => true]); + } else { + $this->record->delete(); + $this->deleted = true; + } + } + + function undelete(): void + { + if(is_null($this->record)) + throw new ISE("Can't undelete a model, that hasn't been flushed to DB. Have you forgotten to call save() first?"); + + $user = CurrentUser::i()->getUser(); + $user_id = is_null($user) ? (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getId(); + + (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 3, $this->record->toArray(), ["deleted" => false]); + + $this->getTable()->where("id", $this->record->id)->update(["deleted" => false]); + } + + function save(?bool $log = true): void + { + if ($log) { + $user = CurrentUser::i(); + $user_id = is_null($user) ? (int)OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"] : $user->getUser()->getId(); + } + + if(is_null($this->record)) { + $this->record = $this->getTable()->insert($this->changes); + + if ($log && $this->getTable()->getName() !== "logs") { + (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 0, $this->record->toArray(), $this->changes); + } + } else { + if ($log && $this->getTable()->getName() !== "logs") { + (new Logs)->create($user_id, $this->getTable()->getName(), get_class($this), 1, $this->record->toArray(), $this->changes); + } + + if ($this->deleted) { + $this->record = $this->getTable()->insert((array)$this->record); + } else { + $this->getTable()->get($this->record->id)->update($this->changes); + $this->record = $this->getTable()->get($this->record->id); + } + } + + $this->changes = []; + } + + function getTableName(): string + { + return $this->getTable()->getName(); + } + + use \Nette\SmartObject; +} diff --git a/Email/hello.eml.latte b/Email/hello.eml.latte index 48fd562ee..8ae664992 100644 --- a/Email/hello.eml.latte +++ b/Email/hello.eml.latte @@ -12,7 +12,7 @@
- Добро пожаловать в OpenVK! Приятного времяприпровождения, надеюсь вам понравится.

Если появились вопросы, касаемые нашего сайта, пишите сюда + Добро пожаловать в OpenVK! Приятного времяприпровождения, надеюсь вам понравится.

Если появились вопросы, касаемые нашего сайта, пишите сюда
diff --git a/README.md b/README.md index 152e05bff..6426cc9e4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# openvkOpenVK +# openvkOpenVK _[Русский](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://openvk.su/support?act=new) (you will need an OpenVK account for this). +To be honest, we don't know whether if it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://ovk.to/support?act=new) (you will need an OpenVK account for this). ## When's the release? @@ -26,11 +26,19 @@ However, OVK makes use of Chandler Application Server. This software requires ex If you want, you can add your instance to the list above so that people can register there. +### System requirements + +Here is our minimum hardware recommendation: + +* **CPU: Recent** (AMD Zen2 or equivalent) quad-core 2GHz+ CPU +* **RAM:** At least 2GB RAM (we recommend 6GB or 8GB for OpenVK with Kafka) +* **Minimum database space:** 10GB + ### 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.1 is supported too, however it was not tested carefully, so be aware. +* PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed. 2. Install MySQL-compatible database. @@ -57,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): @@ -66,12 +74,12 @@ Once you are done, you can login as a system administrator on the network itself * **Password**: `admin` * It is recommended to change the password of the built-in account or disable it. -💡Confused? Full installation walkthrough is available [here](https://docs.openvk.uk/openvk_engine/centos8_installation/) (CentOS 8 [and](https://almalinux.org/) [family](https://yum.oracle.com/oracle-linux-isos.html)). +💡 Confused? Full installation walkthrough is available [here](https://docs.ovk.to/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? +### If my website uses OpenVK, should I release its 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 are planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc). @@ -80,7 +88,7 @@ It depends. You can keep the sources to yourself if you do not plan to distribut You may reach out to us via: * [Bug Tracker](https://github.com/openvk/openvk/projects/1) -* [Ticketing System](https://openvk.su/support?act=new) +* [Ticketing System](https://ovk.to/support?act=new) * Telegram Chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu. * [Reddit](https://www.reddit.com/r/openvk/) * [GitHub Discussions](https://github.com/openvk/openvk/discussions) diff --git a/README_RU.md b/README_RU.md index cc4f672fa..d59cef44f 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,12 +1,12 @@ -# openvkOpenVK +# openvkOpenVK _[English](README.md)_ **OpenVK** — это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент, представленный здесь исходный код проекта пока не является стабильным. -ВКонтакте принадлежит Павлу Дурову и VK Group. +ВКонтакте принадлежит VK (в прошлом Mail.ru Group). -Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OpenVK). +Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://ovk.to/support?act=new) (для этого вам понадобится учетная запись OpenVK). ## Когда выйдет релизная версия? @@ -28,9 +28,9 @@ _[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 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает). +* PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется. 2. Установите MySQL-совместимую базу данных. @@ -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` После этого вы можете войти как системный администратор в саму сеть (регистрация не требуется): @@ -66,7 +66,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions * **Пароль**: `admin` * Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её. -💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.uk/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)). +💡Запутались? Полное руководство по установке доступно [здесь](https://docs.ovk.to/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` соответственно. @@ -80,7 +80,7 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions Вы можете связаться с нами через: * [Баг-трекер](https://github.com/openvk/openvk/projects/1) -* [Помощь в OVK](https://openvk.su/support?act=new) +* [Помощь в OVK](https://ovk.to/support?act=new) * Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала. * [Reddit](https://www.reddit.com/r/openvk/) * [GitHub Discussions](https://github.com/openvk/openvk/discussions) diff --git a/ServiceAPI/Apps.php b/ServiceAPI/Apps.php index 6504c23e9..a09dc49fa 100644 --- a/ServiceAPI/Apps.php +++ b/ServiceAPI/Apps.php @@ -1,8 +1,11 @@ withdrawCoins(); $resolve($coins); } + + function getRegularToken(string $clientName, bool $acceptsStale, callable $resolve, callable $reject): void + { + $token = NULL; + $stale = true; + if($acceptsStale) + $token = (new APITokens)->getStaleByUser($this->user->getId(), $clientName); + + if(is_null($token)) { + $stale = false; + $token = new APIToken; + $token->setUser($this->user); + $token->setPlatform($clientName ?? (new WhichBrowser\Parser(getallheaders()))->toString()); + $token->save(); + } + + $resolve([ + 'is_stale' => $stale, + 'token' => $token->getFormattedToken(), + ]); + } } \ No newline at end of file diff --git a/ServiceAPI/Notes.php b/ServiceAPI/Notes.php new file mode 100644 index 000000000..ea76267e6 --- /dev/null +++ b/ServiceAPI/Notes.php @@ -0,0 +1,44 @@ +user = $user; + $this->notes = new NoteRepo; + } + + function getNote(int $noteId, callable $resolve, callable $reject): void + { + $note = $this->notes->get($noteId); + if(!$note || $note->isDeleted()) + $reject(83, "Note is gone"); + + $noteOwner = $note->getOwner(); + assert($noteOwner instanceof User); + if(!$noteOwner->getPrivacyPermission("notes.read", $this->user)) + $reject(160, "You don't have permission to access this note"); + + if(!$note->canBeViewedBy($this->user)) + $reject(15, "Access to note denied"); + + $resolve([ + "title" => $note->getName(), + "link" => "/note" . $note->getPrettyId(), + "html" => $note->getText(), + "created" => (string) $note->getPublicationTime(), + "author" => [ + "name" => $noteOwner->getCanonicalName(), + "ava" => $noteOwner->getAvatarUrl(), + "link" => $noteOwner->getURL(), + ], + ]); + } +} diff --git a/ServiceAPI/Wall.php b/ServiceAPI/Wall.php index 628ceb227..ed7251ac9 100644 --- a/ServiceAPI/Wall.php +++ b/ServiceAPI/Wall.php @@ -2,25 +2,34 @@ namespace openvk\ServiceAPI; use openvk\Web\Models\Entities\Post; use openvk\Web\Models\Entities\User; -use openvk\Web\Models\Repositories\Posts; +use openvk\Web\Models\Repositories\{Posts, Notes, Videos}; class Wall implements Handler { protected $user; protected $posts; + protected $notes; function __construct(?User $user) { $this->user = $user; $this->posts = new Posts; + $this->notes = new Notes; + $this->videos = new Videos; } function getPost(int $id, callable $resolve, callable $reject): void { $post = $this->posts->get($id); if(!$post || $post->isDeleted()) - $reject("No post with id=$id"); + $reject(53, "No post with id=$id"); + + if($post->getSuggestionType() != 0) + $reject(25, "Can't get suggested post"); + if(!$post->canBeViewedBy($this->user)) + $reject(12, "Access denied"); + $res = (object) []; $res->id = $post->getId(); $res->wall = $post->getTargetWall(); diff --git a/VKAPI/Handlers/Account.php b/VKAPI/Handlers/Account.php index a20c42be2..dcdf18c00 100644 --- a/VKAPI/Handlers/Account.php +++ b/VKAPI/Handlers/Account.php @@ -7,19 +7,32 @@ final class Account extends VKAPIRequestHandler function getProfileInfo(): object { $this->requireUser(); - - return (object) [ - "first_name" => $this->getUser()->getFirstName(), - "id" => $this->getUser()->getId(), - "last_name" => $this->getUser()->getLastName(), - "home_town" => $this->getUser()->getHometown(), - "status" => $this->getUser()->getStatus(), - "bdate" => is_null($this->getUser()->getBirthday()) ? '01.01.1970' : $this->getUser()->getBirthday()->format('%e.%m.%Y'), - "bdate_visibility" => $this->getUser()->getBirthdayPrivacy(), + $user = $this->getUser(); + $return_object = (object) [ + "first_name" => $user->getFirstName(), + "photo_200" => $user->getAvatarURL("normal"), + "nickname" => $user->getPseudo(), + "is_service_account" => false, + "id" => $user->getId(), + "is_verified" => $user->isVerified(), + "verification_status" => $user->isVerified() ? 'verified' : 'unverified', + "last_name" => $user->getLastName(), + "home_town" => $user->getHometown(), + "status" => $user->getStatus(), + "bdate" => is_null($user->getBirthday()) ? '01.01.1970' : $user->getBirthday()->format('%e.%m.%Y'), + "bdate_visibility" => $user->getBirthdayPrivacy(), "phone" => "+420 ** *** 228", # TODO - "relation" => $this->getUser()->getMaritalStatus(), - "sex" => $this->getUser()->isFemale() ? 1 : 2 + "relation" => $user->getMaritalStatus(), + "screen_name" => $user->getShortCode(), + "sex" => $user->isFemale() ? 1 : 2, + #"email" => $user->getEmail(), ]; + + $audio_status = $user->getCurrentAudioStatus(); + if(!is_null($audio_status)) + $return_object->audio_status = $audio_status->toVkApiStruct($user); + + return $return_object; } function getInfo(): object @@ -151,4 +164,68 @@ function saveProfileInfo(string $first_name = "", string $last_name = "", string return (object) $output; } + + function getBalance(): object + { + $this->requireUser(); + if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) + $this->fail(105, "Commerce is disabled on this instance"); + + return (object) ['votes' => $this->getUser()->getCoins()]; + } + + function getOvkSettings(): object + { + $this->requireUser(); + $user = $this->getUser(); + + $settings_list = (object)[ + 'avatar_style' => $user->getStyleAvatar(), + 'style' => $user->getStyle(), + 'show_rating' => !$user->prefersNotToSeeRating(), + 'nsfw_tolerance' => $user->getNsfwTolerance(), + 'post_view' => $user->hasMicroblogEnabled() ? 'microblog' : 'old', + 'main_page' => $user->getMainPage() == 0 ? 'my_page' : 'news', + ]; + + return $settings_list; + } + + function sendVotes(int $receiver, int $value, string $message = ""): object + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) + $this->fail(-105, "Commerce is disabled on this instance"); + + if($receiver < 0) + $this->fail(-248, "Invalid receiver id"); + + if($value < 1) + $this->fail(-248, "Invalid value"); + + if(iconv_strlen($message) > 255) + $this->fail(-249, "Message is too long"); + + if($this->getUser()->getCoins() < $value) + $this->fail(-252, "Not enough votes"); + + $receiver_entity = (new \openvk\Web\Models\Repositories\Users)->get($receiver); + if(!$receiver_entity || $receiver_entity->isDeleted()) + $this->fail(-250, "Invalid receiver"); + + if($receiver_entity->getId() === $this->getUser()->getId()) + $this->fail(-251, "Can't transfer votes to yourself"); + + $this->getUser()->setCoins($this->getUser()->getCoins() - $value); + $this->getUser()->save(); + + $receiver_entity->setCoins($receiver_entity->getCoins() + $value); + $receiver_entity->save(); + + (new \openvk\Web\Models\Entities\Notifications\CoinsTransferNotification($receiver_entity, $this->getUser(), $value, $message))->emit(); + + return (object) ['votes' => $this->getUser()->getCoins()]; + } } diff --git a/VKAPI/Handlers/Audio.php b/VKAPI/Handlers/Audio.php index 3fa68e722..e0991af92 100644 --- a/VKAPI/Handlers/Audio.php +++ b/VKAPI/Handlers/Audio.php @@ -1,22 +1,793 @@ fail(0404, "Audio not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(" . $audio->getPrettyId() . ")"); + + # рофлан ебало + $privApi = $hash && $GLOBALS["csrfCheck"]; + $audioObj = $audio->toVkApiStruct($this->getUser()); + if(!$privApi) { + $audioObj->manifest = false; + $audioObj->keys = false; + } + + if($need_user) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($audio->getOwner()->getId()); + $audioObj->user = (object) [ + "id" => $user->getId(), + "photo" => $user->getAvatarUrl(), + "name" => $user->getCanonicalName(), + "name_gen" => $user->getCanonicalName(), + ]; + } + + return $audioObj; + } + + private function streamToResponse(EntityStream $es, int $offset, int $count, ?string $hash = NULL): object + { + $items = []; + foreach($es->offsetLimit($offset, $count) as $audio) { + $items[] = $this->toSafeAudioStruct($audio, $hash); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + private function validateGenre(?string& $genre_str, ?int $genre_id): void + { + if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre_str"); + } else if(!is_null($genre_id)) { + $genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre_str) + $this->fail(8, "Invalid genre ID $genre_id"); + } + } + + private function audioFromAnyId(string $id): ?AEntity + { + $descriptor = explode("_", $id); + if(sizeof($descriptor) === 1) { + if(ctype_digit($descriptor[0])) { + $audio = (new Audios)->get((int) $descriptor[0]); + } else { + $aid = base64_decode($descriptor[0], true); + if(!$aid) + $this->fail(8, "Invalid audio $id"); + + $audio = (new Audios)->get((int) $aid); + } + } else if(sizeof($descriptor) === 2) { + $audio = (new Audios)->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]); + } else { + $this->fail(8, "Invalid audio $id"); + } + + return $audio; + } + + function getById(string $audios, ?string $hash = NULL, int $need_user = 0): object + { + $this->requireUser(); + + $audioIds = array_unique(explode(",", $audios)); + if(sizeof($audioIds) === 1) { + $audio = $this->audioFromAnyId($audioIds[0]); + + return (object) [ + "count" => 1, + "items" => [ + $this->toSafeAudioStruct($audio, $hash, (bool) $need_user), + ], + ]; + } else if(sizeof($audioIds) > 6000) { + $this->fail(1980, "Can't get more than 6000 audios at once"); + } + + $audios = []; + foreach($audioIds as $id) + $audios[] = $this->getById($id, $hash)->items[0]; + + return (object) [ + "count" => sizeof($audios), + "items" => $audios, + ]; + } + + function isLagtrain(string $audio_id): int + { + $this->requireUser(); + + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + $this->fail(0404, "Audio not found"); + + # Possible information disclosure risks are acceptable :D + return (int) (strpos($audio->getName(), "Lagtrain") !== false); + } + + // TODO stub + function getRecommendations(): object + { + return (object) [ + "count" => 0, + "items" => [], + ]; + } + + function getPopular(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_POPULAR, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getFeed(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_NEW, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function search(string $q, int $auto_complete = 0, int $lyrics = 0, int $performer_only = 0, int $sort = 2, int $search_own = 0, int $offset = 0, int $count = 30, ?string $hash = NULL): object + { + $this->requireUser(); + + if(($auto_complete + $search_own) != 0) + $this->fail(10, "auto_complete and search_own are not supported"); + else if($count > 300 || $count < 1) + $this->fail(8, "count is invalid: $count"); + + $results = (new Audios)->search($q, $sort, (bool) $performer_only, (bool) $lyrics); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getCount(int $owner_id, int $uploaded_only = 0): int + { + $this->requireUser(); + + if($owner_id < 0) { + $owner_id *= -1; + $group = (new Clubs)->get($owner_id); + if(!$group) + $this->fail(0404, "Group not found"); + + return (new Audios)->getClubCollectionSize($group); + } + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + if(!$user) + $this->fail(0404, "User not found"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied"); + + if($uploaded_only) { + return DatabaseConnection::i()->getContext()->table("audios") + ->where([ + "deleted" => false, + "owner" => $owner_id, + ])->count(); + } + + return (new Audios)->getUserCollectionSize($user); + } + + function get(int $owner_id = 0, int $album_id = 0, string $audio_ids = '', int $need_user = 1, int $offset = 0, int $count = 100, int $uploaded_only = 0, int $need_seed = 0, ?string $shuffle_seed = NULL, int $shuffle = 0, ?string $hash = NULL): object { - $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; - - return (object) [ - "count" => 1, - "items" => [(object) [ - "id" => 1, - "owner_id" => 1, - "artist" => "В ОВК ПОКА НЕТ МУЗЫКИ", - "title" => "ЖДИТЕ :)))", - "duration" => 22, - "url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3" - ]] - ]; + $this->requireUser(); + + $shuffleSeed = NULL; + $shuffleSeedStr = NULL; + if($shuffle == 1) { + if(!$shuffle_seed) { + if($need_seed == 1) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeedStr = base64_encode($shuffleSeed); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + } else { + $hOffset = ((int) date("i") * 60) + (int) date("s"); + $thisHour = time() - $hOffset; + $shuffleSeed = $thisHour + $this->getUser()->getId(); + $shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed))); + } + } else { + $shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed))); + $shuffleSeedStr = $shuffle_seed; + } + } + + if($album_id != 0) { + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "album_id invalid"); + else if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Can't open this album for reading"); + + $songs = []; + $list = $album->getAudios($offset, $count, $shuffleSeed); + + foreach($list as $song) + $songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1); + + $response = (object) [ + "count" => sizeof($songs), + "items" => $songs, + ]; + if(!is_null($shuffleSeed)) + $response->shuffle_seed = $shuffleSeedStr; + + return $response; + } + + if(!empty($audio_ids)) { + $audio_ids = explode(",", $audio_ids); + if(!$audio_ids) + $this->fail(10, "Audio::get@L0d186:explode(string): Unknown error"); + else if(sizeof($audio_ids) < 1) + $this->fail(8, "Invalid audio_ids syntax"); + + if(!is_null($shuffleSeed)) + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + + $obj = $this->getById(implode(",", $audio_ids), $hash, $need_user); + if(!is_null($shuffleSeed)) + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $dbCtx = DatabaseConnection::i()->getContext(); + if($uploaded_only == 1) { + if($owner_id <= 0) + $this->fail(8, "uploaded_only can only be used with owner_id > 0"); + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(0602, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + + if(!is_null($shuffleSeed)) { + $audio_ids = []; + $query = $dbCtx->table("audios")->select("virtual_id")->where([ + "owner" => $owner_id, + "deleted" => 0, + ]); + + foreach($query as $res) + $audio_ids[] = $res->virtual_id; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; # audio.getById query + foreach($audio_ids as $aid) + $audio_q .= ",$owner_id" . "_$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $res = (new Audios)->getByUploader((new \openvk\Web\Models\Repositories\Users)->get($owner_id)); + + return $this->streamToResponse($res, $offset, $count, $hash, $need_user); + } + + $query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id); + if(!is_null($shuffleSeed)) { + $audio_ids = []; + foreach($query as $aid) + $audio_ids[] = $aid->audio; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; + foreach($audio_ids as $aid) + $audio_q .= ",$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $items = []; + + if($owner_id > 0) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(50, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + } + + $audios = (new Audios)->getByEntityID($owner_id, $offset, $count); + foreach($audios as $audio) + $items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1); + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; } + + function getLyrics(int $lyrics_id): object + { + $this->requireUser(); + + $audio = (new Audios)->get($lyrics_id); + if(!$audio || !$audio->getLyrics()) + $this->fail(0404, "Not found"); + + if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to lyrics"); + + return (object) [ + "lyrics_id" => $lyrics_id, + "text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()), + ]; + } + + function beacon(int $aid, ?int $gid = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->get($aid); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to listen this audio"); + + $group = NULL; + if(!is_null($gid)) { + $group = (new Clubs)->get($gid); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + } + + return (int) $audio->listen($group ?? $this->getUser()); + } + + function setBroadcast(string $audio, string $target_ids): array + { + $this->requireUser(); + + [$owner, $aid] = explode("_", $audio); + $song = (new Audios)->getByOwnerAndVID((int) $owner, (int) $aid); + $ids = []; + foreach(explode(",", $target_ids) as $id) { + $id = (int) $id; + if($id > 0) { + if ($id != $this->getUser()->getId()) { + $this->fail(600, "Can't listen on behalf of $id"); + } else { + $ids[] = $id; + $this->beacon($song->getId()); + continue; + } + } + + $group = (new Clubs)->get($id * -1); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203,"Insufficient rights to this group"); + + $ids[] = $id; + $this->beacon($song ? $song->getId() : 0, $id * -1); + } + + return $ids; + } + + function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = NULL): object + { + $this->requireUser(); + + if(!in_array($filter, ["all", "friends", "groups"])) + $this->fail(8, "Invalid filter $filter"); + + $broadcastList = $this->getUser()->getBroadcastList($filter); + $items = []; + foreach($broadcastList as $res) { + $struct = $res->toVkApiStruct(); + $status = $res->getCurrentAudioStatus(); + + $struct->status_audio = $status ? $this->toSafeAudioStruct($status) : NULL; + $items[] = $struct; + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + function edit(int $owner_id, int $audio_id, ?string $artist = NULL, ?string $title = NULL, ?string $text = NULL, ?int $genre_id = NULL, ?string $genre_str = NULL, int $no_search = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeModifiedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to edit this audio"); + + if(!is_null($genre_id)) { + $genre = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre) + $this->fail(8, "Invalid genre ID $genre_id"); + + $audio->setGenre($genre); + } else if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre ID $genre_str"); + + $audio->setGenre($genre_str); + } + + $lyrics = 0; + if(!is_null($text)) { + $audio->setLyrics($text); + $lyrics = $audio->getId(); + } + + if(!is_null($artist)) + $audio->setPerformer($artist); + + if(!is_null($title)) + $audio->setName($title); + + $audio->setSearchability(!((bool) $no_search)); + $audio->setEdited(time()); + $audio->save(); + + return $lyrics; + } + + function add(int $audio_id, int $owner_id, ?int $group_id = NULL, ?int $album_id = NULL): string + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if(!is_null($album_id)) + $this->fail(10, "album_id not implemented"); + + // TODO get rid of dups + $to = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $to = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)"); + + try { + $audio->add($to); + } catch(\OverflowException $ex) { + $this->fail(300, "Album is full"); + } + + return $audio->getPrettyId(); + } + + function delete(int $audio_id, int $owner_id, ?int $group_id = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $from = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $from = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + + $audio->remove($from); + + return 1; + } + + function restore(int $audio_id, int $owner_id, ?int $group_id = NULL, ?string $hash = NULL): object + { + $this->requireUser(); + + $vid = $this->add($audio_id, $owner_id, $group_id); + + return $this->getById($vid, $hash)->items[0]; + } + + function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object + { + $this->requireUser(); + + $owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id; + $playlists = []; + + if($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(50, "Access to playlists denied"); + } + + foreach((new Audios)->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 1) + continue; + + $playlists[] = NULL; + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + function searchAlbums(string $query = '', int $offset = 0, int $limit = 25, int $drop_private = 0, int $order = 0, int $from_me = 0): object + { + $this->requireUser(); + + $playlists = []; + $params = []; + $order_str = (['id', 'length', 'listens'][$order] ?? 'id'); + if($from_me === 1) + $params['from_me'] = $this->getUser()->getId(); + + $search = (new Audios)->findPlaylists($query, $params, ['type' => $order_str, 'invert' => false]); + foreach($search->offsetLimit($offset, $limit) as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 0) + $playlists[] = NULL; + + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => $search->size(), + "items" => $playlists, + ]; + } + + function addAlbum(string $title, ?string $description = NULL, int $group_id = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $group = NULL; + if($group_id != 0) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this group"); + } + + $album = new Playlist; + $album->setName($title); + if(!is_null($group)) + $album->setOwner($group_id * -1); + else + $album->setOwner($this->getUser()->getId()); + + if(!is_null($description)) + $album->setDescription($description); + + $album->save(); + if(!is_null($group)) + $album->bookmark($group); + else + $album->bookmark($this->getUser()); + + return $album->getId(); + } + + function editAlbum(int $album_id, ?string $title = NULL, ?string $description = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + if(!is_null($title)) + $album->setName($title); + + if(!is_null($description)) + $album->setDescription($description); + + $album->setEdited(time()); + $album->save(); + + return (int) !(!$title && !$description); + } + + function deleteAlbum(int $album_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $album->delete(); + + return 1; + } + + function moveToAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if(!$audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + $res = 1; + try { + foreach ($audios as $audio) + $res = min($res, (int) $album->add($audio)); + } catch(\OutOfBoundsException $ex) { + return 0; + } + + return $res; + } + + function removeFromAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if($audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + foreach($audios as $audio) + $album->remove($audio); + + return 1; + } + + function copyToAlbum(int $album_id, string $audio_ids): int + { + return $this->moveToAlbum($album_id, $audio_ids); + } + + function bookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->bookmark($this->getUser()); + } + + function unBookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->unbookmark($this->getUser()); + } } diff --git a/VKAPI/Handlers/Board.php b/VKAPI/Handlers/Board.php index 3a4412758..b7cb7e690 100644 --- a/VKAPI/Handlers/Board.php +++ b/VKAPI/Handlers/Board.php @@ -287,11 +287,10 @@ function getComments(int $group_id, int $topic_id, bool $need_likes = false, int { # start_comment_id ne robit $this->requireUser(); - $this->willExecuteWriteAction(); $topic = (new TopicsRepo)->getTopicById($group_id, $topic_id); - if(!$topic || !$topic->getClub() || $topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) { + if(!$topic || !$topic->getClub() || $topic->isDeleted()) { $this->fail(5, "Invalid topic"); } @@ -321,21 +320,21 @@ function getTopics(int $group_id, string $topic_ids = "", int $order = 1, int $o { # order и extended ничё не делают $this->requireUser(); - $this->willExecuteWriteAction(); $arr = []; $club = (new ClubsRepo)->get($group_id); + $topics = array_slice(iterator_to_array((new TopicsRepo)->getClubTopics($club, 1, $count + $offset)), $offset); $arr["count"] = (new TopicsRepo)->getClubTopicsCount($club); $arr["items"] = []; $arr["default_order"] = $order; - $arr["can_add_topics"] = $club->canBeModifiedBy($this->getUser()) ? true : $club->isEveryoneCanCreateTopics() ? true : false; + $arr["can_add_topics"] = $club->canBeModifiedBy($this->getUser()) ? true : ($club->isEveryoneCanCreateTopics() ? true : false); $arr["profiles"] = []; if(empty($topic_ids)) { foreach($topics as $topic) { if($topic->isDeleted()) continue; - $arr["items"][] = $topic->toVkApiStruct($preview, $preview_length); + $arr["items"][] = $topic->toVkApiStruct($preview, $preview_length > 1 ? $preview_length : 90); } } else { $topics = explode(',', $topic_ids); @@ -343,8 +342,9 @@ function getTopics(int $group_id, string $topic_ids = "", int $order = 1, int $o foreach($topics as $topic) { $id = explode("_", $topic); $topicy = (new TopicsRepo)->getTopicById((int)$id[0], (int)$id[1]); - if($topicy) { - $arr["items"] = $topicy->toVkApiStruct(); + + if($topicy && !$topicy->isDeleted()) { + $arr["items"][] = $topicy->toVkApiStruct($preview, $preview_length > 1 ? $preview_length : 90); } } } @@ -406,7 +406,7 @@ private function getApiBoardComment(?Comment $comment, bool $need_likes = false) $res->id = $comment->getId(); $res->from_id = $comment->getOwner()->getId(); $res->date = $comment->getPublicationTime()->timestamp(); - $res->text = $comment->getText(); + $res->text = $comment->getText(false); $res->attachments = []; $res->likes = []; if($need_likes) { diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php index f78735209..51046be44 100644 --- a/VKAPI/Handlers/Friends.php +++ b/VKAPI/Handlers/Friends.php @@ -4,7 +4,7 @@ final class Friends extends VKAPIRequestHandler { - function get(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object + function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 100): object { $i = 0; $offset++; @@ -13,11 +13,23 @@ function get(int $user_id, string $fields = "", int $offset = 0, int $count = 10 $users = new UsersRepo; $this->requireUser(); - - foreach($users->get($user_id)->getFriends($offset, $count) as $friend) { - $friends[$i] = $friend->getId(); - $i++; + + if ($user_id == 0) { + $user_id = $this->getUser()->getId(); } + + $user = $users->get($user_id); + + if(!$user || $user->isDeleted()) + $this->fail(100, "Invalid user"); + + if(!$user->getPrivacyPermission("friends.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his friends."); + + foreach($user->getFriends($offset, $count) as $friend) { + $friends[$i] = $friend->getId(); + $i++; + } $response = $friends; @@ -135,7 +147,7 @@ function areFriends(string $user_ids): array return $response; } - function getRequests(string $fields = "", int $offset = 0, int $count = 100, int $extended = 0): object + function getRequests(string $fields = "", int $out = 0, 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."); @@ -146,9 +158,18 @@ function getRequests(string $fields = "", int $offset = 0, int $count = 100, int $offset++; $followers = []; - foreach($this->getUser()->getFollowers($offset, $count) as $follower) { - $followers[$i] = $follower->getId(); - $i++; + if ($out != 0) { + foreach($this->getUser()->getFollowers($offset, $count) as $follower) { + $followers[$i] = $follower->getId(); + $i++; + } + } + else + { + foreach($this->getUser()->getRequests($offset, $count) as $follower) { + $followers[$i] = $follower->getId(); + $i++; + } } $response = $followers; diff --git a/VKAPI/Handlers/Gifts.php b/VKAPI/Handlers/Gifts.php index 2702924d5..0d1fc5982 100644 --- a/VKAPI/Handlers/Gifts.php +++ b/VKAPI/Handlers/Gifts.php @@ -6,19 +6,33 @@ final class Gifts extends VKAPIRequestHandler { - function get(int $user_id, int $count = 10, int $offset = 0) + function get(int $user_id = NULL, int $count = 10, int $offset = 0) { $this->requireUser(); $i = 0; - $i += $offset; + $server_url = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; - $user = (new UsersRepo)->get($user_id); + if($user_id) + $user = (new UsersRepo)->get($user_id); + else + $user = $this->getUser(); if(!$user || $user->isDeleted()) $this->fail(177, "Invalid user"); + if(!$user->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); + + /* + if(!$user->getPrivacyPermission('gifts.read', $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his gifts");*/ + + + if(!$user->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); + $gift_item = []; $userGifts = array_slice(iterator_to_array($user->getGifts(1, $count, false)), $offset); @@ -36,9 +50,9 @@ function get(int $user_id, int $count = 10, int $offset = 0) "date" => $gift->sent->timestamp(), "gift" => [ "id" => $gift->gift->getId(), - "thumb_256" => $gift->gift->getImage(2), - "thumb_96" => $gift->gift->getImage(2), - "thumb_48" => $gift->gift->getImage(2) + "thumb_256" => $server_url. $gift->gift->getImage(2), + "thumb_96" => $server_url . $gift->gift->getImage(2), + "thumb_48" => $server_url . $gift->gift->getImage(2) ], "privacy" => 0 ]; @@ -62,6 +76,9 @@ function send(int $user_ids, int $gift_id, string $message = "", int $privacy = if(!$user || $user->isDeleted()) $this->fail(177, "Invalid user"); + if(!$user->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); + $gift = (new GiftsRepo)->get($gift_id); if(!$gift) @@ -111,12 +128,13 @@ function delete() $this->fail(501, "Not implemented"); } - # этих методов не было в ВК, но я их добавил чтобы можно было отобразить список подарков + # в vk кстати называется gifts.getCatalog function getCategories(bool $extended = false, int $page = 1) { $cats = (new GiftsRepo)->getCategories($page); $categ = []; $i = 0; + $server_url = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce']) $this->fail(105, "Commerce is disabled on this instance"); @@ -126,8 +144,8 @@ function getCategories(bool $extended = false, int $page = 1) "name" => $cat->getName(), "description" => $cat->getDescription(), "id" => $cat->getId(), - "thumbnail" => $cat->getThumbnailURL(), - ]; + "thumbnail" => $server_url . $cat->getThumbnailURL(), + ]; if($extended == true) { $categ[$i]["localizations"] = []; @@ -164,7 +182,7 @@ function getGiftsInCategory(int $id, int $page = 1) "name" => $gift->getName(), "image" => $gift->getImage(2), "usages_left" => (int)$gift->getUsagesLeft($this->getUser()), - "price" => $gift->getPrice(), # голосов + "price" => $gift->getPrice(), "is_free" => $gift->isFree() ]; } diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index e21c8a095..bd546c3dd 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -2,26 +2,36 @@ namespace openvk\VKAPI\Handlers; use openvk\Web\Models\Repositories\Clubs as ClubsRepo; use openvk\Web\Models\Repositories\Users as UsersRepo; +use openvk\Web\Models\Repositories\Posts as PostsRepo; use openvk\Web\Models\Entities\Club; final class Groups extends VKAPIRequestHandler { - function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 6, bool $online = false): object + function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 6, bool $online = false, string $filter = "groups"): object { $this->requireUser(); + # InfoApp fix + if($filter == "admin" && ($user_id != 0 && $user_id != $this->getUser()->getId())) { + $this->fail(15, 'Access denied: filter admin is available only for current user'); + } + + $clbs = []; if($user_id == 0) { - foreach($this->getUser()->getClubs($offset, false, $count, true) as $club) + foreach($this->getUser()->getClubs($offset, $filter == "admin", $count, true) as $club) $clbs[] = $club; $clbsCount = $this->getUser()->getClubCount(); } else { $users = new UsersRepo; $user = $users->get($user_id); - if(is_null($user)) + if(is_null($user) || $user->isDeleted()) $this->fail(15, "Access denied"); - foreach($user->getClubs($offset, false, $count, true) as $club) + if(!$user->getPrivacyPermission('groups.read', $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his groups."); + + foreach($user->getClubs($offset, $filter == "admin", $count, true) as $club) $clbs[] = $club; $clbsCount = $user->getClubCount(); @@ -80,6 +90,23 @@ function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count break; case "members_count": $rClubs[$i]->members_count = $usr->getFollowersCount(); + break; + case "can_suggest": + $rClubs[$i]->can_suggest = !$usr->canBeModifiedBy($this->getUser()) && $usr->getWallType() == 2; + break; + case "background": + $backgrounds = $usr->getBackDropPictureURLs(); + $rClubs[$i]->background = $backgrounds; + break; + # unstandard feild + case "suggested_count": + if($usr->getWallType() != 2) { + $rClubs[$i]->suggested_count = NULL; + break; + } + + $rClubs[$i]->suggested_count = $usr->getSuggestedPostsCount($this->getUser()); + break; } } @@ -188,7 +215,23 @@ function getById(string $group_ids = "", string $group_id = "", string $fields = case "description": $response[$i]->description = $clb->getDescription(); break; - case "contacts": + case "can_suggest": + $response[$i]->can_suggest = !$clb->canBeModifiedBy($this->getUser()) && $clb->getWallType() == 2; + break; + case "background": + $backgrounds = $clb->getBackDropPictureURLs(); + $response[$i]->background = $backgrounds; + break; + # unstandard feild + case "suggested_count": + if($clb->getWallType() != 2) { + $response[$i]->suggested_count = NULL; + break; + } + + $response[$i]->suggested_count = $clb->getSuggestedPostsCount($this->getUser()); + break; + case "contacts": $contacts; $contactTmp = $clb->getManagers(1, true); @@ -215,23 +258,30 @@ function getById(string $group_ids = "", string $group_id = "", string $fields = return $response; } - function search(string $q, int $offset = 0, int $count = 100) + function search(string $q, int $offset = 0, int $count = 100, string $fields = "screen_name,is_admin,is_member,is_advertiser,photo_50,photo_100,photo_200") { + if($count > 100) { + $this->fail(100, "One of the parameters specified was missing or invalid: count should be less or equal to 100"); + } + $clubs = new ClubsRepo; $array = []; $find = $clubs->find($q); - foreach ($find as $group) + foreach ($find->offsetLimit($offset, $count) as $group) $array[] = $group->getId(); + + if(!$array || sizeof($array) < 1) { + return (object) [ + "count" => 0, + "items" => [], + ]; + } 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 - */ + "items" => $this->getById(implode(',', $array), "", $fields) ]; } @@ -288,11 +338,12 @@ function edit( string $description = NULL, string $screen_name = NULL, string $website = NULL, - int $wall = NULL, + int $wall = -1, int $topics = NULL, int $adminlist = NULL, int $topicsAboveWall = NULL, - int $hideFromGlobalFeed = NULL) + int $hideFromGlobalFeed = NULL, + int $audio = NULL) { $this->requireUser(); $this->willExecuteWriteAction(); @@ -303,17 +354,34 @@ function edit( if(!$club || !$club->canBeModifiedBy($this->getUser())) $this->fail(15, "You can't modify this group."); if(!empty($screen_name) && !$club->setShortcode($screen_name)) $this->fail(103, "Invalid shortcode."); - !is_null($title) ? $club->setName($title) : NULL; - !is_null($description) ? $club->setAbout($description) : NULL; - !is_null($screen_name) ? $club->setShortcode($screen_name) : NULL; - !is_null($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; - !is_null($wall) ? $club->setWall($wall) : NULL; - !is_null($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; - !is_null($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; - !is_null($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; - !is_null($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + !empty($title) ? $club->setName($title) : NULL; + !empty($description) ? $club->setAbout($description) : NULL; + !empty($screen_name) ? $club->setShortcode($screen_name) : NULL; + !empty($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; + + try { + $wall != -1 ? $club->setWall($wall) : NULL; + } catch(\Exception $e) { + $this->fail(50, "Invalid wall value"); + } + + !empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; + !empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; + !empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; - $club->save(); + if (!$club->isHidingFromGlobalFeedEnforced()) { + !empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + } + + in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : NULL; + + try { + $club->save(); + } catch(\TypeError $e) { + $this->fail(15, "Nothing changed"); + } catch(\Exception $e) { + $this->fail(18, "An unknown error occurred: maybe you set an incorrect value?"); + } return 1; } @@ -354,13 +422,20 @@ function getMembers(string $group_id, string $sort = "id_asc", int $offset = 0, $arr->items[] = (object) [ "id" => $member->getId(), - "name" => $member->getCanonicalName(), + "first_name" => $member->getFirstName(), + "last_name" => $member->getLastName(), ]; foreach($filds as $fild) { + $canView = $member->canBeViewedBy($this->getUser()); switch($fild) { case "bdate": - $arr->items[$i]->bdate = $member->getBirthday()->format('%e.%m.%Y'); + if(!$canView) { + $arr->items[$i]->bdate = "01.01.1970"; + break; + } + + $arr->items[$i]->bdate = $member->getBirthday() ? $member->getBirthday()->format('%e.%m.%Y') : NULL; break; case "can_post": $arr->items[$i]->can_post = $club->canBeModifiedBy($member); @@ -369,7 +444,7 @@ function getMembers(string $group_id, string $sort = "id_asc", int $offset = 0, $arr->items[$i]->can_see_all_posts = 1; break; case "can_see_audio": - $arr->items[$i]->can_see_audio = 0; + $arr->items[$i]->can_see_audio = 1; break; case "can_write_private_message": $arr->items[$i]->can_write_private_message = 0; @@ -381,6 +456,11 @@ function getMembers(string $group_id, string $sort = "id_asc", int $offset = 0, $arr->items[$i]->connections = 1; break; case "contacts": + if(!$canView) { + $arr->items[$i]->contacts = "secret@gmail.com"; + break; + } + $arr->items[$i]->contacts = $member->getContactEmail(); break; case "country": @@ -396,15 +476,30 @@ function getMembers(string $group_id, string $sort = "id_asc", int $offset = 0, $arr->items[$i]->has_mobile = false; break; case "last_seen": + if(!$canView) { + $arr->items[$i]->last_seen = 0; + break; + } + $arr->items[$i]->last_seen = $member->getOnline()->timestamp(); break; case "lists": $arr->items[$i]->lists = ""; break; case "online": + if(!$canView) { + $arr->items[$i]->online = false; + break; + } + $arr->items[$i]->online = $member->isOnline(); break; case "online_mobile": + if(!$canView) { + $arr->items[$i]->online_mobile = false; + break; + } + $arr->items[$i]->online_mobile = $member->getOnlinePlatform() == "android" || $member->getOnlinePlatform() == "iphone" || $member->getOnlinePlatform() == "mobile"; break; case "photo_100": @@ -435,12 +530,27 @@ function getMembers(string $group_id, string $sort = "id_asc", int $offset = 0, $arr->items[$i]->schools = 0; break; case "sex": + if(!$canView) { + $arr->items[$i]->sex = -1; + break; + } + $arr->items[$i]->sex = $member->isFemale() ? 1 : 2; break; case "site": + if(!$canView) { + $arr->items[$i]->site = NULL; + break; + } + $arr->items[$i]->site = $member->getWebsite(); break; case "status": + if(!$canView) { + $arr->items[$i]->status = "r"; + break; + } + $arr->items[$i]->status = $member->getStatus(); break; case "universities": @@ -465,10 +575,10 @@ function getSettings(string $group_id) "title" => $club->getName(), "description" => $club->getDescription() != NULL ? $club->getDescription() : "", "address" => $club->getShortcode(), - "wall" => $club->canPost() == true ? 1 : 0, + "wall" => $club->getWallType(), # отличается от вкшных но да ладно "photos" => 1, "video" => 0, - "audio" => 0, + "audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0, "docs" => 0, "topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0, "wiki" => 0, diff --git a/VKAPI/Handlers/Likes.php b/VKAPI/Handlers/Likes.php index 9501b4335..f9262fa15 100644 --- a/VKAPI/Handlers/Likes.php +++ b/VKAPI/Handlers/Likes.php @@ -2,70 +2,205 @@ namespace openvk\VKAPI\Handlers; use openvk\Web\Models\Repositories\Users as UsersRepo; use openvk\Web\Models\Repositories\Posts as PostsRepo; +use openvk\Web\Models\Repositories\Comments as CommentsRepo; +use openvk\Web\Models\Repositories\Videos as VideosRepo; +use openvk\Web\Models\Repositories\Photos as PhotosRepo; +use openvk\Web\Models\Repositories\Notes as NotesRepo; + final class Likes extends VKAPIRequestHandler { - function add(string $type, int $owner_id, int $item_id): object - { - $this->requireUser(); + function add(string $type, int $owner_id, int $item_id): object + { + $this->requireUser(); $this->willExecuteWriteAction(); + $postable = NULL; 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() - ]; + $postable = $post; + break; + case "comment": + $comment = (new CommentsRepo)->get($item_id); + $postable = $comment; + break; + case "video": + $video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id); + $postable = $video; + break; + case "photo": + $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id); + $postable = $photo; + break; + case "note": + $note = (new NotesRepo)->getNoteById($owner_id, $item_id); + $postable = $note; + break; default: $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); } - } - function delete(string $type, int $owner_id, int $item_id): object - { - $this->requireUser(); + if(is_null($postable) || $postable->isDeleted()) + $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); + + if(!$postable->canBeViewedBy($this->getUser() ?? NULL)) { + $this->fail(2, "Access to postable denied"); + } + + $postable->setLike(true, $this->getUser()); + + return (object) [ + "likes" => $postable->getLikesCount() + ]; + } + + function delete(string $type, int $owner_id, int $item_id): object + { + $this->requireUser(); $this->willExecuteWriteAction(); + $postable = NULL; 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() - ]; + $postable = $post; + break; + case "comment": + $comment = (new CommentsRepo)->get($item_id); + $postable = $comment; + break; + case "video": + $video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id); + $postable = $video; + break; + case "photo": + $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id); + $postable = $photo; + break; + case "note": + $note = (new NotesRepo)->getNoteById($owner_id, $item_id); + $postable = $note; + break; default: $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); } - } - + + if(is_null($postable) || $postable->isDeleted()) + $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); + + if(!$postable->canBeViewedBy($this->getUser() ?? NULL)) { + $this->fail(2, "Access to postable denied"); + } + + if(!is_null($postable)) { + $postable->setLike(false, $this->getUser()); + + return (object) [ + "likes" => $postable->getLikesCount() + ]; + } + } + function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object - { - $this->requireUser(); + { + $this->requireUser(); + + $user = (new UsersRepo)->get($user_id); + + if(is_null($user) || $user->isDeleted()) + $this->fail(100, "One of the parameters specified was missing or invalid: user not found"); + + if(!$user->canBeViewedBy($this->getUser())) { + $this->fail(1984, "Access denied: you can't see this user"); + } + $postable = NULL; switch($type) { case "post": - $user = (new UsersRepo)->get($user_id); - if (is_null($user)) - $this->fail(100, "One of the parameters specified was missing or invalid: user not found"); - $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 - ]; + $postable = $post; + break; + case "comment": + $comment = (new CommentsRepo)->get($item_id); + $postable = $comment; + break; + case "video": + $video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id); + $postable = $video; + break; + case "photo": + $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id); + $postable = $photo; + break; + case "note": + $note = (new NotesRepo)->getNoteById($owner_id, $item_id); + $postable = $note; + break; default: $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); } + + if(is_null($postable) || $postable->isDeleted()) + $this->fail(100, "One of the parameters specified was missing or invalid: object not found"); + + if(!$postable->canBeViewedBy($this->getUser())) { + $this->fail(665, "Access to postable denied"); + } + + return (object) [ + "liked" => (int) $postable->hasLikeFrom($user), + "copied" => 0 + ]; } + + function getList(string $type, int $owner_id, int $item_id, bool $extended = false, int $offset = 0, int $count = 10, bool $skip_own = false) + { + $this->requireUser(); + + $object = NULL; + + switch($type) { + case "post": + $object = (new PostsRepo)->getPostById($owner_id, $item_id); + break; + case "comment": + $object = (new CommentsRepo)->get($item_id); + break; + case "photo": + $object = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id); + break; + case "video": + $object = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id); + break; + default: + $this->fail(58, "Invalid type"); + break; + } + + if(!$object || $object->isDeleted()) + $this->fail(56, "Invalid postable"); + + if(!$object->canBeViewedBy($this->getUser())) + $this->fail(665, "Access to postable denied"); + + $res = (object)[ + "count" => $object->getLikesCount(), + "items" => [] + ]; + + $likers = array_slice(iterator_to_array($object->getLikers(1, $offset + $count)), $offset); + + foreach($likers as $liker) { + if($skip_own && $liker->getId() == $this->getUser()->getId()) + continue; + + if(!$extended) + $res->items[] = $liker->getId(); + else + $res->items[] = $liker->toVkApiStruct(NULL, 'photo_50'); + } + + return $res; + } } diff --git a/VKAPI/Handlers/Messages.php b/VKAPI/Handlers/Messages.php index bc9035f53..6fc057e06 100644 --- a/VKAPI/Handlers/Messages.php +++ b/VKAPI/Handlers/Messages.php @@ -65,7 +65,8 @@ function getById(string $message_ids, int $preview_length = 0, int $extended = 0 ]; } - function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0) + function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0, + string $attachment = "") # интересно почему не attachments { $this->requireUser(); $this->willExecuteWriteAction(); @@ -79,7 +80,8 @@ function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $ch $this->fail(946, "Chats are not implemented"); else if($sticker_id !== -1) $this->fail(-151, "Stickers are not implemented"); - else if(empty($message)) + + if(empty($message) && empty($attachment)) $this->fail(100, "Message text is empty or invalid"); # lol recursion @@ -117,6 +119,21 @@ function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $ch if(!$msg) $this->fail(950, "Internal error"); else + if(!empty($attachment)) { + $attachs = parseAttachments($attachment); + + # Работают только фотки, остальное просто не будет отображаться. + if(sizeof($attachs) >= 10) + $this->fail(15, "Too many attachments"); + + foreach($attachs as $attach) { + if($attach && !$attach->isDeleted() && $attach->getOwner()->getId() == $this->getUser()->getId()) + $msg->attach($attach); + else + $this->fail(52, "One of the attachments is invalid"); + } + } + return $msg->getId(); } @@ -393,4 +410,49 @@ function getLongPollServer(int $need_pts = 1, int $lp_version = 3, ?int $group_i return $res; } + + function edit(int $message_id, string $message = "", string $attachment = "", int $peer_id = 0) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $msg = (new MSGRepo)->get($message_id); + + if(empty($message) && empty($attachment)) + $this->fail(100, "Required parameter 'message' missing."); + + if(!$msg || $msg->isDeleted()) + $this->fail(102, "Invalid message"); + + if($msg->getSender()->getId() != $this->getUser()->getId()) + $this->fail(15, "Access to message denied"); + + if(!empty($message)) + $msg->setContent($message); + + $msg->setEdited(time()); + $msg->save(true); + + if(!empty($attachment)) { + $attachs = parseAttachments($attachment); + $newAttachmentsCount = sizeof($attachs); + + $postsAttachments = iterator_to_array($msg->getChildren()); + + if(sizeof($postsAttachments) >= 10) + $this->fail(15, "Message have too many attachments"); + + if(($newAttachmentsCount + sizeof($postsAttachments)) > 10) + $this->fail(158, "Message will have too many attachments"); + + foreach($attachs as $attach) { + if($attach && !$attach->isDeleted() && $attach->getOwner()->getId() == $this->getUser()->getId()) + $msg->attach($attach); + else + $this->fail(52, "One of the attachments is invalid"); + } + } + + return 1; + } } diff --git a/VKAPI/Handlers/Newsfeed.php b/VKAPI/Handlers/Newsfeed.php index d99924304..557a37240 100644 --- a/VKAPI/Handlers/Newsfeed.php +++ b/VKAPI/Handlers/Newsfeed.php @@ -32,6 +32,7 @@ function get(string $fields = "", int $start_from = 0, int $start_time = 0, int ->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) @@ -47,14 +48,24 @@ function get(string $fields = "", int $start_from = 0, int $start_time = 0, int return $response; } - function getGlobal(string $fields = "", int $start_from = 0, int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 0) + function getGlobal(string $fields = "", int $start_from = 0, int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 0, int $rss = 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"; + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; + $queryBase .= "WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0"; 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; @@ -63,6 +74,25 @@ function getGlobal(string $fields = "", int $start_from = 0, int $start_time = 0 $rposts = []; $ids = []; + if($rss == 1) { + $channel = new \Bhaktaraz\RSSGenerator\Channel(); + $channel->title("Global Feed — " . OPENVK_ROOT_CONF['openvk']['appearance']['name']) + ->description('OVK Global feed') + ->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/feed/all"); + + foreach($posts as $item) { + $post = (new PostsRepo)->get($item->id); + if(!$post || $post->isDeleted()) { + continue; + } + + $output = $post->toRss(); + $output->appendTo($channel); + } + + return $channel; + } + foreach($posts as $post) { $rposts[] = (new PostsRepo)->get($post->id)->getPrettyId(); $ids[] = $post->id; @@ -73,4 +103,152 @@ function getGlobal(string $fields = "", int $start_from = 0, int $start_time = 0 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/VKAPI/Handlers/Notes.php b/VKAPI/Handlers/Notes.php index d3dc3468a..620877c9b 100644 --- a/VKAPI/Handlers/Notes.php +++ b/VKAPI/Handlers/Notes.php @@ -40,6 +40,12 @@ function createComment(string $note_id, int $owner_id, string $message, int $rep if($note->getOwner()->isDeleted()) $this->fail(403, "Owner is deleted"); + if(!$note->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); + + if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) + $this->fail(43, "No access"); + if(empty($message) && empty($attachments)) $this->fail(100, "Required parameter 'message' missing."); @@ -115,21 +121,6 @@ function delete(string $note_id) return 1; } - function deleteComment(int $comment_id, int $owner_id = 0) - { - $this->requireUser(); - $this->willExecuteWriteAction(); - - $comment = (new CommentsRepo)->get($comment_id); - - if(!$comment || !$comment->canBeDeletedBy($this->getUser())) - $this->fail(403, "Access to comment denied"); - - $comment->delete(); - - return 1; - } - function edit(string $note_id, string $title = "", string $text = "", int $privacy = 0, int $comment_privacy = 0, string $privacy_view = "", string $privacy_comment = "") { $this->requireUser(); @@ -156,25 +147,6 @@ function edit(string $note_id, string $title = "", string $text = "", int $priva return 1; } - function editComment(int $comment_id, string $message, int $owner_id = NULL) - { - /* - $this->requireUser(); - $this->willExecuteWriteAction(); - - $comment = (new CommentsRepo)->get($comment_id); - - if($comment->getOwner() != $this->getUser()->getId()) - $this->fail(15, "Access to comment denied"); - - $comment->setContent($message); - $comment->setEdited(time()); - $comment->save(); - */ - - return 1; - } - function get(int $user_id, string $note_ids = "", int $offset = 0, int $count = 10, int $sort = 0) { $this->requireUser(); @@ -183,6 +155,12 @@ function get(int $user_id, string $note_ids = "", int $offset = 0, int $count = if(!$user || $user->isDeleted()) $this->fail(15, "Invalid user"); + if(!$user->getPrivacyPermission('notes.read', $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his notes"); + + if(!$user->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); + if(empty($note_ids)) { $notes = array_slice(iterator_to_array((new NotesRepo)->getUserNotes($user, 1, $count + $offset, $sort == 0 ? "ASC" : "DESC")), $offset); $nodez = (object) [ @@ -205,7 +183,7 @@ function get(int $user_id, string $note_ids = "", int $offset = 0, int $count = $items = []; $note = (new NotesRepo)->getNoteById((int)$id[0], (int)$id[1]); - if($note) { + if($note && !$note->isDeleted()) { $nodez->notes[] = $note->toVkApiStruct(); } } @@ -225,10 +203,16 @@ function getById(int $note_id, int $owner_id, bool $need_wiki = false) if($note->isDeleted()) $this->fail(189, "Note is deleted"); - + if(!$note->getOwner() || $note->getOwner()->isDeleted()) $this->fail(177, "Owner does not exists"); + if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) + $this->fail(40, "Access denied: this user chose to hide his notes"); + + if(!$note->canBeViewedBy($this->getUser())) + $this->fail(15, "Access to note denied"); + return $note->toVkApiStruct(); } @@ -246,6 +230,12 @@ function getComments(int $note_id, int $owner_id, int $sort = 1, int $offset = 0 if(!$note->getOwner()) $this->fail(177, "Owner does not exists"); + + if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) + $this->fail(14, "No access"); + + if(!$note->canBeViewedBy($this->getUser())) + $this->fail(15, "Access to note denied"); $arr = (object) [ "count" => $note->getCommentsCount(), diff --git a/VKAPI/Handlers/Notifications.php b/VKAPI/Handlers/Notifications.php new file mode 100644 index 000000000..8a047f800 --- /dev/null +++ b/VKAPI/Handlers/Notifications.php @@ -0,0 +1,83 @@ +requireUser(); + + $res = (object)[ + "items" => [], + "profiles" => [], + "groups" => [], + "last_viewed" => $this->getUser()->getNotificationOffset() + ]; + + if($count > 100) + $this->fail(125, "Count is too big"); + + if(!eventdb()) + $this->fail(1289, "EventDB is disabled on this instance"); + + $notifs = array_slice(iterator_to_array((new Notifs)->getNotificationsByUser($this->getUser(), $this->getUser()->getNotificationOffset(), (bool)$archived, 1, $offset + $count)), $offset); + $tmpProfiles = []; + foreach($notifs as $notif) { + $sxModel = $notif->getModel(1); + + if(!method_exists($sxModel, "getAvatarUrl")) + $sxModel = $notif->getModel(0); + + + $tmpProfiles[] = $sxModel instanceof Club ? $sxModel->getId() * -1 : $sxModel->getId(); + $res->items[] = $notif->toVkApiStruct(); + } + + foreach(array_unique($tmpProfiles) as $id) { + if($id > 0) { + $sxModel = (new Users)->get($id); + $result = (object)[ + "uid" => $sxModel->getId(), + "first_name" => $sxModel->getFirstName(), + "last_name" => $sxModel->getLastName(), + "photo" => $sxModel->getAvatarUrl(), + "photo_medium_rec" => $sxModel->getAvatarUrl("tiny"), + "screen_name" => $sxModel->getShortCode() + ]; + + $res->profiles[] = $result; + } else { + $sxModel = (new Clubs)->get(abs($id)); + $result = $sxModel->toVkApiStruct($this->getUser()); + + $res->groups[] = $result; + } + } + + return $res; + } + + function markAsViewed() + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + try { + $this->getUser()->updateNotificationOffset(); + $this->getUser()->save(); + } catch(\Throwable $e) { + return 0; + } + + return 1; + } +} diff --git a/VKAPI/Handlers/Photos.php b/VKAPI/Handlers/Photos.php index 4ab56832c..1ae99faf7 100644 --- a/VKAPI/Handlers/Photos.php +++ b/VKAPI/Handlers/Photos.php @@ -61,7 +61,7 @@ function getOwnerPhotoUploadServer(int $owner_id = 0): object } return (object) [ - "upload_url" => $this->getPhotoUploadUrl("photo", isset($club) ? 0 : $club->getId()), + "upload_url" => $this->getPhotoUploadUrl("photo", !isset($club) ? 0 : $club->getId()), ]; } @@ -288,7 +288,6 @@ function editAlbum(int $album_id, int $owner_id, string $title, string $descript function getAlbums(int $owner_id, string $album_ids = "", int $offset = 0, int $count = 100, bool $need_system = true, bool $need_covers = true, bool $photo_sizes = false) { $this->requireUser(); - $this->willExecuteWriteAction(); $res = []; @@ -304,7 +303,6 @@ function getAlbums(int $owner_id, string $album_ids = "", int $offset = 0, int $ if(!$user || $user->isDeleted()) $this->fail(2, "Invalid user"); - if(!$user->getPrivacyPermission('photos.read', $this->getUser())) $this->fail(21, "This user chose to hide his albums."); @@ -361,28 +359,22 @@ function getAlbums(int $owner_id, string $album_ids = "", int $offset = 0, int $ function getAlbumsCount(int $user_id = 0, int $group_id = 0) { $this->requireUser(); - $this->willExecuteWriteAction(); - if($user_id == 0 && $group_id == 0 || $user_id > 0 && $group_id > 0) { + if($user_id == 0 && $group_id == 0 || $user_id > 0 && $group_id > 0) $this->fail(21, "Select user_id or group_id"); - } if($user_id > 0) { - $us = (new UsersRepo)->get($user_id); - if(!$us || $us->isDeleted()) { + if(!$us || $us->isDeleted()) $this->fail(21, "Invalid user"); - } - if(!$us->getPrivacyPermission('photos.read', $this->getUser())) { + if(!$us->getPrivacyPermission('photos.read', $this->getUser())) $this->fail(21, "This user chose to hide his albums."); - } return (new Albums)->getUserAlbumsCount($us); } - if($group_id > 0) - { + if($group_id > 0) { $cl = (new Clubs)->get($group_id); if(!$cl) { $this->fail(21, "Invalid club"); @@ -395,7 +387,6 @@ function getAlbumsCount(int $user_id = 0, int $group_id = 0) function getById(string $photos, bool $extended = false, bool $photo_sizes = false) { $this->requireUser(); - $this->willExecuteWriteAction(); $phts = explode(",", $photos); $res = []; @@ -404,17 +395,11 @@ function getById(string $photos, bool $extended = false, bool $photo_sizes = fal $ph = explode("_", $phota); $photo = (new PhotosRepo)->getByOwnerAndVID((int)$ph[0], (int)$ph[1]); - if(!$photo || $photo->isDeleted()) { + if(!$photo || $photo->isDeleted()) $this->fail(21, "Invalid photo"); - } - if($photo->getOwner()->isDeleted()) { - $this->fail(21, "Owner of this photo is deleted"); - } - - if(!$photo->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his photos."); - } + if(!$photo->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); $res[] = $photo->toVkApiStruct($photo_sizes, $extended); } @@ -425,23 +410,20 @@ function getById(string $photos, bool $extended = false, bool $photo_sizes = fal function get(int $owner_id, int $album_id, string $photo_ids = "", bool $extended = false, bool $photo_sizes = false, int $offset = 0, int $count = 10) { $this->requireUser(); - $this->willExecuteWriteAction(); $res = []; if(empty($photo_ids)) { $album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id); - if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); - } - - if(!$album || $album->isDeleted()) { + if(!$album || $album->isDeleted()) $this->fail(21, "Invalid album"); - } + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); $photos = array_slice(iterator_to_array($album->getPhotos(1, $count + $offset)), $offset); - $res["count"] = sizeof($photos); + $res["count"] = $album->size(); foreach($photos as $photo) { if(!$photo || $photo->isDeleted()) continue; @@ -456,12 +438,11 @@ function get(int $owner_id, int $album_id, string $photo_ids = "", bool $extende "items" => [] ]; - foreach($photos as $photo) - { + foreach($photos as $photo) { $id = explode("_", $photo); $phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]); - if($phot && !$phot->isDeleted()) { + if($phot && !$phot->isDeleted() && $phot->canBeViewedBy($this->getUser())) { $res["items"][] = $phot->toVkApiStruct($photo_sizes, $extended); } } @@ -477,13 +458,11 @@ function deleteAlbum(int $album_id, int $group_id = 0) $album = (new Albums)->get($album_id); - if(!$album || $album->canBeModifiedBy($this->getUser())) { + if(!$album || $album->canBeModifiedBy($this->getUser())) $this->fail(21, "Invalid album"); - } - if($album->isDeleted()) { + if($album->isDeleted()) $this->fail(22, "Album already deleted"); - } $album->delete(); @@ -497,13 +476,11 @@ function edit(int $owner_id, int $photo_id, string $caption = "") $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); - if(!$photo) { + if(!$photo) $this->fail(21, "Invalid photo"); - } - if($photo->isDeleted()) { + if($photo->isDeleted()) $this->fail(21, "Photo is deleted"); - } if(!empty($caption)) { $photo->setDescription($caption); @@ -521,17 +498,14 @@ function delete(int $owner_id, int $photo_id, string $photos = "") if(empty($photos)) { $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); - if($this->getUser()->getId() !== $photo->getOwner()->getId()) { + if($this->getUser()->getId() !== $photo->getOwner()->getId()) $this->fail(21, "You can't delete another's photo"); - } - if(!$photo) { + if(!$photo) $this->fail(21, "Invalid photo"); - } - if($photo->isDeleted()) { - $this->fail(21, "Photo already deleted"); - } + if($photo->isDeleted()) + $this->fail(21, "Photo is already deleted"); $photo->delete(); } else { @@ -543,17 +517,14 @@ function delete(int $owner_id, int $photo_id, string $photos = "") $phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]); - if($this->getUser()->getId() !== $phot->getOwner()->getId()) { + if($this->getUser()->getId() !== $phot->getOwner()->getId()) $this->fail(21, "You can't delete another's photo"); - } - if(!$phot) { + if(!$phot) $this->fail(21, "Invalid photo"); - } - if($phot->isDeleted()) { + if($phot->isDeleted()) $this->fail(21, "Photo already deleted"); - } $phot->delete(); } @@ -573,17 +544,11 @@ function deleteComment(int $comment_id, int $owner_id = 0) $this->willExecuteWriteAction(); $comment = (new CommentsRepo)->get($comment_id); - if(!$comment) { + if(!$comment) $this->fail(21, "Invalid comment"); - } - - if(!$comment->canBeModifiedBy($this->getUser())) { - $this->fail(21, "Forbidden"); - } - if($comment->isDeleted()) { - $this->fail(4, "Comment already deleted"); - } + if(!$comment->canBeModifiedBy($this->getUser())) + $this->fail(21, "Access denied"); $comment->delete(); @@ -595,20 +560,16 @@ function createComment(int $owner_id, int $photo_id, string $message = "", strin $this->requireUser(); $this->willExecuteWriteAction(); - if(empty($message) && empty($attachments)) { + if(empty($message) && empty($attachments)) $this->fail(100, "Required parameter 'message' missing."); - } $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); - if(!$photo->getAlbum()->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); - } + if(!$photo || $photo->isDeleted()) + $this->fail(180, "Invalid photo"); - if(!$photo) - $this->fail(180, "Photo not found"); - if($photo->isDeleted()) - $this->fail(189, "Photo is deleted"); + if(!$photo->canBeViewedBy($this->getUser())) + $this->fail(15, "Access to photo denied"); $comment = new Comment; $comment->setOwner($this->getUser()->getId()); @@ -667,24 +628,22 @@ function createComment(int $owner_id, int $photo_id, string $message = "", strin function getAll(int $owner_id, bool $extended = false, int $offset = 0, int $count = 100, bool $photo_sizes = false) { $this->requireUser(); - $this->willExecuteWriteAction(); - if($owner_id < 0) { + if($owner_id < 0) $this->fail(4, "This method doesn't works with clubs"); - } $user = (new UsersRepo)->get($owner_id); - - if(!$user) { + if(!$user) $this->fail(4, "Invalid user"); - } - if(!$user->getPrivacyPermission('photos.read', $this->getUser())) { + if(!$user->getPrivacyPermission('photos.read', $this->getUser())) $this->fail(21, "This user chose to hide his albums."); - } - $photos = array_slice(iterator_to_array((new PhotosRepo)->getEveryUserPhoto($user, 1, $count + $offset)), $offset); - $res = []; + $photos = (new PhotosRepo)->getEveryUserPhoto($user, $offset, $count); + $res = [ + "count" => (new PhotosRepo)->getUserPhotosCount($user), + "items" => [], + ]; foreach($photos as $photo) { if(!$photo || $photo->isDeleted()) continue; @@ -697,22 +656,15 @@ function getAll(int $owner_id, bool $extended = false, int $offset = 0, int $cou function getComments(int $owner_id, int $photo_id, bool $need_likes = false, int $offset = 0, int $count = 100, bool $extended = false, string $fields = "") { $this->requireUser(); - $this->willExecuteWriteAction(); $photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id); $comms = array_slice(iterator_to_array($photo->getComments(1, $offset + $count)), $offset); - if(!$photo) { + if(!$photo || $photo->isDeleted()) $this->fail(4, "Invalid photo"); - } - if(!$photo->getAlbum()->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his photos."); - } - - if($photo->isDeleted()) { - $this->fail(4, "Photo is deleted"); - } + if(!$photo->canBeViewedBy($this->getUser())) + $this->fail(21, "Access denied"); $res = [ "count" => sizeof($comms), @@ -730,4 +682,4 @@ function getComments(int $owner_id, int $photo_id, bool $need_likes = false, int return $res; } -} \ No newline at end of file +} diff --git a/VKAPI/Handlers/Polls.php b/VKAPI/Handlers/Polls.php index be947a442..c84f82830 100755 --- a/VKAPI/Handlers/Polls.php +++ b/VKAPI/Handlers/Polls.php @@ -104,4 +104,67 @@ function deleteVote(int $poll_id) $this->fail(8, "how.to. ook.bacon.in.microwova."); } } + + function getVoters(int $poll_id, int $answer_ids, int $offset = 0, int $count = 6) + { + $this->requireUser(); + + $poll = (new PollsRepo)->get($poll_id); + + if(!$poll) + $this->fail(251, "Invalid poll"); + + if($poll->isAnonymous()) + $this->fail(251, "Access denied: poll is anonymous."); + + $voters = array_slice($poll->getVoters($answer_ids, 1, $offset + $count), $offset); + $res = (object)[ + "answer_id" => $answer_ids, + "users" => [] + ]; + + foreach($voters as $voter) + $res->users[] = $voter->toVkApiStruct(); + + return $res; + } + + function create(string $question, string $add_answers, bool $disable_unvote = false, bool $is_anonymous = false, bool $is_multiple = false, int $end_date = 0) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $options = json_decode($add_answers); + + if(!$options || empty($options)) + $this->fail(62, "Invalid options"); + + if(sizeof($options) > ovkGetQuirk("polls.max-opts")) + $this->fail(51, "Too many options"); + + $poll = new Poll; + $poll->setOwner($this->getUser()); + $poll->setTitle($question); + $poll->setMultipleChoice($is_multiple); + $poll->setAnonymity($is_anonymous); + $poll->setRevotability(!$disable_unvote); + $poll->setOptions($options); + + if($end_date > time()) { + if($end_date > time() + (DAY * 365)) + $this->fail(89, "End date is too big"); + + $poll->setEndDate($end_date); + } + + $poll->save(); + + return $this->getById($poll->getId()); + } + + function edit() + { + #todo + return 1; + } } diff --git a/VKAPI/Handlers/Status.php b/VKAPI/Handlers/Status.php index 843f42bdc..5234a7fc5 100644 --- a/VKAPI/Handlers/Status.php +++ b/VKAPI/Handlers/Status.php @@ -8,13 +8,27 @@ final class Status extends VKAPIRequestHandler function get(int $user_id = 0, int $group_id = 0) { $this->requireUser(); - if($user_id == 0 && $group_id == 0) { - return $this->getUser()->getStatus(); - } else { - if($group_id > 0) - $this->fail(501, "Group statuses are not implemented"); - else - return (new UsersRepo)->get($user_id)->getStatus(); + + if($user_id == 0 && $group_id == 0) + $user_id = $this->getUser()->getId(); + + if($group_id > 0) + $this->fail(501, "Group statuses are not implemented"); + else { + $user = (new UsersRepo)->get($user_id); + + if(!$user || $user->isDeleted() || !$user->canBeViewedBy($this->getUser())) + $this->fail(15, "Invalid user"); + + $audioStatus = $user->getCurrentAudioStatus(); + if($audioStatus) { + return [ + "status" => $user->getStatus(), + "audio" => $audioStatus->toVkApiStruct(), + ]; + } + + return $user->getStatus(); } } diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index 4c61985c7..a847d3280 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -1,8 +1,9 @@ "DELETED", "last_name" => "", "deactivated" => "deleted" - ]; + ]; + } else if($usr->isBanned()) { + $response[$i] = (object)[ + "id" => $usr->getId(), + "first_name" => $usr->getFirstName(true), + "last_name" => $usr->getLastName(true), + "deactivated" => "banned", + "ban_reason" => $usr->getBanReason() + ]; } else if($usrs[$i] == NULL) { } else { $response[$i] = (object)[ "id" => $usr->getId(), - "first_name" => $usr->getFirstName(), - "last_name" => $usr->getLastName(), - "is_closed" => (new Blacklists)->isBanned($usr, $authuser), - "can_access_closed" => !(new Blacklists)->isBanned($usr, $authuser), - "blacklisted" => (new Blacklists)->isBanned($usr, $authuser), + "first_name" => $usr->getFirstName(true), + "last_name" => $usr->getLastName(true), + "is_closed" => $usr->isClosed(), + "can_access_closed" => (bool)$usr->canBeViewedBy($this->getUser()), + "blacklisted" => (new Blacklists)->isBanned($usr, $authuser), "blacklisted_by_me" => (new Blacklists)->isBanned($authuser, $usr) ]; $flds = explode(',', $fields); - - if (!(new Blacklists)->isBanned($usr, $authuser)) - foreach($flds as $field) { + $canView = $usr->canBeViewedBy($this->getUser()); + foreach($flds as $field) { switch($field) { case "verified": $response[$i]->verified = intval($usr->isVerified()); break; case "sex": - $response[$i]->sex = $usr->isFemale() ? 1 : 2; + $response[$i]->sex = $usr->isFemale() ? 1 : ($usr->isNeutral() ? 0 : 2); break; case "has_photo": $response[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1; @@ -91,6 +99,12 @@ function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $ case "status": if($usr->getStatus() != NULL) $response[$i]->status = $usr->getStatus(); + + $audioStatus = $usr->getCurrentAudioStatus(); + + if($audioStatus) + $response[$i]->status_audio = $audioStatus->toVkApiStruct(); + break; case "screen_name": if($usr->getShortCode() != NULL) @@ -138,26 +152,122 @@ function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $ ]; } case "music": + if(!$canView) { + break; + } + $response[$i]->music = $usr->getFavoriteMusic(); break; case "movies": + if(!$canView) { + break; + } + $response[$i]->movies = $usr->getFavoriteFilms(); break; case "tv": + if(!$canView) { + break; + } + $response[$i]->tv = $usr->getFavoriteShows(); break; case "books": + if(!$canView) { + break; + } + $response[$i]->books = $usr->getFavoriteBooks(); break; case "city": + if(!$canView) { + break; + } + $response[$i]->city = $usr->getCity(); break; case "interests": + if(!$canView) { + break; + } + $response[$i]->interests = $usr->getInterests(); break; + case "quotes": + if(!$canView) { + break; + } + + $response[$i]->quotes = $usr->getFavoriteQuote(); + break; + case "email": + if(!$canView) { + break; + } + + $response[$i]->email = $usr->getContactEmail(); + break; + case "telegram": + if(!$canView) { + break; + } + + $response[$i]->telegram = $usr->getTelegram(); + break; + case "about": + if(!$canView) { + break; + } + + $response[$i]->about = $usr->getDescription(); + break; case "rating": + if(!$canView) { + break; + } + $response[$i]->rating = $usr->getRating(); - break; + break; + case "counters": + $response[$i]->counters = (object) [ + "friends_count" => $usr->getFriendsCount(), + "photos_count" => (new Photos)->getUserPhotosCount($usr), + "videos_count" => (new Videos)->getUserVideosCount($usr), + "audios_count" => (new Audios)->getUserCollectionSize($usr), + "notes_count" => (new Notes)->getUserNotesCount($usr) + ]; + break; + case "correct_counters": + $response[$i]->counters = (object) [ + "friends" => $usr->getFriendsCount(), + "photos" => (new Photos)->getUserPhotosCount($usr), + "videos" => (new Videos)->getUserVideosCount($usr), + "audios" => (new Audios)->getUserCollectionSize($usr), + "notes" => (new Notes)->getUserNotesCount($usr), + "groups" => $usr->getClubCount(), + "online_friends" => $usr->getFriendsOnlineCount(), + ]; + break; + case "guid": + $response[$i]->guid = $usr->getChandlerGUID(); + break; + case 'background': + $backgrounds = $usr->getBackDropPictureURLs(); + $response[$i]->background = $backgrounds; + break; + case 'reg_date': + if(!$canView) { + break; + } + + $response[$i]->reg_date = $usr->getRegistrationTime()->timestamp(); + break; + case 'is_dead': + $response[$i]->is_dead = $usr->isDead(); + break; + case 'nickname': + $response[$i]->nickname = $usr->getPseudo(); + break; } } @@ -181,14 +291,16 @@ function getFollowers(int $user_id, string $fields = "", int $offset = 0, int $c $users = new UsersRepo; $this->requireUser(); + + $user = $users->get($user_id); + + if(!$user || $user->isDeleted()) + $this->fail(14, "Invalid user"); - $authuser = $this->getUser(); - $target = $users->get($user_id); - - if ((new Blacklists)->isBanned($target, $authuser)) - $this->fail(15, "Access denied: User is blacklisted"); + if(!$user->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); - foreach($target->getFollowers($offset, $count) as $follower) + foreach($users->get($user_id)->getFollowers($offset, $count) as $follower) $followers[] = $follower->getId(); $response = $followers; @@ -208,88 +320,112 @@ function search(string $q, int $count = 100, string $city = "", string $hometown = "", - int $sex = 2, - int $status = 0, # это про marital status + int $sex = 3, + int $status = 0, # marital_status bool $online = false, - # дальше идут параметры которых нету в vkapi но есть на сайте - string $profileStatus = "", # а это уже нормальный статус + # non standart params: int $sort = 0, - int $before = 0, - int $politViews = 0, - int $after = 0, - string $interests = "", + int $polit_views = 0, string $fav_music = "", string $fav_films = "", string $fav_shows = "", - string $fav_books = "", - string $fav_quotes = "" + string $fav_books = "" ) { - $users = new UsersRepo; - - $sortg = "id ASC"; + if($count > 100) { + $this->fail(100, "One of the parameters specified was missing or invalid: count should be less or equal to 100"); + } - $nfilds = $fields; + $users = new UsersRepo; + $output_sort = ['type' => 'id', 'invert' => false]; + $output_params = [ + "ignore_private" => true, + ]; switch($sort) { + default: case 0: - $sortg = "id DESC"; + $output_sort = ['type' => 'id', 'invert' => false]; break; case 1: - $sortg = "id ASC"; - break; - case 2: - $sortg = "first_name DESC"; - break; - case 3: - $sortg = "first_name ASC"; + $output_sort = ['type' => 'id', 'invert' => true]; break; case 4: - $sortg = "rating DESC"; + $output_sort = ['type' => 'rating', 'invert' => false]; + break; + } - if(!str_contains($nfilds, "rating")) { - $nfilds .= "rating"; - } + if(!empty($city)) + $output_params['city'] = $city; - break; - case 5: - $sortg = "rating DESC"; + if(!empty($hometown)) + $output_params['hometown'] = $hometown; - if(!str_contains($nfilds, "rating")) { - $nfilds .= "rating"; - } + if($sex != 3) + $output_params['gender'] = $sex; - break; - } + if($status != 0) + $output_params['marital_status'] = $status; + + if($polit_views != 0) + $output_params['polit_views'] = $polit_views; - $array = []; + if(!empty($interests)) + $output_params['interests'] = $interests; - $parameters = [ - "city" => !empty($city) ? $city : NULL, - "hometown" => !empty($hometown) ? $hometown : NULL, - "gender" => $sex < 2 ? $sex : NULL, - "maritalstatus" => (bool)$status ? $status : NULL, - "politViews" => (bool)$politViews ? $politViews : NULL, - "is_online" => $online ? 1 : NULL, - "status" => !empty($profileStatus) ? $profileStatus : NULL, - "before" => $before != 0 ? $before : NULL, - "after" => $after != 0 ? $after : NULL, - "interests" => !empty($interests) ? $interests : NULL, - "fav_music" => !empty($fav_music) ? $fav_music : NULL, - "fav_films" => !empty($fav_films) ? $fav_films : NULL, - "fav_shows" => !empty($fav_shows) ? $fav_shows : NULL, - "fav_books" => !empty($fav_books) ? $fav_books : NULL, - "fav_quotes" => !empty($fav_quotes) ? $fav_quotes : NULL, - ]; + if(!empty($fav_music)) + $output_params['fav_music'] = $fav_music; + + if(!empty($fav_films)) + $output_params['fav_films'] = $fav_films; + + if(!empty($fav_shows)) + $output_params['fav_shows'] = $fav_shows; + + if(!empty($fav_books)) + $output_params['fav_books'] = $fav_books; + + if($online) + $output_params['is_online'] = 1; - $find = $users->find($q, $parameters, $sortg); + $array = []; + $find = $users->find($q, $output_params, $output_sort); - foreach ($find as $user) + foreach ($find->offsetLimit($offset, $count) as $user) $array[] = $user->getId(); + if(!$array || sizeof($array) < 1) { + return (object) [ + "count" => 0, + "items" => [], + ]; + } + return (object) [ - "count" => $find->size(), - "items" => $this->get(implode(',', $array), $nfilds, $offset, $count) + "count" => $find->size(), + "items" => $this->get(implode(',', $array), $fields) ]; } + + function report(int $user_id, string $type = "spam", string $comment = "") + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if($user_id == $this->getUser()->getId()) + $this->fail(12, "Can't report yourself."); + + if(sizeof(iterator_to_array((new Reports)->getDuplicates("user", $user_id, NULL, $this->getUser()->getId()))) > 0) + return 1; + + $report = new Report; + $report->setUser_id($this->getUser()->getId()); + $report->setTarget_id($user_id); + $report->setType("user"); + $report->setReason($comment); + $report->setCreated(time()); + $report->save(); + + return 1; + } } diff --git a/VKAPI/Handlers/Utils.php b/VKAPI/Handlers/Utils.php index 5350a64f6..a1c4f3b5f 100644 --- a/VKAPI/Handlers/Utils.php +++ b/VKAPI/Handlers/Utils.php @@ -22,7 +22,7 @@ function resolveScreenName(string $screen_name): object "object_id" => (int) substr($screen_name, strlen("club")), "type" => "group" ]; - } + } else $this->fail(104, "Not found"); } else { $user = (new Users)->getByShortURL($screen_name); if($user) { @@ -39,8 +39,17 @@ function resolveScreenName(string $screen_name): object "type" => "group" ]; } - - return (object) []; + + $this->fail(104, "Not found"); } } + + function resolveGuid(string $guid): object + { + $user = (new Users)->getByChandlerUserId($guid); + if (is_null($user)) + $this->fail(104, "Not found"); + + return $user->toVkApiStruct($this->getUser()); + } } diff --git a/VKAPI/Handlers/VKAPIRequestHandler.php b/VKAPI/Handlers/VKAPIRequestHandler.php index d2fcfc74c..4953dbe14 100644 --- a/VKAPI/Handlers/VKAPIRequestHandler.php +++ b/VKAPI/Handlers/VKAPIRequestHandler.php @@ -28,7 +28,7 @@ protected function getUser(): ?User protected function getPlatform(): ?string { - return $this->platform; + return $this->platform ?? ""; } protected function userAuthorized(): bool diff --git a/VKAPI/Handlers/Video.php b/VKAPI/Handlers/Video.php index 740ccd548..3a14f0484 100755 --- a/VKAPI/Handlers/Video.php +++ b/VKAPI/Handlers/Video.php @@ -11,24 +11,55 @@ final class Video extends VKAPIRequestHandler { - function get(int $owner_id, string $videos, int $offset = 0, int $count = 30, int $extended = 0): object + function get(int $owner_id = 0, string $videos = "", string $fields = "", int $offset = 0, int $count = 30, int $extended = 0): object { $this->requireUser(); - if ($videos) { + if(!empty($videos)) { $vids = explode(',', $videos); - - foreach($vids as $vid) - { + $profiles = []; + $groups = []; + foreach($vids as $vid) { $id = explode("_", $vid); - $items = []; $video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1])); - if($video) { - $items[] = $video->getApiStructure(); + if($video && !$video->isDeleted()) { + $out_video = $video->getApiStructure($this->getUser())->video; + $items[] = $out_video; + if($out_video['owner_id']) { + if($out_video['owner_id'] > 0) + $profiles[] = $out_video['owner_id']; + else + $groups[] = abs($out_video['owner_id']); + } } } + + if($extended == 1) { + $profiles = array_unique($profiles); + $groups = array_unique($groups); + + $profilesFormatted = []; + $groupsFormatted = []; + + foreach($profiles as $prof) { + $profile = (new UsersRepo)->get($prof); + $profilesFormatted[] = $profile->toVkApiStruct($this->getUser(), $fields); + } + + foreach($groups as $gr) { + $group = (new ClubsRepo)->get($gr); + $groupsFormatted[] = $group->toVkApiStruct($this->getUser(), $fields); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + "profiles" => $profilesFormatted, + "groups" => $groupsFormatted, + ]; + } return (object) [ "count" => count($items), @@ -36,16 +67,56 @@ function get(int $owner_id, string $videos, int $offset = 0, int $count = 30, in ]; } else { if ($owner_id > 0) - $user = (new UsersRepo)->get($owner_id); + $user = (new UsersRepo)->get($owner_id); else - $this->fail(1, "Not implemented"); + $this->fail(1, "Not implemented"); - $videos = (new VideosRepo)->getByUser($user, $offset + 1, $count); + if(!$user || $user->isDeleted()) + $this->fail(14, "Invalid user"); + + if(!$user->getPrivacyPermission('videos.read', $this->getUser())) + $this->fail(21, "This user chose to hide his videos."); + + $videos = (new VideosRepo)->getByUserLimit($user, $offset, $count); $videosCount = (new VideosRepo)->getUserVideosCount($user); $items = []; - foreach ($videos as $video) { - $items[] = $video->getApiStructure(); + $profiles = []; + $groups = []; + foreach($videos as $video) { + $video = $video->getApiStructure($this->getUser())->video; + $items[] = $video; + if($video['owner_id']) { + if($video['owner_id'] > 0) + $profiles[] = $video['owner_id']; + else + $groups[] = abs($video['owner_id']); + } + } + + if($extended == 1) { + $profiles = array_unique($profiles); + $groups = array_unique($groups); + + $profilesFormatted = []; + $groupsFormatted = []; + + foreach($profiles as $prof) { + $profile = (new UsersRepo)->get($prof); + $profilesFormatted[] = $profile->toVkApiStruct($this->getUser(), $fields); + } + + foreach($groups as $gr) { + $group = (new ClubsRepo)->get($gr); + $groupsFormatted[] = $group->toVkApiStruct($this->getUser(), $fields); + } + + return (object) [ + "count" => $videosCount, + "items" => $items, + "profiles" => $profilesFormatted, + "groups" => $groupsFormatted, + ]; } return (object) [ @@ -54,4 +125,61 @@ function get(int $owner_id, string $videos, int $offset = 0, int $count = 30, in ]; } } + + function search(string $q = '', int $sort = 0, int $offset = 0, int $count = 10, bool $extended = false, string $fields = ''): object + { + $this->requireUser(); + + $params = []; + $db_sort = ['type' => 'id', 'invert' => false]; + $videos = (new VideosRepo)->find($q, $params, $db_sort); + $items = iterator_to_array($videos->offsetLimit($offset, $count)); + $count = $videos->size(); + + $return_items = []; + $profiles = []; + $groups = []; + foreach($items as $item) { + $return_item = $item->getApiStructure($this->getUser()); + $return_item = $return_item->video; + $return_items[] = $return_item; + + if($return_item['owner_id']) { + if($return_item['owner_id'] > 0) + $profiles[] = $return_item['owner_id']; + else + $groups[] = abs($return_item['owner_id']); + } + } + + if($extended) { + $profiles = array_unique($profiles); + $groups = array_unique($groups); + + $profilesFormatted = []; + $groupsFormatted = []; + + foreach($profiles as $prof) { + $profile = (new UsersRepo)->get($prof); + $profilesFormatted[] = $profile->toVkApiStruct($this->getUser(), $fields); + } + + foreach($groups as $gr) { + $group = (new ClubsRepo)->get($gr); + $groupsFormatted[] = $group->toVkApiStruct($this->getUser(), $fields); + } + + return (object) [ + "count" => $count, + "items" => $return_items, + "profiles" => $profilesFormatted, + "groups" => $groupsFormatted, + ]; + } + + return (object) [ + "count" => $count, + "items" => $return_items, + ]; + } } diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index ee07f3c12..2fe34d652 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -1,7 +1,7 @@ requireUser(); @@ -25,7 +29,7 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 $items = []; $profiles = []; $groups = []; - $cnt = $posts->getPostCountOnUserWall($owner_id); + $cnt = 0; if ($owner_id > 0) $wallOnwer = (new UsersRepo)->get($owner_id); @@ -35,11 +39,56 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 if ($owner_id > 0) if(!$wallOnwer || $wallOnwer->isDeleted()) $this->fail(18, "User was deleted or banned"); + + if(!$wallOnwer->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); 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) { + $iteratorv; + + switch($filter) { + case "all": + $iteratorv = $posts->getPostsFromUsersWall($owner_id, 1, $count, $offset); + $cnt = $posts->getPostCountOnUserWall($owner_id); + break; + case "owner": + $iteratorv = $posts->getOwnersPostsFromWall($owner_id, 1, $count, $offset); + $cnt = $posts->getOwnersCountOnUserWall($owner_id); + break; + case "others": + $iteratorv = $posts->getOthersPostsFromWall($owner_id, 1, $count, $offset); + $cnt = $posts->getOthersCountOnUserWall($owner_id); + break; + case "postponed": + $this->fail(42, "Postponed posts are not implemented."); + break; + case "suggests": + if($owner_id < 0) { + if($wallOnwer->getWallType() != 2) + $this->fail(125, "Group's wall type is open or closed"); + + if($wallOnwer->canBeModifiedBy($this->getUser())) { + $iteratorv = $posts->getSuggestedPosts($owner_id * -1, 1, $count, $offset); + $cnt = $posts->getSuggestedPostsCount($owner_id * -1); + } else { + $iteratorv = $posts->getSuggestedPostsByUser($owner_id * -1, $this->getUser()->getId(), 1, $count, $offset); + $cnt = $posts->getSuggestedPostsCountByUser($owner_id * -1, $this->getUser()->getId()); + } + } else { + $this->fail(528, "Suggested posts avaiable only at groups"); + } + + break; + default: + $this->fail(254, "Invalid filter"); + break; + } + + $iteratorv = iterator_to_array($iteratorv); + + foreach($iteratorv as $post) { $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $attachments = []; @@ -53,7 +102,21 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 } else if($attachment instanceof \openvk\Web\Models\Entities\Poll) { $attachments[] = $this->getApiPoll($attachment, $this->getUser()); } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { - $attachments[] = $attachment->getApiStructure(); + $attachments[] = $attachment->getApiStructure($this->getUser()); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { + if(VKAPI_DECL_VER === '4.100') { + $attachments[] = $attachment->toVkApiStruct(); + } else { + $attachments[] = [ + 'type' => 'note', + 'note' => $attachment->toVkApiStruct() + ]; + } + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -72,57 +135,52 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 else $profiles[] = $attachment->getOwner()->getId(); - $post_source = []; - - if($attachment->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; - } else { - $post_source = (object)[ - "type" => "api", - "platform" => $attachment->getPlatform(true) - ]; - } - $repost[] = [ "id" => $attachment->getVirtualId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), "from_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), "date" => $attachment->getPublicationTime()->timestamp(), - "post_type" => "post", + "post_type" => $attachment->getVkApiType(), "text" => $attachment->getText(false), "attachments" => $repostAttachments, - "post_source" => $post_source, + "post_source" => $attachment->getPostSourceInfo(), ]; + + if ($attachment->getTargetWall() > 0) + $profiles[] = $attachment->getTargetWall(); + else + $groups[] = abs($attachment->getTargetWall()); + if($post->isSigned()) + $profiles[] = $attachment->getOwner()->getId(); } } - $post_source = []; - - if($post->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; - } else { - $post_source = (object)[ - "type" => "api", - "platform" => $post->getPlatform(true) - ]; + $signerId = NULL; + if($post->isSigned()) { + $actualAuthor = $post->getOwner(false); + $signerId = $actualAuthor->getId(); } - $items[] = (object)[ + # TODO "can_pin", "copy_history" и прочее не должны возвращаться, если равны null или false + # Ну и ещё всё надо перенести в toVkApiStruct, а то слишком много дублированного кода + + $post_temp_obj = (object)[ "id" => $post->getVirtualId(), "from_id" => $from_id, "owner_id" => $post->getTargetWall(), "date" => $post->getPublicationTime()->timestamp(), - "post_type" => "post", + "post_type" => $post->getVkApiType(), "text" => $post->getText(false), "copy_history" => $repost, - "can_edit" => 0, # TODO + "can_edit" => $post->canBeEditedBy($this->getUser()), "can_delete" => $post->canBeDeletedBy($this->getUser()), "can_pin" => $post->canBePinnedBy($this->getUser()), "can_archive" => false, # TODO MAYBE "is_archived" => false, "is_pinned" => $post->isPinned(), + "is_explicit" => $post->isExplicit(), "attachments" => $attachments, - "post_source" => $post_source, + "post_source" => $post->getPostSourceInfo(), "comments" => (object)[ "count" => $post->getCommentsCount(), "can_post" => 1 @@ -139,14 +197,42 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 ] ]; + if($post->hasSource()) + $post_temp_obj->copyright = $post->getVkApiCopyright(); + + if($signerId) + $post_temp_obj->signer_id = $signerId; + + if($post->isDeactivationMessage()) + $post_temp_obj->final_post = 1; + + $items[] = $post_temp_obj; + if ($from_id > 0) $profiles[] = $from_id; else $groups[] = $from_id * -1; + if($post->isSigned()) + $profiles[] = $post->getOwner(false)->getId(); + $attachments = NULL; # free attachments so it will not clone everythingg } + if($rss == 1) { + $channel = new \Bhaktaraz\RSSGenerator\Channel(); + $channel->title($wallOnwer->getCanonicalName() . " — " . OPENVK_ROOT_CONF['openvk']['appearance']['name']) + ->description('Wall of ' . $wallOnwer->getCanonicalName()) + ->url(ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/wall" . $wallOnwer->getRealId()); + + foreach($iteratorv as $item) { + $output = $item->toRss(); + $output->appendTo($channel); + } + + return $channel; + } + if($extended == 1) { $profiles = array_unique($profiles); $groups = array_unique($groups); @@ -160,9 +246,9 @@ function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 3 "first_name" => $user->getFirstName(), "id" => $user->getId(), "last_name" => $user->getLastName(), - "can_access_closed" => false, - "is_closed" => false, - "sex" => $user->isFemale() ? 1 : 2, + "can_access_closed" => (bool)$user->canBeViewedBy($this->getUser()), + "is_closed" => $user->isClosed(), + "sex" => $user->isFemale() ? 1 : ($user->isNeutral() ? 0 : 2), "screen_name" => $user->getShortCode(), "photo_50" => $user->getAvatarUrl(), "photo_100" => $user->getAvatarUrl(), @@ -214,8 +300,16 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us foreach($psts as $pst) { $id = explode("_", $pst); - $post = (new PostsRepo)->getPostById(intval($id[0]), intval($id[1])); - if($post) { + $post = (new PostsRepo)->getPostById(intval($id[0]), intval($id[1]), true); + + if($post && !$post->isDeleted()) { + if(!$post->canBeViewedBy($this->getUser())) + continue; + + if($post->getSuggestionType() != 0 && !$post->canBeEditedBy($this->getUser())) { + continue; + } + $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $attachments = []; $repost = []; // чел высрал семь сигарет 😳 помянем 🕯 @@ -225,7 +319,21 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us } else if($attachment instanceof \openvk\Web\Models\Entities\Poll) { $attachments[] = $this->getApiPoll($attachment, $user); } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { - $attachments[] = $attachment->getApiStructure(); + $attachments[] = $attachment->getApiStructure($this->getUser()); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { + if(VKAPI_DECL_VER === '4.100') { + $attachments[] = $attachment->toVkApiStruct(); + } else { + $attachments[] = [ + 'type' => 'note', + 'note' => $attachment->toVkApiStruct() + ]; + } + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()) + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -244,17 +352,6 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us else $profiles[] = $attachment->getOwner()->getId(); - $post_source = []; - - if($attachment->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; - } else { - $post_source = (object)[ - "type" => "api", - "platform" => $attachment->getPlatform(true) - ]; - } - $repost[] = [ "id" => $attachment->getVirtualId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), @@ -263,37 +360,39 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us "post_type" => "post", "text" => $attachment->getText(false), "attachments" => $repostAttachments, - "post_source" => $post_source, + "post_source" => $attachment->getPostSourceInfo(), ]; + + if ($attachment->getTargetWall() > 0) + $profiles[] = $attachment->getTargetWall(); + else + $groups[] = abs($attachment->getTargetWall()); + if($post->isSigned()) + $profiles[] = $attachment->getOwner()->getId(); } } - $post_source = []; - - if($post->getPlatform(true) === NULL) { - $post_source = (object)["type" => "vk"]; - } else { - $post_source = (object)[ - "type" => "api", - "platform" => $post->getPlatform(true) - ]; + if($post->isSigned()) { + $actualAuthor = $post->getOwner(false); + $signerId = $actualAuthor->getId(); } - $items[] = (object)[ + $post_temp_obj = (object)[ "id" => $post->getVirtualId(), "from_id" => $from_id, "owner_id" => $post->getTargetWall(), "date" => $post->getPublicationTime()->timestamp(), - "post_type" => "post", + "post_type" => $post->getVkApiType(), "text" => $post->getText(false), "copy_history" => $repost, - "can_edit" => 0, # TODO + "can_edit" => $post->canBeEditedBy($this->getUser()), "can_delete" => $post->canBeDeletedBy($user), "can_pin" => $post->canBePinnedBy($user), "can_archive" => false, # TODO MAYBE "is_archived" => false, "is_pinned" => $post->isPinned(), - "post_source" => $post_source, + "is_explicit" => $post->isExplicit(), + "post_source" => $post->getPostSourceInfo(), "attachments" => $attachments, "comments" => (object)[ "count" => $post->getCommentsCount(), @@ -311,11 +410,25 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us ] ]; + if($post->hasSource()) + $post_temp_obj->copyright = $post->getVkApiCopyright(); + + if($signerId) + $post_temp_obj->signer_id = $signerId; + + if($post->isDeactivationMessage()) + $post_temp_obj->final_post = 1; + + $items[] = $post_temp_obj; + if ($from_id > 0) $profiles[] = $from_id; else $groups[] = $from_id * -1; + if($post->isSigned()) + $profiles[] = $post->getOwner(false)->getId(); + $attachments = NULL; # free attachments so it will not clone everything $repost = NULL; # same } @@ -330,19 +443,28 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us foreach($profiles as $prof) { $user = (new UsersRepo)->get($prof); - $profilesFormatted[] = (object)[ - "first_name" => $user->getFirstName(), - "id" => $user->getId(), - "last_name" => $user->getLastName(), - "can_access_closed" => false, - "is_closed" => false, - "sex" => $user->isFemale() ? 1 : 2, - "screen_name" => $user->getShortCode(), - "photo_50" => $user->getAvatarUrl(), - "photo_100" => $user->getAvatarUrl(), - "online" => $user->isOnline(), - "verified" => $user->isVerified() - ]; + if($user) { + $profilesFormatted[] = (object)[ + "first_name" => $user->getFirstName(), + "id" => $user->getId(), + "last_name" => $user->getLastName(), + "can_access_closed" => (bool)$user->canBeViewedBy($this->getUser()), + "is_closed" => $user->isClosed(), + "sex" => $user->isFemale() ? 1 : 2, + "screen_name" => $user->getShortCode(), + "photo_50" => $user->getAvatarUrl(), + "photo_100" => $user->getAvatarUrl(), + "online" => $user->isOnline(), + "verified" => $user->isVerified() + ]; + } else { + $profilesFormatted[] = (object)[ + "id" => (int) $prof, + "first_name" => "DELETED", + "last_name" => "", + "deactivated" => "deleted" + ]; + } } foreach($groups as $g) { @@ -371,7 +493,7 @@ function getById(string $posts, int $extended = 0, string $fields = "", User $us ]; } - function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0, string $attachments = ""): object + function post(string $owner_id, string $message = "", string $copyright = "", int $from_group = 0, int $signed = 0, string $attachments = "", int $post_id = 0): object { $this->requireUser(); $this->willExecuteWriteAction(); @@ -381,7 +503,7 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ $wallOwner = ($owner_id > 0 ? (new UsersRepo)->get($owner_id) : (new ClubsRepo)->get($owner_id * -1)) ?? $this->fail(18, "User was deleted or banned"); if($owner_id > 0) - $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->getUser()); + $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->getUser()) && $wallOwner->canBeViewedBy($this->getUser()); else if($owner_id < 0) if($wallOwner->canBeModifiedBy($this->getUser())) $canPost = true; @@ -392,6 +514,46 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ if($canPost == false) $this->fail(15, "Access denied"); + if($post_id > 0) { + if($owner_id > 0) + $this->fail(62, "Suggested posts available only at groups"); + + $post = (new PostsRepo)->getPostById($owner_id, $post_id, true); + + if(!$post || $post->isDeleted()) + $this->fail(32, "Invald post"); + + if($post->getSuggestionType() == 0) + $this->fail(20, "Post is not suggested"); + + if($post->getSuggestionType() == 2) + $this->fail(16, "Post is declined"); + + if(!$post->canBePinnedBy($this->getUser())) + $this->fail(51, "Access denied"); + + $author = $post->getOwner(); + $flags = 0; + $flags |= 0b10000000; + + if($signed == 1) + $flags |= 0b01000000; + + $post->setSuggested(0); + $post->setCreated(time()); + $post->setFlags($flags); + + if(!empty($message) && iconv_strlen($message) > 0) + $post->setContent($message); + + $post->save(); + + if($author->getId() != $this->getUser()->getId()) + (new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit(); + + return (object)["post_id" => $post->getVirtualId()]; + } + $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; if($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) { $manager = $wallOwner->getManager($this->getUser()); @@ -409,7 +571,17 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ if($signed == 1) $flags |= 0b01000000; - if(empty($message) && empty($attachments)) + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'poll', 'audio']); + $final_attachments = []; + $should_be_suggested = $owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2; + foreach($parsed_attachments as $attachment) { + if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + if((empty($message) && (empty($attachments) || sizeof($final_attachments) < 1))) $this->fail(100, "Required parameter 'message' missing."); try { @@ -420,54 +592,23 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ $post->setContent($message); $post->setFlags($flags); $post->setApi_Source_Name($this->getPlatform()); + + if(!is_null($copyright) && !empty($copyright)) { + try { + $post->setSource($copyright); + } catch(\Throwable) {} + } + + if($should_be_suggested) + $post->setSuggested(1); + $post->save(); } catch(\LogicException $ex) { $this->fail(100, "One of the parameters specified was missing or invalid"); } - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - # Аттачи такого вида: [тип][id владельца]_[id вложения] - # Пример: photo1_1 - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); - - $post->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); - - $post->attach($attacc); - } - } + foreach($final_attachments as $attachment) { + $post->attach($attachment); } if($wall > 0 && $wall !== $this->user->identity->getId()) @@ -476,29 +617,60 @@ function post(string $owner_id, string $message = "", int $from_group = 0, int $ return (object)["post_id" => $post->getVirtualId()]; } - function repost(string $object, string $message = "", int $group_id = 0) { + function repost(string $object, string $message = "", string $attachments = "", int $group_id = 0, int $as_group = 0, int $signed = 0) { $this->requireUser(); $this->willExecuteWriteAction(); $postArray; - if(preg_match('/wall((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0) + if(preg_match('/(wall|video|photo)((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0) $this->fail(100, "One of the parameters specified was missing or invalid: object is incorrect"); - - $post = (new PostsRepo)->getPostById((int) $postArray[1], (int) $postArray[2]); - if(!$post || $post->isDeleted()) $this->fail(100, "One of the parameters specified was missing or invalid"); - + + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio']); + $final_attachments = []; + foreach($parsed_attachments as $attachment) { + if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + $repost_entity = NULL; + $repost_type = $postArray[1]; + switch($repost_type) { + default: + case 'wall': + $repost_entity = (new PostsRepo)->getPostById((int) $postArray[2], (int) $postArray[3]); + break; + case 'photo': + $repost_entity = (new PhotosRepo)->getByOwnerAndVID((int) $postArray[2], (int) $postArray[3]); + break; + case 'video': + $repost_entity = (new VideosRepo)->getByOwnerAndVID((int) $postArray[2], (int) $postArray[3]); + break; + } + + if(!$repost_entity || $repost_entity->isDeleted() || !$repost_entity->canBeViewedBy($this->getUser())) $this->fail(100, "One of the parameters specified was missing or invalid"); + $nPost = new Post; $nPost->setOwner($this->user->getId()); if($group_id > 0) { - $club = (new ClubsRepo)->get($group_id); + $club = (new ClubsRepo)->get($group_id); if(!$club) $this->fail(42, "Invalid group"); if(!$club->canBeModifiedBy($this->user)) $this->fail(16, "Access to group denied"); - $nPost->setWall($group_id * -1); + $nPost->setWall($club->getRealId()); + $flags = 0; + if($as_group === 1 || $signed === 1) + $flags |= 0b10000000; + + if($signed === 1) + $flags |= 0b01000000; + + $nPost->setFlags($flags); } else { $nPost->setWall($this->user->getId()); } @@ -506,16 +678,27 @@ function repost(string $object, string $message = "", int $group_id = 0) { $nPost->setContent($message); $nPost->setApi_Source_Name($this->getPlatform()); $nPost->save(); - $nPost->attach($post); - - if($post->getOwner(false)->getId() !== $this->user->getId() && !($post->getOwner() instanceof Club)) - (new RepostNotification($post->getOwner(false), $post, $this->user))->emit(); + + $nPost->attach($repost_entity); + + foreach($parsed_attachments as $attachment) { + $nPost->attach($attachment); + } + + if($repost_type == 'wall' && $repost_entity->getOwner(false)->getId() !== $this->user->getId() && !($repost_entity->getOwner() instanceof Club)) + (new RepostNotification($repost_entity->getOwner(false), $repost_entity, $this->user))->emit(); + + $repost_count = 1; + if($repost_type == 'wall') { + $repost_count = $repost_entity->getRepostCount(); + } return (object) [ "success" => 1, // 👍 "post_id" => $nPost->getVirtualId(), - "reposts_count" => $post->getRepostCount(), - "likes_count" => $post->getLikesCount() + "pretty_id" => $nPost->getPrettyId(), + "reposts_count" => $repost_count, + "likes_count" => $repost_entity->getLikesCount() ]; } @@ -525,6 +708,9 @@ function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $ $post = (new PostsRepo)->getPostById($owner_id, $post_id); if(!$post || $post->isDeleted()) $this->fail(100, "One of the parameters specified was missing or invalid"); + + if(!$post->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); $comments = (new CommentsRepo)->getCommentsByTarget($post, $offset+1, $count, $sort == "desc" ? "DESC" : "ASC"); @@ -542,6 +728,13 @@ function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $ foreach($comment->getChildren() as $attachment) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { + $attachments[] = $attachment->toVkApiStruct(); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -551,7 +744,7 @@ function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $ "date" => $comment->getPublicationTime()->timestamp(), "text" => $comment->getText(false), "post_id" => $post->getVirtualId(), - "owner_id" => $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(), + "owner_id" => method_exists($post, 'isPostedOnBehalfOfGroup') && $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(), "parents_stack" => [], "attachments" => $attachments, "thread" => [ @@ -563,6 +756,9 @@ function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $ ] ]; + if($comment->isFromPostAuthor($post)) + $item['is_from_post_author'] = true; + if($need_likes == true) $item['likes'] = [ "can_like" => 1, @@ -599,7 +795,13 @@ function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $ function getComment(int $owner_id, int $comment_id, bool $extended = false, string $fields = "sex,screen_name,photo_50,photo_100,online_info,online") { $this->requireUser(); - $comment = (new CommentsRepo)->get($comment_id); // один хуй айди всех комментов общий + $comment = (new CommentsRepo)->get($comment_id); # один хуй айди всех комментов общий + + if(!$comment || $comment->isDeleted()) + $this->fail(100, "Invalid comment"); + + if(!$comment->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); $profiles = []; @@ -608,6 +810,18 @@ function getComment(int $owner_id, int $comment_id, bool $extended = false, stri foreach($comment->getChildren() as $attachment) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Video) { + $attachments[] = $attachment->getApiStructure(); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { + $attachments[] = [ + 'type' => 'note', + 'note' => $attachment->toVkApiStruct() + ]; + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = [ + "type" => "audio", + "audio" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -617,7 +831,7 @@ function getComment(int $owner_id, int $comment_id, bool $extended = false, stri "date" => $comment->getPublicationTime()->timestamp(), "text" => $comment->getText(false), "post_id" => $comment->getTarget()->getVirtualId(), - "owner_id" => $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(), + "owner_id" => method_exists($comment->getTarget(), 'isPostedOnBehalfOfGroup') && $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(), "parents_stack" => [], "attachments" => $attachments, "likes" => [ @@ -635,6 +849,9 @@ function getComment(int $owner_id, int $comment_id, bool $extended = false, stri ] ]; + if($comment->isFromPostAuthor()) + $item['is_from_post_author'] = true; + if($extended == true) $profiles[] = $comment->getOwner()->getId(); @@ -650,22 +867,32 @@ function getComment(int $owner_id, int $comment_id, bool $extended = false, stri $response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []); } - - return $response; } - function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0, string $attachments = "") { + function createComment(int $owner_id, int $post_id, string $message = "", int $from_group = 0, string $attachments = "") { $this->requireUser(); $this->willExecuteWriteAction(); $post = (new PostsRepo)->getPostById($owner_id, $post_id); if(!$post || $post->isDeleted()) $this->fail(100, "Invalid post"); + if(!$post->canBeViewedBy($this->getUser())) + $this->fail(15, "Access denied"); + if($post->getTargetWall() < 0) $club = (new ClubsRepo)->get(abs($post->getTargetWall())); - if(empty($message) && empty($attachments)) { + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio']); + $final_attachments = []; + foreach($parsed_attachments as $attachment) { + if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + if((empty($message) && (empty($attachments) || sizeof($final_attachments) < 1))) { $this->fail(100, "Required parameter 'message' missing."); } @@ -686,47 +913,8 @@ function createComment(int $owner_id, int $post_id, string $message, int $from_g $this->fail(1, "ошибка про то что коммент большой слишком"); } - if(!empty($attachments)) { - $attachmentsArr = explode(",", $attachments); - - if(sizeof($attachmentsArr) > 10) - $this->fail(50, "Error: too many attachments"); - - foreach($attachmentsArr as $attac) { - $attachmentType = NULL; - - if(str_contains($attac, "photo")) - $attachmentType = "photo"; - elseif(str_contains($attac, "video")) - $attachmentType = "video"; - else - $this->fail(205, "Unknown attachment type"); - - $attachment = str_replace($attachmentType, "", $attac); - - $attachmentOwner = (int)explode("_", $attachment)[0]; - $attachmentId = (int)end(explode("_", $attachment)); - - $attacc = NULL; - - if($attachmentType == "photo") { - $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Photo does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this photo"); - - $comment->attach($attacc); - } elseif($attachmentType == "video") { - $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); - if(!$attacc || $attacc->isDeleted()) - $this->fail(100, "Video does not exists"); - if($attacc->getOwner()->getId() != $this->getUser()->getId()) - $this->fail(43, "You do not have access to this video"); - - $comment->attach($attacc); - } - } + foreach($final_attachments as $attachment) { + $comment->attach($attachment); } if($post->getOwner()->getId() !== $this->user->getId()) @@ -752,12 +940,200 @@ function deleteComment(int $comment_id) { return 1; } + + function delete(int $owner_id, int $post_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $post = (new PostsRepo)->getPostById($owner_id, $post_id, true); + if(!$post || $post->isDeleted()) + $this->fail(583, "Invalid post"); + + $wallOwner = $post->getWallOwner(); + + if($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->getUser()) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0) + $this->fail(12, "Access denied: you can't delete your accepted post."); + + if($post->getOwnerPost() == $this->getUser()->getId() || $post->getTargetWall() == $this->getUser()->getId() || $owner_id < 0 && $wallOwner->canBeModifiedBy($this->getUser())) { + $post->unwire(); + $post->delete(); + + return 1; + } else { + $this->fail(15, "Access denied"); + } + } + + function edit(int $owner_id, int $post_id, string $message = "", string $attachments = "", string $copyright = NULL, int $explicit = -1, int $from_group = 0, int $signed = 0) { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'poll']); + $final_attachments = []; + foreach($parsed_attachments as $attachment) { + if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + if(empty($message) && sizeof($final_attachments) < 1) { + $this->fail(-66, "Post will be empty, don't saving."); + } + + $post = (new PostsRepo)->getPostById($owner_id, $post_id, true); + + if(!$post || $post->isDeleted()) + $this->fail(102, "Invalid post"); + + if(!$post->canBeEditedBy($this->getUser())) + $this->fail(7, "Access to editing denied"); + + if(!empty($message) || (empty($message) && sizeof($final_attachments) > 0)) + $post->setContent($message); + + $post->setEdited(time()); + if(!is_null($copyright) && !empty($copyright)) { + if($copyright == 'remove') { + $post->resetSource(); + } else { + try { + $post->setSource($copyright); + } catch(\Throwable) {} + } + } + + if($explicit != -1) { + $post->setNsfw($explicit == 1); + } + + $wallOwner = ($owner_id > 0 ? (new UsersRepo)->get($owner_id) : (new ClubsRepo)->get($owner_id * -1)); + $flags = 0; + if($from_group == 1 && $wallOwner instanceof Club && $wallOwner->canBeModifiedBy($this->getUser())) + $flags |= 0b10000000; + /*if($signed == 1) + $flags |= 0b01000000;*/ + + $post->setFlags($flags); + $post->save(true); + + if($attachments == 'remove' || sizeof($final_attachments) > 0) { + foreach($post->getChildren() as $att) { + if(!($att instanceof Post)) { + $post->detach($att); + } + } + + foreach($final_attachments as $attachment) { + $post->attach($attachment); + } + } + + return ["post_id" => $post->getVirtualId()]; + } + + function editComment(int $comment_id, int $owner_id = 0, string $message = "", string $attachments = "") { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $comment = (new CommentsRepo)->get($comment_id); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio']); + $final_attachments = []; + foreach($parsed_attachments as $attachment) { + if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && + !(method_exists($attachment, 'getVoters') && $attachment->getOwner()->getId() != $this->getUser()->getId())) { + $final_attachments[] = $attachment; + } + } + + if(empty($message) && sizeof($final_attachments) < 1) + $this->fail(100, "Required parameter 'message' missing."); + + if(!$comment || $comment->isDeleted()) + $this->fail(102, "Invalid comment"); + + if(!$comment->canBeEditedBy($this->getUser())) + $this->fail(15, "Access to editing comment denied"); + + if(!empty($message) || (empty($message) && sizeof($final_attachments) > 0)) + $comment->setContent($message); + + $comment->setEdited(time()); + $comment->save(true); + + if(sizeof($final_attachments) > 0) { + $comment->unwire(); + foreach($final_attachments as $attachment) { + $comment->attach($attachment); + } + } + + return 1; + } + + function checkCopyrightLink(string $link): int + { + $this->requireUser(); + + try { + $result = check_copyright_link($link); + } catch(\InvalidArgumentException $e) { + $this->fail(3102, "Specified link is incorrect (can't find source)"); + } catch(\LengthException $e) { + $this->fail(3103, "Specified link is incorrect (too long)"); + } catch(\LogicException $e) { + $this->fail(3104, "Link is suspicious"); + } catch(\Throwable $e) { + $this->fail(3102, "Specified link is incorrect"); + } + + return 1; + } + + function pin(int $owner_id, int $post_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $post = (new PostsRepo)->getPostById($owner_id, $post_id); + if(!$post || $post->isDeleted()) + $this->fail(100, "One of the parameters specified was missing or invalid: post_id is undefined"); + + if(!$post->canBePinnedBy($this->getUser())) + return 0; + + if($post->isPinned()) + return 1; + + $post->pin(); + return 1; + } + + function unpin(int $owner_id, int $post_id) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $post = (new PostsRepo)->getPostById($owner_id, $post_id); + if(!$post || $post->isDeleted()) + $this->fail(100, "One of the parameters specified was missing or invalid: post_id is undefined"); + + if(!$post->canBePinnedBy($this->getUser())) + return 0; + + if(!$post->isPinned()) + return 1; + + $post->unpin(); + return 1; + } private function getApiPhoto($attachment) { return [ "type" => "photo", "photo" => [ - "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : NULL, + "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : 0, "date" => $attachment->getPublicationTime()->timestamp(), "id" => $attachment->getVirtualId(), "owner_id" => $attachment->getOwner()->getId(), diff --git a/VKAPI/README.md b/VKAPI/README.md index 75918afcb..08c8408b0 100644 --- a/VKAPI/README.md +++ b/VKAPI/README.md @@ -5,7 +5,7 @@ exceptions. It is still a work-in-progress functionality. **Note**: requests to API are routed through openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers. -[Documentation for API clients](https://docs.openvk.uk/openvk_engine/api/description/) +[Documentation for API clients](https://docs.ovk.to/openvk_engine/api/description/) ## Implementing API methods diff --git a/Web/Models/Entities/APIToken.php b/Web/Models/Entities/APIToken.php index f5744ec33..0cc531487 100644 --- a/Web/Models/Entities/APIToken.php +++ b/Web/Models/Entities/APIToken.php @@ -48,7 +48,7 @@ function revoke(): void $this->delete(); } - function save(): void + function save(?bool $log = false): void { if(is_null($this->getRecord())) $this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36))); diff --git a/Web/Models/Entities/Album.php b/Web/Models/Entities/Album.php index 150cc6257..cc96cc0ba 100644 --- a/Web/Models/Entities/Album.php +++ b/Web/Models/Entities/Album.php @@ -67,11 +67,27 @@ function hasPhoto(Photo $photo): bool return $this->has($photo); } + function canBeViewedBy(?User $user = NULL): bool + { + if($this->isDeleted()) { + return false; + } + + $owner = $this->getOwner(); + + if(get_class($owner) == "openvk\\Web\\Models\\Entities\\User") { + return $owner->canBeViewedBy($user) && $owner->getPrivacyPermission('photos.read', $user); + } else { + return $owner->canBeViewedBy($user); + } + } + function toVkApiStruct(?User $user = NULL, bool $need_covers = false, bool $photo_sizes = false): object { $res = (object) []; $res->id = $this->getPrettyId(); + $res->vid = $this->getId(); $res->thumb_id = !is_null($this->getCoverPhoto()) ? $this->getCoverPhoto()->getPrettyId() : 0; $res->owner_id = $this->getOwner()->getId(); $res->title = $this->getName(); diff --git a/Web/Models/Entities/Application.php b/Web/Models/Entities/Application.php index 74569485f..489756453 100644 --- a/Web/Models/Entities/Application.php +++ b/Web/Models/Entities/Application.php @@ -306,11 +306,14 @@ function withdrawCoins(): void function delete(bool $softly = true): void { if($softly) - throw new \UnexpectedValueException("Can't delete apps softly."); + throw new \UnexpectedValueException("Can't delete apps softly."); // why $cx = DatabaseConnection::i()->getContext(); $cx->table("app_users")->where("app", $this->getId())->delete(); parent::delete(false); } + + function getPublicationTime(): string + { return tr("recently"); } } \ No newline at end of file diff --git a/Web/Models/Entities/Audio.php b/Web/Models/Entities/Audio.php new file mode 100644 index 000000000..23941f4c4 --- /dev/null +++ b/Web/Models/Entities/Audio.php @@ -0,0 +1,479 @@ + 1, + "Pop" => 2, + "Rap" => 3, + "Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK + "Easy Listening" => 4, + "House" => 5, + "Dance" => 5, + "Instrumental" => 6, + "Metal" => 7, + "Alternative" => 21, + "Dubstep" => 8, + "Jazz" => 1001, + "Blues" => 1001, + "Drum & Bass" => 10, + "Trance" => 11, + "Chanson" => 12, + "Ethnic" => 13, + "Acoustic" => 14, + "Vocal" => 14, + "Reggae" => 15, + "Classical" => 16, + "Indie Pop" => 17, + "Speech" => 19, + "Disco" => 22, + "Other" => 18, + ]; + + private function fileLength(string $filename): int + { + if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe")) + throw new \Exception(); + + $error = NULL; + $streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error); + if($error !== 0) + throw new \DomainException("$filename is not recognized as media container"); + else if(empty($streams) || ctype_space($streams)) + throw new \DomainException("$filename does not contain any audio streams"); + + $vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error); + + # check if audio has cover (attached_pic) + preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover); + if(!empty($vstreams) && !ctype_space($vstreams) && ((int)($hasCover[1]) !== 1)) + throw new \DomainException("$filename is a video"); + + $durations = []; + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + if(sizeof($durations[1]) === 0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + + $length = 0; + foreach($durations[1] as $duration) { + $duration = floatval($duration); + if($duration < 1.0 || $duration > 65536.0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + else + $length = max($length, $duration); + } + + return (int) round($length, 0, PHP_ROUND_HALF_EVEN); + } + + /** + * @throws \Exception + */ + protected function saveFile(string $filename, string $hash): bool + { + $duration = $this->fileLength($filename); + + $kid = openssl_random_pseudo_bytes(16); + $key = openssl_random_pseudo_bytes(16); + $tok = openssl_random_pseudo_bytes(28); + $ss = ceil($duration / 15); + + $this->stateChanges("kid", $kid); + $this->stateChanges("key", $key); + $this->stateChanges("token", $tok); + $this->stateChanges("segment_size", $ss); + $this->stateChanges("length", $duration); + + try { + $args = [ + str_replace("enabled", "available", OPENVK_ROOT), + str_replace("enabled", "available", $this->getBaseDir()), + $hash, + $filename, + + bin2hex($kid), + bin2hex($key), + bin2hex($tok), + $ss, + ]; + + if(Shell::isPowershell()) { + Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args) + ->start(); + } else { + Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk + ->start(); // idk, not tested :") + } + + # Wait until processAudio will consume the file + $start = time(); + while(file_exists($filename)) + if(time() - $start > 5) + throw new \RuntimeException("Timed out waiting FFMPEG"); + + } catch(UnknownCommandException $ucex) { + exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR); + } + + return true; + } + + function getTitle(): string + { + return $this->getRecord()->name; + } + + function getPerformer(): string + { + return $this->getRecord()->performer; + } + + function getPerformers(): array + { + return explode(", ", $this->getRecord()->performer); + } + + function getName(): string + { + return $this->getPerformer() . " — " . $this->getTitle(); + } + + function getDownloadName(): string + { + return preg_replace('/[\\/:*?"<>|]/', '_', str_replace(' ', '_', $this->getName())); + } + + function getGenre(): ?string + { + return $this->getRecord()->genre; + } + + function getLyrics(): ?string + { + return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL; + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function getFormattedLength(): string + { + $len = $this->getLength(); + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + function getSegmentSize(): float + { + return $this->getRecord()->segment_size; + } + + function getListens(): int + { + return $this->getRecord()->listens; + } + + function getOriginalURL(bool $force = false): string + { + $disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force; + if(!$this->isAvailable() || $disallowed) + return ovk_scheme(true) + . $_SERVER["HTTP_HOST"] . ":" + . $_SERVER["HTTP_PORT"] + . "/assets/packages/static/openvk/audio/nomusic.mp3"; + + $key = bin2hex($this->getRecord()->token); + + return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3"; + } + + function getURL(?bool $force = false): string + { + if ($this->isWithdrawn()) return ""; + + return parent::getURL(); + } + + function getKeys(): array + { + $keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key); + + return $keys; + } + + function isAnonymous(): bool + { + return false; + } + + function isExplicit(): bool + { + return (bool) $this->getRecord()->explicit; + } + + function isWithdrawn(): bool + { + return (bool) $this->getRecord()->withdrawn; + } + + function isUnlisted(): bool + { + return (bool) $this->getRecord()->unlisted; + } + + # NOTICE may flush model to DB if it was just processed + function isAvailable(): bool + { + if($this->getRecord()->processed) + return true; + + # throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage + if(time() - $this->getRecord()->checked < 5) + return false; + + try { + $fragments = str_replace(".mpd", "_fragments", $this->getFileName()); + $original = "original_" . bin2hex($this->getRecord()->token) . ".mp3"; + if(file_exists("$fragments/$original")) { + # Original gets uploaded after fragments + $this->stateChanges("processed", 0x01); + + return true; + } + } finally { + $this->stateChanges("checked", time()); + $this->save(); + } + + return false; + } + + function isInLibraryOf($entity): bool + { + return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])) != 0; + } + + function add($entity): bool + { + if($this->isInLibraryOf($entity)) + return false; + + $entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1); + $audioRels = DatabaseConnection::i()->getContext()->table("audio_relations"); + if(sizeof($audioRels->where("entity", $entityId)) > 65536) + throw new \OverflowException("Can't have more than 65536 audios in a playlist"); + + $audioRels->insert([ + "entity" => $entityId, + "audio" => $this->getId(), + ]); + + return true; + } + + function remove($entity): bool + { + if(!$this->isInLibraryOf($entity)) + return false; + + DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])->delete(); + + return true; + } + + function listen($entity, Playlist $playlist = NULL): bool + { + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + ])->order("index DESC")->fetch(); + + if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) { + $listensTable->insert([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + "time" => time(), + "playlist" => $playlist ? $playlist->getId() : NULL, + ]); + + if($entity instanceof User) { + $this->stateChanges("listens", ($this->getListens() + 1)); + $this->save(); + + if($playlist) { + $playlist->incrementListens(); + $playlist->save(); + } + } + + $entity->setLast_played_track($this->getId()); + $entity->save(); + + return true; + } + + $lastListen->update([ + "time" => time(), + ]); + + return false; + } + + /** + * Returns compatible with VK API 4.x, 5.x structure. + * + * Always sets album(_id) to NULL at this time. + * If genre is not present in VK genre list, fallbacks to "Other". + * The url and manifest properties will be set to false if the audio can't be played (processing, removed). + * + * Aside from standard VK properties, this method will also return some OVK extended props: + * 1. added - Is in the library of $user? + * 2. editable - Can be edited by $user? + * 3. withdrawn - Removed due to copyright request? + * 4. ready - Can be played at this time? + * 5. genre_str - Full name of genre, NULL if it's undefined + * 6. manifest - URL to MPEG-DASH manifest + * 7. keys - ClearKey DRM keys + * 8. explicit - Marked as NSFW? + * 9. searchable - Can be found via search? + * 10. unique_id - Unique ID of audio + * + * @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to nomusic.mp3, + * unless $forceURLExposure is set to true. + * + * @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models. + * + * @param ?User $user user, relative to whom "added", "editable" will be set + * @param bool $forceURLExposure force set "url" regardless of config + */ + function toVkApiStruct(?User $user = NULL, bool $forceURLExposure = false): object + { + $obj = (object) []; + $obj->unique_id = base64_encode((string) $this->getId()); + $obj->id = $obj->aid = $this->getVirtualId(); + $obj->artist = $this->getPerformer(); + $obj->title = $this->getTitle(); + $obj->duration = $this->getLength(); + $obj->album_id = $obj->album = NULL; # i forgor to implement + $obj->url = false; + $obj->manifest = false; + $obj->keys = false; + $obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match + $obj->genre_str = $this->getGenre(); + $obj->owner_id = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $obj->owner_id *= -1; + + $obj->lyrics = NULL; + if(!is_null($this->getLyrics())) + $obj->lyrics = $this->getId(); + + $obj->added = $user && $this->isInLibraryOf($user); + $obj->editable = $user && $this->canBeModifiedBy($user); + $obj->searchable = !$this->isUnlisted(); + $obj->explicit = $this->isExplicit(); + $obj->withdrawn = $this->isWithdrawn(); + $obj->ready = $this->isAvailable() && !$obj->withdrawn; + if($obj->ready) { + $obj->url = $this->getOriginalURL($forceURLExposure); + $obj->manifest = $this->getURL(); + $obj->keys = $this->getKeys(); + } + + return $obj; + } + + function setOwner(int $oid): void + { + # WARNING: API implementation won't be able to handle groups like that, don't remove + if($oid <= 0) + throw new \OutOfRangeException("Only users can be owners of audio!"); + + $this->stateChanges("owner", $oid); + } + + function setGenre(string $genre): void + { + if(!in_array($genre, Audio::genres)) { + $this->stateChanges("genre", NULL); + return; + } + + $this->stateChanges("genre", $genre); + } + + function setCopyrightStatus(bool $withdrawn = true): void { + $this->stateChanges("withdrawn", $withdrawn); + } + + function setSearchability(bool $searchable = true): void { + $this->stateChanges("unlisted", !$searchable); + } + + function setToken(string $tok): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKid(string $kid): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKey(string $key): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setLength(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function setSegment_Size(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("audio_relations")->where("audio", $this->getId()) + ->delete(); + $ctx->table("audio_listens")->where("audio", $this->getId()) + ->delete(); + $ctx->table("playlist_relations")->where("media", $this->getId()) + ->delete(); + + parent::delete($softly); + } +} diff --git a/Web/Models/Entities/Ban.php b/Web/Models/Entities/Ban.php new file mode 100644 index 000000000..3962c6cbf --- /dev/null +++ b/Web/Models/Entities/Ban.php @@ -0,0 +1,66 @@ +getRecord()->id; + } + + function getReason(): ?string + { + return $this->getRecord()->reason; + } + + function getUser(): ?User + { + return (new Users)->get($this->getRecord()->user); + } + + function getInitiator(): ?User + { + return (new Users)->get($this->getRecord()->initiator); + } + + function getStartTime(): int + { + return $this->getRecord()->iat; + } + + function getEndTime(): int + { + return $this->getRecord()->exp; + } + + function getTime(): int + { + return $this->getRecord()->time; + } + + function isPermanent(): bool + { + return $this->getEndTime() === 0; + } + + function isRemovedManually(): bool + { + return (bool) $this->getRecord()->removed_manually; + } + + function isOver(): bool + { + return $this->isRemovedManually(); + } + + function whoRemoved(): ?User + { + return (new Users)->get($this->getRecord()->removed_by); + } +} diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index b321a4761..6121b69d8 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -3,7 +3,7 @@ use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\{User, Manager}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Managers}; +use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Managers, Posts}; use Nette\Database\Table\{ActiveRow, GroupedSelection}; use Chandler\Database\DatabaseConnection as DB; use Chandler\Security\User as ChandlerUser; @@ -23,6 +23,10 @@ class Club extends RowModel const NOT_RELATED = 0; const SUBSCRIBED = 1; const REQUEST_SENT = 2; + + const WALL_CLOSED = 0; + const WALL_OPEN = 1; + const WALL_LIMITED = 2; function getId(): int { @@ -38,13 +42,19 @@ function getAvatarPhoto(): ?Photo 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); } + + function getWallType(): int + { + return $this->getRecord()->wall; + } function getAvatarLink(): string { @@ -143,6 +153,11 @@ function isHideFromGlobalFeedEnabled(): bool return (bool) $this->getRecord()->hide_from_global_feed; } + function isHidingFromGlobalFeedEnforced(): bool + { + return (bool) $this->getRecord()->enforce_hiding_from_global_feed; + } + function getType(): int { return $this->getRecord()->type; @@ -182,6 +197,14 @@ function setShortCode(?string $code = NULL): ?bool $this->stateChanges("shortcode", $code); return true; } + + function setWall(int $type) + { + if($type > 2 || $type < 0) + throw new \LogicException("Invalid wall"); + + $this->stateChanges("wall", $type); + } function isSubscriptionAccepted(User $user): bool { @@ -224,7 +247,7 @@ function getPostViewStats(bool $unique = false): ?array "shape" => "spline", "color" => "#597da3", ], - "name" => $unique ? "Полный охват" : "Все просмотры", + "name" => $unique ? tr("full_coverage") : tr("all_views"), ], "subs" => [ "x" => array_reverse(range(1, 7)), @@ -235,7 +258,7 @@ function getPostViewStats(bool $unique = false): ?array "color" => "#b05c91", ], "fill" => "tozeroy", - "name" => $unique ? "Охват подписчиков" : "Просмотры подписчиков", + "name" => $unique ? tr("subs_coverage") : tr("subs_views"), ], "viral" => [ "x" => array_reverse(range(1, 7)), @@ -246,7 +269,7 @@ function getPostViewStats(bool $unique = false): ?array "color" => "#4d9fab", ], "fill" => "tozeroy", - "name" => $unique ? "Виральный охват" : "Виральные просмотры", + "name" => $unique ? tr("viral_coverage") : tr("viral_views"), ], ]; } @@ -272,7 +295,7 @@ function getFollowersQuery(string $sort = "follower ASC"): GroupedSelection return false; } - return $query; + return $query->group("follower"); } function getFollowersCount(): int @@ -291,6 +314,21 @@ function getFollowers(int $page = 1, int $perPage = 6, string $sort = "follower yield $rel; } } + + function getSuggestedPostsCount(User $user = NULL) + { + $count = 0; + + if(is_null($user)) + return NULL; + + if($this->canBeModifiedBy($user)) + $count = (new Posts)->getSuggestedPostsCount($this->getId()); + else + $count = (new Posts)->getSuggestedPostsCountByUser($this->getId(), $user->getId()); + + return $count; + } function getManagers(int $page = 1, bool $ignoreHidden = false): \Traversable { @@ -351,44 +389,112 @@ function canBeModifiedBy(User $user): bool } function getWebsite(): ?string - { - return $this->getRecord()->website; - } + { + return $this->getRecord()->website; + } + + function ban(string $reason): void + { + $this->setBlock_Reason($reason); + $this->save(); + } + + function unban(): void + { + $this->setBlock_Reason(null); + $this->save(); + } + + function canBeViewedBy(?User $user = NULL) + { + return is_null($this->getBanReason()); + } function getAlert(): ?string { return $this->getRecord()->alert; } + + function getRealId(): int + { + return $this->getId() * -1; + } + + function isEveryoneCanUploadAudios(): bool + { + return (bool) $this->getRecord()->everyone_can_upload_audios; + } + + function canUploadAudio(?User $user): bool + { + if(!$user) + return NULL; + + return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); + } + + function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this); + } - function toVkApiStruct(?User $user = NULL): object + function toVkApiStruct(?User $user = NULL, string $fields = ''): object { - $res = []; + $res = (object) []; $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 = $this->canBeModifiedBy($user); + $res->can_access_closed = true; - if($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 = $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 = $this->canBeModifiedBy($user) ? 1 : $this->isEveryoneCanCreateTopics() ? 1 : 0; - - $res->can_post = $this->canBeModifiedBy($user) ? 1 : $this->canPost() ? 1 : 0; - - return (object) $res; + return $res; } use Traits\TBackDrops; use Traits\TSubscribable; + use Traits\TAudioStatuses; + use Traits\TIgnorable; } diff --git a/Web/Models/Entities/Comment.php b/Web/Models/Entities/Comment.php index bd833a82a..acd5d7b57 100644 --- a/Web/Models/Entities/Comment.php +++ b/Web/Models/Entities/Comment.php @@ -11,7 +11,7 @@ class Comment extends Post function getPrettyId(): string { - return $this->getRecord()->id; + return (string)$this->getRecord()->id; } function getVirtualId(): int @@ -28,6 +28,11 @@ function getTarget(): ?Postable return $entity; } + function getPageURL(): string + { + return '#'; + } + /** * May return fake owner (group), if flags are [1, (*)] * @@ -46,8 +51,11 @@ function getOwner(bool $honourFlags = true, bool $real = false): RowModel return parent::getOwner($honourFlags, $real); } - function canBeDeletedBy(User $user): bool + function canBeDeletedBy(User $user = NULL): bool { + if(!$user) + return false; + return $this->getOwner()->getId() == $user->getId() || $this->getTarget()->getOwner()->getId() == $user->getId() || $this->getTarget() instanceof Post && $this->getTarget()->getTargetWall() < 0 && (new Clubs)->get(abs($this->getTarget()->getTargetWall()))->canBeModifiedBy($user) || @@ -61,7 +69,7 @@ function toVkApiStruct(?User $user = NULL, bool $need_likes = false, bool $exten $res->id = $this->getId(); $res->from_id = $this->getOwner()->getId(); $res->date = $this->getPublicationTime()->timestamp(); - $res->text = $this->getText(); + $res->text = $this->getText(false); $res->attachments = []; $res->parents_stack = []; @@ -74,8 +82,12 @@ function toVkApiStruct(?User $user = NULL, bool $need_likes = false, bool $exten foreach($this->getChildren() as $attachment) { if($attachment->isDeleted()) continue; - - $res->attachments[] = $attachment->toVkApiStruct(); + + if($attachment instanceof \openvk\Web\Models\Entities\Photo) { + $res->attachments[] = $attachment->toVkApiStruct(); + } else if($attachment instanceof \openvk\Web\Models\Entities\Video) { + $res->attachments[] = $attachment->toVkApiStruct($this->getUser()); + } } if($need_likes) { @@ -85,4 +97,82 @@ function toVkApiStruct(?User $user = NULL, bool $need_likes = false, bool $exten } return $res; } + + function getURL(): string + { + return "/wall" . $this->getTarget()->getPrettyId() . "#_comment" . $this->getId(); + } + + function canBeViewedBy(?User $user = NULL): bool + { + if($this->isDeleted() || $this->getTarget()->isDeleted()) { + return false; + } + + return $this->getTarget()->canBeViewedBy($user); + } + + function isFromPostAuthor($target = NULL) + { + if(!$target) + $target = $this->getTarget(); + + $target_owner = $target->getOwner(); + $comment_owner = $this->getOwner(); + + if($target_owner->getRealId() === $comment_owner->getRealId()) + return true; + + # TODO: make it work with signer_id + + return false; + } + + function toNotifApiStruct() + { + $res = (object)[]; + + $res->id = $this->getId(); + $res->owner_id = $this->getOwner()->getId(); + $res->date = $this->getPublicationTime()->timestamp(); + $res->text = $this->getText(false); + $res->post = NULL; # todo + + return $res; + } + + function canBeEditedBy(?User $user = NULL): bool + { + if(!$user) + return false; + + return $user->getId() == $this->getOwner(false)->getId(); + } + + function getTargetURL(): string + { + $target = $this->getTarget(); + $target_name = 'wall'; + + if(!$target) { + return '/404'; + } + + switch(get_class($target)) { + case 'openvk\Web\Models\Entities\Note': + $target_name = 'note'; + break; + case 'openvk\Web\Models\Entities\Photo': + $target_name = 'photo'; + break; + case 'openvk\Web\Models\Entities\Video': + $target_name = 'video'; + break; + case 'openvk\Web\Models\Entities\Topic': + $target_name = 'topic'; + break; + } + + return $target_name . $target->getPrettyId(); + } } diff --git a/Web/Models/Entities/Correspondence.php b/Web/Models/Entities/Correspondence.php index e4b7d96eb..a972425e5 100644 --- a/Web/Models/Entities/Correspondence.php +++ b/Web/Models/Entities/Correspondence.php @@ -131,7 +131,7 @@ function getMessages(int $capBehavior = 1, ?int $cap = NULL, ?int $limit = NULL, */ function getPreviewMessage(): ?Message { - $messages = $this->getMessages(1, NULL, 1); + $messages = $this->getMessages(1, NULL, 1, 0); return $messages[0] ?? NULL; } diff --git a/Web/Models/Entities/IP.php b/Web/Models/Entities/IP.php index ecea92cad..0d9b8fd0f 100644 --- a/Web/Models/Entities/IP.php +++ b/Web/Models/Entities/IP.php @@ -92,7 +92,7 @@ function rateLimit(int $actionComplexity = 1): int $this->stateChanges("rate_limit_counter", $aCounter); $this->stateChanges("rate_limit_violation_counter_start", $vCounterSessionStart); $this->stateChanges("rate_limit_violation_counter", $vCounter); - $this->save(); + $this->save(false); } } @@ -105,11 +105,11 @@ function setIp(string $ip): void $this->stateChanges("ip", $ip); } - function save(): void + function save(?bool $log = false): void { if(is_null($this->getRecord())) $this->stateChanges("first_seen", time()); - parent::save(); + parent::save($log); } } diff --git a/Web/Models/Entities/Media.php b/Web/Models/Entities/Media.php index 9377f3e8e..648d3564e 100644 --- a/Web/Models/Entities/Media.php +++ b/Web/Models/Entities/Media.php @@ -121,14 +121,14 @@ function setFile(array $file): void $this->stateChanges("hash", $hash); } - function save(): void + function save(?bool $log = false): void { if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) { $this->stateChanges("processed", 0); $this->stateChanges("last_checked", time()); } - parent::save(); + parent::save($log); } function delete(bool $softly = true): void diff --git a/Web/Models/Entities/MediaCollection.php b/Web/Models/Entities/MediaCollection.php index 05f3835c1..1f0619886 100644 --- a/Web/Models/Entities/MediaCollection.php +++ b/Web/Models/Entities/MediaCollection.php @@ -17,7 +17,17 @@ abstract class MediaCollection extends RowModel protected $specialNames = []; - private $relations; + protected $relations; + + /** + * Maximum amount of items Collection can have + */ + const MAX_ITEMS = INF; + + /** + * Maximum amount of Collections with same "owner" allowed + */ + const MAX_COUNT = INF; function __construct(?ActiveRow $ar = NULL) { @@ -70,18 +80,29 @@ function getDescription(): ?string } abstract function getCoverURL(): ?string; - - function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + + function fetchClassic(int $offset = 0, ?int $limit = NULL): \Traversable { - $related = $this->getRecord()->related("$this->relTableName.collection")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("media ASC"); + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("media ASC"); + foreach($related as $rel) { $media = $rel->ref($this->entityTableName, "media"); if(!$media) continue; - + yield new $this->entityClassName($media); } } + + function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + { + $page = max(1, $page); + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + + return $this->fetchClassic($perPage * ($page - 1), $perPage); + } function size(): int { @@ -110,7 +131,7 @@ function isCreatedBySystem(): bool { return $this->getRecord()->special_type !== 0; } - + function add(RowModel $entity): bool { $this->entitySuitable($entity); @@ -118,6 +139,10 @@ function add(RowModel $entity): bool if(!$this->allowDuplicates) if($this->has($entity)) return false; + + if(self::MAX_ITEMS != INF) + if(sizeof($this->relations->where("collection", $this->getId())) > self::MAX_ITEMS) + throw new \OutOfBoundsException("Collection is full"); $this->relations->insert([ "collection" => $this->getId(), @@ -127,14 +152,14 @@ function add(RowModel $entity): bool return true; } - function remove(RowModel $entity): void + function remove(RowModel $entity): bool { $this->entitySuitable($entity); - $this->relations->where([ + return $this->relations->where([ "collection" => $this->getId(), "media" => $entity->getId(), - ])->delete(); + ])->delete() > 0; } function has(RowModel $entity): bool @@ -148,6 +173,33 @@ function has(RowModel $entity): bool return !is_null($rel); } - + + function save(?bool $log = false): void + { + $thisTable = DatabaseConnection::i()->getContext()->table($this->tableName); + if(self::MAX_COUNT != INF) + if(isset($this->changes["owner"])) + if(sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of collections"); + + if(is_null($this->getRecord())) + if(!isset($this->changes["created"])) + $this->stateChanges("created", time()); + else + $this->stateChanges("edited", time()); + + parent::save($log); + } + + function delete(bool $softly = true): void + { + if(!$softly) { + $this->relations->where("collection", $this->getId()) + ->delete(); + } + + parent::delete($softly); + } + use Traits\TOwnable; } diff --git a/Web/Models/Entities/Message.php b/Web/Models/Entities/Message.php index 97f3cf69b..29e6578e5 100644 --- a/Web/Models/Entities/Message.php +++ b/Web/Models/Entities/Message.php @@ -66,7 +66,7 @@ function getSendTimeHumanized(): string $dateTime = new DateTime($this->getRecord()->created); if($dateTime->format("%d.%m.%y") == ovk_strftime_safe("%d.%m.%y", time())) { - return $dateTime->format("%T %p"); + return $dateTime->format("%T"); } else { return $dateTime->format("%d.%m.%y"); } @@ -123,7 +123,11 @@ function simplify(): array ], ]; } else { - throw new \Exception("Unknown attachment type: " . get_class($attachment)); + $attachments[] = [ + "type" => "unknown" + ]; + + # throw new \Exception("Unknown attachment type: " . get_class($attachment)); } } diff --git a/Web/Models/Entities/NoSpamLog.php b/Web/Models/Entities/NoSpamLog.php new file mode 100644 index 000000000..48d723c90 --- /dev/null +++ b/Web/Models/Entities/NoSpamLog.php @@ -0,0 +1,71 @@ +getRecord()->id; + } + + function getUser(): ?User + { + return (new Users)->get($this->getRecord()->user); + } + + function getModel(): string + { + return $this->getRecord()->model; + } + + function getRegex(): ?string + { + return $this->getRecord()->regex; + } + + function getRequest(): ?string + { + return $this->getRecord()->request; + } + + function getCount(): int + { + return $this->getRecord()->count; + } + + function getTime(): DateTime + { + return new DateTime($this->getRecord()->time); + } + + function getItems(): ?array + { + return explode(",", $this->getRecord()->items); + } + + function getTypeRaw(): int + { + return $this->getRecord()->ban_type; + } + + function getType(): string + { + switch ($this->getTypeRaw()) { + case 1: return "О"; + case 2: return "Б"; + case 3: return "ОБ"; + default: return (string) $this->getTypeRaw(); + } + } + + function isRollbacked(): bool + { + return !is_null($this->getRecord()->rollback); + } +} diff --git a/Web/Models/Entities/Note.php b/Web/Models/Entities/Note.php index d259c2a50..33ac6b040 100644 --- a/Web/Models/Entities/Note.php +++ b/Web/Models/Entities/Note.php @@ -118,19 +118,29 @@ function getSource(): string { return $this->getRecord()->source; } + + function canBeViewedBy(?User $user = NULL): bool + { + if($this->isDeleted() || $this->getOwner()->isDeleted()) { + return false; + } + + return $this->getOwner()->getPrivacyPermission('notes.read', $user) && $this->getOwner()->canBeViewedBy($user); + } function toVkApiStruct(): object { $res = (object) []; - $res->id = $this->getId(); + $res->type = "note"; + $res->id = $this->getVirtualId(); $res->owner_id = $this->getOwner()->getId(); $res->title = $this->getName(); $res->text = $this->getText(); $res->date = $this->getPublicationTime()->timestamp(); $res->comments = $this->getCommentsCount(); $res->read_comments = $this->getCommentsCount(); - $res->view_url = "/note".$this->getOwner()->getId()."_".$this->getId(); + $res->view_url = "/note".$this->getOwner()->getId()."_".$this->getVirtualId(); $res->privacy_view = 1; $res->can_comment = 1; $res->text_wiki = "r"; diff --git a/Web/Models/Entities/Notifications/NewSuggestedPostsNotification.php b/Web/Models/Entities/Notifications/NewSuggestedPostsNotification.php new file mode 100644 index 000000000..e1795b08a --- /dev/null +++ b/Web/Models/Entities/Notifications/NewSuggestedPostsNotification.php @@ -0,0 +1,13 @@ +encodeType($this->originModel); + $target_m = $this->encodeType($this->targetModel); + + $info = [ + "type" => "", + "parent" => NULL, + "feedback" => NULL, + ]; + + switch($this->getActionCode()) { + case 0: + $info["type"] = "like_post"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = $this->getModel(1)->toVkApiStruct(); + break; + case 1: + $info["type"] = "copy_post"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = NULL; # todo + break; + case 2: + switch($origin_m) { + case 19: + $info["type"] = "comment_video"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = NULL; # айди коммента не сохраняется в бд( ну пиздец блять + break; + case 13: + $info["type"] = "comment_photo"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = NULL; + break; + # unstandart (vk forgor about notes) + case 10: + $info["type"] = "comment_note"; + $info["parent"] = $this->getModel(0)->toVkApiStruct(); + $info["feedback"] = NULL; + break; + case 14: + $info["type"] = "comment_post"; + $info["parent"] = $this->getModel(0)->toNotifApiStruct(); + $info["feedback"] = NULL; + break; + # unused (users don't have topics bruh) + case 21: + $info["type"] = "comment_topic"; + $info["parent"] = $this->getModel(0)->toVkApiStruct(0, 90); + break; + default: + $info["type"] = "comment_unknown"; + break; + } + + break; + case 3: + $info["type"] = "wall"; + $info["feedback"] = $this->getModel(0)->toNotifApiStruct(); + break; + case 4: + switch($target_m) { + case 14: + $info["type"] = "mention"; + $info["feedback"] = $this->getModel(1)->toNotifApiStruct(); + break; + case 19: + $info["type"] = "mention_comment_video"; + $info["parent"] = $this->getModel(1)->toNotifApiStruct(); + break; + case 13: + $info["type"] = "mention_comment_photo"; + $info["parent"] = $this->getModel(1)->toNotifApiStruct(); + break; + # unstandart + case 10: + $info["type"] = "mention_comment_note"; + $info["parent"] = $this->getModel(1)->toVkApiStruct(); + break; + case 21: + $info["type"] = "mention_comments"; + break; + default: + $info["type"] = "mention_comment_unknown"; + break; + } + break; + case 5: + $info["type"] = "make_you_admin"; + $info["parent"] = $this->getModel(0)->toVkApiStruct($this->getModel(1)); + break; + # Нужно доделать после мержа #935 + case 6: + $info["type"] = "wall_publish"; + break; + # В вк не было такого уведомления, так что unstandart + case 7: + $info["type"] = "new_posts_in_club"; + break; + # В вк при передаче подарков приходит сообщение, а не уведомление, так что unstandart + case 9601: + $info["type"] = "sent_gift"; + $info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1)); + break; + case 9602: + $info["type"] = "voices_transfer"; + $info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1)); + break; + case 9603: + $info["type"] = "up_rating"; + $info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1)); + $info["parent"]->count = $this->getData(); + break; + default: + $info["type"] = NULL; + break; + } + + return $info; + } + + function toVkApiStruct() + { + $res = (object)[]; + + $info = $this->getVkApiInfo(); + $res->type = $info["type"]; + $res->date = $this->getDateTime()->timestamp(); + $res->parent = $info["parent"]; + $res->feedback = $info["feedback"]; + $res->reply = NULL; # Ответы на комментарии не реализованы + return $res; + } } diff --git a/Web/Models/Entities/Notifications/PostAcceptedNotification.php b/Web/Models/Entities/Notifications/PostAcceptedNotification.php new file mode 100644 index 000000000..bb99d34e7 --- /dev/null +++ b/Web/Models/Entities/Notifications/PostAcceptedNotification.php @@ -0,0 +1,13 @@ +stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46))); $this->stateChanges("timestamp", time()); - parent::save(); + parent::save($log); } } diff --git a/Web/Models/Entities/Photo.php b/Web/Models/Entities/Photo.php index 29526daaf..90c532b27 100644 --- a/Web/Models/Entities/Photo.php +++ b/Web/Models/Entities/Photo.php @@ -114,7 +114,7 @@ protected function saveFile(string $filename, string $hash): bool return true; } - function crop(real $left, real $top, real $width, real $height): void + function crop(float $left, float $top, float $width, float $height): void { if(isset($this->changes["hash"])) $hash = $this->changes["hash"]; @@ -300,7 +300,7 @@ function toVkApiStruct(bool $photo_sizes = true, bool $extended = false): object { $res = (object) []; - $res->id = $res->pid = $this->getId(); + $res->id = $res->pid = $this->getVirtualId(); $res->owner_id = $res->user_id = $this->getOwner()->getId(); $res->aid = $res->album_id = NULL; $res->width = $this->getDimensions()[0]; @@ -308,7 +308,7 @@ function toVkApiStruct(bool $photo_sizes = true, bool $extended = false): object $res->date = $res->created = $this->getPublicationTime()->timestamp(); if($photo_sizes) { - $res->sizes = $this->getVkApiSizes(); + $res->sizes = array_values($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"); @@ -328,6 +328,19 @@ function toVkApiStruct(bool $photo_sizes = true, bool $extended = false): object return $res; } + + function canBeViewedBy(?User $user = NULL): bool + { + if($this->isDeleted() || $this->getOwner()->isDeleted()) { + return false; + } + + if(!is_null($this->getAlbum())) { + return $this->getAlbum()->canBeViewedBy($user); + } else { + return $this->getOwner()->canBeViewedBy($user); + } + } static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo { @@ -347,4 +360,20 @@ static function fastMake(int $owner, string $description = "", array $file, ?Alb return $photo; } + + function toNotifApiStruct() + { + $res = (object)[]; + + $res->id = $this->getVirtualId(); + $res->owner_id = $this->getOwner()->getId(); + $res->aid = 0; + $res->src = $this->getURLBySizeId("tiny"); + $res->src_big = $this->getURLBySizeId("normal"); + $res->src_small = $this->getURLBySizeId("miniscule"); + $res->text = $this->getDescription(); + $res->created = $this->getPublicationTime()->timestamp(); + + return $res; + } } diff --git a/Web/Models/Entities/Playlist.php b/Web/Models/Entities/Playlist.php new file mode 100644 index 000000000..ac36b4f34 --- /dev/null +++ b/Web/Models/Entities/Playlist.php @@ -0,0 +1,282 @@ +importTable = DatabaseConnection::i()->getContext()->table("playlist_imports"); + } + + function getCoverURL(string $size = "normal"): ?string + { + $photo = (new Photos)->get((int) $this->getRecord()->cover_photo_id); + return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURLBySizeId($size); + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function fetchClassic(int $offset = 0, ?int $limit = NULL): \Traversable + { + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("index ASC"); + + foreach($related as $rel) { + $media = $rel->ref($this->entityTableName, "media"); + if(!$media) + continue; + + yield new $this->entityClassName($media); + } + } + + function getAudios(int $offset = 0, ?int $limit = NULL, ?int $shuffleSeed = NULL): \Traversable + { + if(!$shuffleSeed) { + foreach ($this->fetchClassic($offset, $limit) as $e) + yield $e; # No, I can't return, it will break with [] + + return; + } + + $ids = []; + foreach($this->relations->select("media AS i")->where("collection", $this->getId()) as $rel) + $ids[] = $rel->i; + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, $offset, $limit ?? OPENVK_DEFAULT_PER_PAGE); + foreach($ids as $id) + yield (new Audios)->get($id); + } + + function add(RowModel $audio): bool + { + if($res = parent::add($audio)) { + $this->stateChanges("length", $this->getRecord()->length + $audio->getLength()); + $this->save(); + } + + return $res; + } + + function remove(RowModel $audio): bool + { + if($res = parent::remove($audio)) { + $this->stateChanges("length", $this->getRecord()->length - $audio->getLength()); + $this->save(); + } + + return $res; + } + + function isBookmarkedBy(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + return !is_null($this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->fetch()); + } + + function bookmark(RowModel $entity): bool + { + if($this->isBookmarkedBy($entity)) + return false; + + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + if($this->importTable->where("entity", $id)->count() > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of playlists"); + + $this->importTable->insert([ + "entity" => $id, + "playlist" => $this->getId(), + ]); + + return true; + } + + function unbookmark(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + $count = $this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->delete(); + + return $count > 0; + } + + function getDescription(): ?string + { + return $this->getRecord()->description; + } + + function getDescriptionHTML(): ?string + { + return htmlspecialchars($this->getRecord()->description, ENT_DISALLOWED | ENT_XHTML); + } + + function getListens() + { + return $this->getRecord()->listens; + } + + function toVkApiStruct(?User $user = NULL): object + { + $oid = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $oid *= -1; + + return (object) [ + "id" => $this->getId(), + "owner_id" => $oid, + "title" => $this->getName(), + "description" => $this->getDescription(), + "size" => $this->size(), + "length" => $this->getLength(), + "created" => $this->getCreationTime()->timestamp(), + "modified" => $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL, + "accessible" => $this->canBeViewedBy($user), + "editable" => $this->canBeModifiedBy($user), + "bookmarked" => $this->isBookmarkedBy($user), + "listens" => $this->getListens(), + "cover_url" => $this->getCoverURL(), + "searchable" => !$this->isUnlisted(), + ]; + } + + function setLength(): void + { + throw new \LogicException("Can't set length of playlist manually"); + } + + function resetLength(): bool + { + $this->stateChanges("length", 0); + + return true; + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("playlist_imports")->where("playlist", $this->getId()) + ->delete(); + + parent::delete($softly); + } + + function hasAudio(Audio $audio): bool + { + $ctx = DatabaseConnection::i()->getContext(); + return !is_null($ctx->table("playlist_relations")->where([ + "collection" => $this->getId(), + "media" => $audio->getId() + ])->fetch()); + } + + function getCoverPhotoId(): ?int + { + return $this->getRecord()->cover_photo_id; + } + + function getCoverPhoto(): ?Photo + { + return (new Photos)->get((int) $this->getRecord()->cover_photo_id); + } + + function canBeModifiedBy(User $user): bool + { + if(!$user) + return false; + + if($this->getOwner() instanceof User) + return $user->getId() == $this->getOwner()->getId(); + else + return $this->getOwner()->canBeModifiedBy($user); + } + + function getLengthInMinutes(): int + { + return (int)round($this->getLength() / 60, PHP_ROUND_HALF_DOWN); + } + + function fastMakeCover(int $owner, array $file) + { + $cover = new Photo; + $cover->setOwner($owner); + $cover->setDescription("Playlist cover image"); + $cover->setFile($file); + $cover->setCreated(time()); + $cover->save(); + + $this->setCover_photo_id($cover->getId()); + + return $cover; + } + + function getURL(): string + { + return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId(); + } + + function incrementListens() + { + $this->stateChanges("listens", ($this->getListens() + 1)); + } + + function getMetaDescription(): string + { + $length = $this->getLengthInMinutes(); + + $props = []; + $props[] = tr("audios_count", $this->size()); + $props[] = "" . tr("listens_count", $this->getListens()) . ""; + if($length > 0) $props[] = tr("minutes_count", $length); + $props[] = tr("created_playlist") . " " . $this->getPublicationTime(); + # if($this->getEditTime()) $props[] = tr("updated_playlist") . " " . $this->getEditTime(); + + return implode(" • ", $props); + } + + function isUnlisted(): bool + { + return (bool)$this->getRecord()->unlisted; + } +} diff --git a/Web/Models/Entities/Poll.php b/Web/Models/Entities/Poll.php index 6f2885b1f..60a39bc5c 100644 --- a/Web/Models/Entities/Poll.php +++ b/Web/Models/Entities/Poll.php @@ -4,7 +4,7 @@ use openvk\Web\Util\DateTime; use \UnexpectedValueException; use Nette\InvalidStateException; -use openvk\Web\Models\Repositories\Users; +use openvk\Web\Models\Repositories\{Users, Posts}; use Chandler\Database\DatabaseConnection; use openvk\Web\Models\Exceptions\PollLockedException; use openvk\Web\Models\Exceptions\AlreadyVotedException; @@ -165,7 +165,7 @@ function hasVoted(User $user): bool function canVote(User $user): bool { - return !$this->hasEnded() && !$this->hasVoted($user); + return !$this->hasEnded() && !$this->hasVoted($user) && !is_null($this->getAttachedPost()) && $this->getAttachedPost()->getSuggestionType() == 0; } function vote(User $user, array $optionIds): void @@ -278,13 +278,24 @@ static function import(User $owner, string $xml): Poll return $poll; } + + function canBeViewedBy(?User $user = NULL): bool + { + # waiting for #935 :( + /*if(!is_null($this->getAttachedPost())) { + return $this->getAttachedPost()->canBeViewedBy($user); + } else {*/ + return true; + #} + + } - function save(): void + function save(?bool $log = false): void { if(empty($this->choicesToPersist)) throw new InvalidStateException; - parent::save(); + parent::save($log); foreach($this->choicesToPersist as $option) { DatabaseConnection::i()->getContext()->table("poll_options")->insert([ "poll" => $this->getId(), @@ -292,4 +303,17 @@ function save(): void ]); } } + + function getAttachedPost() + { + $post = DatabaseConnection::i()->getContext()->table("attachments") + ->where( + ["attachable_type" => static::class, + "attachable_id" => $this->getId()])->fetch(); + + if(!is_null($post->target_id)) + return (new Posts)->get($post->target_id); + else + return NULL; + } } diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php index 429419018..30c6eac3d 100644 --- a/Web/Models/Entities/Post.php +++ b/Web/Models/Entities/Post.php @@ -78,6 +78,45 @@ function isPinned(): bool { return (bool) $this->getRecord()->pinned; } + + function hasSource(): bool + { + return $this->getRecord()->source != NULL; + } + + function getSource(bool $format = false) + { + $orig_source = $this->getRecord()->source; + if(!str_contains($orig_source, "https://") && !str_contains($orig_source, "http://")) + $orig_source = "https://" . $orig_source; + + if(!$format) + return $orig_source; + + return $this->formatLinks($orig_source); + } + + function setSource(string $source) + { + $result = check_copyright_link($source); + + $this->stateChanges("source", $source); + } + + function resetSource() + { + $this->stateChanges("source", NULL); + } + + function getVkApiCopyright(): object + { + return (object)[ + 'id' => 0, + 'link' => $this->getSource(false), + 'name' => $this->getSource(false), + 'type' => 'link', + ]; + } function isAd(): bool { @@ -133,6 +172,10 @@ function getPlatform(bool $forAPI = false): ?string case 'openvk_legacy_ios': return 'iphone'; break; + + case 'windows_phone': + return 'wphone'; + break; case 'vika_touch': // кика хохотач ахахахаххахахахахах case 'vk4me': @@ -175,6 +218,31 @@ function getPlatformDetails(): array "img" => NULL ]; } + + function getPostSourceInfo(): array + { + $post_source = ["type" => "vk"]; + if($this->getPlatform(true) !== NULL) { + $post_source = [ + "type" => "api", + "platform" => $this->getPlatform(true) + ]; + } + + if($this->isUpdateAvatarMessage()) + $post_source['data'] = 'profile_photo'; + + return $post_source; + } + + function getVkApiType(): string + { + $type = 'post'; + if($this->getSuggestionType() != 0) + $type = 'suggest'; + + return $type; + } function pin(): void { @@ -197,16 +265,25 @@ function unpin(): void $this->save(); } - function canBePinnedBy(User $user): bool + function canBePinnedBy(User $user = NULL): bool { + if(!$user) + return false; + if($this->getTargetWall() < 0) return (new Clubs)->get(abs($this->getTargetWall()))->canBeModifiedBy($user); return $this->getTargetWall() === $user->getId(); } - function canBeDeletedBy(User $user): bool + function canBeDeletedBy(User $user = NULL): bool { + if(!$user) + return false; + + if($this->getTargetWall() < 0 && !$this->getWallOwner()->canBeModifiedBy($user) && $this->getWallOwner()->getWallType() != 1 && $this->getSuggestionType() == 0) + return false; + return $this->getOwnerPost() === $user->getId() || $this->canBePinnedBy($user); } @@ -224,7 +301,7 @@ function toggleLike(User $user): bool { $liked = parent::toggleLike($user); - if($this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club) && !$this instanceof Comment) + if(!$user->isPrivateLikes() && $this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club) && !$this instanceof Comment) (new LikeNotification($this->getOwner(false), $this, $user))->emit(); foreach($this->getChildren() as $attachment) @@ -245,6 +322,139 @@ function deletePost(): void $this->unwire(); $this->save(); } + + function canBeViewedBy(?User $user = NULL): bool + { + if($this->isDeleted()) { + return false; + } + + return $this->getWallOwner()->canBeViewedBy($user); + } + + function getSuggestionType() + { + return $this->getRecord()->suggested; + } + + function getPageURL(): string + { + return "/wall".$this->getPrettyId(); + } + + function toNotifApiStruct() + { + $res = (object)[]; + + $res->id = $this->getVirtualId(); + $res->to_id = $this->getOwner() instanceof Club ? $this->getOwner()->getId() * -1 : $this->getOwner()->getId(); + $res->from_id = $res->to_id; + $res->date = $this->getPublicationTime()->timestamp(); + $res->text = $this->getText(false); + $res->attachments = []; # todo + + $res->copy_owner_id = NULL; # todo + $res->copy_post_id = NULL; # todo + + return $res; + } + + function canBeEditedBy(?User $user = NULL): bool + { + if(!$user) + return false; + + if($this->isDeactivationMessage() || $this->isUpdateAvatarMessage()) + return false; + + if($this->getTargetWall() > 0) + return $this->getPublicationTime()->timestamp() + WEEK > time() && $user->getId() == $this->getOwner(false)->getId(); + else { + if($this->isPostedOnBehalfOfGroup()) + return $this->getWallOwner()->canBeModifiedBy($user); + else + return $user->getId() == $this->getOwner(false)->getId(); + } + + return $user->getId() == $this->getOwner(false)->getId(); + } + + function toRss(): \Bhaktaraz\RSSGenerator\Item + { + $domain = ovk_scheme(true).$_SERVER["HTTP_HOST"]; + $description = $this->getText(false); + $title = str_replace("\n", "", ovk_proc_strtr($description, 79)); + $description_html = $description; + $url = $domain."/wall".$this->getPrettyId(); + + if($this->isUpdateAvatarMessage()) + $title = tr('upd_in_general'); + if($this->isDeactivationMessage()) + $title = tr('post_deact_in_general'); + + $author = $this->getOwner(); + $target_wall = $this->getWallOwner(); + $author_name = escape_html($author->getCanonicalName()); + if($this->isExplicit()) + $title = 'NSFW: ' . $title; + + foreach($this->getChildren() as $child) { + if($child instanceof Photo) { + $child_page = $domain.$child->getPageURL(); + $child_url = $child->getURL(); + $description_html .= "

"; + } elseif($child instanceof Video) { + $child_page = $domain.'/video'.$child->getPrettyId(); + + if($child->getType() != 1) { + $description_html .= "". + "
". + "
". + "".escape_html($child->getName())."
"; + } else { + $description_html .= "". + "
". + "getVideoDriver()->getURL()."\">".escape_html($child->getName())."
"; + } + } elseif($child instanceof Audio) { + if(!$child->isWithdrawn()) { + $description_html .= "
" + ."".escape_html($child->getName()).":" + ."
" + ."" + ."
"; + } + } elseif($child instanceof Poll) { + $description_html .= "
".tr('poll').": ".escape_html($child->getTitle()); + } elseif($child instanceof Note) { + $description_html .= "
".tr('note').": ".escape_html($child->getName()); + } + } + + $description_html .= "
".tr('author').": " . $author_name . ""; + + if($target_wall->getRealId() != $author->getRealId()) + $description_html .= "
".tr('on_wall').": " . escape_html($target_wall->getCanonicalName()) . ""; + + if($this->isSigned()) { + $signer = $this->getOwner(false); + $description_html .= "
".tr('sign_short').": " . escape_html($signer->getCanonicalName()) . ""; + } + + if($this->hasSource()) + $description_html .= "
".tr('source').": ".escape_html($this->getSource()); + + $item = new \Bhaktaraz\RSSGenerator\Item(); + $item->title($title) + ->url($url) + ->guid($url) + ->creator($author_name) + ->pubDate($this->getPublicationTime()->timestamp()) + ->content(str_replace("\n", "
", $description_html)); + + return $item; + } use Traits\TRichText; } diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index b4f8b4c6b..fc6cb8caf 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -33,8 +33,9 @@ function getOwner(bool $real = false): RowModel { $oid = (int) $this->getRecord()->owner; if(!$real && $this->isAnonymous()) - $oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; - + $oid = (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; + + $oid = abs($oid); if($oid > 0) return (new Users)->get($oid); else @@ -84,19 +85,26 @@ function getLikesCount(): int return sizeof(DB::i()->getContext()->table("likes")->where([ "model" => static::class, "target" => $this->getRecord()->id, - ])); + ])->group("origin")); } - # TODO add pagination - function getLikers(): \Traversable + function getLikers(int $page = 1, ?int $perPage = NULL): \Traversable { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $sel = DB::i()->getContext()->table("likes")->where([ "model" => static::class, "target" => $this->getRecord()->id, - ]); + ])->page($page, $perPage); - foreach($sel as $like) - yield (new Users)->get($like->origin); + foreach($sel as $like) { + $user = (new Users)->get($like->origin); + if($user->isPrivateLikes() && OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]) { + $user = (new Users)->get((int) OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]); + } + + yield $user; + } } function isAnonymous(): bool @@ -129,10 +137,15 @@ function setLike(bool $liked, User $user): void "target" => $this->getRecord()->id, ]; - if($liked) - DB::i()->getContext()->table("likes")->insert($searchData); - else - DB::i()->getContext()->table("likes")->where($searchData)->delete(); + if($liked) { + if(!$this->hasLikeFrom($user)) { + DB::i()->getContext()->table("likes")->insert($searchData); + } + } else { + if($this->hasLikeFrom($user)) { + DB::i()->getContext()->table("likes")->where($searchData)->delete(); + } + } } function hasLikeFrom(User $user): bool @@ -151,7 +164,7 @@ function setVirtual_Id(int $id): void throw new ISE("Setting virtual id manually is forbidden"); } - function save(): void + function save(?bool $log = false): void { $vref = $this->upperNodeReferenceColumnName; @@ -166,11 +179,11 @@ function save(): void $this->stateChanges("created", time()); $this->stateChanges("virtual_id", $pCount + 1); - } else { + } /*else { $this->stateChanges("edited", time()); - } + }*/ - parent::save(); + parent::save($log); } use Traits\TAttachmentHost; diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php new file mode 100644 index 000000000..33bf84f77 --- /dev/null +++ b/Web/Models/Entities/Report.php @@ -0,0 +1,160 @@ +getRecord()->id; + } + + function getStatus(): int + { + return $this->getRecord()->status; + } + + function getContentType(): string + { + return $this->getRecord()->type; + } + + function getReason(): string + { + return $this->getRecord()->reason; + } + + function getTime(): DateTime + { + return new DateTime($this->getRecord()->date); + } + + function isDeleted(): bool + { + if ($this->getRecord()->deleted === 0) + { + return false; + } elseif ($this->getRecord()->deleted === 1) { + return true; + } + } + + function authorId(): int + { + return $this->getRecord()->user_id; + } + + function getUser(): User + { + return (new Users)->get((int) $this->getRecord()->user_id); + } + + function getContentId(): int + { + return (int) $this->getRecord()->target_id; + } + + function getContentObject() + { + if ($this->getContentType() == "post") return (new Posts)->get($this->getContentId()); + else if ($this->getContentType() == "photo") return (new Photos)->get($this->getContentId()); + else if ($this->getContentType() == "video") return (new Videos)->get($this->getContentId()); + else if ($this->getContentType() == "group") return (new Clubs)->get($this->getContentId()); + else if ($this->getContentType() == "comment") return (new Comments)->get($this->getContentId()); + else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); + else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); + else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); + else if ($this->getContentType() == "audio") return (new Audios)->get($this->getContentId()); + else return null; + } + + function getAuthor(): RowModel + { + return $this->getContentObject()->getOwner(); + } + + function getReportAuthor(): User + { + return (new Users)->get($this->getRecord()->user_id); + } + + function banUser($initiator) + { + $reason = $this->getContentType() !== "user" ? ("**content-" . $this->getContentType() . "-" . $this->getContentId() . "**") : ("Подозрительная активность"); + $this->getAuthor()->ban($reason, false, time() + $this->getAuthor()->getNewBanTime(), $initiator); + } + + function deleteContent() + { + if ($this->getContentType() !== "user") { + $pubTime = $this->getContentObject()->getPublicationTime(); + if (method_exists($this->getContentObject(), "getName")) { + $name = $this->getContentObject()->getName(); + $placeholder = "$pubTime ($name)"; + } else { + $placeholder = "$pubTime"; + } + + if ($this->getAuthor() instanceof Club) { + $name = $this->getAuthor()->getName(); + $this->getAuthor()->getOwner()->adminNotify("Ваш контент, который опубликовали $placeholder в созданной вами группе \"$name\" был удалён модераторами инстанса. За повторные или серьёзные нарушения группу могут заблокировать."); + } else { + $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $placeholder был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); + } + $this->getContentObject()->delete($this->getContentType() !== "app"); + } + + $this->delete(); + } + + function getDuplicates(): \Traversable + { + return (new Reports)->getDuplicates($this->getContentType(), $this->getContentId(), $this->getId()); + } + + function getDuplicatesCount(): int + { + return count(iterator_to_array($this->getDuplicates())); + } + + function hasDuplicates(): bool + { + return $this->getDuplicatesCount() > 0; + } + + function getContentName(): string + { + $content_object = $this->getContentObject(); + if(!$content_object) { + return 'unknown'; + } + + if (method_exists($content_object, "getCanonicalName")) + return $content_object->getCanonicalName(); + + return $this->getContentType() . " #" . $this->getContentId(); + } + + public function delete(bool $softly = true): void + { + if ($this->hasDuplicates()) { + foreach ($this->getDuplicates() as $duplicate) { + $duplicate->setDeleted(1); + $duplicate->save(); + } + } + + $this->setDeleted(1); + $this->save(); + } +} diff --git a/Web/Models/Entities/Topic.php b/Web/Models/Entities/Topic.php index 89b3cb390..d4257816e 100644 --- a/Web/Models/Entities/Topic.php +++ b/Web/Models/Entities/Topic.php @@ -68,6 +68,12 @@ function getLastComment(): ?Comment return isset($array[0]) ? $array[0] : NULL; } + function getFirstComment(): ?Comment + { + $array = iterator_to_array($this->getComments(1)); + return $array[0] ?? NULL; + } + function getUpdateTime(): DateTime { $lastComment = $this->getLastComment(); @@ -83,4 +89,40 @@ function deleteTopic(): void $this->unwire(); $this->save(); } + + function toVkApiStruct(int $preview = 0, int $preview_length = 90): object + { + $res = (object)[]; + + $res->id = $this->getId(); + $res->title = $this->getTitle(); + $res->created = $this->getPublicationTime()->timestamp(); + + if($this->getOwner() instanceof User) { + $res->created_by = $this->getOwner()->getId(); + } else { + $res->created_by = $this->getOwner()->getId() * -1; + } + + $res->updated = $this->getUpdateTime()->timestamp(); + + if($this->getLastComment()) { + if($this->getLastComment()->getOwner() instanceof User) { + $res->updated_by = $this->getLastComment()->getOwner()->getId(); + } else { + $res->updated_by = $this->getLastComment()->getOwner()->getId() * -1; + } + } + + $res->is_closed = (int)$this->isClosed(); + $res->is_fixed = (int)$this->isPinned(); + $res->comments = $this->getCommentsCount(); + + if($preview == 1) { + $res->first_comment = $this->getFirstComment() ? ovk_proc_strtr($this->getFirstComment()->getText(false), $preview_length) : NULL; + $res->last_comment = $this->getLastComment() ? ovk_proc_strtr($this->getLastComment()->getText(false), $preview_length) : NULL; + } + + return $res; + } } diff --git a/Web/Models/Entities/Traits/TAttachmentHost.php b/Web/Models/Entities/Traits/TAttachmentHost.php index cbe7cad24..a574e6579 100644 --- a/Web/Models/Entities/Traits/TAttachmentHost.php +++ b/Web/Models/Entities/Traits/TAttachmentHost.php @@ -1,6 +1,7 @@ get($rel->attachable_id); } } + + function getChildrenWithLayout(int $w, int $h = -1): object + { + if($h < 0) + $h = $w; + + $children = iterator_to_array($this->getChildren()); + $skipped = $photos = $result = []; + foreach($children as $child) { + if($child instanceof Photo || $child instanceof Video && $child->getDimensions()) { + $photos[] = $child; + continue; + } + + $skipped[] = $child; + } + + $height = "unset"; + $width = $w; + if(sizeof($photos) < 2) { + if(isset($photos[0])) + $result[] = ["100%", "unset", $photos[0], "unset"]; + } else { + $mak = new Makima($photos); + $layout = $mak->computeMasonryLayout($w, $h); + $height = $layout->height; + $width = $layout->width; + for($i = 0; $i < sizeof($photos); $i++) { + $tile = $layout->tiles[$i]; + $result[] = [$tile->width . "px", $tile->height . "px", $photos[$i], "left"]; + } + } + + return (object) [ + "width" => $width . "px", + "height" => $height . "px", + "tiles" => $result, + "extras" => $skipped, + ]; + } function attach(Attachable $attachment): void { diff --git a/Web/Models/Entities/Traits/TAudioStatuses.php b/Web/Models/Entities/Traits/TAudioStatuses.php new file mode 100644 index 000000000..f957a104e --- /dev/null +++ b/Web/Models/Entities/Traits/TAudioStatuses.php @@ -0,0 +1,38 @@ +getRealId() < 0) return true; + return (bool) $this->getRecord()->audio_broadcast_enabled; + } + + function getCurrentAudioStatus() + { + if(!$this->isBroadcastEnabled()) return NULL; + + $audioId = $this->getRecord()->last_played_track; + + if(!$audioId) return NULL; + $audio = (new Audios)->get($audioId); + + if(!$audio || $audio->isDeleted()) + return NULL; + + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $this->getRealId(), + "audio" => $audio->getId(), + "time >" => (time() - $audio->getLength()) - 10, + ])->fetch(); + + if($lastListen) + return $audio; + + return NULL; + } +} diff --git a/Web/Models/Entities/Traits/TIgnorable.php b/Web/Models/Entities/Traits/TIgnorable.php new file mode 100644 index 000000000..3e21420b7 --- /dev/null +++ b/Web/Models/Entities/Traits/TIgnorable.php @@ -0,0 +1,55 @@ +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/Traits/TOwnable.php b/Web/Models/Entities/Traits/TOwnable.php index 9dc9ce2a3..90182ddfd 100644 --- a/Web/Models/Entities/Traits/TOwnable.php +++ b/Web/Models/Entities/Traits/TOwnable.php @@ -4,6 +4,16 @@ trait TOwnable { + function canBeViewedBy(?User $user = NULL): bool + { + # TODO: #950 + if($this->isDeleted()) { + return false; + } + + return true; + } + function canBeModifiedBy(User $user): bool { if(method_exists($this, "isCreatedBySystem")) diff --git a/Web/Models/Entities/Traits/TRichText.php b/Web/Models/Entities/Traits/TRichText.php index dc78a0345..75fbee5b2 100644 --- a/Web/Models/Entities/Traits/TRichText.php +++ b/Web/Models/Entities/Traits/TRichText.php @@ -38,8 +38,19 @@ private function formatLinks(string &$text): string $href = str_replace("#", "#", $matches[1]); $href = rawurlencode(str_replace(";", ";", $href)); $link = str_replace("#", "#", $matches[3]); + # this string breaks ampersands $link = str_replace(";", ";", $link); $rel = $this->isAd() ? "sponsored" : "ugc"; + + $server_domain = str_replace(':' . $_SERVER['SERVER_PORT'], '', $_SERVER['HTTP_HOST']); + if(str_contains($link, $server_domain)) { + $replaced_link = str_replace(':' . $_SERVER['SERVER_PORT'], '', $link); + $replaced_link = str_replace($server_domain, '', $replaced_link); + + return "$link" . htmlentities($matches[4]); + } + + $link = htmlentities(urldecode($link)); return "$link" . htmlentities($matches[4]); }), @@ -123,7 +134,7 @@ function getText(bool $html = true): string $text = preg_replace_callback("%([\n\r\s]|^)(\#([\p{L}_0-9][\p{L}_0-9\(\)\-\']+[\p{L}_0-9\(\)]|[\p{L}_0-9]{1,2}))%Xu", function($m) { $slug = rawurlencode($m[3]); - return "$m[1]$m[2]"; + return "$m[1]$m[2]"; }, $text); $text = $this->formatEmojis($text); diff --git a/Web/Models/Entities/Traits/TSubscribable.php b/Web/Models/Entities/Traits/TSubscribable.php index 802bc4272..7d1c0d284 100644 --- a/Web/Models/Entities/Traits/TSubscribable.php +++ b/Web/Models/Entities/Traits/TSubscribable.php @@ -39,4 +39,25 @@ function toggleSubscription(User $user): bool $sub->delete(); return false; } + + function changeFlags(User $user, int $flags, bool $reverse): bool + { + $ctx = DatabaseConnection::i()->getContext(); + $data = [ + "follower" => $reverse ? $this->getId() : $user->getId(), + "model" => static::class, + "target" => $reverse ? $user->getId() : $this->getId(), + ]; + $sub = $ctx->table("subscriptions")->where($data); + + bdump($data); + + if (!$sub) + return false; + + $sub->update([ + 'flags' => $flags + ]); + return true; + } } diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index ac7524a4f..34789f4f9 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -4,8 +4,8 @@ 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, Photos, Gifts, Notifications, Blacklists}; +use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift, Audio}; +use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos, Blacklists}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; use Chandler\Database\DatabaseConnection; @@ -39,11 +39,14 @@ protected function _abstractRelationGenerator(string $filename, int $page = 1, i $query = "SELECT id FROM\n" . file_get_contents(__DIR__ . "/../sql/$filename.tsql"); $query .= "\n LIMIT " . $limit . " OFFSET " . ( ($page - 1) * $limit ); + $ids = []; $rels = DatabaseConnection::i()->getConnection()->query($query, $id, $id); foreach($rels as $rel) { $rel = (new Users)->get($rel->id); if(!$rel) continue; - + if(in_array($rel->getId(), $ids)) continue; + $ids[] = $rel->getId(); + yield $rel; } } @@ -109,7 +112,7 @@ function getURL(): string 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"]; @@ -118,7 +121,9 @@ function getAvatarUrl(string $size = "miniscule"): string 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 @@ -187,7 +192,7 @@ function getFullName(): string function getMorphedName(string $case = "genitive", bool $fullName = true): string { $name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName(); - if(!preg_match("%^[А-яё\-]+$%", $name)) + if(!preg_match("%[А-яё\-]+$%", $name)) return $name; # name is probably not russian $inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE); @@ -238,11 +243,60 @@ function getAlert(): ?string return $this->getRecord()->alert; } - function getBanReason(): ?string + function getTextForContentBan(string $type): string + { + switch ($type) { + case "post": return "за размещение от Вашего лица таких записей:"; + case "photo": return "за размещение от Вашего лица таких фотографий:"; + case "video": return "за размещение от Вашего лица таких видеозаписей:"; + case "group": return "за подозрительное вступление от Вашего лица в группу:"; + case "comment": return "за размещение от Вашего лица таких комментариев:"; + case "note": return "за размещение от Вашего лица таких заметок:"; + case "app": return "за создание от Вашего имени подозрительных приложений."; + default: return "за размещение от Вашего лица такого контента:"; + } + } + + function getRawBanReason(): ?string { return $this->getRecord()->block_reason; } + function getBanReason(?string $for = null) + { + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver()) return null; + + $reason = $ban->getReason(); + + preg_match('/\*\*content-(post|photo|video|group|comment|note|app|noSpamTemplate|user)-(\d+)\*\*$/', $reason, $matches); + if (sizeof($matches) === 3) { + $content_type = $matches[1]; $content_id = (int) $matches[2]; + if (in_array($content_type, ["noSpamTemplate", "user"])) { + $reason = "Подозрительная активность"; + } else { + if ($for !== "banned") { + $reason = "Подозрительная активность"; + } else { + $reason = [$this->getTextForContentBan($content_type), $content_type]; + switch ($content_type) { + case "post": $reason[] = (new Posts)->get($content_id); break; + case "photo": $reason[] = (new Photos)->get($content_id); break; + case "video": $reason[] = (new Videos)->get($content_id); break; + case "group": $reason[] = (new Clubs)->get($content_id); break; + case "comment": $reason[] = (new Comments)->get($content_id); break; + case "note": $reason[] = (new Notes)->get($content_id); break; + case "app": $reason[] = (new Applications)->get($content_id); break; + case "user": $reason[] = (new Users)->get($content_id); break; + default: $reason[] = null; + } + } + } + } + + return $reason; + } + function getBanInSupportReason(): ?string { return $this->getRecord()->block_in_support_reason; @@ -296,10 +350,11 @@ function getMaritalStatus(): int return $this->getRecord()->marital_status; } - function getLocalizedMaritalStatus(): string + function getLocalizedMaritalStatus(?bool $prefix = false): string { $status = $this->getMaritalStatus(); $string = "relationship_$status"; + if ($prefix) $string .= "_prefix"; if($this->isFemale()) { $res = tr($string . "_fem"); if($res != ("@" . $string . "_fem")) @@ -309,6 +364,17 @@ function getLocalizedMaritalStatus(): string return tr($string); } + function getMaritalStatusUser(): ?User + { + if (!$this->getRecord()->marital_status_user) return NULL; + return (new Users)->get($this->getRecord()->marital_status_user); + } + + function getMaritalStatusUserPrefix(): ?string + { + return $this->getLocalizedMaritalStatus(true); + } + function getContactEmail(): ?string { return $this->getRecord()->email_contact; @@ -403,6 +469,7 @@ function getLeftMenuItemStatus(string $id): bool "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -410,6 +477,7 @@ function getLeftMenuItemStatus(string $id): bool "news", "links", "poster", + "apps", ], ])->get($id); } @@ -429,6 +497,8 @@ function getPrivacySetting(string $id): int "friends.add", "wall.write", "messages.write", + "audios.read", + "likes.read", ], ])->get($id); } @@ -444,6 +514,9 @@ function getPrivacyPermission(string $permission, ?User $user = NULL): bool return $user->isAdmin() && !OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["blacklists"]["applyToAdmins"]; } + if($permission != "messages.write" && !$this->canBeViewedBy($user)) + return false; + switch($permStatus) { case User::PRIVACY_ONLY_FRIENDS: return $this->getSubscriptionStatus($user) === User::SUBSCRIPTION_MUTUAL; @@ -525,6 +598,16 @@ function getFollowersCount(): int return $this->_abstractRelationCount("get-followers"); } + function getRequests(int $page = 1, int $limit = 6): \Traversable + { + return $this->_abstractRelationGenerator("get-requests", $page, $limit); + } + + function getRequestsCount(): int + { + return $this->_abstractRelationCount("get-requests"); + } + function getSubscriptions(int $page = 1, int $limit = 6): \Traversable { return $this->_abstractRelationGenerator("get-subscriptions-user", $page, $limit); @@ -670,8 +753,8 @@ function generate2faBackupCodes(): void for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) { $codes[] = [ - owner => $this->getId(), - code => random_int(10000000, 99999999) + "owner" => $this->getId(), + "code" => random_int(10000000, 99999999) ]; } @@ -731,7 +814,29 @@ function getNsfwTolerance(): int function isFemale(): bool { - return (bool) $this->getRecord()->sex; + return $this->getRecord()->sex == 1; + } + + function isNeutral(): bool + { + return (bool) $this->getRecord()->sex == 2; + } + + function getLocalizedPronouns(): string + { + switch ($this->getRecord()->sex) { + case 0: + return tr('male'); + case 1: + return tr('female'); + case 2: + return tr('neutral'); + } + } + + function getPronouns(): int + { + return $this->getRecord()->sex; } function isVerified(): bool @@ -833,7 +938,7 @@ function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous ]); } - function ban(string $reason, bool $deleteSubscriptions = true, ?int $unban_time = NULL): void + function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = NULL, ?int $initiator = NULL): void { if($deleteSubscriptions) { $subs = DatabaseConnection::i()->getContext()->table("subscriptions"); @@ -846,8 +951,33 @@ function ban(string $reason, bool $deleteSubscriptions = true, ?int $unban_time $subs->delete(); } - $this->setBlock_Reason($reason); - $this->setUnblock_time($unban_time); + $iat = time(); + $ban = new Ban; + $ban->setUser($this->getId()); + $ban->setReason($reason); + $ban->setInitiator($initiator); + $ban->setIat($iat); + $ban->setExp($unban_time !== "permanent" ? $unban_time : 0); + $ban->setTime($unban_time === "permanent" ? 0 : ($unban_time ? ($unban_time - $iat) : 0)); + $ban->save(); + + $this->setBlock_Reason($ban->getId()); + // $this->setUnblock_time($unban_time); + $this->save(); + } + + function unban(int $removed_by): void + { + $ban = (new Bans)->get((int) $this->getRawBanReason()); + if (!$ban || $ban->isOver()) + return; + + $ban->setRemoved_Manually(true); + $ban->setRemoved_By($removed_by); + $ban->save(); + + $this->setBlock_Reason(NULL); + // $user->setUnblock_time(NULL); $this->save(); } @@ -935,6 +1065,8 @@ function setPrivacySetting(string $id, int $status): void "friends.add", "wall.write", "messages.write", + "audios.read", + "likes.read", ], ])->set($id, $status)->toInteger()); } @@ -945,6 +1077,7 @@ function setLeftMenuItemStatus(string $id, bool $status): void "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -952,6 +1085,7 @@ function setLeftMenuItemStatus(string $id, bool $status): void "news", "links", "poster", + "apps", ], ])->set($id, (int) $status)->toInteger(); @@ -1016,7 +1150,7 @@ function updOnline(string $platform): bool { $this->setOnline(time()); $this->setClient_name($platform); - $this->save(); + $this->save(false); return true; } @@ -1034,7 +1168,7 @@ function changeEmail(string $email): void function adminNotify(string $message): bool { - $admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]; + $admId = (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]; if(!$admId) return false; else if(is_null($admin = (new Users)->get($admId))) @@ -1101,10 +1235,19 @@ function isAdmin(): bool { return $this->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL); } + + function isDead(): bool + { + return $this->onlineStatus() == 2; + } function getUnbanTime(): ?string { - return !is_null($this->getRecord()->unblock_time) ? date('d.m.Y', $this->getRecord()->unblock_time) : NULL; + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) return null; + if ($this->canUnbanThemself()) return tr("today"); + + return date('d.m.Y', $ban->getEndTime()); } function canUnbanThemself(): bool @@ -1112,13 +1255,105 @@ function canUnbanThemself(): bool if (!$this->isBanned()) return false; - if ($this->getRecord()->unblock_time > time() || $this->getRecord()->unblock_time == 0) - return false; + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) return false; + + return $ban->getEndTime() <= time() && !$ban->isPermanent(); + } + + function getNewBanTime() + { + $bans = iterator_to_array((new Bans)->getByUser($this->getid())); + if (!$bans || count($bans) === 0) + return 0; + + $last_ban = end($bans); + if (!$last_ban) return 0; + + if ($last_ban->isPermanent()) return "permanent"; + + $values = [0, 3600, 7200, 86400, 172800, 604800, 1209600, 3024000, 9072000]; + $response = 0; + $i = 0; + + foreach ($values as $value) { + $i++; + if ($last_ban->getTime() === 0 && $value === 0) continue; + if ($last_ban->getTime() < $value) { + $response = $value; + break; + } else if ($last_ban->getTime() >= $value) { + if ($i < count($values)) continue; + $response = "permanent"; + break; + } + } + return $response; + } + + function getProfileType(): int + { + # 0 — открытый профиль, 1 — закрытый + return $this->getRecord()->profile_type; + } + + function canBeViewedBy(?User $user = NULL): bool + { + if(!is_null($user)) { + if($this->getId() == $user->getId()) { + return true; + } + + if($user->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)) { + return true; + } + + if($this->getProfileType() == 0) { + return true; + } else { + if($user->getSubscriptionStatus($this) == User::SUBSCRIPTION_MUTUAL) { + return true; + } else { + return false; + } + } + + } else { + if($this->getProfileType() == 0) { + if($this->getPrivacySetting("page.read") == 3) { + return true; + } else { + return false; + } + } else { + return false; + } + } return true; } - function toVkApiStruct(): object + function isClosed(): bool + { + return (bool) $this->getProfileType(); + } + + function isHideFromGlobalFeedEnabled(): bool + { + return $this->isClosed(); + } + + function getRealId() + { + return $this->getId(); + } + + function isPrivateLikes(): bool + { + return $this->getPrivacySetting("likes.read") == User::PRIVACY_NO_ONE; + } + + function toVkApiStruct(?User $user = NULL, string $fields = ''): object { $res = (object) []; @@ -1126,15 +1361,141 @@ function toVkApiStruct(): object $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; - # TODO: Perenesti syuda vsyo ostalnoyie + $res->is_closed = $this->isClosed(); + + if(!is_null($user)) + $res->can_access_closed = (bool)$this->canBeViewedBy($user); + + if(!is_array($fields)) + $fields = explode(',', $fields); + + $avatar_photo = $this->getAvatarPhoto(); + foreach($fields as $field) { + switch($field) { + case 'is_dead': + $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); + } + + function getBroadcastList(string $filter = "friends", bool $shuffle = false) + { + $dbContext = DatabaseConnection::i()->getContext(); + $entityIds = []; + $query = $dbContext->table("subscriptions")->where("follower", $this->getRealId()); + + if($filter != "all") + $query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User")); + + foreach($query as $_rel) { + $entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target; + } + + if($shuffle) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + + $entityIds = knuth_shuffle($entityIds, $shuffleSeed); + } + + $entityIds = array_slice($entityIds, 0, 10); + + $returnArr = []; + + foreach($entityIds as $id) { + $entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id)); + + if($id > 0 && $entit->isDeleted()) continue; + $returnArr[] = $entit; + } + + 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/Entities/Video.php b/Web/Models/Entities/Video.php index 4c652fddf..d134645e9 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -1,7 +1,7 @@ stateChanges("length", (int) round($length, 0, PHP_ROUND_HALF_EVEN)); + + preg_match('%width=([0-9\.]++)%', $streams, $width); + preg_match('%height=([0-9\.]++)%', $streams, $height); + if(!empty($width) && !empty($height)) { + $this->stateChanges("width", $width[1]); + $this->stateChanges("height", $height[1]); + } try { if(!is_dir($dirId = dirname($this->pathFromHash($hash)))) @@ -115,21 +129,23 @@ function getOwnerVideo(): int return $this->getRecord()->owner; } - function getApiStructure(): object + function getApiStructure(?User $user = NULL): object { - return (object)[ + $fromYoutube = $this->getType() == Video::TYPE_EMBED; + $dimensions = $this->getDimensions(); + $res = (object)[ "type" => "video", "video" => [ "can_comment" => 1, - "can_like" => 0, // we don't h-have wikes in videos - "can_repost" => 0, + "can_like" => 1, // we don't h-have wikes in videos + "can_repost" => 1, "can_subscribe" => 1, "can_add_to_faves" => 0, "can_add" => 0, "comments" => $this->getCommentsCount(), "date" => $this->getPublicationTime()->timestamp(), "description" => $this->getDescription(), - "duration" => 0, // я хуй знает как получить длину видео + "duration" => $this->getLength(), "image" => [ [ "url" => $this->getThumbnailURL(), @@ -138,44 +154,51 @@ function getApiStructure(): object "with_padding" => 1 ] ], - "width" => 640, - "height" => 480, + "width" => $dimensions ? $dimensions[0] : 640, + "height" => $dimensions ? $dimensions[1] : 480, "id" => $this->getVirtualId(), "owner_id" => $this->getOwner()->getId(), "user_id" => $this->getOwner()->getId(), "title" => $this->getName(), "is_favorite" => false, - "player" => $this->getURL(), - "files" => [ + "player" => !$fromYoutube ? $this->getURL() : $this->getVideoDriver()->getURL(), + "files" => !$fromYoutube ? [ "mp4_480" => $this->getURL() - ], + ] : NULL, + "platform" => $fromYoutube ? "youtube" : NULL, "added" => 0, "repeat" => 0, "type" => "video", "views" => 0, - "likes" => [ - "count" => 0, - "user_likes" => 0 - ], + "is_processed" => $this->isProcessed(), "reposts" => [ "count" => 0, "user_reposted" => 0 ] ] ]; + + if(!is_null($user)) { + $res->video["likes"] = [ + "count" => $this->getLikesCount(), + "user_likes" => $this->hasLikeFrom($user) + ]; + } + + return $res; } - function toVkApiStruct(): object + function toVkApiStruct(?User $user): object { - return $this->getApiStructure(); + return $this->getApiStructure($user); } function setLink(string $link): string { if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/youtube.txt"), $link, $matches)) { $pointer = "YouTube:$matches[1]"; - } else if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/vimeo.txt"), $link, $matches)) { - $pointer = "Vimeo:$matches[1]"; + /*} else if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/vimeo.txt"), $link, $matches)) { + $pointer = "Vimeo:$matches[1]";*/ } else { throw new ISE("Invalid link"); } @@ -217,4 +240,110 @@ static function fastMake(int $owner, string $name = "Unnamed Video.ogv", string return $video; } + + function fillDimensions() + { + $hash = $this->getRecord()->hash; + $path = $this->pathFromHash($hash); + if(!file_exists($path)) { + $this->stateChanges("width", 0); + $this->stateChanges("height", 0); + $this->stateChanges("length", 0); + $this->save(); + return false; + } + + $streams = Shell::ffprobe("-i", $path, "-show_streams", "-select_streams v", "-loglevel error")->execute($error); + $durations = []; + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + + $length = 0; + foreach($durations[1] as $duration) { + $duration = floatval($duration); + if($duration < 1.0) + continue; + else + $length = max($length, $duration); + } + $this->stateChanges("length", (int) round($length, 0, PHP_ROUND_HALF_EVEN)); + + preg_match('%width=([0-9\.]++)%', $streams, $width); + preg_match('%height=([0-9\.]++)%', $streams, $height); + + if(!empty($width) && !empty($height)) { + $this->stateChanges("width", $width[1]); + $this->stateChanges("height", $height[1]); + } + + $this->save(); + + return true; + } + + function getDimensions() + { + if($this->getType() == Video::TYPE_EMBED) return [320, 180]; + + $width = $this->getRecord()->width; + $height = $this->getRecord()->height; + + if(!$width) return NULL; + return $width != 0 ? [$width, $height] : NULL; + } + + function getLength() + { + return $this->getRecord()->length; + } + + function getFormattedLength(): string + { + $len = $this->getLength(); + if(!$len) return "00:00"; + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + function getPageURL(): string + { + return "/video".$this->getPrettyId(); + } + + function canBeViewedBy(?User $user = NULL): bool + { + if($this->isDeleted() || $this->getOwner()->isDeleted()) { + return false; + } + + if(get_class($this->getOwner()) == "openvk\\Web\\Models\\Entities\\User") { + return $this->getOwner()->canBeViewedBy($user) && $this->getOwner()->getPrivacyPermission('videos.read', $user); + } else { + # Groups doesn't have videos but ok + return $this->getOwner()->canBeViewedBy($user); + } + } + + function toNotifApiStruct() + { + $fromYoutube = $this->getType() == Video::TYPE_EMBED; + $res = (object)[]; + + $res->id = $this->getVirtualId(); + $res->owner_id = $this->getOwner()->getId(); + $res->title = $this->getName(); + $res->description = $this->getDescription(); + $res->duration = $this->getLength(); + $res->link = "/video".$this->getOwner()->getId()."_".$this->getVirtualId(); + $res->image = $this->getThumbnailURL(); + $res->date = $this->getPublicationTime()->timestamp(); + $res->views = 0; + $res->player = !$fromYoutube ? $this->getURL() : $this->getVideoDriver()->getURL(); + + return $res; + } } diff --git a/Web/Models/Repositories/APITokens.php b/Web/Models/Repositories/APITokens.php index 592688a82..8a9f28701 100644 --- a/Web/Models/Repositories/APITokens.php +++ b/Web/Models/Repositories/APITokens.php @@ -23,4 +23,13 @@ function getByCode(string $code, bool $withRevoked = false): ?APIToken return $token; } + + function getStaleByUser(int $userId, string $platform, bool $withRevoked = false): ?APIToken + { + return $this->toEntity($this->table->where([ + 'user' => $userId, + 'platform' => $platform, + 'deleted' => $withRevoked, + ])->fetch()); + } } diff --git a/Web/Models/Repositories/Albums.php b/Web/Models/Repositories/Albums.php index b38ee4627..f99848c40 100644 --- a/Web/Models/Repositories/Albums.php +++ b/Web/Models/Repositories/Albums.php @@ -130,7 +130,7 @@ function getAlbumByOwnerAndId(int $owner, int $id) "owner" => $owner, "id" => $id ])->fetch(); - - return new Album($album); + + return $album ? new Album($album) : NULL; } } diff --git a/Web/Models/Repositories/Applications.php b/Web/Models/Repositories/Applications.php index 0687856e6..c09060676 100644 --- a/Web/Models/Repositories/Applications.php +++ b/Web/Models/Repositories/Applications.php @@ -67,11 +67,21 @@ function getInstalledCount(User $user): int return sizeof($this->appRels->where("user", $user->getId())); } - function find(string $query, array $pars = [], string $sort = "id"): Util\EntityStream + function find(string $query = "", array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; + $query = "%$query%"; $result = $this->apps->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where("enabled", 1); + $order_str = 'id'; + + switch($order['type']) { + case 'id': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + if($order_str) + $result->order($order_str); - return new Util\EntityStream("Application", $result->order("$sort")); + return new Util\EntityStream("Application", $result); } } \ No newline at end of file diff --git a/Web/Models/Repositories/Audios.php b/Web/Models/Repositories/Audios.php new file mode 100644 index 000000000..1b8b174dd --- /dev/null +++ b/Web/Models/Repositories/Audios.php @@ -0,0 +1,318 @@ +context = DatabaseConnection::i()->getContext(); + $this->audios = $this->context->table("audios"); + $this->rels = $this->context->table("audio_relations"); + + $this->playlists = $this->context->table("playlists"); + $this->playlistImports = $this->context->table("playlist_imports"); + $this->playlistRels = $this->context->table("playlist_relations"); + } + + function get(int $id): ?Audio + { + $audio = $this->audios->get($id); + if(!$audio) + return NULL; + + return new Audio($audio); + } + + function getPlaylist(int $id): ?Playlist + { + $playlist = $this->playlists->get($id); + if(!$playlist) + return NULL; + + return new Playlist($playlist); + } + + function getByOwnerAndVID(int $owner, int $vId): ?Audio + { + $audio = $this->audios->where([ + "owner" => $owner, + "virtual_id" => $vId, + ])->fetch(); + if(!$audio) return NULL; + + return new Audio($audio); + } + + function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist + { + $playlist = $this->playlists->where([ + "owner" => $owner, + "id" => $vId, + ])->fetch(); + if(!$playlist) return NULL; + + return new Playlist($playlist); + } + + function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->rels->where("entity", $entity)->limit($limit, $offset)->order("index DESC"); + foreach($iter as $rel) { + $audio = $this->get($rel->audio); + if(!$audio || $audio->isDeleted()) { + $deleted++; + continue; + } + + yield $audio; + } + } + + function getPlaylistsByEntityId(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->playlistImports->where("entity", $entity)->limit($limit, $offset); + foreach($iter as $rel) { + $playlist = $this->getPlaylist($rel->playlist); + if(!$playlist || $playlist->isDeleted()) { + $deleted++; + continue; + } + + yield $playlist; + } + } + + function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getRandomThreeAudiosByEntityId(int $id): Array + { + $iter = $this->rels->where("entity", $id); + $ids = []; + + foreach($iter as $it) + $ids[] = $it->audio; + + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, 0, 3); + $audios = []; + + foreach($ids as $id) { + $audio = $this->get((int)$id); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + + return $audios; + } + + function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getCollectionSizeByEntityId(int $id): int + { + return sizeof($this->rels->where("entity", $id)); + } + + function getUserCollectionSize(User $user): int + { + return sizeof($this->rels->where("entity", $user->getId())); + } + + function getClubCollectionSize(Club $club): int + { + return sizeof($this->rels->where("entity", $club->getId() * -1)); + } + + function getUserPlaylistsCount(User $user): int + { + return sizeof($this->playlistImports->where("entity", $user->getId())); + } + + function getClubPlaylistsCount(Club $club): int + { + return sizeof($this->playlistImports->where("entity", $club->getId() * -1)); + } + + function getByUploader(User $user): EntityStream + { + $search = $this->audios->where([ + "owner" => $user->getId(), + "deleted" => 0, + ]); + + return new EntityStream("Audio", $search); + } + + function getGlobal(int $order, ?string $genreId = NULL): EntityStream + { + $search = $this->audios->where([ + "deleted" => 0, + "unlisted" => 0, + "withdrawn" => 0, + ])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC"); + + if(!is_null($genreId)) + $search = $search->where("genre", $genreId); + + return new EntityStream("Audio", $search); + } + + function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream + { + $columns = $performerOnly ? "performer" : "performer, name"; + $order = (["created", "length", "listens"][$sortMode] ?? "") . " DESC"; + + $search = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ])->where("MATCH ($columns) AGAINST (? IN BOOLEAN MODE)", "%$query%")->order($order); + + if($withLyrics) + $search = $search->where("lyrics IS NOT NULL"); + + return new EntityStream("Audio", $search); + } + + function searchPlaylists(string $query): EntityStream + { + $search = $this->playlists->where([ + "unlisted" => 0, + "deleted" => 0, + ])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query); + + return new EntityStream("Playlist", $search); + } + + function getNew(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25)); + } + + function getPopular(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("listens > 0")->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("listens DESC")->limit(25)); + } + + function isAdded(int $user_id, int $audio_id): bool + { + return !is_null($this->rels->where([ + "entity" => $user_id, + "audio" => $audio_id + ])->fetch()); + } + + function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false], int $page = 1, ?int $perPage = NULL): \Traversable + { + $query = "%$query%"; + $result = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + /*"withdrawn" => 0, + "processed" => 1,*/ + ]); + $order_str = (in_array($order['type'], ['id', 'length', 'listens']) ? $order['type'] : 'id') . ' ' . ($order['invert'] ? 'ASC' : 'DESC');; + + if($params["only_performers"] == "1") { + $result->where("performer LIKE ?", $query); + } else { + $result->where("CONCAT_WS(' ', performer, name) LIKE ?", $query); + } + + foreach($params as $paramName => $paramValue) { + if(is_null($paramValue) || $paramValue == '') continue; + + switch($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + case "with_lyrics": + $result->where("lyrics IS NOT NULL"); + break; + case 'genre': + if($paramValue == 'any') break; + + $result->where("genre", $paramValue); + break; + } + } + + if($order_str) + $result->order($order_str); + + return new Util\EntityStream("Audio", $result); + } + + function findPlaylists(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): \Traversable + { + $query = "%$query%"; + $result = $this->playlists->where([ + "deleted" => 0, + ])->where("CONCAT_WS(' ', name, description) LIKE ?", $query); + $order_str = (in_array($order['type'], ['id', 'length', 'listens']) ? $order['type'] : 'id') . ' ' . ($order['invert'] ? 'ASC' : 'DESC'); + + if(is_null($params['from_me']) || empty($params['from_me'])) + $result->where(["unlisted" => 0]); + + foreach($params as $paramName => $paramValue) { + if(is_null($paramValue) || $paramValue == '') continue; + + switch($paramName) { + # БУДЬ МАКСИМАЛЬНО АККУРАТЕН С ДАННЫМ ПАРАМЕТРОМ + case "from_me": + $result->where("owner", $paramValue); + break; + } + } + + if($order_str) + $result->order($order_str); + + return new Util\EntityStream("Playlist", $result); + } +} diff --git a/Web/Models/Repositories/Bans.php b/Web/Models/Repositories/Bans.php new file mode 100644 index 000000000..7123459df --- /dev/null +++ b/Web/Models/Repositories/Bans.php @@ -0,0 +1,33 @@ +context = DB::i()->getContext(); + $this->bans = $this->context->table("bans"); + } + + function toBan(?ActiveRow $ar): ?Ban + { + return is_null($ar) ? NULL : new Ban($ar); + } + + function get(int $id): ?Ban + { + return $this->toBan($this->bans->get($id)); + } + + function getByUser(int $user_id): \Traversable + { + foreach ($this->bans->where("user", $user_id) as $ban) + yield new Ban($ban); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/ChandlerGroups.php b/Web/Models/Repositories/ChandlerGroups.php index 45af2a620..30b706e29 100644 --- a/Web/Models/Repositories/ChandlerGroups.php +++ b/Web/Models/Repositories/ChandlerGroups.php @@ -45,4 +45,9 @@ function getPermissionsById(string $UUID): \Traversable { foreach($this->perms->where("group", $UUID) as $perm) yield $perm; } + + function isUserAMember(string $GID, string $UID): bool + { + return ($this->context->query("SELECT * FROM `ChandlerACLRelations` WHERE `group` = ? AND `user` = ?", $GID, $UID)) !== NULL; + } } diff --git a/Web/Models/Repositories/ChandlerUsers.php b/Web/Models/Repositories/ChandlerUsers.php index a827afac2..510e58604 100644 --- a/Web/Models/Repositories/ChandlerUsers.php +++ b/Web/Models/Repositories/ChandlerUsers.php @@ -28,7 +28,8 @@ function get(int $id): ?ChandlerUser function getById(string $UUID): ?ChandlerUser { - return new ChandlerUser($this->users->where("id", $UUID)->fetch()); + $user = $this->users->where("id", $UUID)->fetch(); + return $user ? new ChandlerUser($user) : NULL; } function getList(int $page = 1): \Traversable diff --git a/Web/Models/Repositories/Clubs.php b/Web/Models/Repositories/Clubs.php index 04bb30abd..de5d92695 100644 --- a/Web/Models/Repositories/Clubs.php +++ b/Web/Models/Repositories/Clubs.php @@ -42,18 +42,42 @@ function get(int $id): ?Club { return $this->toClub($this->clubs->get($id)); } - - function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable + + function getByIds(array $ids = []): array { - $query = "%$query%"; - $result = $this->clubs->where("name LIKE ? OR about LIKE ?", $query, $query); - - return new Util\EntityStream("Club", $result->order($sort)); + $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%"; + $result = $this->clubs; + $order_str = 'id'; + + switch($order['type']) { + case 'id': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + $result = $result->where("name LIKE ? OR about LIKE ?", $query, $query); + + if($order_str) + $result->order($order_str); + + return new Util\EntityStream("Club", $result); } function getCount(): int { - return sizeof(clone $this->clubs); + return (clone $this->clubs)->count('*'); } function getPopularClubs(): \Traversable diff --git a/Web/Models/Repositories/Comments.php b/Web/Models/Repositories/Comments.php index f4b8e5ace..811d1358c 100644 --- a/Web/Models/Repositories/Comments.php +++ b/Web/Models/Repositories/Comments.php @@ -60,34 +60,31 @@ function getCommentsCountByTarget(Postable $target): int ])); } - function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream + function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; + $result = $this->comments->where("content LIKE ?", "%$query%")->where("deleted", 0); + $order_str = 'id'; - $notNullParams = []; - - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after") - $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; - - $result = $this->comments->where("content LIKE ?", $query)->where("deleted", 0); - $nnparamsCount = sizeof($notNullParams); + switch($order['type']) { + case 'id': + $order_str = 'created ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "before": - $result->where("created < ?", $paramValue); - break; - case "after": - $result->where("created > ?", $paramValue); - break; - } + foreach($params as $paramName => $paramValue) { + switch($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; } } - return new Util\EntityStream("Comment", $result->order("$sort")); + if($order_str) + $result->order($order_str); + + return new Util\EntityStream("Comment", $result); } } diff --git a/Web/Models/Repositories/CurrentUser.php b/Web/Models/Repositories/CurrentUser.php new file mode 100644 index 000000000..c6cb942bf --- /dev/null +++ b/Web/Models/Repositories/CurrentUser.php @@ -0,0 +1,49 @@ +user = $user; + + if ($ip) + $this->ip = $ip; + + if ($useragent) + $this->useragent = $useragent; + } + + public static function get($user, $ip, $useragent) + { + if (self::$instance === null) self::$instance = new self($user, $ip, $useragent); + return self::$instance; + } + + public function getUser(): User + { + return $this->user; + } + + public function getIP(): string + { + return $this->ip; + } + + public function getUserAgent(): string + { + return $this->useragent; + } + + public static function i() + { + return self::$instance; + } +} diff --git a/Web/Models/Repositories/Gifts.php b/Web/Models/Repositories/Gifts.php index f36b82a56..3baa23976 100644 --- a/Web/Models/Repositories/Gifts.php +++ b/Web/Models/Repositories/Gifts.php @@ -42,4 +42,10 @@ function getCategories(int $page, ?int $perPage = NULL, &$count = nullptr): \Tra foreach($cats as $cat) yield new GiftCategory($cat); } + + function getCategoriesCount(): int + { + $cats = $this->cats->where("deleted", false); + return $cats->count(); + } } diff --git a/Web/Models/Repositories/IPs.php b/Web/Models/Repositories/IPs.php index c4485b73f..59fc65708 100644 --- a/Web/Models/Repositories/IPs.php +++ b/Web/Models/Repositories/IPs.php @@ -24,7 +24,7 @@ function get(string $ip): ?IP if(!$res) { $res = new IP; $res->setIp($ip); - $res->save(); + $res->save(false); return $res; } diff --git a/Web/Models/Repositories/Messages.php b/Web/Models/Repositories/Messages.php index 4c870806b..538338ed5 100644 --- a/Web/Models/Repositories/Messages.php +++ b/Web/Models/Repositories/Messages.php @@ -52,7 +52,6 @@ function getCorrespondenciesCount(RowModel $correspondent): ?int $query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql"); DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql")); $count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt; - bdump($count); return $count; } } diff --git a/Web/Models/Repositories/NoSpamLogs.php b/Web/Models/Repositories/NoSpamLogs.php new file mode 100644 index 000000000..f8dd49806 --- /dev/null +++ b/Web/Models/Repositories/NoSpamLogs.php @@ -0,0 +1,34 @@ +context = DatabaseConnection::i()->getContext(); + $this->noSpamLogs = $this->context->table("noSpam_templates"); + } + + private function toNoSpamLog(?ActiveRow $ar): ?NoSpamLog + { + return is_null($ar) ? NULL : new NoSpamLog($ar); + } + + function get(int $id): ?NoSpamLog + { + return $this->toNoSpamLog($this->noSpamLogs->get($id)); + } + + function getList(array $filter = []): \Traversable + { + foreach ($this->noSpamLogs->where($filter)->order("`id` DESC") as $log) + yield new NoSpamLog($log); + } +} diff --git a/Web/Models/Repositories/Notifications.php b/Web/Models/Repositories/Notifications.php index bec0b04f8..ccbf7c278 100644 --- a/Web/Models/Repositories/Notifications.php +++ b/Web/Models/Repositories/Notifications.php @@ -30,7 +30,7 @@ private function getModel(int $code, int $id): object return (new $repoClassName)->get($id); } - private function getQuery(User $user, bool $count = false, int $offset, bool $archived = false, int $page = 1, ?int $perPage = NULL): string + private function getQuery(User $user, bool $count, int $offset, bool $archived = false, int $page = 1, ?int $perPage = NULL): string { $query = "SELECT " . ($count ? "COUNT(*) AS cnt" : "*") . " FROM notifications WHERE recipientType=0 "; $query .= "AND timestamp " . ($archived ? "<" : ">") . "$offset AND recipientId=" . $user->getId(); diff --git a/Web/Models/Repositories/Photos.php b/Web/Models/Repositories/Photos.php index 88c7e804c..a024747c2 100644 --- a/Web/Models/Repositories/Photos.php +++ b/Web/Models/Repositories/Photos.php @@ -33,14 +33,26 @@ function getByOwnerAndVID(int $owner, int $vId): ?Photo return new Photo($photo); } - function getEveryUserPhoto(User $user): \Traversable + function getEveryUserPhoto(User $user, int $offset = 0, int $limit = 10): \Traversable { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; $photos = $this->photos->where([ - "owner" => $user->getId() - ]); + "owner" => $user->getId(), + "deleted" => 0 + ])->order("id DESC"); - foreach($photos as $photo) { + foreach($photos->limit($limit, $offset) as $photo) { yield new Photo($photo); } } + + function getUserPhotosCount(User $user) + { + $photos = $this->photos->where([ + "owner" => $user->getId(), + "deleted" => 0 + ]); + + return sizeof($photos); + } } diff --git a/Web/Models/Repositories/Posts.php b/Web/Models/Repositories/Posts.php index c354f1525..f5fef2829 100644 --- a/Web/Models/Repositories/Posts.php +++ b/Web/Models/Repositories/Posts.php @@ -53,19 +53,64 @@ function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = NULL, ? $offset--; } } - } else if(!is_null($offset)) { + } else if(!is_null($offset) && $pinPost) { $offset--; } $sel = $this->posts->where([ - "wall" => $user, - "pinned" => false, - "deleted" => false, + "wall" => $user, + "pinned" => false, + "deleted" => false, + "suggested" => 0, ])->order("created DESC")->limit($perPage, $offset); foreach($sel as $post) yield new Post($post); } + + function getOwnersPostsFromWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts->where([ + "wall" => $user, + "deleted" => false, + "suggested" => 0, + ]); + + if($user > 0) + $sel->where("owner", $user); + else + $sel->where("flags !=", 0); + + $sel->order("created DESC")->limit($perPage, $offset); + + foreach($sel as $post) + yield new Post($post); + } + + function getOthersPostsFromWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts->where([ + "wall" => $user, + "deleted" => false, + "suggested" => 0, + ]); + + if($user > 0) + $sel->where("owner !=", $user); + else + $sel->where("flags", 0); + + $sel->order("created DESC")->limit($perPage, $offset); + + foreach($sel as $post) + yield new Post($post); + } function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = NULL): \Traversable { @@ -74,6 +119,7 @@ function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = NULL) ->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag") ->where("deleted", 0) ->order("created DESC") + ->where("suggested", 0) ->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); foreach($sel as $post) @@ -85,14 +131,22 @@ function getPostCountByHashtag(string $hashtag): int $hashtag = "#$hashtag"; $sel = $this->posts ->where("content LIKE ?", "%$hashtag%") - ->where("deleted", 0); + ->where("deleted", 0) + ->where("suggested", 0); return sizeof($sel); } - function getPostById(int $wall, int $post): ?Post + function getPostById(int $wall, int $post, bool $forceSuggestion = false): ?Post { - $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch(); + $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post]); + + if(!$forceSuggestion) { + $post->where("suggested", 0); + } + + $post = $post->fetch(); + if(!is_null($post)) return new Post($post); else @@ -100,45 +154,113 @@ function getPostById(int $wall, int $post): ?Post } - function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream + function find(string $query = "", array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; + $query = "%$query%"; + $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0)->where("suggested", 0); + $order_str = 'id'; - $notNullParams = []; - - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after") - $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; + switch($order['type']) { + case 'id': + $order_str = 'created ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } - $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0); - $nnparamsCount = sizeof($notNullParams); + foreach($params as $paramName => $paramValue) { + if(is_null($paramValue) || $paramValue == '') continue; - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "before": - $result->where("created < ?", $paramValue); - break; - case "after": - $result->where("created > ?", $paramValue); - break; - } + switch($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + /*case 'die_in_agony': + $result->where("nsfw", 1); + break; + case 'ads': + $result->where("ad", 1); + break;*/ + # БУДЬ МАКСИМАЛЬНО АККУРАТЕН С ДАННЫМ ПАРАМЕТРОМ + case 'from_me': + $result->where("owner", $paramValue); + break; } } + if($order_str) + $result->order($order_str); - return new Util\EntityStream("Post", $result->order("$sort")); + return new Util\EntityStream("Post", $result); } function getPostCountOnUserWall(int $user): int { - return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])); + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])); + } + + function getOwnersCountOnUserWall(int $user): int + { + if($user > 0) + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "owner" => $user])); + else + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags !=", 0)); + } + + function getOthersCountOnUserWall(int $user): int + { + if($user > 0) + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])->where("owner !=", $user)); + else + return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags", 0)); + } + + function getSuggestedPosts(int $club, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts + ->where("deleted", 0) + ->where("wall", $club * -1) + ->order("created DESC") + ->where("suggested", 1) + ->limit($perPage, $offset); + + foreach($sel as $post) + yield new Post($post); + } + + function getSuggestedPostsCount(int $club) + { + return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1])); + } + + function getSuggestedPostsByUser(int $club, int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable + { + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + $offset ??= $perPage * ($page - 1); + + $sel = $this->posts + ->where("deleted", 0) + ->where("wall", $club * -1) + ->where("owner", $user) + ->order("created DESC") + ->where("suggested", 1) + ->limit($perPage, $offset); + + foreach($sel as $post) + yield new Post($post); + } + + function getSuggestedPostsCountByUser(int $club, int $user): int + { + return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1, "owner" => $user])); } function getCount(): int { - return sizeof(clone $this->posts); + return (clone $this->posts)->count('*'); } } diff --git a/Web/Models/Repositories/Reports.php b/Web/Models/Repositories/Reports.php new file mode 100644 index 000000000..edce8980f --- /dev/null +++ b/Web/Models/Repositories/Reports.php @@ -0,0 +1,67 @@ +context = DatabaseConnection::i()->getContext(); + $this->reports = $this->context->table("reports"); + } + + private function toReport(?ActiveRow $ar): ?Report + { + return is_null($ar) ? NULL : new Report($ar); + } + + function getReports(int $state = 0, int $page = 1, ?string $type = NULL, ?bool $pagination = true): \Traversable + { + $filter = ["deleted" => 0]; + if ($type) $filter["type"] = $type; + + $reports = $this->reports->where($filter)->order("created DESC")->group("target_id, type"); + if ($pagination) + $reports = $reports->page($page, 15); + + foreach($reports as $t) + yield new Report($t); + } + + function getReportsCount(int $state = 0): int + { + return sizeof($this->reports->where(["deleted" => 0, "type" => $state])->group("target_id, type")); + } + + function get(int $id): ?Report + { + return $this->toReport($this->reports->get($id)); + } + + function getByContentId(int $id): ?Report + { + $post = $this->reports->where(["deleted" => 0, "content_id" => $id])->fetch(); + + if($post) + return new Report($post); + else + return null; + } + + function getDuplicates(string $type, int $target_id, ?int $orig = NULL, ?int $user_id = NULL): \Traversable + { + $filter = ["deleted" => 0, "type" => $type, "target_id" => $target_id]; + if ($orig) $filter[] = "id != $orig"; + if ($user_id) $filter["user_id"] = $user_id; + + foreach ($this->reports->where($filter) as $report) + yield new Report($report); + } + + use \Nette\SmartObject; +} diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php index 0eec33065..ed5a87a51 100644 --- a/Web/Models/Repositories/Users.php +++ b/Web/Models/Repositories/Users.php @@ -28,6 +28,18 @@ function get(int $id): ?User { 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 { @@ -44,101 +56,94 @@ function getByShortURL(string $url): ?User return $alias->getUser(); } - function getByChandlerUser(ChandlerUser $user): ?User + function getByChandlerUserId(string $cid): ?User { - return $this->toUser($this->users->where("user", $user->getId())->fetch()); + return $this->toUser($this->users->where("user", $cid)->fetch()); } - function find(string $query, array $pars = [], string $sort = "id DESC"): Util\EntityStream + function getByChandlerUser(?ChandlerUser $user): ?User { - $query = "%$query%"; + return $user ? $this->getByChandlerUserId($user->getId()) : NULL; + } + + function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream + { + $query = "%$query%"; $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo, shortcode) LIKE ?", $query)->where("deleted", 0); - - $notNullParams = []; - $nnparamsCount = 0; - - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after" && $paramName != "gender" && $paramName != "maritalstatus" && $paramName != "politViews") - $paramValue != NULL ? $notNullParams += ["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams += ["$paramName" => "$paramValue"] : NULL; - - $nnparamsCount = sizeof($notNullParams); - - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "hometown": - $result->where("hometown LIKE ?", $paramValue); - break; - case "city": - $result->where("city LIKE ?", $paramValue); - break; - case "maritalstatus": - $result->where("marital_status ?", $paramValue); - break; - case "status": - $result->where("status LIKE ?", $paramValue); - break; - case "politViews": - $result->where("polit_views ?", $paramValue); - break; - case "email": - $result->where("email_contact LIKE ?", $paramValue); - break; - case "telegram": - $result->where("telegram LIKE ?", $paramValue); - break; - case "site": - $result->where("telegram LIKE ?", $paramValue); - break; - case "address": - $result->where("address LIKE ?", $paramValue); - break; - case "is_online": - $result->where("online >= ?", time() - 900); - break; - case "interests": - $result->where("interests LIKE ?", $paramValue); - break; - case "fav_mus": - $result->where("fav_music LIKE ?", $paramValue); - break; - case "fav_films": - $result->where("fav_films LIKE ?", $paramValue); - break; - case "fav_shows": - $result->where("fav_shows LIKE ?", $paramValue); - break; - case "fav_books": - $result->where("fav_books LIKE ?", $paramValue); - break; - case "fav_quote": - $result->where("fav_quote LIKE ?", $paramValue); - break; - case "before": - $result->where("UNIX_TIMESTAMP(since) < ?", $paramValue); - break; - case "after": - $result->where("UNIX_TIMESTAMP(since) > ?", $paramValue); - break; - case "gender": - $result->where("sex ?", $paramValue); - break; - } + $order_str = 'id'; + + switch($order['type']) { + case 'id': + case 'reg_date': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + case 'rating': + $order_str = 'rating DESC'; + break; + } + + foreach($params as $paramName => $paramValue) { + if(is_null($paramValue) || $paramValue == '') continue; + + switch($paramName) { + case "hometown": + $result->where("hometown LIKE ?", "%$paramValue%"); + break; + case "city": + $result->where("city LIKE ?", "%$paramValue%"); + break; + case "marital_status": + $result->where("marital_status ?", $paramValue); + break; + case "polit_views": + $result->where("polit_views ?", $paramValue); + break; + case "is_online": + $result->where("online >= ?", time() - 900); + break; + case "fav_mus": + $result->where("fav_music LIKE ?", "%$paramValue%"); + break; + case "fav_films": + $result->where("fav_films LIKE ?", "%$paramValue%"); + break; + case "fav_shows": + $result->where("fav_shows LIKE ?", "%$paramValue%"); + break; + case "fav_books": + $result->where("fav_books LIKE ?", "%$paramValue%"); + break; + case "before": + $result->where("UNIX_TIMESTAMP(since) < ?", $paramValue); + break; + case "after": + $result->where("UNIX_TIMESTAMP(since) > ?", $paramValue); + break; + case "gender": + if((int) $paramValue == 3) break; + $result->where("sex ?", (int) $paramValue); + break; + case "ignore_id": + $result->where("id != ?", $paramValue); + break; + case "ignore_private": + $result->where("profile_type", 0); + break; } } + if($order_str) + $result->order($order_str); - return new Util\EntityStream("User", $result->order($sort)); + return new Util\EntityStream("User", $result); } function getStatistics(): object { return (object) [ - "all" => sizeof(clone $this->users), - "active" => sizeof((clone $this->users)->where("online > 0")), - "online" => sizeof((clone $this->users)->where("online >= ?", time() - 900)), + "all" => (clone $this->users)->count('*'), + "active" => (clone $this->users)->where("online > 0")->count('*'), + "online" => (clone $this->users)->where("online >= ?", time() - 900)->count('*'), ]; } diff --git a/Web/Models/Repositories/Videos.php b/Web/Models/Repositories/Videos.php index 632733496..8dbda7ab8 100644 --- a/Web/Models/Repositories/Videos.php +++ b/Web/Models/Repositories/Videos.php @@ -39,6 +39,13 @@ function getByUser(User $user, int $page = 1, ?int $perPage = NULL): \Traversabl $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; foreach($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->page($page, $perPage)->order("created DESC") as $video) yield new Video($video); + } + + function getByUserLimit(User $user, int $offset = 0, int $limit = 10): \Traversable + { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + foreach($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->limit($limit, $offset)->order("created DESC") as $video) + yield new Video($video); } function getUserVideosCount(User $user): int @@ -46,35 +53,43 @@ function getUserVideosCount(User $user): int return sizeof($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])); } - function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream + function find(string $query = "", array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $query = "%$query%"; + $query = "%$query%"; + $result = $this->videos->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where("deleted", 0)->where("unlisted", 0); + $order_str = 'id'; - $notNullParams = []; - - foreach($pars as $paramName => $paramValue) - if($paramName != "before" && $paramName != "after") - $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; - else - $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; - - $result = $this->videos->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where("deleted", 0); - $nnparamsCount = sizeof($notNullParams); + switch($order['type']) { + case 'id': + $order_str = 'id ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } - if($nnparamsCount > 0) { - foreach($notNullParams as $paramName => $paramValue) { - switch($paramName) { - case "before": - $result->where("created < ?", $paramValue); - break; - case "after": - $result->where("created > ?", $paramValue); - break; - } + foreach($params as $paramName => $paramValue) { + switch($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + case 'only_youtube': + if((int) $paramValue != 1) break; + $result->where("link != ?", 'NULL'); + break; } } + if($order_str) + $result->order($order_str); + + return new Util\EntityStream("Video", $result); + } + + function getLastVideo(User $user) + { + $video = $this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->order("id DESC")->fetch(); - return new Util\EntityStream("Video", $result->order("$sort")); + return new Video($video); } } diff --git a/Web/Models/shell/processAudio.ps1 b/Web/Models/shell/processAudio.ps1 new file mode 100644 index 000000000..f60a9aede --- /dev/null +++ b/Web/Models/shell/processAudio.ps1 @@ -0,0 +1,39 @@ +$ovkRoot = $args[0] +$storageDir = $args[1] +$fileHash = $args[2] +$hashPart = $fileHash.substring(0, 2) +$filename = $args[3] +$audioFile = [System.IO.Path]::GetTempFileName() +$temp = [System.IO.Path]::GetTempFileName() + +$keyID = $args[4] +$key = $args[5] +$token = $args[6] +$seg = $args[7] + +$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID" +$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree + +Remove-Item $temp +Remove-Item $audioFile +New-Item -ItemType "directory" $temp +New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments') +New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments') +Set-Location -Path $temp + +Move-Item $filename $audioFile +ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key ` + -encryption_kid $keyID -map 0:a -vn -c:a aac -ar 44100 -seg_duration $seg ` + -use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') ` + -media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' ` + "$fileHash.mpd" + +ffmpeg -i $audioFile -vn -ar 44100 "original_$token.mp3" +Move-Item "original_$token.mp3" ($fileHash + '_fragments') + +Get-ChildItem -Path ($fileHash + '_fragments/*') | Move-Item -Destination ("$storageDir/$hashPart/$fileHash" + '_fragments') +Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart" + +cd .. +Remove-Item -Recurse $temp +Remove-Item $audioFile diff --git a/Web/Models/shell/processAudio.sh b/Web/Models/shell/processAudio.sh new file mode 100644 index 000000000..fa8346e06 --- /dev/null +++ b/Web/Models/shell/processAudio.sh @@ -0,0 +1,35 @@ +ovkRoot=$1 +storageDir=$2 +fileHash=$3 +hashPart=$(echo $fileHash | cut -c1-2) +filename=$4 +audioFile=$(mktemp) +temp=$(mktemp -d) + +keyID=$5 +key=$6 +token=$7 +seg=$8 + +trap 'rm -f "$temp" "$audioFile"' EXIT + +mkdir -p "$temp/$fileHash"_fragments +mkdir -p "$storageDir/$hashPart/$fileHash"_fragments +cd "$temp" + +mv "$filename" "$audioFile" +ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \ + -encryption_kid "$keyID" -map 0 -vn -c:a aac -ar 44100 -seg_duration "$seg" \ + -use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \ + -media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \ + "$fileHash.mpd" + +ffmpeg -i "$audioFile" -vn -ar 44100 "original_$token.mp3" +mv "original_$token.mp3" "$fileHash"_fragments + +mv "$fileHash"_fragments "$storageDir/$hashPart" +mv "$fileHash.mpd" "$storageDir/$hashPart" + +cd .. +rm -rf "$temp" +rm -f "$audioFile" diff --git a/Web/Models/shell/processVideo.ps1 b/Web/Models/shell/processVideo.ps1 index f2cc99188..59f53c72b 100644 --- a/Web/Models/shell/processVideo.ps1 +++ b/Web/Models/shell/processVideo.ps1 @@ -13,7 +13,7 @@ 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" -ffmpeg -i $temp -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2 +ffmpeg -i $temp -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=480:-1,setsar=1" -y $temp2 Move-Item $temp2 "$dir$hashT/$hash.mp4" Remove-Item $temp diff --git a/Web/Models/shell/processVideo.sh b/Web/Models/shell/processVideo.sh index ca2c6d99d..f906544db 100644 --- a/Web/Models/shell/processVideo.sh +++ b/Web/Models/shell/processVideo.sh @@ -3,7 +3,7 @@ tmpfile="$RANDOM-$(date +%s%N)" cp $2 "/tmp/vid_$tmpfile.bin" nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif -nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y "/tmp/ffmOi$tmpfile.mp4" +nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=480:-1,setsar=1" -y "/tmp/ffmOi$tmpfile.mp4" rm -rf $3${4:0:2}/$4.mp4 mv "/tmp/ffmOi$tmpfile.mp4" $3${4:0:2}/$4.mp4 diff --git a/Web/Models/sql/get-followers.tsql b/Web/Models/sql/get-followers.tsql index ae23d63a3..b07b1c690 100644 --- a/Web/Models/sql/get-followers.tsql +++ b/Web/Models/sql/get-followers.tsql @@ -1,5 +1,5 @@ - (SELECT follower AS __id FROM - (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 + (SELECT DISTINCT(follower) AS __id FROM + (SELECT follower, flags FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 LEFT JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 ON u0.follower = u1.target WHERE u1.target IS NULL) u2 diff --git a/Web/Models/sql/get-friends.tsql b/Web/Models/sql/get-friends.tsql index dc7b07306..4831c9684 100644 --- a/Web/Models/sql/get-friends.tsql +++ b/Web/Models/sql/get-friends.tsql @@ -1,4 +1,4 @@ - (SELECT follower AS __id FROM + (SELECT DISTINCT(follower) AS __id FROM (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 INNER JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 diff --git a/Web/Models/sql/get-online-friends.tsql b/Web/Models/sql/get-online-friends.tsql index ad6465543..7e81c621b 100755 --- a/Web/Models/sql/get-online-friends.tsql +++ b/Web/Models/sql/get-online-friends.tsql @@ -1,4 +1,4 @@ - (SELECT follower AS __id FROM + (SELECT DISTINCT(follower) AS __id FROM (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 INNER JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 diff --git a/Web/Models/sql/get-requests.tsql b/Web/Models/sql/get-requests.tsql new file mode 100755 index 000000000..0220e3400 --- /dev/null +++ b/Web/Models/sql/get-requests.tsql @@ -0,0 +1,6 @@ + (SELECT DISTINCT(follower) AS __id FROM + (SELECT follower FROM subscriptions WHERE target=? AND flags=0 AND model="openvk\\Web\\Models\\Entities\\User") u0 + LEFT JOIN + (SELECT target FROM subscriptions WHERE follower=? AND flags=0 AND model="openvk\\Web\\Models\\Entities\\User") u1 + ON u0.follower = u1.target WHERE u1.target IS NULL) u2 +INNER JOIN profiles ON profiles.id = u2.__id \ No newline at end of file diff --git a/Web/Models/sql/get-subscriptions-user.tsql b/Web/Models/sql/get-subscriptions-user.tsql index 722db194a..ecf8ad79c 100644 --- a/Web/Models/sql/get-subscriptions-user.tsql +++ b/Web/Models/sql/get-subscriptions-user.tsql @@ -1,4 +1,4 @@ - (SELECT target AS __id FROM + (SELECT DISTINCT(target) AS __id FROM (SELECT follower FROM subscriptions WHERE target=? AND model="openvk\\Web\\Models\\Entities\\User") u0 RIGHT JOIN (SELECT target FROM subscriptions WHERE follower=? AND model="openvk\\Web\\Models\\Entities\\User") u1 diff --git a/Web/Presenters/AboutPresenter.php b/Web/Presenters/AboutPresenter.php index 9115a019b..49ff1c327 100644 --- a/Web/Presenters/AboutPresenter.php +++ b/Web/Presenters/AboutPresenter.php @@ -109,6 +109,10 @@ function renderRobotsTxt(): void . "# lack of rights to access the admin panel)\n\n" . "User-Agent: *\n" . "Disallow: /albums/create\n" + . "Disallow: /assets/packages/static/openvk/img/banned.jpg\n" + . "Disallow: /assets/packages/static/openvk/img/camera_200.png\n" + . "Disallow: /assets/packages/static/openvk/img/flags/\n" + . "Disallow: /assets/packages/static/openvk/img/oof.apng\n" . "Disallow: /videos/upload\n" . "Disallow: /invite\n" . "Disallow: /groups_create\n" @@ -141,6 +145,6 @@ function renderHumansTxt(): void function renderDev(): void { - $this->redirect("https://docs.openvk.uk/"); + $this->redirect("https://docs.ovk.to/"); } } diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index 530cb7f84..b8f480ca0 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -1,7 +1,21 @@ users = $users; $this->clubs = $clubs; @@ -21,7 +37,9 @@ function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gift $this->gifts = $gifts; $this->bannedLinks = $bannedLinks; $this->chandlerGroups = $chandlerGroups; - + $this->audios = $audios; + $this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs"); + parent::__construct(); } @@ -30,6 +48,13 @@ private function warnIfNoCommerce(): void if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) $this->flash("warn", tr("admin_commerce_disabled"), tr("admin_commerce_disabled_desc")); } + + private function warnIfLongpoolBroken(): void + { + bdump(is_writable(CHANDLER_ROOT . '/tmp/events.bin')); + if(file_exists(CHANDLER_ROOT . '/tmp/events.bin') == false || is_writable(CHANDLER_ROOT . '/tmp/events.bin') == false) + $this->flash("warn", tr("admin_longpool_broken"), tr("admin_longpool_broken_desc", CHANDLER_ROOT . '/tmp/events.bin')); + } private function searchResults(object $repo, &$count) { @@ -39,6 +64,15 @@ private function searchResults(object $repo, &$count) $count = $repo->find($query)->size(); return $repo->find($query)->page($page, 20); } + + private function searchPlaylists(&$count) + { + $query = $this->queryParam("q") ?? ""; + $page = (int) ($this->queryParam("p") ?? 1); + + $count = $this->audios->findPlaylists($query)->size(); + return $this->audios->findPlaylists($query)->page($page, 20); + } function onStartup(): void { @@ -49,7 +83,7 @@ function onStartup(): void function renderIndex(): void { - + $this->warnIfLongpoolBroken(); } function renderUsers(): void @@ -83,8 +117,13 @@ function renderUser(int $id): void if($user->onlineStatus() != $this->postParam("online")) $user->setOnline(intval($this->postParam("online"))); $user->setVerified(empty($this->postParam("verify") ? 0 : 1)); if($this->postParam("add-to-group")) { - $query = "INSERT INTO `ChandlerACLRelations` (`user`, `group`) VALUES ('" . $user->getChandlerGUID() . "', '" . $this->postParam("add-to-group") . "')"; - DatabaseConnection::i()->getConnection()->query($query); + if (!(new ChandlerGroups)->isUserAMember($user->getChandlerGUID(), $this->postParam("add-to-group"))) { + $query = "INSERT INTO `ChandlerACLRelations` (`user`, `group`) VALUES ('" . $user->getChandlerGUID() . "', '" . $this->postParam("add-to-group") . "')"; + DatabaseConnection::i()->getConnection()->query($query); + } + } + if($this->postParam("password")) { + $user->getChandlerUser()->updatePassword($this->postParam("password")); } $user->save(); @@ -122,10 +161,12 @@ function renderClub(int $id): void $club->setShortCode($this->postParam("shortcode")); $club->setVerified(empty($this->postParam("verify") ? 0 : 1)); $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed") ? 0 : 1)); + $club->setEnforce_Hiding_From_Global_Feed(empty($this->postParam("enforce_hiding_from_global_feed") ? 0 : 1)); $club->save(); break; case "ban": - $club->setBlock_reason($this->postParam("ban_reason")); + $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? $this->postParam("ban_reason") : NULL; + $club->setBlock_reason($reason); $club->save(); break; } @@ -275,7 +316,7 @@ function renderGift(int $id): void $this->notFound(); $gift->delete(); - $this->flashFail("succ", "Gift moved successfully", "This gift will now be in Recycle Bin."); + $this->flashFail("succ", tr("admin_gift_moved_successfully"), tr("admin_gift_moved_to_recycle")); break; case "copy": case "move": @@ -294,7 +335,7 @@ function renderGift(int $id): void $catTo->addGift($gift); $name = $catTo->getName(); - $this->flash("succ", "Gift moved successfully", "This gift will now be in $name."); + $this->flash("succ", tr("admin_gift_moved_successfully"), "This gift will now be in $name."); $this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/"); break; default: @@ -325,10 +366,10 @@ function renderGift(int $id): void $gift->setUsages((int) $this->postParam("usages")); if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) { if(!$gift->setImage($_FILES["pic"]["tmp_name"])) - $this->flashFail("err", "Не удалось сохранить подарок", "Изображение подарка кривое."); + $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_bad_image")); } else if($gen) { # If there's no gift pic but it's newly created - $this->flashFail("err", "Не удалось сохранить подарок", "Пожалуйста, загрузите изображение подарка."); + $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_no_image")); } $gift->save(); @@ -352,13 +393,19 @@ function renderQuickBan(int $id): void { $this->assertNoCSRF(); - $unban_time = strtotime($this->queryParam("date")) ?: NULL; + if (str_contains($this->queryParam("reason"), "*")) + exit(json_encode([ "error" => "Incorrect reason" ])); + + $unban_time = strtotime($this->queryParam("date")) ?: "permanent"; $user = $this->users->get($id); if(!$user) exit(json_encode([ "error" => "User does not exist" ])); - - $user->ban($this->queryParam("reason"), true, $unban_time); + + if ($this->queryParam("incr")) + $unban_time = time() + $user->getNewBanTime(); + + $user->ban($this->queryParam("reason"), true, $unban_time, $this->user->identity->getId()); exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ])); } @@ -369,9 +416,17 @@ function renderQuickUnban(int $id): void $user = $this->users->get($id); if(!$user) exit(json_encode([ "error" => "User does not exist" ])); - + + $ban = (new Bans)->get((int)$user->getRawBanReason()); + if (!$ban || $ban->isOver()) + exit(json_encode([ "error" => "User is not banned" ])); + + $ban->setRemoved_Manually(true); + $ban->setRemoved_By($this->user->identity->getId()); + $ban->save(); + $user->setBlock_Reason(NULL); - $user->setUnblock_time(NULL); + // $user->setUnblock_time(NULL); $user->save(); exit(json_encode([ "success" => true ])); } @@ -457,6 +512,14 @@ function renderUnbanLink(int $id): void $this->redirect("/admin/bannedLinks"); } + function renderBansHistory(int $user_id) :void + { + $user = (new Users)->get($user_id); + if (!$user) $this->notFound(); + + $this->template->bans = (new Bans)->getByUser($user_id); + } + function renderChandlerGroups(): void { $this->template->groups = (new ChandlerGroups)->getList(); @@ -547,4 +610,87 @@ function renderChandlerUser(string $UUID): void $this->redirect("/admin/users/id" . $user->getId()); } + + function renderMusic(): void + { + $this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios"; + if ($this->template->mode === "audios") + $this->template->audios = $this->searchResults($this->audios, $this->template->count); + else + $this->template->playlists = $this->searchPlaylists($this->template->count); + } + + function renderEditMusic(int $audio_id): void + { + $audio = $this->audios->get($audio_id); + $this->template->audio = $audio; + + try { + $this->template->owner = $audio->getOwner()->getId(); + } catch(\Throwable $e) { + $this->template->owner = 1; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $audio->setName($this->postParam("name")); + $audio->setPerformer($this->postParam("performer")); + $audio->setLyrics($this->postParam("text")); + $audio->setGenre($this->postParam("genre")); + $audio->setOwner((int) $this->postParam("owner")); + $audio->setExplicit(!empty($this->postParam("explicit"))); + $audio->setDeleted(!empty($this->postParam("deleted"))); + $audio->setWithdrawn(!empty($this->postParam("withdrawn"))); + $audio->save(); + } + } + + function renderEditPlaylist(int $playlist_id): void + { + $playlist = $this->audios->getPlaylist($playlist_id); + $this->template->playlist = $playlist; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $playlist->setName($this->postParam("name")); + $playlist->setDescription($this->postParam("description")); + $playlist->setCover_Photo_Id((int) $this->postParam("photo")); + $playlist->setOwner((int) $this->postParam("owner")); + $playlist->setDeleted(!empty($this->postParam("deleted"))); + $playlist->save(); + } + } + + function renderLogs(): void + { + $filter = []; + + if ($this->queryParam("id")) { + $id = (int) $this->queryParam("id"); + $filter["id"] = $id; + $this->template->id = $id; + } + if ($this->queryParam("type") !== NULL && $this->queryParam("type") !== "any") { + $type = in_array($this->queryParam("type"), [0, 1, 2, 3]) ? (int) $this->queryParam("type") : 0; + $filter["type"] = $type; + $this->template->type = $type; + } + if ($this->queryParam("uid")) { + $user = $this->queryParam("uid"); + $filter["user"] = $user; + $this->template->user = $user; + } + if ($this->queryParam("obj_id")) { + $obj_id = (int) $this->queryParam("obj_id"); + $filter["object_id"] = $obj_id; + $this->template->obj_id = $obj_id; + } + if ($this->queryParam("obj_type") !== NULL && $this->queryParam("obj_type") !== "any") { + $obj_type = "openvk\\Web\\Models\\Entities\\" . $this->queryParam("obj_type"); + $filter["object_model"] = $obj_type; + $this->template->obj_type = $obj_type; + } + + $logs = iterator_to_array((new Logs)->search($filter)); + $this->template->logs = $logs; + $this->template->object_types = (new Logs)->getTypes(); + } } diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php new file mode 100644 index 000000000..aa8801f51 --- /dev/null +++ b/Web/Presenters/AudioPresenter.php @@ -0,0 +1,827 @@ +audios = $audios; + } + + function renderPopular(): void + { + $this->renderList(NULL, "popular"); + } + + function renderNew(): void + { + $this->renderList(NULL, "new"); + } + + function renderList(?int $owner = NULL, ?string $mode = "list"): void + { + $this->assertUserLoggedIn(); + $this->template->_template = "Audio/List.xml"; + $page = (int)($this->queryParam("p") ?? 1); + $audios = []; + + if ($mode === "list") { + $entity = NULL; + if ($owner < 0) { + $entity = (new Clubs)->get($owner * -1); + if (!$entity || $entity->isBanned()) + $this->redirect("/audios" . $this->user->id); + + $audios = $this->audios->getByClub($entity, $page, 10); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } else { + $entity = (new Users)->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) + $this->redirect("/audios" . $this->user->id); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $audios = $this->audios->getByUser($entity, $page, 10); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } + + if (!$entity) + $this->notFound(); + + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else if ($mode === "new") { + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + } else if ($mode === "playlists") { + if($owner < 0) { + $entity = (new Clubs)->get(abs($owner)); + if (!$entity || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + $playlists = $this->audios->getPlaylistsByClub($entity, $page, OPENVK_DEFAULT_PER_PAGE); + $playlistsCount = $this->audios->getClubPlaylistsCount($entity); + } else { + $entity = (new Users)->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $playlists = $this->audios->getPlaylistsByUser($entity, $page, OPENVK_DEFAULT_PER_PAGE); + $playlistsCount = $this->audios->getUserPlaylistsCount($entity); + } + + $this->template->playlists = iterator_to_array($playlists); + $this->template->playlistsCount = $playlistsCount; + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else if ($mode === 'alone_audio') { + $audios = [$this->template->alone_audio]; + $audiosCount = 1; + + $this->template->owner = $this->user->identity; + $this->template->ownerId = $this->user->id; + } + + // $this->renderApp("owner=$owner"); + if ($audios !== []) { + $this->template->audios = iterator_to_array($audios); + $this->template->audiosCount = $audiosCount; + } + + $this->template->mode = $mode; + $this->template->page = $page; + + if(in_array($mode, ["list", "new", "popular"]) && $this->user->identity && $page < 2) + $this->template->friendsAudios = $this->user->identity->getBroadcastList("all", true); + } + + function renderEmbed(int $owner, int $id): void + { + $audio = $this->audios->getByOwnerAndVID($owner, $id); + if(!$audio) { + header("HTTP/1.1 404 Not Found"); + exit("" . tr("audio_embed_not_found") . "."); + } else if($audio->isDeleted()) { + header("HTTP/1.1 410 Not Found"); + exit("" . tr("audio_embed_deleted") . "."); + } else if($audio->isWithdrawn()) { + header("HTTP/1.1 451 Unavailable for legal reasons"); + exit("" . tr("audio_embed_withdrawn") . "."); + } else if(!$audio->canBeViewedBy(NULL)) { + header("HTTP/1.1 403 Forbidden"); + exit("" . tr("audio_embed_forbidden") . "."); + } else if(!$audio->isAvailable()) { + header("HTTP/1.1 425 Too Early"); + exit("" . tr("audio_embed_processing") . "."); + } + + $this->template->audio = $audio; + } + + function renderUpload(): void + { + $this->assertUserLoggedIn(); + + $group = NULL; + $playlist = NULL; + $isAjax = $this->postParam("ajax", false) == 1; + + if(!is_null($this->queryParam("gid")) && !is_null($this->queryParam("playlist"))) { + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + if(!is_null($this->queryParam("gid"))) { + $gid = (int) $this->queryParam("gid"); + $group = (new Clubs)->get($gid); + if(!$group) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + + if(!$group->canUploadAudio($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + if(!is_null($this->queryParam("playlist"))) { + $playlist_id = (int)$this->queryParam("playlist"); + $playlist = (new Audios)->getPlaylist($playlist_id); + if(!$playlist || $playlist->isDeleted()) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + + if(!$playlist->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + + $this->template->playlist = $playlist; + $this->template->owner = $playlist->getOwner(); + } + + $this->template->group = $group; + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $upload = $_FILES["blob"]; + if(isset($upload) && file_exists($upload["tmp_name"])) { + if($upload["size"] > self::MAX_AUDIO_SIZE) + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large"), null, $isAjax); + } else { + $err = !isset($upload) ? 65536 : $upload["error"]; + $err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT); + $readableError = tr("error_generic"); + + switch($upload["error"]) { + default: + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $readableError = tr("file_too_big"); + break; + case UPLOAD_ERR_PARTIAL: + $readableError = tr("file_loaded_partially"); + break; + case UPLOAD_ERR_NO_FILE: + $readableError = tr("file_not_uploaded"); + break; + case UPLOAD_ERR_NO_TMP_DIR: + $readableError = "Missing a temporary folder."; + break; + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_EXTENSION: + $readableError = "Failed to write file to disk. "; + break; + } + + $this->flashFail("err", tr("error"), $readableError . " " . tr("error_code", $err), null, $isAjax); + } + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "Other" : $this->postParam("genre"); + $nsfw = ($this->postParam("explicit") ?? "off") === "on"; + $is_unlisted = ($this->postParam("unlisted") ?? "off") === "on"; + + if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, $isAjax); + + $audio = new Audio; + $audio->setOwner($this->user->id); + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + $audio->setUnlisted($is_unlisted); + + try { + $audio->setFile($upload); + } catch(\DomainException $ex) { + $e = $ex->getMessage(); + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large") . " $e.", null, $isAjax); + } catch(\RuntimeException $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_timeout"), null, $isAjax); + } catch(\BadMethodCallException $ex) { + $this->flashFail("err", tr("error"), "хз", null, $isAjax); + } catch(\Exception $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax); + } + + $audio->save(); + + if($playlist) { + $playlist->add($audio); + } else { + $audio->add($group ?? $this->user->identity); + } + + if(!$isAjax) + $this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId()); + else { + $redirectLink = "/audios"; + + if(!is_null($group)) + $redirectLink .= $group->getRealId(); + else + $redirectLink .= $this->user->id; + + if($playlist) + $redirectLink = "/playlist" . $playlist->getPrettyId(); + + $this->returnJson([ + "success" => true, + "redirect_link" => $redirectLink, + ]); + } + } + + function renderAloneAudio(int $owner_id, int $audio_id): void + { + $this->assertUserLoggedIn(); + + $found_audio = $this->audios->get($audio_id); + if(!$found_audio || $found_audio->isDeleted() || !$found_audio->canBeViewedBy($this->user->identity)) { + $this->notFound(); + } + + $this->template->alone_audio = $found_audio; + $this->renderList(NULL, 'alone_audio'); + } + + function renderListen(int $id): void + { + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->assertNoCSRF(); + + if(is_null($this->user)) + $this->returnJson(["success" => false]); + + $audio = $this->audios->get($id); + + if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) { + if(!empty($this->postParam("playlist"))) { + $playlist = (new Audios)->getPlaylist((int)$this->postParam("playlist")); + + if(!$playlist || $playlist->isDeleted() || !$playlist->canBeViewedBy($this->user->identity) || !$playlist->hasAudio($audio)) + $playlist = NULL; + } + + $listen = $audio->listen($this->user->identity, $playlist); + + $returnArr = ["success" => $listen]; + + if($playlist) + $returnArr["new_playlists_listens"] = $playlist->getListens(); + + $this->returnJson($returnArr); + } + + $this->returnJson(["success" => false]); + } else { + $this->redirect("/"); + } + } + + function renderSearch(): void + { + $this->redirect("/search?section=audios"); + } + + function renderNewPlaylist(): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + $owner = $this->user->id; + + if ($this->requestParam("gid")) { + $club = (new Clubs)->get((int) abs((int)$this->requestParam("gid"))); + if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) + $this->redirect("/audios" . $this->user->id); + + $owner = ($club->getId() * -1); + + $this->template->club = $club; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $is_unlisted = (int)$this->postParam('is_unlisted'); + $is_ajax = (int)$this->postParam('ajax') == 1; + $audios = array_slice(explode(",", $this->postParam("audios")), 0, 1000); + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name"), NULL, $is_ajax); + + $playlist = new Playlist; + $playlist->setOwner($owner); + $playlist->setName(substr($title, 0, 125)); + $playlist->setDescription(substr($description, 0, 2045)); + if($is_unlisted == 1) + $playlist->setUnlisted(true); + + if($_FILES["cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo"), NULL, $is_ajax); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo"), NULL, $is_ajax); + } + } + + $playlist->save(); + + foreach($audios as $audio) { + $audio = $this->audios->get((int)$audio); + if(!$audio || $audio->isDeleted()) + continue; + + $playlist->add($audio); + } + + $playlist->bookmark(isset($club) ? $club : $this->user->identity); + if($is_ajax) { + $this->returnJson([ + 'success' => true, + 'redirect' => '/playlist' . $owner . "_" . $playlist->getId() + ]); + } + $this->redirect("/playlist" . $owner . "_" . $playlist->getId()); + } else { + $this->template->owner = $owner; + } + } + + function renderPlaylistAction(int $id) { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $playlist = $this->audios->getPlaylist($id); + + if(!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "error", tr("invalid_playlist"), null, true); + + switch ($this->queryParam("act")) { + case "bookmark": + if(!$playlist->isBookmarkedBy($this->user->identity)) + $playlist->bookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_already_bookmarked"), null, true); + + break; + case "unbookmark": + if($playlist->isBookmarkedBy($this->user->identity)) + $playlist->unbookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_not_bookmarked"), null, true); + + break; + case "delete": + if($playlist->canBeModifiedBy($this->user->identity)) { + $tmOwner = $playlist->getOwner(); + $playlist->delete(); + } else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $this->returnJson(["success" => true, "id" => $tmOwner->getRealId()]); + break; + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderEditPlaylist(int $owner_id, int $virtual_id) + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + if (!$playlist || $playlist->isDeleted() || !$playlist->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + $this->template->playlist = $playlist; + + $audios = iterator_to_array($playlist->fetch(1, $playlist->size())); + $this->template->audios = array_slice($audios, 0, 1000); + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $is_ajax = (int)$this->postParam('ajax') == 1; + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $is_unlisted = (int)$this->postParam('is_unlisted'); + $new_audios = !empty($this->postParam("audios")) ? explode(",", rtrim($this->postParam("audios"), ",")) : []; + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + + $playlist->setName(ovk_proc_strtr($title, 125)); + $playlist->setDescription(ovk_proc_strtr($description, 2045)); + $playlist->setEdited(time()); + $playlist->resetLength(); + $playlist->setUnlisted((bool)$is_unlisted); + + if($_FILES["cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo")); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + DatabaseConnection::i()->getContext()->table("playlist_relations")->where([ + "collection" => $playlist->getId() + ])->delete(); + + foreach ($new_audios as $new_audio) { + $audio = (new Audios)->get((int)$new_audio); + if(!$audio || $audio->isDeleted()) + continue; + + $playlist->add($audio); + } + + if($is_ajax) { + $this->returnJson([ + 'success' => true, + 'redirect' => '/playlist' . $playlist->getPrettyId() + ]); + } + $this->redirect("/playlist".$playlist->getPrettyId()); + } + + function renderPlaylist(int $owner_id, int $virtual_id): void + { + $this->assertUserLoggedIn(); + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int)($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted()) + $this->notFound(); + + $this->template->playlist = $playlist; + $this->template->page = $page; + $this->template->cover = $playlist->getCoverPhoto(); + $this->template->cover_url = $this->template->cover ? $this->template->cover->getURL() : "/assets/packages/static/openvk/img/song.jpg"; + $this->template->audios = iterator_to_array($playlist->fetch($page, 10)); + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->isBookmarked = $this->user->identity && $playlist->isBookmarkedBy($this->user->identity); + $this->template->isMy = $this->user->identity && $playlist->getOwner()->getId() === $this->user->id; + $this->template->canEdit = $this->user->identity && $playlist->canBeModifiedBy($this->user->identity); + $this->template->count = $playlist->size(); + } + + function renderAction(int $audio_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $audio = $this->audios->get($audio_id); + + if(!$audio || $audio->isDeleted()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + switch ($this->queryParam("act")) { + case "add": + if($audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if(!$audio->isInLibraryOf($this->user->identity)) + $audio->add($this->user->identity); + else + $this->flashFail("err", "error", tr("do_have_audio"), null, true); + + break; + + case "remove": + if($audio->isInLibraryOf($this->user->identity)) + $audio->remove($this->user->identity); + else + $this->flashFail("err", "error", tr("do_not_have_audio"), null, true); + + break; + case "remove_club": + $club = (new Clubs)->get((int)$this->postParam("club")); + + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + if($audio->isInLibraryOf($club)) + $audio->remove($club); + else + $this->flashFail("err", "error", tr("group_hasnt_audio"), null, true); + + break; + case "add_to_club": + $detailed = []; + if($audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if(empty($this->postParam("clubs"))) + $this->flashFail("err", "error", 'clubs not passed', null, true); + + $clubs_arr = explode(',', $this->postParam("clubs")); + $count = sizeof($clubs_arr); + if($count < 1 || $count > 10) { + $this->flashFail("err", "error", tr('too_many_or_to_lack'), null, true); + } + + foreach($clubs_arr as $club_id) { + $club = (new Clubs)->get((int)$club_id); + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + continue; + + if(!$audio->isInLibraryOf($club)) { + $detailed[$club_id] = true; + $audio->add($club); + } else { + $detailed[$club_id] = false; + continue; + } + } + + $this->returnJson(["success" => true, 'detailed' => $detailed]); + break; + case "add_to_playlist": + $detailed = []; + if($audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if(empty($this->postParam("playlists"))) + $this->flashFail("err", "error", 'playlists not passed', null, true); + + $playlists_arr = explode(',', $this->postParam("playlists")); + $count = sizeof($playlists_arr); + if($count < 1 || $count > 10) { + $this->flashFail("err", "error", tr('too_many_or_to_lack'), null, true); + } + + foreach($playlists_arr as $playlist_id) { + $pid = explode('_', $playlist_id); + $playlist = (new Audios)->getPlaylistByOwnerAndVID((int)$pid[0], (int)$pid[1]); + if(!$playlist || !$playlist->canBeModifiedBy($this->user->identity)) + continue; + + if(!$playlist->hasAudio($audio)) { + $playlist->add($audio); + $detailed[$playlist_id] = true; + } else { + $detailed[$playlist_id] = false; + continue; + } + } + + $this->returnJson(["success" => true, 'detailed' => $detailed]); + break; + case "delete": + if($audio->canBeModifiedBy($this->user->identity)) + $audio->delete(); + else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + break; + case "edit": + $audio = $this->audios->get($audio_id); + if (!$audio || $audio->isDeleted() || $audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if ($audio->getOwner()->getId() !== $this->user->id) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre"); + $nsfw = (int)($this->postParam("explicit") ?? 0) === 1; + $unlisted = (int)($this->postParam("unlisted") ?? 0) === 1; + if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, true); + + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + $audio->setSearchability($unlisted); + $audio->setEdited(time()); + $audio->save(); + + $this->returnJson(["success" => true, "new_info" => [ + "name" => ovk_proc_strtr($audio->getTitle(), 40), + "performer" => ovk_proc_strtr($audio->getPerformer(), 40), + "lyrics" => nl2br($audio->getLyrics() ?? ""), + "lyrics_unformatted" => $audio->getLyrics() ?? "", + "explicit" => $audio->isExplicit(), + "genre" => $audio->getGenre(), + "unlisted" => $audio->isUnlisted(), + ]]); + break; + + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderPlaylists(int $owner) + { + $this->renderList($owner, "playlists"); + } + + function renderApiGetContext() + { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $ctx_type = $this->postParam("context"); + $ctx_id = (int)($this->postParam("context_entity")); + $page = (int)($this->postParam("page") ?? 1); + $perPage = 10; + + switch($ctx_type) { + default: + case "entity_audios": + if($ctx_id >= 0) { + $entity = $ctx_id != 0 ? (new Users)->get($ctx_id) : $this->user->identity; + + if(!$entity || !$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByUser($entity, $page, $perPage); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } else { + $entity = (new Clubs)->get(abs($ctx_id)); + + if(!$entity || $entity->isBanned()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByClub($entity, $page, $perPage); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } + break; + case "new_audios": + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + break; + case "popular_audios": + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + break; + case "playlist_context": + $playlist = $this->audios->getPlaylist($ctx_id); + + if (!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $playlist->fetch($page, 10); + $audiosCount = $playlist->size(); + break; + case "search_context": + $stream = $this->audios->search($this->postParam("query"), 2, $this->postParam("type") === "by_performer"); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + break; + case "classic_search_context": + $data = json_decode($this->postParam("context_entity"), true); + + $params = []; + $order = [ + "type" => $data['order'] ?? 'id', + "invert" => (int)$data['invert'] == 1 ? true : false + ]; + + if($data['genre'] && $data['genre'] != 'any') + $params['genre'] = $data['genre']; + + if($data['only_performers'] && (int)$data['only_performers'] == 1) + $params['only_performers'] = '1'; + + if($data['with_lyrics'] && (int)$data['with_lyrics'] == 1) + $params['with_lyrics'] = '1'; + + $stream = $this->audios->find($data['query'], $params, $order); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + break; + case 'alone_audio': + $found_audio = $this->audios->get($ctx_id); + if(!$found_audio || $found_audio->isDeleted() || !$found_audio->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", "Error", "Not found", 89, true); + } + + $audios = [$found_audio]; + $audiosCount = 1; + break; + } + + $pagesCount = ceil($audiosCount / $perPage); + + # костылёк для получения плееров в пикере аудиозаписей + if((int)($this->postParam("returnPlayers")) === 1) { + $this->template->audios = $audios; + $this->template->page = $page; + $this->template->pagesCount = $pagesCount; + $this->template->count = $audiosCount; + + return 0; + } + + $audiosArr = []; + + foreach($audios as $audio) { + $output_array = []; + $output_array['id'] = $audio->getId(); + $output_array['name'] = $audio->getTitle(); + $output_array['performer'] = $audio->getPerformer(); + + if(!$audio->isWithdrawn()) { + $output_array['keys'] = $audio->getKeys(); + $output_array['url'] = $audio->getUrl(); + } + + $output_array['length'] = $audio->getLength(); + $output_array['available'] = $audio->isAvailable(); + $output_array['withdrawn'] = $audio->isWithdrawn(); + + $audiosArr[] = $output_array; + } + + $resultArr = [ + "success" => true, + "page" => $page, + "perPage" => $perPage, + "pagesCount" => $pagesCount, + "count" => $audiosCount, + "items" => $audiosArr, + ]; + + $this->returnJson($resultArr); + } +} diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php index 2f1789008..6db7758c7 100644 --- a/Web/Presenters/AuthPresenter.php +++ b/Web/Presenters/AuthPresenter.php @@ -1,7 +1,7 @@ setFirst_Name($this->postParam("first_name")); $user->setLast_Name($this->postParam("last_name")); - $user->setSex((int)($this->postParam("sex") === "female")); + switch ($this->postParam("pronouns")) { + case 'male': + $user->setSex(0); + break; + case 'female': + $user->setSex(1); + break; + case 'neutral': + $user->setSex(2); + break; + } $user->setEmail($this->postParam("email")); $user->setSince(date("Y-m-d H:i:s")); $user->setRegistering_Ip(CONNECTING_IP); @@ -110,7 +120,7 @@ function renderRegister(): void $this->flashFail("err", tr("failed_to_register"), tr("user_already_exists")); $user->setUser($chUser->getId()); - $user->save(); + $user->save(false); if(!is_null($referer)) { $user->toggleSubscription($referer); @@ -131,6 +141,7 @@ function renderRegister(): void $this->authenticator->authenticate($chUser->getId()); $this->redirect("/id" . $user->getId()); + $user->save(); } } @@ -217,6 +228,7 @@ function renderFinishRestoringPassword(): void return; } + $this->template->disable_ajax = 1; $this->template->is2faEnabled = $request->getUser()->is2faEnabled(); if($_SERVER["REQUEST_METHOD"] === "POST") { @@ -345,9 +357,16 @@ function renderUnbanThemself(): void $this->flashFail("err", tr("error"), tr("forbidden")); $user = $this->users->get($this->user->id); + $ban = (new Bans)->get((int)$user->getRawBanReason()); + if (!$ban || $ban->isOver() || $ban->isPermanent()) + $this->flashFail("err", tr("error"), tr("forbidden")); + + $ban->setRemoved_Manually(2); + $ban->setRemoved_By($this->user->identity->getId()); + $ban->save(); $user->setBlock_Reason(NULL); - $user->setUnblock_Time(NULL); + // $user->setUnblock_Time(NULL); $user->save(); $this->flashFail("succ", tr("banned_unban_title"), tr("banned_unban_description")); diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 970b8f198..5987281d0 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -3,6 +3,8 @@ final class BlobPresenter extends OpenVKPresenter { + protected $banTolerant = true; + private function getDirName($dir): string { if(gettype($dir) === "integer") { @@ -16,6 +18,8 @@ private function getDirName($dir): string function renderFile(/*string*/ $dir, string $name, string $format) { + header("Access-Control-Allow-Origin: *"); + $dir = $this->getDirName($dir); $base = realpath(OPENVK_ROOT . "/storage/$dir"); $path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format"); @@ -35,5 +39,5 @@ function renderFile(/*string*/ $dir, string $name, string $format) readfile($path); exit; - } + } } diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index dad79ac42..7ab74ac09 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post}; use openvk\Web\Models\Entities\Notifications\CommentNotification; -use openvk\Web\Models\Repositories\{Comments, Clubs}; +use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos, Audios}; final class CommentPresenter extends OpenVKPresenter { @@ -22,9 +22,17 @@ function renderLike(int $id): void $comment = (new Comments)->get($id); if(!$comment || $comment->isDeleted()) $this->notFound(); + + if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); if(!is_null($this->user)) $comment->toggleLike($this->user->identity); - + if($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } + $this->redirect($_SERVER["HTTP_REFERER"]); } @@ -40,6 +48,10 @@ function renderMakeComment(string $repo, int $eId): void $entity = $repo->get($eId); if(!$entity) $this->notFound(); + if(!$entity->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + if($entity instanceof Topic && $entity->isClosed()) $this->notFound(); @@ -48,9 +60,9 @@ function renderMakeComment(string $repo, int $eId): void else if($entity instanceof Topic) $club = $entity->getClub(); - if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) - $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); - + if ($entity instanceof Post && $entity->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + $flags = 0; if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) $flags |= 0b10000000; @@ -60,31 +72,28 @@ function renderMakeComment(string $repo, int $eId): void try { $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"]); } catch(ISE $ex) { - $this->flashFail("err", "Не удалось опубликовать пост", "Файл изображения повреждён, слишком велик или одна сторона изображения в разы больше другой."); + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_when_publishing_comment_description")); } } - - # TODO move to trait - try { - $photo = NULL; - $video = NULL; - if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { - $album = NULL; - if($wall > 0 && $wall === $this->user->id) - $album = (new Albums)->getUserWallAlbum($wallOwner); - - $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album); + + $horizontal_attachments = []; + $vertical_attachments = []; + if(!empty($this->postParam("horizontal_attachments"))) { + $horizontal_attachments_array = array_slice(explode(",", $this->postParam("horizontal_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if(sizeof($horizontal_attachments_array) > 0) { + $horizontal_attachments = parseAttachments($horizontal_attachments_array, ['photo', 'video']); } - - if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { - $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]); + } + + if(!empty($this->postParam("vertical_attachments"))) { + $vertical_attachments_array = array_slice(explode(",", $this->postParam("vertical_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if(sizeof($vertical_attachments_array) > 0) { + $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note']); } - } catch(ISE $ex) { - $this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); } - if(empty($this->postParam("text")) && !$photo && !$video) - $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий пустой или слишком большой."); + if(empty($this->postParam("text")) && sizeof($horizontal_attachments) < 1 && sizeof($vertical_attachments) < 1) + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); try { $comment = new Comment; @@ -96,14 +105,24 @@ function renderMakeComment(string $repo, int $eId): void $comment->setFlags($flags); $comment->save(); } catch (\LengthException $ex) { - $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой."); + $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big")); } - if(!is_null($photo)) - $comment->attach($photo); - - if(!is_null($video)) - $comment->attach($video); + foreach($horizontal_attachments as $horizontal_attachment) { + if(!$horizontal_attachment || $horizontal_attachment->isDeleted() || !$horizontal_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $comment->attach($horizontal_attachment); + } + + foreach($vertical_attachments as $vertical_attachment) { + if(!$vertical_attachment || $vertical_attachment->isDeleted() || !$vertical_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $comment->attach($vertical_attachment); + } if($entity->getOwner()->getId() !== $this->user->identity->getId()) if(($owner = $entity->getOwner()) instanceof User) @@ -118,7 +137,7 @@ function renderMakeComment(string $repo, int $eId): void if($mentionee instanceof User) (new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit(); - $this->flashFail("succ", "Комментарий добавлен", "Ваш комментарий появится на странице."); + $this->flashFail("succ", tr("comment_is_added"), tr("comment_is_added_desc")); } function renderDeleteComment(int $id): void @@ -129,13 +148,15 @@ function renderDeleteComment(int $id): void $comment = (new Comments)->get($id); if(!$comment) $this->notFound(); if(!$comment->canBeDeletedBy($this->user->identity)) - $this->throwError(403, "Forbidden", "У вас недостаточно прав чтобы редактировать этот ресурс."); - + $this->throwError(403, "Forbidden", tr("error_access_denied")); + if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + $comment->delete(); $this->flashFail( "succ", - "Успешно", - "Этот комментарий больше не будет показыватся.
Отметить как спам?" + tr("success"), + tr("comment_will_not_appear") ); } } diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php index 8f59bdcb0..6e85a29d3 100644 --- a/Web/Presenters/GiftsPresenter.php +++ b/Web/Presenters/GiftsPresenter.php @@ -20,9 +20,12 @@ function renderUserGifts(int $user): void $this->assertUserLoggedIn(); $user = $this->users->get($user); - if(!$user) + if(!$user || $user->isDeleted()) $this->notFound(); + if(!$user->canBeViewedBy($this->user->identity ?? NULL)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + $this->template->user = $user; $this->template->page = $page = (int) ($this->queryParam("p") ?? 1); $this->template->count = $user->getGiftCount(); @@ -41,6 +44,7 @@ function renderGiftMenu(): void $this->template->user = $user; $this->template->iterator = $cats; + $this->template->count = $this->gifts->getCategoriesCount(); $this->template->_template = "Gifts/Menu.xml"; } @@ -49,7 +53,10 @@ function renderGiftList(): void $user = $this->users->get((int) ($this->queryParam("user") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); if(!$user || !$cat) - $this->flashFail("err", "Не удалось подарить", "Пользователь или набор не существуют."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_user_not_exists")); + + if(!$user->canBeViewedBy($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); $this->template->page = $page = (int) ($this->queryParam("p") ?? 1); $gifts = $cat->getGifts($page, null, $this->template->count); @@ -66,14 +73,17 @@ function renderConfirmGift(): void $gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); if(!$user || !$cat || !$gift || !$cat->hasGift($gift)) - $this->flashFail("err", "Не удалось подарить", "Не удалось подтвердить права на подарок."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_no_rights_gifts")); if(!$gift->canUse($this->user->identity)) - $this->flashFail("err", "Не удалось подарить", "У вас больше не осталось таких подарков."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_no_more_gifts")); + if(!$user->canBeViewedBy($this->user->identity ?? NULL)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + $coinsLeft = $this->user->identity->getCoins() - $gift->getPrice(); if($coinsLeft < 0) - $this->flashFail("err", "Не удалось подарить", "Ору нищ не пук."); + $this->flashFail("err", tr("error_when_gifting"), tr("error_no_money")); $this->template->_template = "Gifts/Confirm.xml"; if($_SERVER["REQUEST_METHOD"] !== "POST") { @@ -91,7 +101,7 @@ function renderConfirmGift(): void $user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous"))); $gift->used(); - $this->flash("succ", "Подарок отправлен", "Вы отправили подарок " . $user->getFirstName() . " за " . $gift->getPrice() . " голосов."); + $this->flash("succ", tr("gift_sent"), tr("gift_sent_desc", $user->getFirstName(), $gift->getPrice())); $this->redirect($user->getURL()); } diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index 4f671df35..61b6d67ce 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -3,7 +3,7 @@ use openvk\Web\Models\Entities\{Club, Photo, Post}; use Nette\InvalidStateException; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios, Posts}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -24,12 +24,26 @@ function renderView(int $id): void if(!$club) { $this->notFound(); } else { - $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); - $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); - $this->template->topics = (new Topics)->getLastTopics($club, 3); - $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + if ($club->isBanned()) { + $this->template->_template = "Group/Banned.xml"; + } else { + $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); + $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); + $this->template->topics = (new Topics)->getLastTopics($club, 3); + $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId()); + $this->template->audiosCount = (new Audios)->getClubCollectionSize($club); + } + + if(!is_null($this->user->identity) && $club->getWallType() == 2) { + if(!$club->canBeModifiedBy($this->user->identity)) + $this->template->suggestedPostsCountByUser = (new Posts)->getSuggestedPostsCountByUser($club->getId(), $this->user->id); + else + $this->template->suggestedPostsCountByEveryone = (new Posts)->getSuggestedPostsCount($club->getId()); + } $this->template->club = $club; + $this->template->ignore_status = $club->isIgnoredBy($this->user->identity); } } @@ -39,7 +53,7 @@ function renderCreate(): void $this->willExecuteWriteAction(); if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!empty($this->postParam("name"))) + if(!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) { $club = new Club; $club->setName($this->postParam("name")); @@ -50,7 +64,7 @@ function renderCreate(): void $club->save(); } catch(\PDOException $ex) { if($ex->getCode() == 23000) - $this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору."); + $this->flashFail("err", tr("error"), tr("error_on_server_side")); else throw $ex; } @@ -58,7 +72,7 @@ function renderCreate(): void $club->toggleSubscription($this->user->identity); $this->redirect("/club" . $club->getId()); }else{ - $this->flashFail("err", "Ошибка", "Вы не ввели название группы."); + $this->flashFail("err", tr("error"), tr("error_no_group_name")); } } } @@ -72,6 +86,7 @@ function renderSub(): void $club = $this->clubs->get((int) $this->postParam("id")); if(!$club) exit("Invalid state"); + if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); $club->toggleSubscription($this->user->identity); @@ -83,6 +98,8 @@ function renderFollowers(int $id): void $this->assertUserLoggedIn(); $this->template->club = $this->clubs->get($id); + if ($this->template->club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); + $this->template->onlyShowManagers = $this->queryParam("onlyAdmins") == "1"; if($this->template->onlyShowManagers) { $this->template->followers = NULL; @@ -118,12 +135,14 @@ function renderModifyAdmin(int $id): void $this->badRequest(); $club = $this->clubs->get($id); + if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); + $user = (new Users)->get((int) $user); if(!$user || !$club) $this->notFound(); if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) - $this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); if(!is_null($hidden)) { if($club->getOwner()->getId() == $user->getId()) { @@ -141,9 +160,9 @@ function renderModifyAdmin(int $id): void } if($hidden) { - $this->flashFail("succ", "Операция успешна", "Теперь " . $user->getCanonicalName() . " будет показываться как обычный подписчик всем кроме других администраторов"); + $this->flashFail("succ", tr("success_action"), tr("x_is_now_hidden", $user->getCanonicalName())); } else { - $this->flashFail("succ", "Операция успешна", "Теперь все будут знать про то что " . $user->getCanonicalName() . " - администратор"); + $this->flashFail("succ", tr("success_action"), tr("x_is_now_showed", $user->getCanonicalName())); } } elseif($removeComment) { if($club->getOwner()->getId() == $user->getId()) { @@ -155,11 +174,11 @@ function renderModifyAdmin(int $id): void $manager->save(); } - $this->flashFail("succ", "Операция успешна", "Комментарий к администратору удален"); + $this->flashFail("succ", tr("success_action"), tr("comment_is_deleted")); } elseif($comment) { if(mb_strlen($comment) > 36) { $commentLength = (string) mb_strlen($comment); - $this->flashFail("err", "Ошибка", "Комментарий слишком длинный ($commentLength символов вместо 36 символов)"); + $this->flashFail("err", tr("error"), tr("comment_is_too_long", $commentLength)); } if($club->getOwner()->getId() == $user->getId()) { @@ -171,16 +190,16 @@ function renderModifyAdmin(int $id): void $manager->save(); } - $this->flashFail("succ", "Операция успешна", "Комментарий к администратору изменён"); + $this->flashFail("succ", tr("success_action"), tr("comment_is_changed")); }else{ if($club->canBeModifiedBy($user)) { $club->removeManager($user); - $this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " более не администратор."); + $this->flashFail("succ", tr("success_action"), tr("x_no_more_admin", $user->getCanonicalName())); } else { $club->addManager($user); (new ClubModeratorNotification($user, $club, $this->user->identity))->emit(); - $this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " назначен(а) администратором."); + $this->flashFail("succ", tr("success_action"), tr("x_is_admin", $user->getCanonicalName())); } } @@ -194,6 +213,8 @@ function renderEdit(int $id): void $club = $this->clubs->get($id); if(!$club || !$club->canBeModifiedBy($this->user->identity)) $this->notFound(); + else if ($club->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); else $this->template->club = $club; @@ -201,13 +222,22 @@ function renderEdit(int $id): void if(!$club->setShortcode( empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode") )) $this->flashFail("err", tr("error"), tr("error_shorturl_incorrect")); - $club->setName(empty($this->postParam("name")) ? $club->getName() : $this->postParam("name")); + $club->setName((empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $club->getName() : $this->postParam("name")); $club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about")); - $club->setWall(empty($this->postParam("wall")) ? 0 : 1); + try { + $club->setWall(empty($this->postParam("wall")) ? 0 : (int)$this->postParam("wall")); + } catch(\Exception $e) { + $this->flashFail("err", tr("error"), tr("error_invalid_wall_value")); + } + $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1); - $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1); + $club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1); + + if (!$club->isHidingFromGlobalFeedEnforced()) { + $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed") ? 0 : 1)); + } $website = $this->postParam("website") ?? ""; if(empty($website)) @@ -234,7 +264,7 @@ function renderEdit(int $id): void (new Albums)->getClubAvatarAlbum($club)->addPhoto($photo); } catch(ISE $ex) { $name = $album->getName(); - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию."); + $this->flashFail("err", tr("error"), tr("error_when_uploading_photo")); } } @@ -242,58 +272,110 @@ function renderEdit(int $id): void $club->save(); } catch(\PDOException $ex) { if($ex->getCode() == 23000) - $this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору."); + $this->flashFail("err", tr("error"), tr("error_on_server_side")); else throw $ex; } - $this->flash("succ", "Изменения сохранены", "Новые данные появятся в вашей группе."); + $this->flash("succ", tr("changes_saved"), tr("new_changes_desc")); } } function renderSetAvatar(int $id) { - $photo = new Photo; + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $club = $this->clubs->get($id); - if($_SERVER["REQUEST_METHOD"] === "POST" && $_FILES["ava"]["error"] === UPLOAD_ERR_OK) { + + if(!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", tr("error"), tr("forbidden"), NULL, true); + + if($_SERVER["REQUEST_METHOD"] === "POST" && $_FILES["blob"]["error"] === UPLOAD_ERR_OK) { try { + $photo = new Photo; + $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; if($anon && $this->user->id === $club->getOwner()->getId()) $anon = $club->isOwnerHidden(); else if($anon) $anon = $club->getManager($this->user->identity)->isHidden(); + $photo->setOwner($this->user->id); $photo->setDescription("Club image"); - $photo->setFile($_FILES["ava"]); + $photo->setFile($_FILES["blob"]); $photo->setCreated(time()); $photo->setAnonymous($anon); $photo->save(); (new Albums)->getClubAvatarAlbum($club)->addPhoto($photo); - $flags = 0; - $flags |= 0b00010000; - $flags |= 0b10000000; - - $post = new Post; - $post->setOwner($this->user->id); - $post->setWall($club->getId()*-1); - $post->setCreated(time()); - $post->setContent(""); - $post->setFlags($flags); - $post->save(); - $post->attach($photo); - - } catch(ISE $ex) { - $name = $album->getName(); - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию."); + if($this->postParam("on_wall") == 1) { + $post = new Post; + + $post->setOwner($this->user->id); + $post->setWall($club->getId() * -1); + $post->setCreated(time()); + $post->setContent(""); + + $flags = 0; + $flags |= 0b00010000; + $flags |= 0b10000000; + + $post->setFlags($flags); + $post->save(); + + $post->attach($photo); + } + + } catch(\Throwable $ex) { + $this->flashFail("err", tr("error"), tr("error_when_uploading_photo"), NULL, true); } + + $this->returnJson([ + "success" => true, + "new_photo" => $photo->getPrettyId(), + "url" => $photo->getURL(), + ]); + } else { + return " "; } - $this->returnJson([ - "url" => $photo->getURL(), - "id" => $photo->getPrettyId() - ]); } + + function renderDeleteAvatar(int $id) { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $club = $this->clubs->get($id); + + if(!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", tr("error"), tr("forbidden"), NULL, true); + + $avatar = $club->getAvatarPhoto(); + + if(!$avatar) + $this->flashFail("succ", tr("error"), "no avatar bro", NULL, true); + + $avatar->isolate(); + + $newAvatar = $club->getAvatarPhoto(); + + if(!$newAvatar) + $this->returnJson([ + "success" => true, + "has_new_photo" => false, + "new_photo" => NULL, + "url" => "/assets/packages/static/openvk/img/camera_200.png", + ]); + else + $this->returnJson([ + "success" => true, + "has_new_photo" => true, + "new_photo" => $newAvatar->getPrettyId(), + "url" => $newAvatar->getURL(), + ]); + } + function renderEditBackdrop(int $id): void { $this->assertUserLoggedIn(); @@ -338,11 +420,13 @@ function renderStatistics(int $id): void $this->assertUserLoggedIn(); if(!eventdb()) - $this->flashFail("err", "Ошибка подключения", "Не удалось подключится к службе телеметрии."); + $this->flashFail("err", tr("connection_error"), tr("connection_error_desc")); $club = $this->clubs->get($id); if(!$club->canBeModifiedBy($this->user->identity)) $this->notFound(); + else if ($club->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); else $this->template->club = $club; @@ -375,6 +459,7 @@ function renderChangeOwner(int $id, int $newOwnerId): void $this->flashFail("err", tr("error"), tr("incorrect_password")); $club = $this->clubs->get($id); + if ($club->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); $newOwner = (new Users)->get($newOwnerId); if($this->user->id !== $club->getOwner()->getId()) $this->flashFail("err", tr("error"), tr("forbidden")); @@ -396,4 +481,37 @@ function renderChangeOwner(int $id, int $newOwnerId): void $this->flashFail("succ", tr("information_-1"), tr("group_owner_setted", $newOwner->getCanonicalName(), $club->getName())); } + + function renderSuggested(int $id): void + { + $this->assertUserLoggedIn(); + + $club = $this->clubs->get($id); + if(!$club) + $this->notFound(); + else + $this->template->club = $club; + + if($club->getWallType() == 0) { + $this->flash("err", tr("error_suggestions"), tr("error_suggestions_closed")); + $this->redirect("/club".$club->getId()); + } + + if($club->getWallType() == 1) { + $this->flash("err", tr("error_suggestions"), tr("error_suggestions_open")); + $this->redirect("/club".$club->getId()); + } + + if(!$club->canBeModifiedBy($this->user->identity)) { + $this->template->posts = iterator_to_array((new Posts)->getSuggestedPostsByUser($club->getId(), $this->user->id, (int) ($this->queryParam("p") ?? 1))); + $this->template->count = (new Posts)->getSuggestedPostsCountByUser($club->getId(), $this->user->id); + $this->template->type = "my"; + } else { + $this->template->posts = iterator_to_array((new Posts)->getSuggestedPosts($club->getId(), (int) ($this->queryParam("p") ?? 1))); + $this->template->count = (new Posts)->getSuggestedPostsCount($club->getId()); + $this->template->type = "everyone"; + } + + $this->template->page = (int) ($this->queryParam("p") ?? 1); + } } diff --git a/Web/Presenters/InternalAPIPresenter.php b/Web/Presenters/InternalAPIPresenter.php index 1a1076591..725465068 100644 --- a/Web/Presenters/InternalAPIPresenter.php +++ b/Web/Presenters/InternalAPIPresenter.php @@ -1,5 +1,6 @@ postParam("parentType", false) == "post") { + $post = (new Posts)->getPostById($owner_id, $post_id, true); + } else { + $post = (new Comments)->get($post_id); + } + + + if(is_null($post)) { + $this->returnJson([ + "success" => 0 + ]); + } else { + $response = []; + $attachments = $post->getChildren(); + foreach($attachments as $attachment) + { + if($attachment instanceof \openvk\Web\Models\Entities\Photo) + { + $response[$attachment->getPrettyId()] = [ + "url" => $attachment->getURLBySizeId('larger'), + "id" => $attachment->getPrettyId(), + ]; + } + } + $this->returnJson([ + "success" => 1, + "body" => $response + ]); + } + } + + function renderGetPostTemplate(int $owner_id, int $post_id) { + if($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + exit("ты‍ не по адресу"); + } + + $type = $this->queryParam("type", false); + if($type == "post") { + $post = (new Posts)->getPostById($owner_id, $post_id, true); + } else { + $post = (new Comments)->get($post_id); + } + + if(!$post || !$post->canBeEditedBy($this->user->identity)) { + exit(''); + } + + header("Content-Type: text/plain"); + + if($type == 'post') { + $this->template->_template = 'components/post.xml'; + $this->template->post = $post; + $this->template->commentSection = false; + } elseif($type == 'comment') { + $this->template->_template = 'components/comment.xml'; + $this->template->comment = $post; + } else { + exit(''); + } + } } diff --git a/Web/Presenters/MessengerPresenter.php b/Web/Presenters/MessengerPresenter.php index d5ffb9887..cec99cff5 100644 --- a/Web/Presenters/MessengerPresenter.php +++ b/Web/Presenters/MessengerPresenter.php @@ -63,6 +63,7 @@ function renderApp(int $sel): void $this->flash("err", tr("warning"), tr("user_may_not_reply")); } + $this->template->disable_ajax = 1; $this->template->selId = $sel; $this->template->correspondent = $correspondent; } @@ -128,7 +129,7 @@ function renderApiGetMessages(int $sel, int $lastMsg): void $messages = []; $correspondence = new Correspondence($this->user->identity, $correspondent); - foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg) as $message) + foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg, NULL, 0) as $message) $messages[] = $message->simplify(); header("Content-Type: application/json"); diff --git a/Web/Presenters/NoSpamPresenter.php b/Web/Presenters/NoSpamPresenter.php new file mode 100644 index 000000000..714cac93c --- /dev/null +++ b/Web/Presenters/NoSpamPresenter.php @@ -0,0 +1,386 @@ +assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $targetDir = __DIR__ . '/../Models/Entities/'; + $mode = in_array($this->queryParam("act"), ["form", "templates", "rollback", "reports"]) ? $this->queryParam("act") : "form"; + + if ($mode === "form") { + $this->template->_template = "NoSpam/Index"; + $this->template->disable_ajax = 1; + $foundClasses = []; + foreach (Finder::findFiles('*.php')->from($targetDir) as $file) { + $content = file_get_contents($file->getPathname()); + $namespacePattern = '/namespace\s+([^\s;]+)/'; + $classPattern = '/class\s+([^\s{]+)/'; + preg_match($namespacePattern, $content, $namespaceMatches); + preg_match($classPattern, $content, $classMatches); + + if (isset($namespaceMatches[1]) && isset($classMatches[1])) { + $classNamespace = trim($namespaceMatches[1]); + $className = trim($classMatches[1]); + $fullClassName = $classNamespace . '\\' . $className; + + if ($classNamespace === NoSpamPresenter::ENTITIES_NAMESPACE && class_exists($fullClassName)) { + $foundClasses[] = $className; + } + } + } + + $models = []; + + foreach ($foundClasses as $class) { + $r = new \ReflectionClass(NoSpamPresenter::ENTITIES_NAMESPACE . "\\$class"); + if (!$r->isAbstract() && $r->getName() !== NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") + $models[] = $class; + } + $this->template->models = $models; + } else if ($mode === "templates") { + $this->template->_template = "NoSpam/Templates.xml"; + $this->template->disable_ajax = 1; + $filter = []; + if ($this->queryParam("id")) { + $filter["id"] = (int)$this->queryParam("id"); + } + $this->template->templates = iterator_to_array((new NoSpamLogs)->getList($filter)); + } else if ($mode === "reports") { + $this->redirect("/scumfeed"); + } else { + $template = (new NoSpamLogs)->get((int)$this->postParam("id")); + if (!$template || $template->isRollbacked()) + $this->returnJson(["success" => false, "error" => "Шаблон не найден"]); + + $model = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $template->getModel(); + $items = $template->getItems(); + if (count($items) > 0) { + $db = DatabaseConnection::i()->getContext(); + + $unbanned_ids = []; + foreach ($items as $_item) { + try { + $item = new $model; + $table_name = $item->getTableName(); + $item = $db->table($table_name)->get((int)$_item); + if (!$item) continue; + + $item = new $model($item); + + if (key_exists("deleted", $item->unwrap()) && $item->isDeleted()) { + $item->setDeleted(0); + $item->save(); + } + + if (in_array($template->getTypeRaw(), [2, 3])) { + $owner = NULL; + $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; + + if (method_exists($item, "ban")) { + $owner = $item; + } else { + foreach ($methods as $method) { + if (method_exists($item, $method)) { + $owner = $item->$method(); + break; + } + } + } + + $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); + + if (!in_array($_id, $unbanned_ids)) { + $owner->unban($this->user->id); + $unbanned_ids[] = $_id; + } + } + } catch (\Throwable $e) { + $this->returnJson(["success" => false, "error" => $e->getMessage()]); + } + } + } else { + $this->returnJson(["success" => false, "error" => "Объекты не найдены"]); + } + + $template->setRollback(true); + $template->save(); + + $this->returnJson(["success" => true]); + } + } + + function renderSearch(): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + $this->assertNoCSRF(); + $this->willExecuteWriteAction(); + + function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, ?string $ip = NULL, ?string $useragent = NULL, ?int $ts = NULL, ?int $te = NULL, $user = NULL) + { + $db = DatabaseConnection::i()->getContext(); + if ($table && ($ip || $useragent || $ts || $te || $user)) { + $conditions = []; + + if ($ip) $conditions[] = "`ip` REGEXP '$ip'"; + if ($useragent) $conditions[] = "`useragent` REGEXP '$useragent'"; + if ($ts) $conditions[] = "`ts` < $ts"; + if ($te) $conditions[] = "`ts` > $te"; + if ($user) { + $users = new Users; + + $_user = $users->getByChandlerUser((new ChandlerUsers)->getById($user)) + ?? $users->get((int)$user) + ?? $users->getByAddress($user) + ?? NULL; + + if ($_user) { + $conditions[] = "`user` = '" . $_user->getChandlerGUID() . "'"; + } + } + + $whereStart = "WHERE `object_table` = '$table'"; + if ($table === "profiles") { + $whereStart .= "AND `type` = 0"; + } + + $conditions = count($conditions) > 0 ? "AND (" . implode(" AND ", $conditions) . ")" : ""; + $response = []; + + if ($conditions) { + $logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`"); + + foreach ($logs as $log) { + $log = (new Logs)->get($log->id); + $object = $log->getObject()->unwrap(); + + if (!$object) continue; + if ($where) { + if (str_starts_with($where, " AND")) { + $where = substr_replace($where, "", 0, strlen(" AND")); + } + + $a = $db->query("SELECT * FROM `$table` WHERE $where")->fetchAll(); + foreach ($a as $o) { + if ($object->id == $o["id"]) { + $response[] = $object; + } + } + + } else { + $response[] = $object; + } + } + } + + return $response; + } + } + + try { + $response = []; + $processed = 0; + + $where = $this->postParam("where"); + $ip = addslashes($this->postParam("ip")); + $useragent = addslashes($this->postParam("useragent")); + $searchTerm = addslashes($this->postParam("q")); + $ts = (int)$this->postParam("ts"); + $te = (int)$this->postParam("te"); + $user = addslashes($this->postParam("user")); + + if ($where) { + $where = explode(";", $where)[0]; + } + + if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user) + $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]); + + $models = explode(",", $this->postParam("models")); + + foreach ($models as $_model) { + $model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $_model; + if (!class_exists($model_name)) { + continue; + } + + $model = new $model_name; + + $c = new \ReflectionClass($model_name); + if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") { + continue; + } + + $db = DatabaseConnection::i()->getContext(); + $table = $model->getTableName(); + $columns = $db->getStructure()->getColumns($table); + + if ($searchTerm) { + $conditions = []; + $need_deleted = false; + foreach ($columns as $column) { + if ($column["name"] == "deleted") { + $need_deleted = true; + } else { + $conditions[] = "`$column[name]` REGEXP '$searchTerm'"; + } + } + $conditions = implode(" OR ", $conditions); + + $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)"); + if ($need_deleted) $where .= " AND (`deleted` = 0)"; + } + + $rows = []; + + if (str_starts_with($where, " AND")) { + if ($searchTerm && !$this->postParam("where")) { + $where = substr_replace($where, "", 0, strlen(" AND")); + } else { + $where = "(" . $this->postParam("where") . ")" . $where; + } + } + + if ($ip || $useragent || $ts || $te || $user) { + $rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user); + } else { + if (!$where) { + $rows = []; + } else { + $result = $db->query("SELECT * FROM `$table` WHERE $where"); + $rows = $result->fetchAll(); + } + } + + if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) { + foreach ($rows as $key => $object) { + $object = (array)$object; + $_obj = []; + foreach ($object as $key => $value) { + foreach ($columns as $column) { + if ($column["name"] === $key && in_array(strtoupper($column["nativetype"]), ["BLOB", "BINARY", "VARBINARY", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB"])) { + $value = "[BINARY]"; + break; + } + } + + $_obj[$key] = $value; + $_obj["__model_name"] = $_model; + } + $response[] = $_obj; + } + } else { + $ids = []; + + foreach ($rows as $object) { + $object = new $model_name($db->table($table)->get($object->id)); + if (!$object) continue; + $ids[] = $object->getId(); + } + + $log = new NoSpamLog; + $log->setUser($this->user->id); + $log->setModel($_model); + if ($searchTerm) { + $log->setRegex($searchTerm); + } else { + $log->setRequest($where); + } + $log->setBan_Type((int)$this->postParam("ban")); + $log->setCount(count($rows)); + $log->setTime(time()); + $log->setItems(implode(",", $ids)); + $log->save(); + + $banned_ids = []; + foreach ($rows as $object) { + $object = new $model_name($db->table($table)->get($object->id)); + if (!$object) continue; + + $owner = NULL; + $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; + + if (method_exists($object, "ban")) { + $owner = $object; + } else { + foreach ($methods as $method) { + if (method_exists($object, $method)) { + $owner = $object->$method(); + break; + } + } + } + + if ($owner instanceof User && $owner->getId() === $this->user->id) { + if (count($rows) === 1) { + $this->returnJson(["success" => false, "error" => "\"Производственная травма\" — Вы не можете блокировать или удалять свой же контент"]); + } else { + continue; + } + } + + if (in_array((int)$this->postParam("ban"), [2, 3])) { + $reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? addslashes($this->postParam("ban_reason")) : ("**content-noSpamTemplate-" . $log->getId() . "**"); + $is_forever = (string)$this->postParam("is_forever") === "true"; + $unban_time = $is_forever ? 0 : (int)$this->postParam("unban_time") ?? NULL; + + if ($owner) { + $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); + if (!in_array($_id, $banned_ids)) { + if ($owner instanceof User) { + if (!$unban_time && !$is_forever) + $unban_time = time() + $owner->getNewBanTime(); + + $owner->ban($reason, false, $unban_time, $this->user->id); + } else { + $owner->ban("Подозрительная активность"); + } + + $banned_ids[] = $_id; + } + } + } + + if (in_array((int)$this->postParam("ban"), [1, 3])) + $object->delete(); + } + + $processed++; + } + } + + $this->returnJson(["success" => true, "processed" => $processed, "count" => count($response), "list" => $response]); + } catch (\Throwable $e) { + $this->returnJson(["success" => false, "error" => $e->getMessage()]); + } + } +} diff --git a/Web/Presenters/NotesPresenter.php b/Web/Presenters/NotesPresenter.php index 50437ad7a..37475013a 100644 --- a/Web/Presenters/NotesPresenter.php +++ b/Web/Presenters/NotesPresenter.php @@ -22,15 +22,10 @@ function renderList(int $owner): void if(!$user->getPrivacyPermission('notes.read', $this->user->identity ?? NULL)) $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - $this->template->notes = $this->notes->getUserNotes($user, (int)($this->queryParam("p") ?? 1)); + $this->template->page = (int)($this->queryParam("p") ?? 1); + $this->template->notes = $this->notes->getUserNotes($user, $this->template->page); $this->template->count = $this->notes->getUserNotesCount($user); $this->template->owner = $user; - $this->template->paginatorConf = (object) [ - "count" => $this->template->count, - "page" => $this->queryParam("p") ?? 1, - "amount" => NULL, - "perPage" => OPENVK_DEFAULT_PER_PAGE, - ]; } function renderView(int $owner, int $note_id): void @@ -40,6 +35,8 @@ function renderView(int $owner, int $note_id): void $this->notFound(); if(!$note->getOwner()->getPrivacyPermission('notes.read', $this->user->identity ?? NULL)) $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + if(!$note->canBeViewedBy($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); $this->template->cCount = $note->getCommentsCount(); $this->template->cPage = (int) ($this->queryParam("p") ?? 1); @@ -107,7 +104,7 @@ function renderEdit(int $owner, int $note_id): void if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted()) $this->notFound(); if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $this->template->note = $note; if($_SERVER["REQUEST_METHOD"] === "POST") { @@ -135,11 +132,11 @@ function renderDelete(int $owner, int $id): void if(!$note) $this->notFound(); if($note->getOwner()->getId() . "_" . $note->getId() !== $owner . "_" . $id || $note->isDeleted()) $this->notFound(); if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $name = $note->getName(); $note->delete(); - $this->flash("succ", "Заметка удалена", "Заметка \"$name\" была успешно удалена."); + $this->flash("succ", tr("note_is_deleted"), tr("note_x_is_now_deleted", $name)); $this->redirect("/notes" . $this->user->id); } } diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php old mode 100755 new mode 100644 index 710713e52..1d03a1c28 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -7,7 +7,7 @@ use Latte\Engine as TemplatingEngine; use openvk\Web\Models\Entities\IP; use openvk\Web\Themes\Themepacks; -use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets}; +use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Reports, CurrentUser}; use WhichBrowser; abstract class OpenVKPresenter extends SimplePresenter @@ -198,6 +198,9 @@ function onStartup(): void { $user = Authenticator::i()->getUser(); + if(!$this->template) + $this->template = new \stdClass; + $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"); @@ -211,6 +214,7 @@ function onStartup(): void $this->user->id = $this->user->identity->getId(); $this->template->thisUser = $this->user->identity; $this->template->userTainted = $user->isTainted(); + CurrentUser::get($this->user->identity, $_SERVER["REMOTE_ADDR"], $_SERVER["HTTP_USER_AGENT"]); if($this->user->identity->isDeleted() && !$this->deactivationTolerant) { if($this->user->identity->isDeactivated()) { @@ -252,15 +256,17 @@ function onStartup(): void $userValidated = 1; $cacheTime = 0; # Force no cache - if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { + if(!property_exists($this, 'silent') && $this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { $this->user->identity->setOnline(time()); $this->user->identity->setClient_name(NULL); - $this->user->identity->save(); + $this->user->identity->save(false); } $this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1); - if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) + if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) { $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); + $this->template->reportNotAnsweredCount = (new Reports)->getReportsCount(0); + } } header("X-OpenVK-User-Validated: $userValidated"); @@ -268,7 +274,7 @@ function onStartup(): void setlocale(LC_TIME, ...(explode(";", tr("__locale")))); if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"]["all"]) { - if (OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$this->presenterName]) { + if ($this->presenterName && OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$this->presenterName]) { $this->pass("openvk!Maintenance->section", $this->presenterName); } } else { @@ -276,6 +282,11 @@ function onStartup(): void $this->redirect("/maintenances/"); } } + + if($_SERVER['HTTP_X_OPENVK_AJAX_QUERY'] == '1' && $this->user->identity) { + error_reporting(0); + header('Content-Type: text/plain; charset=UTF-8'); + } parent::onStartup(); } @@ -301,7 +312,7 @@ function onBeforeRender(): void $theme = Themepacks::i()[Session::i()->get("_sessionTheme", "ovk")]; } else if($this->requestParam("themePreview")) { $theme = Themepacks::i()[$this->requestParam("themePreview")]; - } else if($this->user->identity !== NULL && $this->user->identity->getTheme()) { + } else if($this->user !== NULL && $this->user->identity !== NULL && $this->user->identity->getTheme()) { $theme = $this->user->identity->getTheme(); } diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index a7a3ebb9e..f85245720 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -1,6 +1,6 @@ getPrivacyPermission('photos.read', $this->user->identity ?? NULL)) $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - $this->template->albums = $this->albums->getUserAlbums($user, $this->queryParam("p") ?? 1); + $this->template->albums = $this->albums->getUserAlbums($user, (int)($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getUserAlbumsCount($user); $this->template->owner = $user; $this->template->canEdit = false; @@ -37,7 +37,7 @@ function renderAlbumList(int $owner): void } else { $club = (new Clubs)->get(abs($owner)); if(!$club) $this->notFound(); - $this->template->albums = $this->albums->getClubAlbums($club, $this->queryParam("p") ?? 1); + $this->template->albums = $this->albums->getClubAlbums($club, (int)($this->queryParam("p") ?? 1)); $this->template->count = $this->albums->getClubAlbumsCount($club); $this->template->owner = $club; $this->template->canEdit = false; @@ -47,7 +47,7 @@ function renderAlbumList(int $owner): void $this->template->paginatorConf = (object) [ "count" => $this->template->count, - "page" => $this->queryParam("p") ?? 1, + "page" => (int)($this->queryParam("p") ?? 1), "amount" => NULL, "perPage" => OPENVK_DEFAULT_PER_PAGE, ]; @@ -67,7 +67,7 @@ function renderCreateAlbum(): void } if($_SERVER["REQUEST_METHOD"] === "POST") { - if(empty($this->postParam("name"))) + if(empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) $this->flashFail("err", tr("error"), tr("error_segmentation")); else if(strlen($this->postParam("name")) > 36) $this->flashFail("err", tr("error"), tr("error_data_too_big", "name", 36, "bytes")); @@ -95,19 +95,19 @@ function renderEditAlbum(int $owner, int $id): void if(!$album) $this->notFound(); if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity) || $album->isDeleted()) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $this->template->album = $album; if($_SERVER["REQUEST_METHOD"] === "POST") { if(strlen($this->postParam("name")) > 36) $this->flashFail("err", tr("error"), tr("error_data_too_big", "name", 36, "bytes")); - $album->setName(empty($this->postParam("name")) ? $album->getName() : $this->postParam("name")); + $album->setName((empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $album->getName() : $this->postParam("name")); $album->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $album->setEdited(time()); $album->save(); - $this->flash("succ", "Изменения сохранены", "Новые данные приняты."); + $this->flash("succ", tr("changes_saved"), tr("new_data_accepted")); } } @@ -121,13 +121,13 @@ function renderDeleteAlbum(int $owner, int $id): void if(!$album) $this->notFound(); if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); $name = $album->getName(); $owner = $album->getOwner(); $album->delete(); - $this->flash("succ", "Альбом удалён", "Альбом $name был успешно удалён."); + $this->flash("succ", tr("album_is_deleted"), tr("album_x_is_deleted", $name)); $this->redirect("/albums" . ($owner instanceof Club ? "-" : "") . $owner->getId()); } @@ -138,10 +138,8 @@ function renderAlbum(int $owner, int $id): void if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); - if ((new Blacklists)->isBanned($album->getOwner(), $this->user->identity)) { - if (!$this->user->identity->isAdmin() OR $this->user->identity->isAdmin() AND OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["blacklists"]["applyToAdmins"]) - $this->flashFail("err", tr("forbidden"), tr("user_blacklisted_you")); - } + if(!$album->canBeViewedBy($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); if($owner > 0 /* bc we currently don't have perms for clubs */) { $ownerObject = (new Users)->get($owner); @@ -153,7 +151,7 @@ function renderAlbum(int $owner, int $id): void $this->template->photos = iterator_to_array( $album->getPhotos( (int) ($this->queryParam("p") ?? 1), 20) ); $this->template->paginatorConf = (object) [ "count" => $album->getPhotosCount(), - "page" => $this->queryParam("p") ?? 1, + "page" => (int)($this->queryParam("p") ?? 1), "amount" => sizeof($this->template->photos), "perPage" => 20, "atBottom" => true @@ -164,11 +162,9 @@ function renderPhoto(int $ownerId, int $photoId): void { $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); if(!$photo || $photo->isDeleted()) $this->notFound(); - - if ((new Blacklists)->isBanned($photo->getOwner(), $this->user->identity)) { - if (!$this->user->identity->isAdmin() OR $this->user->identity->isAdmin() AND OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["blacklists"]["applyToAdmins"]) - $this->flashFail("err", tr("forbidden"), tr("user_blacklisted_you")); - } + + if(!$photo->canBeViewedBy($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); if(!is_null($this->queryParam("from"))) { if(preg_match("%^album([0-9]++)$%", $this->queryParam("from"), $matches) === 1) { @@ -183,6 +179,7 @@ function renderPhoto(int $ownerId, int $photoId): void $this->template->cCount = $photo->getCommentsCount(); $this->template->cPage = (int) ($this->queryParam("p") ?? 1); $this->template->comments = iterator_to_array($photo->getComments($this->template->cPage)); + $this->template->owner = $photo->getOwner(); } function renderAbsolutePhoto($id): void @@ -216,13 +213,13 @@ function renderEditPhoto(int $ownerId, int $photoId): void $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); if(!$photo) $this->notFound(); if(is_null($this->user) || $this->user->id != $ownerId) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); if($_SERVER["REQUEST_METHOD"] === "POST") { $photo->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $photo->save(); - $this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с фоткой."); + $this->flash("succ", tr("changes_saved"), tr("new_description_will_appear")); $this->redirect("/photo" . $photo->getPrettyId()); } @@ -232,39 +229,83 @@ function renderEditPhoto(int $ownerId, int $photoId): void function renderUploadPhoto(): void { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); - - if(is_null($this->queryParam("album"))) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED."); - - [$owner, $id] = explode("_", $this->queryParam("album")); - $album = $this->albums->get((int) $id); + $this->willExecuteWriteAction(true); + + if(is_null($this->queryParam("album"))) { + $album = $this->albums->getUserWallAlbum($this->user->identity); + } else { + [$owner, $id] = explode("_", $this->queryParam("album")); + $album = $this->albums->get((int) $id); + } + if(!$album) - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в DELETED."); - if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true); + + # Для быстрой загрузки фоток из пикера фотографий нужен альбом, но юзер не может загружать фото + # в системные альбомы, так что так. + if(is_null($this->user) || !is_null($this->queryParam("album")) && !$album->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true); if($_SERVER["REQUEST_METHOD"] === "POST") { - if(!isset($_FILES["blob"])) - $this->flashFail("err", "Нету фотографии", "Выберите файл."); - - try { - $photo = new Photo; - $photo->setOwner($this->user->id); - $photo->setDescription($this->postParam("desc")); - $photo->setFile($_FILES["blob"]); - $photo->setCreated(time()); - $photo->save(); - } catch(ISE $ex) { - $name = $album->getName(); - $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name."); + if($this->queryParam("act") == "finish") { + $result = json_decode($this->postParam("photos"), true); + + foreach($result as $photoId => $description) { + $phot = $this->photos->get($photoId); + + if(!$phot || $phot->isDeleted() || $phot->getOwner()->getId() != $this->user->id) + continue; + + if(iconv_strlen($description) > 255) + $this->flashFail("err", tr("error"), tr("description_too_long"), 500, true); + + $phot->setDescription($description); + $phot->save(); + + $album = $phot->getAlbum(); + } + + $this->returnJson(["success" => true, + "album" => $album->getId(), + "owner" => $album->getOwner() instanceof User ? $album->getOwner()->getId() : $album->getOwner()->getId() * -1]); } + + if(!isset($_FILES)) + $this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true); - $album->addPhoto($photo); - $album->setEdited(time()); - $album->save(); + $photos = []; + if((int)$this->postParam("count") > 10) + $this->flashFail("err", tr("no_photo"), "ты еблан", 500, true); + + for($i = 0; $i < $this->postParam("count"); $i++) { + try { + $photo = new Photo; + $photo->setOwner($this->user->id); + $photo->setDescription(""); + $photo->setFile($_FILES["photo_".$i]); + $photo->setCreated(time()); + $photo->save(); - $this->redirect("/photo" . $photo->getPrettyId() . "?from=album" . $album->getId()); + $photos[] = [ + "url" => $photo->getURLBySizeId("tiny"), + "id" => $photo->getId(), + "vid" => $photo->getVirtualId(), + "owner" => $photo->getOwner()->getId(), + "link" => $photo->getURL(), + "pretty_id" => $photo->getPrettyId(), + ]; + } catch(ISE $ex) { + $name = $album->getName(); + $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name.", 500, true); + } + + $album->addPhoto($photo); + $album->setEdited(time()); + $album->save(); + } + + $this->returnJson(["success" => true, + "photos" => $photos]); } else { $this->template->album = $album; } @@ -280,7 +321,7 @@ function renderUnlinkPhoto(int $owner, int $albumId, int $photoId): void if(!$album || !$photo) $this->notFound(); if(!$album->hasPhoto($photo)) $this->notFound(); if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); if($_SERVER["REQUEST_METHOD"] === "POST") { $this->assertNoCSRF(); @@ -288,7 +329,7 @@ function renderUnlinkPhoto(int $owner, int $albumId, int $photoId): void $album->setEdited(time()); $album->save(); - $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); + $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc")); $this->redirect("/album" . $album->getPrettyId()); } } @@ -296,18 +337,48 @@ function renderUnlinkPhoto(int $owner, int $albumId, int $photoId): void function renderDeletePhoto(int $ownerId, int $photoId): void { $this->assertUserLoggedIn(); - $this->willExecuteWriteAction(); + $this->willExecuteWriteAction($_SERVER["REQUEST_METHOD"] === "POST"); $this->assertNoCSRF(); $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); if(!$photo) $this->notFound(); if(is_null($this->user) || $this->user->id != $ownerId) - $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); - + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + + if(!is_null($album = $photo->getAlbum())) + $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + else + $redirect = "/id0"; + $photo->isolate(); $photo->delete(); - $this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); - $this->redirect("/id0"); + if($_SERVER["REQUEST_METHOD"] === "POST") + $this->returnJson(["success" => true]); + + $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc")); + $this->redirect($redirect); + } + + function renderLike(int $wall, int $post_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $this->assertNoCSRF(); + + $photo = $this->photos->getByOwnerAndVID($wall, $post_id); + if(!$photo || $photo->isDeleted() || !$photo->canBeViewedBy($this->user->identity)) $this->notFound(); + + if(!is_null($this->user)) { + $photo->toggleLike($this->user->identity); + } + + if($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } + + $this->redirect("$_SERVER[HTTP_REFERER]"); } } diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php new file mode 100644 index 000000000..a627efa4c --- /dev/null +++ b/Web/Presenters/ReportPresenter.php @@ -0,0 +1,153 @@ +reports = $reports; + + parent::__construct(); + } + + function renderList(): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + if ($_SERVER["REQUEST_METHOD"] === "POST") + $this->assertNoCSRF(); + + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]) ? $this->queryParam("act") : NULL; + + if (!$this->queryParam("orig")) { + $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); + $this->template->count = $this->reports->getReportsCount(); + } else { + $orig = $this->reports->get((int) $this->queryParam("orig")); + if (!$orig) $this->redirect("/scumfeed"); + + $this->template->reports = $orig->getDuplicates(); + $this->template->count = $orig->getDuplicatesCount(); + $this->template->orig = $orig->getId(); + } + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $this->queryParam("p") ?? 1, + "amount" => NULL, + "perPage" => 15, + ]; + $this->template->mode = $act ?? "all"; + $this->template->disable_ajax = 1; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $reports = []; + foreach ($this->reports->getReports(0, 0, $act, false) as $report) { + $reports[] = [ + "id" => $report->getId(), + "author" => [ + "id" => $report->getReportAuthor()->getId(), + "url" => $report->getReportAuthor()->getURL(), + "name" => $report->getReportAuthor()->getCanonicalName(), + "is_female" => $report->getReportAuthor()->isFemale() + ], + "content" => [ + "name" => $report->getContentName(), + "type" => $report->getContentType(), + "id" => $report->getContentId(), + "url" => $report->getContentType() === "user" ? (new Users)->get((int) $report->getContentId())->getURL() : NULL + ], + "duplicates" => $report->getDuplicatesCount(), + ]; + } + $this->returnJson(["reports" => $reports]); + } + } + + function renderView(int $id): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $report = $this->reports->get($id); + if(!$report || $report->isDeleted()) + $this->notFound(); + + $this->template->report = $report; + $this->template->disable_ajax = 1; + } + + function renderCreate(int $id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if(!$id) + exit(json_encode([ "error" => tr("error_segmentation") ])); + + if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) { + if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { + $report = new Report; + $report->setUser_id($this->user->id); + $report->setTarget_id($id); + $report->setType($this->queryParam("type")); + $report->setReason($this->queryParam("reason")); + $report->setCreated(time()); + $report->save(); + } + + exit(json_encode([ "reason" => $this->queryParam("reason") ])); + } else { + exit(json_encode([ "error" => "Unable to submit a report on this content type" ])); + } + } + + function renderAction(int $id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $report = $this->reports->get($id); + if(!$report || $report->isDeleted()) $this->notFound(); + + if ($this->postParam("ban")) { + $report->deleteContent(); + $report->banUser($this->user->identity->getId()); + + $this->flash("suc", tr("death"), tr("user_successfully_banned")); + } else if ($this->postParam("delete")) { + $report->deleteContent(); + + $this->flash("suc", tr("nehay"), tr("content_is_deleted")); + } else if ($this->postParam("ignore")) { + $report->delete(); + + $this->flash("suc", tr("nehay"), tr("report_is_ignored")); + } else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) { + if ($report->getContentType() !== "group") + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + + $club = $report->getContentObject(); + if (!$club || $club->isBanned()) + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); + + if ($this->postParam("banClubOwner")) { + $club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId()); + } else { + $club->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**"); + } + + $report->delete(); + + $this->flash("suc", tr("death"), ($this->postParam("banClubOwner") ? tr("group_owner_is_banned") : tr("group_is_banned"))); + } + + $this->redirect("/scumfeed"); + } +} diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php index fadf9954c..9e16450e8 100644 --- a/Web/Presenters/SearchPresenter.php +++ b/Web/Presenters/SearchPresenter.php @@ -1,7 +1,7 @@ users = $users; - $this->clubs = $clubs; + $this->users = new Users; + $this->clubs = new Clubs; $this->posts = new Posts; - $this->comments = new Comments; $this->videos = new Videos; $this->apps = new Applications; - $this->notes = new Notes; + $this->audios = new Audios; parent::__construct(); } function renderIndex(): void { - $query = $this->queryParam("query") ?? ""; - $type = $this->queryParam("type") ?? "users"; - $sorter = $this->queryParam("sort") ?? "id"; - $invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC"; - $page = (int) ($this->queryParam("p") ?? 1); - - $this->willExecuteWriteAction(); - if($query != "") - $this->assertUserLoggedIn(); - + $this->assertUserLoggedIn(); + + $query = $this->queryParam("q") ?? ""; + $section = $this->queryParam("section") ?? "users"; + $order = $this->queryParam("order") ?? "id"; + $invert = (int) ($this->queryParam("invert") ?? 0) == 1; + $page = (int) ($this->queryParam("p") ?? 1); + # https://youtu.be/pSAWM5YuXx8 + # https://youtu.be/FfNZRhIn2Vk $repos = [ "groups" => "clubs", "users" => "users", "posts" => "posts", - "comments" => "comments", "videos" => "videos", - "audios" => "posts", + "audios" => "audios", "apps" => "apps", - "notes" => "notes" + "audios_playlists" => "audios" + ]; + $parameters = [ + "ignore_private" => true, ]; - switch($sorter) { + foreach($_REQUEST as $param_name => $param_value) { + if(is_null($param_value)) continue; + + switch($param_name) { + default: + $parameters[$param_name] = $param_value; + break; + case 'marital_status': + case 'polit_views': + if((int) $param_value == 0) continue; + $parameters[$param_name] = $param_value; + + break; + case 'is_online': + if((int) $param_value == 1) + $parameters['is_online'] = 1; + + break; + case 'only_performers': + if((int) $param_value == 1 || $param_value == 'on') + $parameters['only_performers'] = true; + + break; + case 'with_lyrics': + if($param_value == 'on' || $param_value == '1') + $parameters['with_lyrics'] = true; + + break; + # дай бог работал этот case + case 'from_me': + if((int) $param_value != 1) continue; + $parameters['from_me'] = $this->user->id; + + break; + } + } + + $repo = $repos[$section] or $this->throwError(400, "Bad Request", "Invalid search entity $section."); + + $results = NULL; + switch($section) { default: - case "id": - $sort = "id " . $invert; + $results = $this->{$repo}->find($query, $parameters, ['type' => $order, 'invert' => $invert]); + break; + case 'audios_playlists': + $results = $this->{$repo}->findPlaylists($query, $parameters, ['type' => $order, 'invert' => $invert]); break; - case "name": - $sort = "first_name " . $invert; - break; - case "rating": - $sort = "rating " . $invert; - break; } - - $parameters = [ - "type" => $this->queryParam("type"), - "city" => $this->queryParam("city") != "" ? $this->queryParam("city") : NULL, - "maritalstatus" => $this->queryParam("maritalstatus") != 0 ? $this->queryParam("maritalstatus") : NULL, - "with_photo" => $this->queryParam("with_photo"), - "status" => $this->queryParam("status") != "" ? $this->queryParam("status") : NULL, - "politViews" => $this->queryParam("politViews") != 0 ? $this->queryParam("politViews") : NULL, - "email" => $this->queryParam("email"), - "telegram" => $this->queryParam("telegram"), - "site" => $this->queryParam("site") != "" ? "https://".$this->queryParam("site") : NULL, - "address" => $this->queryParam("address"), - "is_online" => $this->queryParam("is_online") == 1 ? 1 : NULL, - "interests" => $this->queryParam("interests") != "" ? $this->queryParam("interests") : NULL, - "fav_mus" => $this->queryParam("fav_mus") != "" ? $this->queryParam("fav_mus") : NULL, - "fav_films" => $this->queryParam("fav_films") != "" ? $this->queryParam("fav_films") : NULL, - "fav_shows" => $this->queryParam("fav_shows") != "" ? $this->queryParam("fav_shows") : NULL, - "fav_books" => $this->queryParam("fav_books") != "" ? $this->queryParam("fav_books") : NULL, - "fav_quote" => $this->queryParam("fav_quote") != "" ? $this->queryParam("fav_quote") : NULL, - "hometown" => $this->queryParam("hometown") != "" ? $this->queryParam("hometown") : NULL, - "before" => $this->queryParam("datebefore") != "" ? strtotime($this->queryParam("datebefore")) : NULL, - "after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL, - "gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL - ]; - - $repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type."); - $results = $this->{$repo}->find($query, $parameters, $sort); - $iterator = $results->page($page); + $iterator = $results->page($page, OPENVK_DEFAULT_PER_PAGE); $count = $results->size(); - $this->template->iterator = iterator_to_array($iterator); + $this->template->order = $order; + $this->template->invert = $invert; + $this->template->data = $this->template->iterator = iterator_to_array($iterator); $this->template->count = $count; - $this->template->type = $type; + $this->template->section = $section; $this->template->page = $page; + $this->template->perPage = OPENVK_DEFAULT_PER_PAGE; + $this->template->query = $query; + $this->template->atSearch = true; + + $this->template->paginatorConf = (object) [ + "page" => $page, + "count" => $count, + "amount" => sizeof($this->template->data), + "perPage" => $this->template->perPage, + "atBottom" => false, + "tidy" => true, + "space" => 6, + 'pageCount' => ceil($count / $this->template->perPage), + ]; + $this->template->extendedPaginatorConf = clone $this->template->paginatorConf; + $this->template->extendedPaginatorConf->space = 11; + $this->template->paginatorConf->atTop = true; } } diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index c4d729ea1..b834d7126 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -67,7 +67,7 @@ function renderIndex(): void $this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id); if($this->template->mode === "list") { $this->template->page = (int) ($this->queryParam("p") ?? 1); - $this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page); + $this->template->tickets = iterator_to_array($this->tickets->getTicketsByUserId($this->user->id, $this->template->page)); } if($this->template->mode === "new") @@ -385,7 +385,7 @@ function renderEditAgent(int $id): void $agent->setNumerate((int) $this->postParam("number") ?? NULL); $agent->setIcon($this->postParam("avatar")); $agent->save(); - $this->flashFail("succ", "Успех", "Профиль отредактирован."); + $this->flashFail("succ", tr("agent_profile_edited")); } else { $agent = new SupportAgent; $agent->setAgent($this->user->identity->getId()); @@ -393,7 +393,27 @@ function renderEditAgent(int $id): void $agent->setNumerate((int) $this->postParam("number") ?? NULL); $agent->setIcon($this->postParam("avatar")); $agent->save(); - $this->flashFail("succ", "Успех", "Профиль создан. Теперь пользователи видят Ваши псевдоним и аватарку вместо стандартных аватарки и номера."); + $this->flashFail("succ", tr("agent_profile_created_1"), tr("agent_profile_created_2")); } } + + function renderCloseTicket(int $id): void + { + $this->assertUserLoggedIn(); + $this->assertNoCSRF(); + $this->willExecuteWriteAction(); + + $ticket = $this->tickets->get($id); + + if($ticket->isDeleted() === 1 || $ticket->getType() === 2 || $ticket->getUserId() !== $this->user->id) { + header("HTTP/1.1 403 Forbidden"); + header("Location: /support/view/" . $id); + exit; + } + + $ticket->setType(2); + $ticket->save(); + + $this->flashFail("succ", tr("ticket_changed"), tr("ticket_changed_comment")); + } } diff --git a/Web/Presenters/TopicsPresenter.php b/Web/Presenters/TopicsPresenter.php index e7b08ac3d..92d67e841 100644 --- a/Web/Presenters/TopicsPresenter.php +++ b/Web/Presenters/TopicsPresenter.php @@ -111,7 +111,7 @@ function renderCreate(int $clubId): void $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]); } } catch(ISE $ex) { - $this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); + $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_file_too_big")); $this->redirect("/topic" . $topic->getPrettyId()); } @@ -126,7 +126,7 @@ function renderCreate(int $clubId): void $comment->setFlags($flags); $comment->save(); } catch (\LengthException $ex) { - $this->flash("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой."); + $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_too_big")); $this->redirect("/topic" . $topic->getPrettyId()); } diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 3dd4c9d0d..5afddb84b 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -5,7 +5,7 @@ use openvk\Web\Themes\Themepacks; use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Blacklists}; +use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Audios, Blacklists}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Util\Validator; use Chandler\Security\Authenticator; @@ -32,21 +32,14 @@ function renderView(int $id): void { $user = $this->users->get($id); - if ($this->user->identity) - if ($this->blacklists->isBanned($user, $this->user->identity)) { - if ($this->user->identity->isAdmin()) { - if (OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["blacklists"]["applyToAdmins"]) { - $this->flashFail("err", tr("forbidden"), tr("user_blacklisted_you")); - } - } else { - $this->flashFail("err", tr("forbidden"), tr("user_blacklisted_you")); - } - } - - if(!$user || $user->isDeleted()) { + if(!$user || $user->isDeleted() || !$user->canBeViewedBy($this->user->identity)) { if(!is_null($user) && $user->isDeactivated()) { $this->template->_template = "User/deactivated.xml"; + $this->template->user = $user; + } else if(!is_null($user) && !$user->canBeViewedBy($this->user->identity)) { + $this->template->_template = "User/private.xml"; + $this->template->user = $user; } else { $this->template->_template = "User/deleted.xml"; @@ -59,11 +52,15 @@ function renderView(int $id): void $this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->notes = (new Notes)->getUserNotes($user, 1, 4); $this->template->notesCount = (new Notes)->getUserNotesCount($user); - $this->template->blacklists = (new Blacklists); + $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($user->getId()); + $this->template->audiosCount = (new Audios)->getUserCollectionSize($user); + $this->template->audioStatus = $user->getCurrentAudioStatus(); $this->template->user = $user; - $this->template->isBlacklistedThem = $this->template->blacklists->isBanned($this->user->identity, $user); - $this->template->isBlacklistedByThem = $this->template->blacklists->isBanned($user, $this->user->identity); + + if($id !== $this->user->id) { + $this->template->ignore_status = $user->isIgnoredBy($this->user->identity); + } } } @@ -72,7 +69,7 @@ function renderFriends(int $id): void $this->assertUserLoggedIn(); $user = $this->users->get($id); - $page = abs($this->queryParam("p") ?? 1); + $page = abs((int)($this->queryParam("p") ?? 1)); if(!$user) $this->notFound(); elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL)) @@ -89,7 +86,7 @@ function renderFriends(int $id): void if(!is_null($this->user)) { if($this->template->mode !== "friends" && $this->user->id !== $id) { $name = $user->getFullName(); - $this->flash("err", "Ошибка доступа", "Вы не можете просматривать полный список подписок $name."); + $this->flash("err", tr("error_access_denied_short"), tr("error_viewing_subs", $name)); $this->redirect($user->getURL()); } @@ -124,11 +121,11 @@ function renderPinClub(): void $this->notFound(); if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) - $this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс.", NULL, true); + $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), NULL, true); $isClubPinned = $this->user->identity->isClubPinned($club); if(!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10) - $this->flashFail("err", "Ошибка", "Находится в левом меню могут максимум 10 групп", NULL, true); + $this->flashFail("err", tr("error"), tr("error_max_pinned_clubs"), NULL, true); if($club->getOwner()->getId() === $this->user->identity->getId()) { $club->setOwner_Club_Pinned(!$isClubPinned); @@ -180,12 +177,36 @@ function renderEdit(): void if ($this->postParam("marialstatus") <= 8 && $this->postParam("marialstatus") >= 0) $user->setMarital_Status($this->postParam("marialstatus")); + + if ($this->postParam("maritalstatus-user")) { + if (in_array((int) $this->postParam("marialstatus"), [0, 1, 8])) { + $user->setMarital_Status_User(NULL); + } else { + $mUser = (new Users)->getByAddress($this->postParam("maritalstatus-user")); + if ($mUser) { + if ($mUser->getId() !== $this->user->id) { + $user->setMarital_Status_User($mUser->getId()); + } + } + } + } if ($this->postParam("politViews") <= 9 && $this->postParam("politViews") >= 0) $user->setPolit_Views($this->postParam("politViews")); - if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0) - $user->setSex($this->postParam("gender")); + if ($this->postParam("pronouns") <= 2 && $this->postParam("pronouns") >= 0) + switch ($this->postParam("pronouns")) { + case '0': + $user->setSex(0); + break; + case '1': + $user->setSex(1); + break; + case '2': + $user->setSex(2); + break; + } + $user->setAudio_broadcast_enabled($this->checkbox("broadcast_music")); if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) { if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"]) @@ -254,10 +275,11 @@ function renderEdit(): void } elseif($_GET['act'] === "status") { if(mb_strlen($this->postParam("status")) > 255) { $statusLength = (string) mb_strlen($this->postParam("status")); - $this->flashFail("err", "Ошибка", "Статус слишком длинный ($statusLength символов вместо 255 символов)", NULL, true); + $this->flashFail("err", tr("error"), tr("error_status_too_long", $statusLength), NULL, true); } $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); + $user->setAudio_broadcast_enabled($this->postParam("broadcast") == 1); $user->save(); $this->returnJson([ @@ -298,7 +320,7 @@ function renderVerifyPhone(): void if($_SERVER["REQUEST_METHOD"] === "POST") { if(!$user->verifyNumber($this->postParam("code") ?? 0)) - $this->flashFail("err", "Ошибка", "Не удалось подтвердить номер телефона: неверный код."); + $this->flashFail("err", tr("error"), tr("invalid_code")); $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); } @@ -314,13 +336,15 @@ function renderSub(): void $user = $this->users->get((int) $this->postParam("id")); if(!$user) exit("Invalid state"); - $user->toggleSubscription($this->user->identity); + if ($this->postParam("act") == "rej") + $user->changeFlags($this->user->identity, 0b10000000, true); + else + $user->toggleSubscription($this->user->identity); - $this->redirect($user->getURL()); + $this->redirect($_SERVER['HTTP_REFERER']); } - function renderSetAvatar() - { + function renderSetAvatar() { $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); @@ -331,8 +355,8 @@ function renderSetAvatar() $photo->setFile($_FILES["blob"]); $photo->setCreated(time()); $photo->save(); - } catch(ISE $ex) { - $this->flashFail("err", tr("error"), tr("error_upload_failed")); + } catch(\Throwable $ex) { + $this->flashFail("err", tr("error"), tr("error_upload_failed"), NULL, (int)$this->postParam("ajax", true) == 1); } $album = (new Albums)->getUserAvatarAlbum($this->user->identity); @@ -343,23 +367,57 @@ function renderSetAvatar() $flags = 0; $flags |= 0b00010000; - $post = new Post; - $post->setOwner($this->user->id); - $post->setWall($this->user->id); - $post->setCreated(time()); - $post->setContent(""); - $post->setFlags($flags); - $post->save(); - $post->attach($photo); - if($this->postParam("ava", true) == (int)1) { + if($this->postParam("on_wall") == 1) { + $post = new Post; + $post->setOwner($this->user->id); + $post->setWall($this->user->id); + $post->setCreated(time()); + $post->setContent(""); + $post->setFlags($flags); + $post->save(); + + $post->attach($photo); + } + + if((int)$this->postParam("ajax", true) == 1) { $this->returnJson([ - "url" => $photo->getURL(), - "id" => $photo->getPrettyId() + "success" => true, + "new_photo" => $photo->getPrettyId(), + "url" => $photo->getURL(), ]); } else { $this->flashFail("succ", tr("photo_saved"), tr("photo_saved_comment")); } } + + function renderDeleteAvatar() { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $avatar = $this->user->identity->getAvatarPhoto(); + + if(!$avatar) + $this->flashFail("succ", tr("error"), "no avatar bro", NULL, true); + + $avatar->isolate(); + + $newAvatar = $this->user->identity->getAvatarPhoto(); + + if(!$newAvatar) + $this->returnJson([ + "success" => true, + "has_new_photo" => false, + "new_photo" => NULL, + "url" => "/assets/packages/static/openvk/img/camera_200.png", + ]); + else + $this->returnJson([ + "success" => true, + "has_new_photo" => true, + "new_photo" => $newAvatar->getPrettyId(), + "url" => $newAvatar->getURL(), + ]); + } function renderSettings(): void { @@ -447,11 +505,17 @@ function renderSettings(): void "friends.add", "wall.write", "messages.write", + "audios.read", + "likes.read", ]; foreach($settings as $setting) { $input = $this->postParam(str_replace(".", "_", $setting)); - $user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting)))); + $user->setPrivacySetting($setting, min(3, (int)abs((int)$input ?? $user->getPrivacySetting($setting)))); } + + $prof = $this->postParam("profile_type") == 1 || $this->postParam("profile_type") == 0 ? (int)$this->postParam("profile_type") : 0; + $user->setProfile_type($prof); + } else if($_GET['act'] === "finance.top-up") { $token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3"); $voucher = (new Vouchers)->getByToken($token); @@ -491,6 +555,7 @@ function renderSettings(): void } else if($_GET['act'] === "lMenu") { $settings = [ "menu_bildoj" => "photos", + "menu_muziko" => "audios", "menu_filmetoj" => "videos", "menu_mesagoj" => "messages", "menu_notatoj" => "notes", @@ -498,6 +563,7 @@ function renderSettings(): void "menu_novajoj" => "news", "menu_ligiloj" => "links", "menu_standardo" => "poster", + "menu_aplikoj" => "apps" ]; foreach($settings as $checkbox => $setting) $user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox)); diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index 4cf6e0509..fd283321d 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -6,9 +6,11 @@ use openvk\Web\Models\Entities\{User, APIToken}; use openvk\Web\Models\Repositories\{Users, APITokens}; use lfkeitel\phptotp\{Base32, Totp}; +use WhichBrowser; final class VKAPIPresenter extends OpenVKPresenter { + protected $silent = true; private function logRequest(string $object, string $method): void { $date = date(DATE_COOKIE); @@ -98,20 +100,21 @@ function onStartup(): void function renderPhotoUpload(string $signature): void { - $secret = CHANDLER_ROOT_CONF["security"]["secret"]; - $computedSignature = hash_hmac("sha3-224", $_SERVER["QUERY_STRING"], $secret); + $secret = CHANDLER_ROOT_CONF["security"]["secret"]; + $queryString = rawurldecode($_SERVER["QUERY_STRING"]); + $computedSignature = hash_hmac("sha3-224", $queryString, $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"])); + $data = unpack("vDOMAIN/Z10FIELD/vMF/vMP/PTIME/PUSER/PGROUP", base64_decode($queryString)); if((time() - $data["TIME"]) > 600) { header("HTTP/1.1 422 Unprocessable Entity"); exit("Expired"); } - $folder = __DIR__ . "../../tmp/api-storage/photos"; + $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")); @@ -184,8 +187,12 @@ function renderPhotoUpload(string $signature): void function renderRoute(string $object, string $method): void { + $callback = $this->queryParam("callback"); $authMechanism = $this->queryParam("auth_mechanism") ?? "token"; if($authMechanism === "roaming") { + if($callback) + $this->fail(-1, "User authorization failed: roaming mechanism is unavailable with jsonp.", $object, $method); + if(!$this->user->identity) $this->fail(5, "User authorization failed: roaming mechanism is selected, but user is not logged in.", $object, $method); else @@ -216,9 +223,13 @@ function renderRoute(string $object, string $method): void if(!is_callable([$handler, $method])) $this->badMethod($object, $method); + $has_rss = false; $route = new \ReflectionMethod($handler, $method); $params = []; foreach($route->getParameters() as $parameter) { + if($parameter->getName() == 'rss') + $has_rss = true; + $val = $this->requestParam($parameter->getName()); if(is_null($val)) { if($parameter->allowsNull()) @@ -231,8 +242,19 @@ function renderRoute(string $object, string $method): void $this->badMethodCall($object, $method, $parameter->getName()); } - settype($val, $parameter->getType()->getName()); - $params[] = $val; + try { + // Проверка типа параметра + $type = $parameter->getType(); + if (($type && !$type->isBuiltin()) || is_null($val)) { + $params[] = $val; + } else { + settype($val, $parameter->getType()->getName()); + $params[] = $val; + } + } catch (\Throwable $e) { + // Just ignore the exception, since + // some args are intended for internal use + } } define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false); @@ -243,13 +265,31 @@ function renderRoute(string $object, string $method): void $this->fail($ex->getCode(), $ex->getMessage(), $object, $method); } - $result = json_encode([ - "response" => $res, - ]); + $result = NULL; + + if($this->queryParam("rss") == '1' && $has_rss) { + $feed = new \Bhaktaraz\RSSGenerator\Feed(); + $res->appendTo($feed); + + $result = strval($feed); + + header("Content-Type: application/rss+xml;charset=UTF-8"); + } else { + $result = json_encode([ + "response" => $res, + ]); + + if($callback) { + $result = $callback . '(' . $result . ')'; + header('Content-Type: application/javascript'); + } else { + header("Content-Type: application/json"); + } + } $size = strlen($result); - header("Content-Type: application/json"); header("Content-Length: $size"); + exit($result); } @@ -279,17 +319,31 @@ function renderTokenLogin(): void $this->fail(28, "Invalid 2FA code", "internal", "acquireToken"); } - $platform = $this->requestParam("client_name"); - - $token = new APIToken; - $token->setUser($user); - $token->setPlatform(is_null($platform) ? "api" : $platform); - $token->save(); + $token = NULL; + $tokenIsStale = true; + $platform = $this->requestParam("client_name"); + $acceptsStale = $this->requestParam("accepts_stale"); + if($acceptsStale == "1") { + if(is_null($platform)) + $this->fail(101, "accepts_stale can only be used with explicitly set client_name", "internal", "acquireToken"); + + $token = (new APITokens)->getStaleByUser($uId, $platform); + } + + if(is_null($token)) { + $tokenIsStale = false; + + $token = new APIToken; + $token->setUser($user); + $token->setPlatform($platform ?? (new WhichBrowser\Parser(getallheaders()))->toString()); + $token->save(); + } $payload = json_encode([ "access_token" => $token->getFormattedToken(), "expires_in" => 0, "user_id" => $uId, + "is_stale" => $tokenIsStale, ]); $size = strlen($payload); @@ -297,4 +351,42 @@ function renderTokenLogin(): void header("Content-Length: $size"); exit($payload); } + + function renderOAuthLogin() { + $this->assertUserLoggedIn(); + + $client = $this->queryParam("client_name"); + $postmsg = $this->queryParam("prefers_postMessage") ?? '0'; + $stale = $this->queryParam("accepts_stale") ?? '0'; + $origin = NULL; + $url = $this->queryParam("redirect_uri"); + if(is_null($url) || is_null($client)) + exit("Error: redirect_uri and client_name params are required."); + + if($url != "about:blank") { + if(!filter_var($url, FILTER_VALIDATE_URL)) + exit("Error: Invalid URL passed to redirect_uri."); + + $parsedUrl = (object) parse_url($url); + if($parsedUrl->scheme != 'https' && $parsedUrl->scheme != 'http') + exit("Error: redirect_uri should either point to about:blank or to a web resource."); + + $origin = "$parsedUrl->scheme://$parsedUrl->host"; + if(!is_null($parsedUrl->port ?? NULL)) + $origin .= ":$parsedUrl->port"; + + $url .= strpos($url, '?') === false ? '?' : '&'; + } else { + $url .= "#"; + if($postmsg == '1') { + exit("Error: prefers_postMessage can only be set if redirect_uri is not about:blank"); + } + } + + $this->template->clientName = $client; + $this->template->usePostMessage = $postmsg == '1'; + $this->template->acceptsStale = $stale == '1'; + $this->template->origin = $origin; + $this->template->redirectUri = $url; + } } diff --git a/Web/Presenters/VideosPresenter.php b/Web/Presenters/VideosPresenter.php index 113e4e87c..58d60c436 100644 --- a/Web/Presenters/VideosPresenter.php +++ b/Web/Presenters/VideosPresenter.php @@ -39,15 +39,12 @@ function renderList(int $id): void function renderView(int $owner, int $vId): void { $user = $this->users->get($owner); - if(!$user) $this->notFound(); - if(!$user->getPrivacyPermission('videos.read', $this->user->identity ?? NULL)) { - if ((new Blacklists)->isBanned($user, $this->user->identity)) - $this->flashFail("err", tr("forbidden"), tr("user_blacklisted_you")); + $video = $this->videos->getByOwnerAndVID($owner, $vId); + if(!$user) $this->notFound(); + if(!$video || $video->isDeleted()) $this->notFound(); + if(!$user->getPrivacyPermission('videos.read', $this->user->identity ?? NULL)) $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - } - - if($this->videos->getByOwnerAndVID($owner, $vId)->isDeleted()) $this->notFound(); $this->template->user = $user; $this->template->video = $this->videos->getByOwnerAndVID($owner, $vId); @@ -62,9 +59,10 @@ function renderUpload(): void $this->willExecuteWriteAction(); if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) - $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); + $this->flashFail("err", tr("error"), tr("video_uploads_disabled")); if($_SERVER["REQUEST_METHOD"] === "POST") { + $is_ajax = (int)($this->postParam('ajax') ?? '0') == 1; if(!empty($this->postParam("name"))) { $video = new Video; $video->setOwner($this->user->id); @@ -78,18 +76,29 @@ function renderUpload(): void else if(!empty($this->postParam("link"))) $video->setLink($this->postParam("link")); else - $this->flashFail("err", "Нету видеозаписи", "Выберите файл или укажите ссылку."); + $this->flashFail("err", tr("no_video_error"), tr("no_video_description"), 10, $is_ajax); } catch(\DomainException $ex) { - $this->flashFail("err", "Произошла ошибка", "Файл повреждён или не содержит видео." ); + $this->flashFail("err", tr("error_video"), tr("file_corrupted"), 10, $is_ajax); } catch(ISE $ex) { - $this->flashFail("err", "Произошла ошибка", "Возможно, ссылка некорректна."); + $this->flashFail("err", tr("error_video"), tr("link_incorrect"), 10, $is_ajax); } + if((int)($this->postParam("unlisted") ?? '0') == 1) { + $video->setUnlisted(true); + } + $video->save(); + if($is_ajax) { + $object = $video->getApiStructure(); + $this->returnJson([ + 'payload' => $object->video, + ]); + } + $this->redirect("/video" . $video->getPrettyId()); } else { - $this->flashFail("err", "Произошла ошибка", "Видео не может быть опубликовано без названия."); + $this->flashFail("err", tr("error_video"), tr("no_name_error"), 10, $is_ajax); } } } @@ -103,14 +112,14 @@ function renderEdit(int $owner, int $vId): void if(!$video) $this->notFound(); if(is_null($this->user) || $this->user->id !== $owner) - $this->flashFail("err", "Ошибка доступа", "Вы не имеете права редактировать этот ресурс."); + $this->flashFail("err", tr("access_denied_error"), tr("access_denied_error_description")); if($_SERVER["REQUEST_METHOD"] === "POST") { $video->setName(empty($this->postParam("name")) ? NULL : $this->postParam("name")); $video->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $video->save(); - $this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с видосиком."); + $this->flash("succ", tr("changes_saved"), tr("changes_saved_video_comment")); $this->redirect("/video" . $video->getPrettyId()); } @@ -132,9 +141,35 @@ function renderRemove(int $owner, int $vid): void $video->deleteVideo($owner, $vid); } } else { - $this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт."); + $this->flashFail("err", tr("cant_delete_video"), tr("cant_delete_video_comment")); } $this->redirect("/videos" . $owner); } + + function renderLike(int $owner, int $video_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $this->assertNoCSRF(); + + $video = $this->videos->getByOwnerAndVID($owner, $video_id); + if(!$video || $video->isDeleted() || $video->getOwner()->isDeleted()) $this->notFound(); + + if(method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user->identity)) { + $this->flashFail("err", tr("error"), tr("forbidden")); + } + + if(!is_null($this->user)) { + $video->toggleLike($this->user->identity); + } + + if($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } + + $this->redirect("$_SERVER[HTTP_REFERER]"); + } } diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 6b9ec183c..dc0b29362 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -2,8 +2,8 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; -use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification}; -use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums}; +use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification, PostAcceptedNotification, NewSuggestedPostsNotification}; +use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; use Bhaktaraz\RSSGenerator\Item; @@ -46,13 +46,13 @@ private function logPostsViewed(array &$posts, int $wall): void function renderWall(int $user, bool $embedded = false): void { $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); + if ($owner->isBanned() || !$owner->canBeViewedBy($this->user->identity)) + $this->flashFail("err", tr("error"), tr("forbidden")); + if(is_null($this->user)) { $canPost = false; } else if($user > 0) { - if(!$owner->isBanned()) - $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); - else - $this->flashFail("err", tr("error"), tr("forbidden")); + $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); } else if($user < 0) { if($owner->canBeModifiedBy($this->user->identity)) $canPost = true; @@ -66,11 +66,32 @@ function renderWall(int $user, bool $embedded = false): void $this->template->oObj = $owner; if($user < 0) $this->template->club = $owner; + + $iterator = NULL; + $count = 0; + $type = $this->queryParam("type") ?? "all"; + + switch($type) { + default: + case "all": + $iterator = $this->posts->getPostsFromUsersWall($user, (int) ($_GET["p"] ?? 1)); + $count = $this->posts->getPostCountOnUserWall($user); + break; + case "owners": + $iterator = $this->posts->getOwnersPostsFromWall($user, (int) ($_GET["p"] ?? 1)); + $count = $this->posts->getOwnersCountOnUserWall($user); + break; + case "others": + $iterator = $this->posts->getOthersPostsFromWall($user, (int) ($_GET["p"] ?? 1)); + $count = $this->posts->getOthersCountOnUserWall($user); + break; + } $this->template->owner = $user; $this->template->canPost = $canPost; - $this->template->count = $this->posts->getPostCountOnUserWall($user); - $this->template->posts = iterator_to_array($this->posts->getPostsFromUsersWall($user, (int) ($_GET["p"] ?? 1))); + $this->template->count = $count; + $this->template->type = $type; + $this->template->posts = iterator_to_array($iterator); $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => (int) ($_GET["p"] ?? 1), @@ -93,13 +114,15 @@ function renderRSS(int $user): void if(is_null($this->user)) { $canPost = false; } else if($user > 0) { - if(!$owner->isBanned()) + if(!$owner->isBanned() && $owner->canBeViewedBy($this->user->identity)) $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); else $this->flashFail("err", tr("error"), tr("forbidden")); } else if($user < 0) { if($owner->canBeModifiedBy($this->user->identity)) $canPost = true; + else if ($owner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); else $canPost = $owner->canPost(); } else { @@ -148,11 +171,12 @@ function renderFeed(): void ->select("id") ->where("wall IN (?)", $ids) ->where("deleted", 0) + ->where("suggested", 0) ->order("created DESC"); $this->template->paginatorConf = (object) [ "count" => sizeof($posts), "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts->page((int) ($_GET["p"] ?? 1), $perPage)), + "amount" => $posts->page((int) ($_GET["p"] ?? 1), $perPage)->count(), "perPage" => $perPage, ]; $this->template->posts = []; @@ -166,12 +190,23 @@ function renderGlobalFeed(): void $page = (int) ($_GET["p"] ?? 1); $pPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50); - - $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"; + + $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; + $queryBase .= "WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0"; 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(*)"}; @@ -180,7 +215,7 @@ function renderGlobalFeed(): void $this->template->paginatorConf = (object) [ "count" => $count, "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts), + "amount" => $posts->getRowCount(), "perPage" => $pPage, ]; foreach($posts as $post) @@ -212,11 +247,12 @@ function renderMakePost(int $wall): void $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) ?? $this->flashFail("err", tr("failed_to_publish_post"), tr("error_4")); + + if ($wallOwner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if($wall > 0) { - if(!$wallOwner->isBanned()) - $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); - else - $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); + $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); } else if($wall < 0) { if($wallOwner->canBeModifiedBy($this->user->identity)) $canPost = true; @@ -228,10 +264,7 @@ function renderMakePost(int $wall): void if(!$canPost) $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); - - if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) - $this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); - + $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { $manager = $wallOwner->getManager($this->user->identity); @@ -249,25 +282,22 @@ function renderMakePost(int $wall): void if($this->postParam("force_sign") === "on") $flags |= 0b01000000; - try { - $photo = NULL; - $video = NULL; - if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { - $album = NULL; - if(!$anon && $wall > 0 && $wall === $this->user->id) - $album = (new Albums)->getUserWallAlbum($wallOwner); - - $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon); + $horizontal_attachments = []; + $vertical_attachments = []; + if(!empty($this->postParam("horizontal_attachments"))) { + $horizontal_attachments_array = array_slice(explode(",", $this->postParam("horizontal_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if(sizeof($horizontal_attachments_array) > 0) { + $horizontal_attachments = parseAttachments($horizontal_attachments_array, ['photo', 'video']); } - - if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) - $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"], $anon); - } catch(\DomainException $ex) { - $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted")); - } catch(ISE $ex) { - $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large")); } - + + if(!empty($this->postParam("vertical_attachments"))) { + $vertical_attachments_array = array_slice(explode(",", $this->postParam("vertical_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); + if(sizeof($vertical_attachments_array) > 0) { + $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note']); + } + } + try { $poll = NULL; $xml = $this->postParam("poll"); @@ -278,10 +308,11 @@ function renderMakePost(int $wall): void } catch(\UnexpectedValueException $e) { $this->flashFail("err", tr("failed_to_publish_post"), "Poll format invalid"); } - - if(empty($this->postParam("text")) && !$photo && !$video && !$poll) + + if(empty($this->postParam("text")) && sizeof($horizontal_attachments) < 1 && sizeof($vertical_attachments) < 1 && !$poll) $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); + $should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2; try { $post = new Post; $post->setOwner($this->user->id); @@ -291,16 +322,36 @@ function renderMakePost(int $wall): void $post->setAnonymous($anon); $post->setFlags($flags); $post->setNsfw($this->postParam("nsfw") === "on"); + + if(!empty($this->postParam("source")) && $this->postParam("source") != 'none') { + try { + $post->setSource($this->postParam("source")); + } catch(\Throwable) {} + } + + if($should_be_suggested) + $post->setSuggested(1); + $post->save(); } catch (\LengthException $ex) { $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); } - if(!is_null($photo)) - $post->attach($photo); - - if(!is_null($video)) - $post->attach($video); + foreach($horizontal_attachments as $horizontal_attachment) { + if(!$horizontal_attachment || $horizontal_attachment->isDeleted() || !$horizontal_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $post->attach($horizontal_attachment); + } + + foreach($vertical_attachments as $vertical_attachment) { + if(!$vertical_attachment || $vertical_attachment->isDeleted() || !$vertical_attachment->canBeViewedBy($this->user->identity)) { + continue; + } + + $post->attach($vertical_attachment); + } if(!is_null($poll)) $post->attach($poll); @@ -312,12 +363,19 @@ function renderMakePost(int $wall): void if($wall > 0) $excludeMentions[] = $wall; - $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); - foreach($mentions as $mentionee) - if($mentionee instanceof User) - (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); + if(!$should_be_suggested) { + $mentions = iterator_to_array($post->resolveMentions($excludeMentions)); + + foreach($mentions as $mentionee) + if($mentionee instanceof User) + (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); + } - $this->redirect($wallOwner->getURL()); + if($should_be_suggested) { + $this->redirect("/club".$wallOwner->getId()."/suggested"); + } else { + $this->redirect($wallOwner->getURL()); + } } function renderPost(int $wall, int $post_id): void @@ -325,19 +383,25 @@ function renderPost(int $wall, int $post_id): void $post = $this->posts->getPostById($wall, $post_id); if(!$post || $post->isDeleted()) $this->notFound(); + + if(!$post->canBeViewedBy($this->user->identity)) + $this->flashFail("err", tr("error"), tr("forbidden")); $this->logPostView($post, $wall); $this->template->post = $post; if ($post->getTargetWall() > 0) { - $this->template->wallOwner = (new Users)->get($post->getTargetWall()); - $this->template->isWallOfGroup = false; + $this->template->wallOwner = (new Users)->get($post->getTargetWall()); + $this->template->isWallOfGroup = false; if($this->template->wallOwner->isBanned()) $this->flashFail("err", tr("error"), tr("forbidden")); - } else { - $this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall())); - $this->template->isWallOfGroup = true; - } + } else { + $this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall())); + $this->template->isWallOfGroup = true; + + if ($this->template->wallOwner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + } $this->template->cCount = $post->getCommentsCount(); $this->template->cPage = (int) ($_GET["p"] ?? 1); $this->template->comments = iterator_to_array($post->getComments($this->template->cPage)); @@ -351,10 +415,19 @@ function renderLike(int $wall, int $post_id): void $post = $this->posts->getPostById($wall, $post_id); if(!$post || $post->isDeleted()) $this->notFound(); - + + if ($post->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if(!is_null($this->user)) { $post->toggleLike($this->user->identity); } + + if($_SERVER["REQUEST_METHOD"] === "POST") { + $this->returnJson([ + 'success' => true, + ]); + } $this->redirect("$_SERVER[HTTP_REFERER]#postGarter=" . $post->getId()); } @@ -369,6 +442,9 @@ function renderShare(int $wall, int $post_id): void if(!$post || $post->isDeleted()) $this->notFound(); + + if ($post->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); $where = $this->postParam("type") ?? "wall"; $groupId = NULL; @@ -419,7 +495,7 @@ function renderDelete(int $wall, int $post_id): void $this->assertUserLoggedIn(); $this->willExecuteWriteAction(); - $post = $this->posts->getPostById($wall, $post_id); + $post = $this->posts->getPostById($wall, $post_id, true); if(!$post) $this->notFound(); $user = $this->user->id; @@ -427,10 +503,16 @@ function renderDelete(int $wall, int $post_id): void $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) ?? $this->flashFail("err", tr("failed_to_delete_post"), tr("error_4")); + if ($wallOwner->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); + if($wall < 0) $canBeDeletedByOtherUser = $wallOwner->canBeModifiedBy($this->user->identity); else $canBeDeletedByOtherUser = false; if(!is_null($user)) { + if($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->user->identity) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0) + $this->flashFail("err", tr("failed_to_delete_post"), tr("error_deleting_suggested")); + if($post->getOwnerPost() == $user || $post->getTargetWall() == $user || $canBeDeletedByOtherUser) { $post->unwire(); $post->delete(); @@ -450,6 +532,9 @@ function renderPin(int $wall, int $post_id): void $post = $this->posts->getPostById($wall, $post_id); if(!$post) $this->notFound(); + + if ($post->getWallOwner()->isBanned()) + $this->flashFail("err", tr("error"), tr("forbidden")); if(!$post->canBePinnedBy($this->user->identity)) $this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); @@ -463,4 +548,133 @@ function renderPin(int $wall, int $post_id): void # TODO localize message based on language and ?act=(un)pin $this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment")); } + + function renderAccept() { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + if($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + exit("Ты дебил, это точка апи."); + } + + $id = $this->postParam("id"); + $sign = $this->postParam("sign") == 1; + $content = $this->postParam("new_content"); + + $post = (new Posts)->get((int)$id); + + if(!$post || $post->isDeleted()) + $this->flashFail("err", "Error", tr("error_accepting_invalid_post"), NULL, true); + + if($post->getSuggestionType() == 0) + $this->flashFail("err", "Error", tr("error_accepting_not_suggested_post"), NULL, true); + + if($post->getSuggestionType() == 2) + $this->flashFail("err", "Error", tr("error_accepting_declined_post"), NULL, true); + + if(!$post->canBePinnedBy($this->user->identity)) + $this->flashFail("err", "Error", "Can't accept this post.", NULL, true); + + $author = $post->getOwner(); + + $flags = 0; + $flags |= 0b10000000; + + if($sign) + $flags |= 0b01000000; + + $post->setSuggested(0); + $post->setCreated(time()); + $post->setApi_Source_Name(NULL); + $post->setFlags($flags); + + if(mb_strlen($content) > 0) + $post->setContent($content); + + $post->save(); + + if($author->getId() != $this->user->id) + (new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit(); + + $this->returnJson([ + "success" => true, + "id" => $post->getPrettyId(), + "new_count" => (new Posts)->getSuggestedPostsCount($post->getWallOwner()->getId()) + ]); + } + + function renderDecline() { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + if($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + exit("Ты дебил, это метод апи."); + } + + $id = $this->postParam("id"); + $post = (new Posts)->get((int)$id); + + if(!$post || $post->isDeleted()) + $this->flashFail("err", "Error", tr("error_declining_invalid_post"), NULL, true); + + if($post->getSuggestionType() == 0) + $this->flashFail("err", "Error", tr("error_declining_not_suggested_post"), NULL, true); + + if($post->getSuggestionType() == 2) + $this->flashFail("err", "Error", tr("error_declining_declined_post"), NULL, true); + + if(!$post->canBePinnedBy($this->user->identity)) + $this->flashFail("err", "Error", "Can't decline this post.", NULL, true); + + $post->setSuggested(2); + $post->setDeleted(1); + $post->save(); + + $this->returnJson([ + "success" => true, + "new_count" => (new Posts)->getSuggestedPostsCount($post->getWallOwner()->getId()) + ]); + } + + function renderLikers(string $type, int $owner_id, int $item_id) + { + $this->assertUserLoggedIn(); + + $item = NULL; + $display_name = $type; + switch($type) { + default: + $this->notFound(); + break; + case 'wall': + $item = $this->posts->getPostById($owner_id, $item_id); + $display_name = 'post'; + break; + case 'comment': + $item = (new \openvk\Web\Models\Repositories\Comments)->get($item_id); + break; + case 'photo': + $item = (new \openvk\Web\Models\Repositories\Photos)->getByOwnerAndVID($owner_id, $item_id); + break; + case 'video': + $item = (new \openvk\Web\Models\Repositories\Videos)->getByOwnerAndVID($owner_id, $item_id); + break; + } + + if(!$item || $item->isDeleted() || !$item->canBeViewedBy($this->user->identity)) + $this->notFound(); + + $page = (int)($this->queryParam('p') ?? 1); + $count = $item->getLikesCount(); + $likers = iterator_to_array($item->getLikers($page, OPENVK_DEFAULT_PER_PAGE)); + + $this->template->item = $item; + $this->template->type = $display_name; + $this->template->iterator = $likers; + $this->template->count = $count; + $this->template->page = $page; + $this->template->perPage = OPENVK_DEFAULT_PER_PAGE; + } } diff --git a/Web/Presenters/templates/@banned.xml b/Web/Presenters/templates/@banned.xml index 48c29ddbe..7640838c1 100644 --- a/Web/Presenters/templates/@banned.xml +++ b/Web/Presenters/templates/@banned.xml @@ -10,8 +10,19 @@ {_banned_alt}

- {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}
- {tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape} + {var $ban = $thisUser->getBanReason("banned")} + {if is_string($ban)} + {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}
+ {tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape} + {else} + {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape} +

+ Эта страница была заморожена {$ban[0]|noescape} + {if $ban[1] !== "app"} + {include "Report/ViewContent.xml", type => $ban[1], object => $ban[2]} + {/if} +
+ {/if} {if !$thisUser->getUnbanTime()} {_banned_perm} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 7bb7e194f..b7d84b96d 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -13,17 +13,27 @@ {script "js/node_modules/jquery/dist/jquery.min.js"} + {script "js/node_modules/jquery-ui/dist/jquery-ui.min.js"} {script "js/node_modules/umbrellajs/umbrella.min.js"} {script "js/l10n.js"} {script "js/openvk.cls.js"} + {script "js/utils.js"} + {script "js/node_modules/dashjs/dist/dash.all.min.js"} + {css "js/node_modules/tippy.js/dist/backdrop.css"} + {css "js/node_modules/cropperjs/dist/cropper.css"} {css "js/node_modules/tippy.js/dist/border.css"} {css "js/node_modules/tippy.js/dist/svg-arrow.css"} {css "js/node_modules/tippy.js/themes/light.css"} + {css "js/node_modules/jquery-ui/themes/base/resizable.css"} {script "js/node_modules/@popperjs/core/dist/umd/popper.min.js"} {script "js/node_modules/tippy.js/dist/tippy-bundle.umd.min.js"} {script "js/node_modules/handlebars/dist/handlebars.min.js"} + {script "js/node_modules/react/dist/react-with-addons.min.js"} + {script "js/node_modules/react-dom/dist/react-dom.min.js"} + {script "js/vnd_literallycanvas.js"} + {css "js/node_modules/literallycanvas/lib/css/literallycanvas.css"} {if $isTimezoned == NULL} {script "js/timezone.js"} @@ -38,16 +48,40 @@

- Вы вошли как {$thisUser->getCanonicalName()}. Пожалуйста, уважайте - право на тайну переписки других людей и не злоупотребляйте подменой пользователя. - Нажмите здесь, чтобы выйти. + {_you_entered_as} {$thisUser->getCanonicalName()}. {_please_rights} + {_click_on} {_there}, {_to_leave}.

FOR TESTING PURPOSES ONLY
-
+
+
+
+
+ +
+ {_close} +
+
+
+ +
+ + +
+
+ + +
+ +
+
+
+
{if isset($backdrops) && !is_null($backdrops)}
@@ -56,12 +90,19 @@ {/if}
- ⬆ {_to_top} +
+ + {_to_top} +
+ +
+ +
-
+
{if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}{$instance_name}{/if}
{ifset $thisUser} @@ -70,60 +111,45 @@ {_header_log_out}
{else} -