Merge branch 'blacklist' into blacklist

This commit is contained in:
Vladimir Barinov 2023-06-15 12:36:25 +03:00 committed by GitHub
commit 2f99196fe5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
332 changed files with 17963 additions and 4454 deletions

58
.github/workflows/build-base.yaml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Build base images
on:
schedule:
- cron: '0 0 * * *'
env:
BASE_IMAGE_NAME: php
BASE_IMAGE_VERSION: "8.1"
jobs:
build-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
lfs: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build cli image
run: |
IMAGE_NAME=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-cli
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/base-php-cli.Dockerfile --build-arg VERSION=$BASE_IMAGE_VERSION
build-apache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
lfs: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build apache image
run: |
IMAGE_NAME=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME:$BASE_IMAGE_VERSION-apache
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/base-php-apache.Dockerfile --build-arg VERSION=$BASE_IMAGE_VERSION

64
.github/workflows/build.yaml vendored Normal file
View file

@ -0,0 +1,64 @@
name: Build images
on:
push:
# Publish `master` as Docker `latest` image.
branches:
- master
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
BASE_IMAGE_NAME: openvk
DB_IMAGE_NAME: mariadb
EVENT_IMAGE_NAME: mariadb
DB_VERSION: "10.9"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
arch: ['x86_64']
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
with:
lfs: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build base image
run: |
IMAGE_ID=ghcr.io/${{ github.repository }}/$BASE_IMAGE_NAME
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
[ "$VERSION" == "master" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_ID:$VERSION . --push -f install/automated/docker/openvk.Dockerfile --build-arg GITREPO=${{ github.repository }}
- name: Build MariaDB primary image
run: |
IMAGE_NAME=ghcr.io/${{ github.repository }}/$DB_IMAGE_NAME:$DB_VERSION-primary
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/mariadb-primary.Dockerfile --build-arg VERSION=$DB_VERSION
- name: Build MariaDB event image
run: |
IMAGE_NAME=ghcr.io/${{ github.repository }}/$EVENT_IMAGE_NAME:$DB_VERSION-eventdb
docker buildx build --platform linux/amd64,linux/arm64 -t $IMAGE_NAME . --push -f install/automated/docker/mariadb-eventdb.Dockerfile --build-arg VERSION=$DB_VERSION

15
.github/workflows/codeberg-mirror.yml vendored Normal file
View file

@ -0,0 +1,15 @@
name: Codeberg Mirroring
on: push
jobs:
to_codeberg:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: "git@codeberg.org:openvk/openvk.git"
ssh_private_key: ${{ secrets.CODEBERG_MIRRORSSH }}

4
.gitignore vendored
View file

@ -1,5 +1,6 @@
vendor vendor
openvk.yml openvk.yml
chandler.yml
update.pid update.pid
update.pid.old update.pid.old
Web/static/js/node_modules Web/static/js/node_modules
@ -10,5 +11,8 @@ tmp/*
themepacks/* themepacks/*
!themepacks/.gitkeep !themepacks/.gitkeep
!themepacks/openvk_modern !themepacks/openvk_modern
!themepacks/midnight
storage/* storage/*
!storage/.gitkeep !storage/.gitkeep
.idea

View file

@ -1,82 +0,0 @@
FROM fedora:33
#update and install httpd
RUN dnf -y update && dnf -y autoremove && dnf install -y httpd
#Let's install Remi repos for PHP 7.4:
RUN dnf -y install https://rpms.remirepo.net/fedora/remi-release-$(rpm -E %fedora).rpm
#Then enable modules that we need:
RUN dnf -y module enable php:remi-7.4 && \
dnf -y module enable nodejs:14
#And install dependencies:
RUN dnf -y --skip-broken install php php-cli php-common unzip php-zip php-yaml php-gd php-pdo_mysql nodejs git
#Don't forget about Yarn and Composer:
RUN npm i -g yarn && \
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php --filename=composer2 --install-dir=/bin --snapshot && \
rm composer-setup.php
#We will use Mariadb for DB:
RUN dnf -y install mysql mysql-server && \
systemctl enable mariadb && \
echo 'skip-grant-tables' >> /etc/my.cnf
#Additionally, you can install ffmpeg for processing videos.
#You will need to use RPMFusion repo to install it:
RUN dnf -y install --nogpgcheck https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm && \
dnf -y install --nogpgcheck https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
#Then install SDL2 and ffmpeg:
RUN dnf -y install --nogpgcheck SDL2 ffmpeg
#Install Chandler and OpenVk/Capcha-extention in /opt:
RUN cd /opt && \
git clone https://github.com/samukhin/chandler.git && \
cd chandler/ && \
composer2 install && \
mv chandler-example.yml chandler.yml && \
cd extensions/available/ && \
git clone https://github.com/samukhin/commitcaptcha.git && \
cd commitcaptcha/ && \
composer2 install && \
cd .. && \
git clone https://github.com/samukhin/openvk.git && \
cd openvk/ && \
composer2 install && \
cd Web/static/js && \
yarn install && \
cd ../../../ && \
mv openvk-example.yml openvk.yml && \
ln -s /opt/chandler/extensions/available/commitcaptcha/ /opt/chandler/extensions/enabled/commitcaptcha && \
ln -s /opt/chandler/extensions/available/openvk/ /opt/chandler/extensions/enabled/openvk
#Create database
RUN cp /opt/chandler/extensions/available/openvk/install/automated/common/create_db.service /etc/systemd/system/ && \
chmod 644 /etc/systemd/system/create_db.service && \
chmod 777 /opt/chandler/extensions/available/openvk/install/automated/common/autoexec && \
systemctl enable create_db
#Make the user apache owner of the chandler folder:
RUN cd /opt && \
chown -R apache: chandler/
#Now let's create config file /etc/httpd/conf.d/10-openvk.conf and
#Also enable rewrite_module by creating /etc/httpd/conf.modules.d/02-rewrite.conf
RUN cp /opt/chandler/extensions/available/openvk/install/automated/common/10-openvk.conf /etc/httpd/conf.d/ && \
cp /opt/chandler/extensions/available/openvk/install/automated/common/02-rewrite.conf /etc/httpd/conf.modules.d/
#Make directory for OpenVK logs and make the user apache owner of it:
RUN mkdir /var/log/openvk && \
chown apache: /var/log/openvk/
#And start Apache:
#RUN systemctl enable httpd
#For login
RUN dnf -y install passwd && passwd -d root
#Start systemd
CMD ["/sbin/init"]

View file

@ -2,29 +2,25 @@
_[Русский](README_RU.md)_ _[Русский](README_RU.md)_
**OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VK. Code provided here is not stable yet. **OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VKontakte. Code provided here is not stable yet.
VKontakte belongs to Pavel Durov and VK Group. VKontakte belongs to Pavel Durov and VK Group.
To be honest, we don't know whether it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug-tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OVK account for this). To be honest, we don't know whether if it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OpenVK account for this).
## When's the release? ## When's the release?
We will release OpenVK as soon as it's ready. As for now you can: We will release OpenVK as soon as it's ready. As for now, you can:
* `git clone` this repo's master branch (use `git pull` to update) * `git clone` this repo's master branch (use `git pull` to update)
* Grab a prebuilt OpenVK distro from [GitHub artifacts](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip) * Grab a prebuilt OpenVK distro from [GitHub artifacts](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip)
## Instances ## Instances
* **[openvk.su](https://openvk.su/)** A list of instances can be found in [our wiki of this repository](https://github.com/openvk/openvk/wiki/Instances).
* **[openvk.uk](https://openvk.uk)** - official mirror of openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - yet another official mirror of openvk.su without TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
## Can I create my own OpenVK instance? ## Can I create my own OpenVK instance?
Yes! And you're very welcome to. Yes! And you are very welcome to.
However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. these extensions are available on most of ISPManager hostings). However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. these extensions are available on most of ISPManager hostings).
@ -34,12 +30,12 @@ If you want, you can add your instance to the list above so that people can regi
1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler)
* PHP 8.1 is supported, but it was not tested carefully, be aware of that. * PHP 8.1 is supported too, however it was not tested carefully, so be aware.
2. Install MySQL-compatible database. 2. Install MySQL-compatible database.
* We recommend using Percona Server, but any MySQL-compatible server should work * We recommend using Percona Server, but any MySQL-compatible server should work too.
* Server should be compatible with at least MySQL 5.6, MySQL 8.0+ recommended. * Server should be compatible with at least MySQL 5.6, MySQL 8.0+ is recommended.
* Support for MySQL 4.1+ is WIP, replace `utf8mb4` and `utf8mb4_unicode_520_ci` with `utf8` and `utf8_unicode_ci` in SQLs. * Support for MySQL 4.1+ is WIP, replace `utf8mb4` and `utf8mb4_unicode_520_ci` with `utf8` and `utf8_unicode_ci` in SQLs.
3. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this: 3. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this:
@ -70,24 +66,27 @@ Once you are done, you can login as a system administrator on the network itself
* **Password**: `admin` * **Password**: `admin`
* It is recommended to change the password of the built-in account or disable it. * 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.su/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.openvk.uk/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 it's sources?
It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you're planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc). It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you are planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc).
## Where can I get assistance? ## Where can I get assistance?
You may reach out to us via: You may reach out to us via:
* [Bug-tracker](https://github.com/openvk/openvk/projects/1) * [Bug Tracker](https://github.com/openvk/openvk/projects/1)
* [Ticketing system](https://openvk.su/support?act=new) * [Ticketing System](https://openvk.su/support?act=new)
* Telegram chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu. * Telegram Chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu.
* [Reddit](https://www.reddit.com/r/openvk/) * [Reddit](https://www.reddit.com/r/openvk/)
* [Discussions](https://github.com/openvk/openvk/discussions) * [GitHub Discussions](https://github.com/openvk/openvk/discussions)
* Matrix chat: #openvk:matrix.org * Matrix Chat: #openvk:matrix.org
**Attention**: bug tracker, board, telegram and matrix chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), please use contact us directly at this email: **openvk [at] tutanota [dot] com** **Attention**: bug tracker, board, Telegram and Matrix chat are public places, ticketing system is being served by volunteers. If you need to report something that should not be immediately disclosed to general public (for instance, a vulnerability), please contact us directly via this email: **openvk [at] tutanota [dot] com**
<a href="https://codeberg.org/OpenVK/openvk"> <a href="https://codeberg.org/OpenVK/openvk">
<img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60"> <img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">

View file

@ -2,11 +2,11 @@
_[English](README.md)_ _[English](README.md)_
**OpenVK** - это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент представленный здесь исходный код проекта пока не является стабильным. **OpenVK** это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент, представленный здесь исходный код проекта пока не является стабильным.
ВКонтакте принадлежит Павлу Дурову и VK Group. ВКонтакте принадлежит Павлу Дурову и VK Group.
Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OVK). Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OpenVK).
## Когда выйдет релизная версия? ## Когда выйдет релизная версия?
@ -16,19 +16,15 @@ _[English](README.md)_
## Инстанции ## Инстанции
* **[openvk.su](https://openvk.su/)** Список инстанций находится в [нашей вики этого репозитория](https://github.com/openvk/openvk/wiki/Instances-(RU)).
* **[openvk.uk](https://openvk.uk)** - официальное зеркало openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - ещё одно официальное зеркало openvk.su без TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
## Могу ли я создать свою собственную инстанцию OpenVK? ## Могу ли я создать свою собственную инстанцию OpenVK?
Да! И всегда пожалуйста. Да! И всегда пожалуйста.
Однако, OVK использует Chandler Application Server. Это программное обеспечение требует расширений, которые могут быть не предоставлены вашим хостинг-провайдером (а именно, sodium и yaml. эти расширения доступны на большинстве хостингов ISPManager). Однако, OpenVK использует Chandler Application Server. Это программное обеспечение требует расширений, которые могут быть не предоставлены вашим хостинг-провайдером (а именно, sodium и yaml. Эти расширения доступны на большинстве хостингов ISPManager).
Если вы хотите, вы можете добавить вашу инстанцию в список выше, чтобы люди могли зарегистрироваться там. Если хотите, вы можете добавить вашу инстанцию в список выше, чтобы люди могли зарегистрироваться там.
### Процедура установки ### Процедура установки
@ -38,7 +34,7 @@ _[English](README.md)_
2. Установите MySQL-совместимую базу данных. 2. Установите MySQL-совместимую базу данных.
* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать * Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать.
* Сервер должен поддерживать хотя бы MySQL 5.6, рекомендуется использовать MySQL 8.0+. * Сервер должен поддерживать хотя бы MySQL 5.6, рекомендуется использовать MySQL 8.0+.
* Поддержка для MySQL 4.1+ находится в процессе, а пока замените `utf8mb4` и `utf8mb4_unicode_520_ci` на `utf8` и `utf8_unicode_ci` в SQL-файлах, соответственно. * Поддержка для MySQL 4.1+ находится в процессе, а пока замените `utf8mb4` и `utf8mb4_unicode_520_ci` на `utf8` и `utf8_unicode_ci` в SQL-файлах, соответственно.
@ -70,7 +66,10 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* **Пароль**: `admin` * **Пароль**: `admin`
* Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её. * Перед использованием встроенной учетной записи рекомендуется сменить пароль или отключить её.
💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.su/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)). 💡Запутались? Полное руководство по установке доступно [здесь](https://docs.openvk.uk/openvk_engine/centos8_installation/) (CentOS 8 [и](https://almalinux.org/ru/) [семейство](https://yum.oracle.com/oracle-linux-isos.html)).
# Установка в Docker/Kubernetes
Подробные иструкции можно найти в `install/automated/docker/README.md` и `install/automated/kubernetes/README.md` соответственно.
### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты? ### Если мой сайт использует OpenVK, должен ли я публиковать его исходные тексты?
@ -84,10 +83,10 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* [Помощь в OVK](https://openvk.su/support?act=new) * [Помощь в OVK](https://openvk.su/support?act=new)
* Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала. * Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала.
* [Reddit](https://www.reddit.com/r/openvk/) * [Reddit](https://www.reddit.com/r/openvk/)
* [Обсуждения](https://github.com/openvk/openvk/discussions) * [GitHub Discussions](https://github.com/openvk/openvk/discussions)
* Чат в Matrix: #ovk:matrix.org * Чат в Matrix: #ovk:matrix.org
**Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**. **Внимание**: баг-трекер, форум, Telegram- и Matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собачка] tutanota [точка] com**.
<a href="https://codeberg.org/OpenVK/openvk"> <a href="https://codeberg.org/OpenVK/openvk">
<img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60"> <img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">

View file

@ -55,6 +55,11 @@ class Apps implements Handler
return; return;
} }
if($amount < 0) {
$reject(552, "Payment amount is invalid");
return;
}
$coinsLeft = $this->user->getCoins() - $amount; $coinsLeft = $this->user->getCoins() - $amount;
if($coinsLeft < 0) { if($coinsLeft < 0) {
$reject(41, "Not enough money"); $reject(41, "Not enough money");

39
ServiceAPI/Groups.php Normal file
View file

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Clubs;
class Groups implements Handler
{
protected $user;
protected $groups;
function __construct(?User $user)
{
$this->user = $user;
$this->groups = new Clubs;
}
function getWriteableClubs(callable $resolve, callable $reject)
{
$clubs = [];
$wclubs = $this->groups->getWriteableClubs($this->user->getId());
$count = $this->groups->getWriteableClubsCount($this->user->getId());
if(!$count) {
$reject("You don't have any groups with write access");
return;
}
foreach($wclubs as $club) {
$clubs[] = [
"name" => $club->getName(),
"id" => $club->getId(),
"avatar" => $club->getAvatarUrl() # если в овк когда-нибудь появится крутой список с аватарками, то можно использовать это поле
];
}
$resolve($clubs);
}
}

50
ServiceAPI/Mentions.php Normal file
View file

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Users, Clubs};
class Mentions implements Handler
{
protected $user;
function __construct(?User $user)
{
$this->user = $user;
}
function resolve(int $id, callable $resolve, callable $reject): void
{
if($id > 0) {
$user = (new Users)->get($id);
if(!$user) {
$reject("Not found");
return;
}
$resolve([
"url" => $user->getURL(),
"name" => $user->getFullName(),
"ava" => $user->getAvatarURL("miniscule"),
"about" => $user->getStatus() ?? "",
"online" => ($user->isFemale() ? tr("was_online_f") : tr("was_online_m")) . " " . $user->getOnline(),
"verif" => $user->isVerified(),
]);
return;
}
$club = (new Clubs)->get(abs($id));
if(!$club) {
$reject("Not found");
return;
}
$resolve([
"url" => $club->getURL(),
"name" => $club->getName(),
"ava" => $club->getAvatarURL("miniscule"),
"about" => $club->getDescription() ?? "",
"online" => tr("participants", $club->getFollowersCount()),
"verif" => $club->isVerified(),
]);
}
}

70
ServiceAPI/Polls.php Normal file
View file

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use Chandler\MVC\Routing\Router;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Exceptions\{AlreadyVotedException, InvalidOptionException, PollLockedException};
use openvk\Web\Models\Repositories\Polls as PollRepo;
use UnexpectedValueException;
class Polls implements Handler
{
protected $user;
protected $polls;
function __construct(?User $user)
{
$this->user = $user;
$this->polls = new PollRepo;
}
private function getPollHtml(int $poll): string
{
return Router::i()->execute("/poll$poll", "SAPI");
}
function vote(int $pollId, string $options, callable $resolve, callable $reject): void
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$options = explode(",", $options);
$poll->vote($this->user, $options);
} catch(AlreadyVotedException $ex) {
$reject("Poll state changed: user has already voted.");
return;
} catch(PollLockedException $ex) {
$reject("Poll state changed: poll has ended.");
return;
} catch(InvalidOptionException $ex) {
$reject("Foreign options passed.");
return;
} catch(UnexpectedValueException $ex) {
$reject("Too much options passed.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
function unvote(int $pollId, callable $resolve, callable $reject): void
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$poll->revokeVote($this->user);
} catch(PollLockedException $ex) {
$reject("Votes can't be revoked from this poll.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
}

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Exceptions\InvalidUserNameException;
final class Account extends VKAPIRequestHandler final class Account extends VKAPIRequestHandler
{ {
@ -13,7 +14,7 @@ final class Account extends VKAPIRequestHandler
"last_name" => $this->getUser()->getLastName(), "last_name" => $this->getUser()->getLastName(),
"home_town" => $this->getUser()->getHometown(), "home_town" => $this->getUser()->getHometown(),
"status" => $this->getUser()->getStatus(), "status" => $this->getUser()->getStatus(),
"bdate" => $this->getUser()->getBirthday()->format('%e.%m.%Y'), "bdate" => is_null($this->getUser()->getBirthday()) ? '01.01.1970' : $this->getUser()->getBirthday()->format('%e.%m.%Y'),
"bdate_visibility" => $this->getUser()->getBirthdayPrivacy(), "bdate_visibility" => $this->getUser()->getBirthdayPrivacy(),
"phone" => "+420 ** *** 228", # TODO "phone" => "+420 ** *** 228", # TODO
"relation" => $this->getUser()->getMaritalStatus(), "relation" => $this->getUser()->getMaritalStatus(),
@ -44,13 +45,12 @@ final class Account extends VKAPIRequestHandler
{ {
$this->requireUser(); $this->requireUser();
$this->getUser()->setOnline(time()); $this->getUser()->updOnline($this->getPlatform());
$this->getUser()->save();
return 1; return 1;
} }
function setOffline(): object function setOffline(): int
{ {
$this->requireUser(); $this->requireUser();
@ -66,6 +66,8 @@ final class Account extends VKAPIRequestHandler
function getCounters(string $filter = ""): object function getCounters(string $filter = ""): object
{ {
$this->requireUser();
return (object) [ return (object) [
"friends" => $this->getUser()->getFollowersCount(), "friends" => $this->getUser()->getFollowersCount(),
"notifications" => $this->getUser()->getNotificationsCount(), "notifications" => $this->getUser()->getNotificationsCount(),
@ -74,4 +76,79 @@ final class Account extends VKAPIRequestHandler
# TODO: Filter # TODO: Filter
} }
function saveProfileInfo(string $first_name = "", string $last_name = "", string $screen_name = "", int $sex = -1, int $relation = -1, string $bdate = "", int $bdate_visibility = -1, string $home_town = "", string $status = ""): object
{
$this->requireUser();
$this->willExecuteWriteAction();
$user = $this->getUser();
$output = [
"changed" => 0,
];
if(!empty($first_name) || !empty($last_name)) {
$output["name_request"] = [
"id" => random_int(1, 2048), # For compatibility with original VK API
"status" => "success",
"first_name" => !empty($first_name) ? $first_name : $user->getFirstName(),
"last_name" => !empty($last_name) ? $last_name : $user->getLastName(),
];
try {
if(!empty($first_name))
$user->setFirst_name($first_name);
if(!empty($last_name))
$user->setLast_Name($last_name);
} catch (InvalidUserNameException $e) {
$output["name_request"]["status"] = "declined";
return (object) $output;
}
}
if(!empty($screen_name))
if (!$user->setShortCode($screen_name))
$this->fail(1260, "Invalid screen name");
# For compatibility with original VK API
if($sex > 0)
$user->setSex($sex == 1 ? 1 : 0);
if($relation > -1)
$user->setMarital_Status($relation);
if(!empty($bdate)) {
$birthday = strtotime($bdate);
if (!is_int($birthday))
$this->fail(100, "invalid value of bdate.");
$user->setBirthday($birthday);
}
# For compatibility with original VK API
switch($bdate_visibility) {
case 0:
$this->fail(946, "Hiding date of birth is not implemented.");
break;
case 1:
$user->setBirthday_privacy(0);
break;
case 2:
$user->setBirthday_privacy(1);
}
if(!empty($home_town))
$user->setHometown($home_town);
if(!empty($status))
$user->setStatus($status);
if($sex > 0 || $relation > -1 || $bdate_visibility > 1 || !empty("$first_name$last_name$screen_name$bdate$home_town$status")) {
$output["changed"] = 1;
$user->save();
}
return (object) $output;
}
} }

429
VKAPI/Handlers/Board.php Normal file
View file

@ -0,0 +1,429 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\VKAPI\Handlers\Wall;
use openvk\Web\Models\Repositories\Topics as TopicsRepo;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
use openvk\Web\Models\Repositories\Photos as PhotosRepo;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
use openvk\Web\Models\Entities\{Topic, Comment, User, Photo, Video};
final class Board extends VKAPIRequestHandler
{
# 13/13
function addTopic(int $group_id, string $title, string $text = "", bool $from_group = true, string $attachments = "")
{
$this->requireUser();
$this->willExecuteWriteAction();
$club = (new ClubsRepo)->get($group_id);
if(!$club) {
$this->fail(403, "Invalid club");
}
if(!$club->canBeModifiedBy($this->getUser()) && !$club->isEveryoneCanCreateTopics()) {
$this->fail(403, "Access to club denied");
}
$flags = 0;
if($from_group == true && $club->canBeModifiedBy($this->getUser()))
$flags |= 0b10000000;
$topic = new Topic;
$topic->setGroup($club->getId());
$topic->setOwner($this->getUser()->getId());
$topic->setTitle(ovk_proc_strtr($title, 127));
$topic->setCreated(time());
$topic->setFlags($flags);
$topic->save();
if(!empty($text)) {
$comment = new Comment;
$comment->setOwner($this->getUser()->getId());
$comment->setModel(get_class($topic));
$comment->setTarget($topic->getId());
$comment->setContent($text);
$comment->setCreated(time());
$comment->setFlags($flags);
$comment->save();
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);
}
}
}
}
return $topic->getId();
}
function closeTopic(int $group_id, int $topic_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$topic = (new TopicsRepo)->getTopicById($group_id, $topic_id);
if(!$topic || !$topic->getClub() || !$topic->getClub()->canBeModifiedBy($this->getUser())) {
return 0;
}
if(!$topic->isClosed()) {
$topic->setClosed(1);
$topic->save();
}
return 1;
}
function createComment(int $group_id, int $topic_id, string $message = "", string $attachments = "", bool $from_group = true)
{
$this->requireUser();
$this->willExecuteWriteAction();
if(empty($message) && empty($attachments)) {
$this->fail(100, "Required parameter 'message' missing.");
}
$topic = (new TopicsRepo)->getTopicById($group_id, $topic_id);
if(!$topic || $topic->isDeleted() || $topic->isClosed()) {
$this->fail(100, "Topic is deleted, closed or invalid.");
}
$flags = 0;
if($from_group != 0 && !is_null($topic->getClub()) && $topic->getClub()->canBeModifiedBy($this->user))
$flags |= 0b10000000;
if(strlen($message) > 300) {
$this->fail(20, "Comment is too long.");
}
$comment = new Comment;
$comment->setOwner($this->getUser()->getId());
$comment->setModel(get_class($topic));
$comment->setTarget($topic->getId());
$comment->setContent($message);
$comment->setCreated(time());
$comment->setFlags($flags);
$comment->save();
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);
}
}
}
return $comment->getId();
}
function deleteComment(int $comment_id, int $group_id = 0, int $topic_id = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
$comment = (new CommentsRepo)->get($comment_id);
if($comment->isDeleted() || !$comment || !$comment->canBeDeletedBy($this->getUser()))
$this->fail(403, "Access to comment denied");
$comment->delete();
return 1;
}
function deleteTopic(int $group_id, int $topic_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$topic = (new TopicsRepo)->getTopicById($group_id, $topic_id);
if(!$topic || !$topic->getClub() || $topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) {
return 0;
}
$topic->deleteTopic();
return 1;
}
function editComment(int $comment_id, int $group_id = 0, int $topic_id = 0, string $message, string $attachments)
{
/*
$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 editTopic(int $group_id, int $topic_id, string $title)
{
$this->requireUser();
$this->willExecuteWriteAction();
$topic = (new TopicsRepo)->getTopicById($group_id, $topic_id);
if(!$topic || !$topic->getClub() || $topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) {
return 0;
}
$topic->setTitle(ovk_proc_strtr($title, 127));
$topic->save();
return 1;
}
function fixTopic(int $group_id, int $topic_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$topic = (new TopicsRepo)->getTopicById($group_id, $topic_id);
if(!$topic || !$topic->getClub() || !$topic->getClub()->canBeModifiedBy($this->getUser())) {
return 0;
}
$topic->setPinned(1);
$topic->save();
return 1;
}
function getComments(int $group_id, int $topic_id, bool $need_likes = false, int $start_comment_id = 0, int $offset = 0, int $count = 40, bool $extended = false, string $sort = "asc")
{
# 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())) {
$this->fail(5, "Invalid topic");
}
$arr = [
"items" => []
];
$comms = array_slice(iterator_to_array($topic->getComments(1, $count + $offset)), $offset);
foreach($comms as $comm) {
$arr["items"][] = $this->getApiBoardComment($comm, $need_likes);
if($extended) {
if($comm->getOwner() instanceof \openvk\Web\Models\Entities\User) {
$arr["profiles"][] = $comm->getOwner()->toVkApiStruct();
}
if($comm->getOwner() instanceof \openvk\Web\Models\Entities\Club) {
$arr["groups"][] = $comm->getOwner()->toVkApiStruct();
}
}
}
return $arr;
}
function getTopics(int $group_id, string $topic_ids = "", int $order = 1, int $offset = 0, int $count = 40, bool $extended = false, int $preview = 0, int $preview_length = 90)
{
# 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["profiles"] = [];
if(empty($topic_ids)) {
foreach($topics as $topic) {
if($topic->isDeleted()) continue;
$arr["items"][] = $topic->toVkApiStruct($preview, $preview_length);
}
} else {
$topics = explode(',', $topic_ids);
foreach($topics as $topic) {
$id = explode("_", $topic);
$topicy = (new TopicsRepo)->getTopicById((int)$id[0], (int)$id[1]);
if($topicy) {
$arr["items"] = $topicy->toVkApiStruct();
}
}
}
return $arr;
}
function openTopic(int $group_id, int $topic_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$topic = (new TopicsRepo)->getTopicById($group_id, $topic_id);
if(!$topic || !$topic->getClub() || !$topic->isDeleted() || !$topic->getClub()->canBeModifiedBy($this->getUser())) {
return 0;
}
if($topic->isClosed()) {
$topic->setClosed(0);
$topic->save();
}
return 1;
}
function restoreComment(int $group_id, int $topic_id, int $comment_id)
{
$this->fail(501, "Not implemented");
}
function unfixTopic(int $group_id, int $topic_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$topic = (new TopicsRepo)->getTopicById($group_id, $topic_id);
if(!$topic || !$topic->getClub() || !$topic->getClub()->canBeModifiedBy($this->getUser())) {
return 0;
}
if($topic->isPinned()) {
$topic->setClosed(0);
$topic->save();
}
$topic->setPinned(0);
$topic->save();
return 1;
}
private function getApiBoardComment(?Comment $comment, bool $need_likes = false)
{
$res = (object) [];
$res->id = $comment->getId();
$res->from_id = $comment->getOwner()->getId();
$res->date = $comment->getPublicationTime()->timestamp();
$res->text = $comment->getText();
$res->attachments = [];
$res->likes = [];
if($need_likes) {
$res->likes = [
"count" => $comment->getLikesCount(),
"user_likes" => (int) $comment->hasLikeFrom($this->getUser()),
"can_like" => 1 # а чё типо не может ахахаххахах
];
}
foreach($comment->getChildren() as $attachment) {
if($attachment->isDeleted())
continue;
$res->attachments[] = $attachment->toVkApiStruct();
}
return $res;
}
}

View file

@ -66,6 +66,7 @@ final class Friends extends VKAPIRequestHandler
function add(string $user_id): int function add(string $user_id): int
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
$users = new UsersRepo; $users = new UsersRepo;
$user = $users->get(intval($user_id)); $user = $users->get(intval($user_id));
@ -96,6 +97,7 @@ final class Friends extends VKAPIRequestHandler
function delete(string $user_id): int function delete(string $user_id): int
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
$users = new UsersRepo; $users = new UsersRepo;
@ -107,7 +109,7 @@ final class Friends extends VKAPIRequestHandler
return 1; return 1;
default: default:
fail(15, "Access denied: No friend or friend request found."); $this->fail(15, "Access denied: No friend or friend request found.");
} }
} }
@ -152,10 +154,7 @@ final class Friends extends VKAPIRequestHandler
$response = $followers; $response = $followers;
$usersApi = new Users($this->getUser()); $usersApi = new Users($this->getUser());
if($extended == 1)
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count); $response = $usersApi->get(implode(',', $followers), $fields, 0, $count);
else
$response = $usersApi->get(implode(',', $followers), "", 0, $count);
foreach($response as $user) foreach($response as $user)
$user->user_id = $user->id; $user->user_id = $user->id;

174
VKAPI/Handlers/Gifts.php Normal file
View file

@ -0,0 +1,174 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\Gifts as GiftsRepo;
use openvk\Web\Models\Entities\Notifications\GiftNotification;
final class Gifts extends VKAPIRequestHandler
{
function get(int $user_id, int $count = 10, int $offset = 0)
{
$this->requireUser();
$i = 0;
$i += $offset;
$user = (new UsersRepo)->get($user_id);
if(!$user || $user->isDeleted())
$this->fail(177, "Invalid user");
$gift_item = [];
$userGifts = array_slice(iterator_to_array($user->getGifts(1, $count, false)), $offset);
if(sizeof($userGifts) < 0) {
return NULL;
}
foreach($userGifts as $gift) {
if($i < $count) {
$gift_item[] = [
"id" => $i,
"from_id" => $gift->anon == true ? 0 : $gift->sender->getId(),
"message" => $gift->caption == NULL ? "" : $gift->caption,
"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)
],
"privacy" => 0
];
}
$i+=1;
}
return $gift_item;
}
function send(int $user_ids, int $gift_id, string $message = "", int $privacy = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
$user = (new UsersRepo)->get((int) $user_ids);
if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce'])
$this->fail(105, "Commerce is disabled on this instance");
if(!$user || $user->isDeleted())
$this->fail(177, "Invalid user");
$gift = (new GiftsRepo)->get($gift_id);
if(!$gift)
$this->fail(165, "Invalid gift");
$price = $gift->getPrice();
$coinsLeft = $this->getUser()->getCoins() - $price;
if(!$gift->canUse($this->getUser()))
return (object)
[
"success" => 0,
"user_ids" => $user_ids,
"error" => "You don't have any more of these gifts."
];
if($coinsLeft < 0)
return (object)
[
"success" => 0,
"user_ids" => $user_ids,
"error" => "You don't have enough voices."
];
$user->gift($this->getUser(), $gift, $message);
$gift->used();
$this->getUser()->setCoins($coinsLeft);
$this->getUser()->save();
$notification = new GiftNotification($user, $this->getUser(), $gift, $message);
$notification->emit();
return (object)
[
"success" => 1,
"user_ids" => $user_ids,
"withdraw_votes" => $price
];
}
function delete()
{
$this->requireUser();
$this->willExecuteWriteAction();
$this->fail(501, "Not implemented");
}
# этих методов не было в ВК, но я их добавил чтобы можно было отобразить список подарков
function getCategories(bool $extended = false, int $page = 1)
{
$cats = (new GiftsRepo)->getCategories($page);
$categ = [];
$i = 0;
if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce'])
$this->fail(105, "Commerce is disabled on this instance");
foreach($cats as $cat) {
$categ[$i] = [
"name" => $cat->getName(),
"description" => $cat->getDescription(),
"id" => $cat->getId(),
"thumbnail" => $cat->getThumbnailURL(),
];
if($extended == true) {
$categ[$i]["localizations"] = [];
foreach(getLanguages() as $lang) {
$code = $lang["code"];
$categ[$i]["localizations"][$code] =
[
"name" => $cat->getName($code),
"desc" => $cat->getDescription($code),
];
}
}
$i++;
}
return $categ;
}
function getGiftsInCategory(int $id, int $page = 1)
{
$this->requireUser();
if(!OPENVK_ROOT_CONF['openvk']['preferences']['commerce'])
$this->fail(105, "Commerce is disabled on this instance");
if(!(new GiftsRepo)->getCat($id))
$this->fail(177, "Category not found");
$giftz = ((new GiftsRepo)->getCat($id))->getGifts($page);
$gifts = [];
foreach($giftz as $gift) {
$gifts[] = [
"name" => $gift->getName(),
"image" => $gift->getImage(2),
"usages_left" => (int)$gift->getUsagesLeft($this->getUser()),
"price" => $gift->getPrice(), # голосов
"is_free" => $gift->isFree()
];
}
return $gifts;
}
}

View file

@ -2,6 +2,7 @@
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo; use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
use openvk\Web\Models\Repositories\Users as UsersRepo; use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Club;
final class Groups extends VKAPIRequestHandler final class Groups extends VKAPIRequestHandler
{ {
@ -10,7 +11,7 @@ final class Groups extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
if($user_id == 0) { if($user_id == 0) {
foreach($this->getUser()->getClubs($offset+1) as $club) foreach($this->getUser()->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $this->getUser()->getClubCount(); $clbsCount = $this->getUser()->getClubCount();
} else { } else {
@ -20,7 +21,7 @@ final class Groups extends VKAPIRequestHandler
if(is_null($user)) if(is_null($user))
$this->fail(15, "Access denied"); $this->fail(15, "Access denied");
foreach($user->getClubs($offset+1) as $club) foreach($user->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $user->getClubCount(); $clbsCount = $user->getClubCount();
@ -33,17 +34,9 @@ final class Groups extends VKAPIRequestHandler
$ic = $count; $ic = $count;
if(!empty($clbs)) { if(!empty($clbs)) {
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) { for($i=0; $i < $ic; $i++) {
$usr = $clbs[$i]; $usr = $clbs[$i];
if(is_null($usr)) { if(is_null($usr)) {
$rClubs[$i] = (object)[
"id" => $clbs[$i],
"name" => "DELETED",
"deactivated" => "deleted"
];
} else if($clbs[$i] == NULL) {
} else { } else {
$rClubs[$i] = (object) [ $rClubs[$i] = (object) [
@ -102,23 +95,32 @@ final class Groups extends VKAPIRequestHandler
]; ];
} }
function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array function getById(string $group_ids = "", string $group_id = "", string $fields = "", int $offset = 0, int $count = 500): ?array
{ {
/* Both offset and count SHOULD be used only in OpenVK code,
not in your app or script, since it's not oficially documented by VK */
$clubs = new ClubsRepo; $clubs = new ClubsRepo;
if($group_ids == NULL && $group_id != NULL) if(empty($group_ids) && !empty($group_id))
$group_ids = $group_id; $group_ids = $group_id;
if($group_ids == NULL && $group_id == NULL) if(empty($group_ids) && empty($group_id))
$this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined"); $this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined");
$clbs = explode(',', $group_ids); $clbs = explode(',', $group_ids);
$response; $response = array();
$ic = sizeof($clbs); $ic = sizeof($clbs);
if(sizeof($clbs) > $count)
$ic = $count;
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) { for($i=0; $i < $ic; $i++) {
if($i > 500) if($i > 500 || $clbs[$i] == 0)
break; break;
if($clbs[$i] < 0) if($clbs[$i] < 0)
@ -142,6 +144,7 @@ final class Groups extends VKAPIRequestHandler
"screen_name" => $clb->getShortCode() ?? "club".$clb->getId(), "screen_name" => $clb->getShortCode() ?? "club".$clb->getId(),
"is_closed" => false, "is_closed" => false,
"type" => "group", "type" => "group",
"is_member" => !is_null($this->getUser()) ? (int) $clb->getSubscriptionStatus($this->getUser()) : 0,
"can_access_closed" => true, "can_access_closed" => true,
]; ];
@ -183,7 +186,7 @@ final class Groups extends VKAPIRequestHandler
$response[$i]->site = $clb->getWebsite(); $response[$i]->site = $clb->getWebsite();
break; break;
case "description": case "description":
$response[$i]->desctiption = $clb->getDescription(); $response[$i]->description = $clb->getDescription();
break; break;
case "contacts": case "contacts":
$contacts; $contacts;
@ -204,10 +207,6 @@ final class Groups extends VKAPIRequestHandler
else else
$response[$i]->can_post = $clb->canPost(); $response[$i]->can_post = $clb->canPost();
break; break;
case "is_member":
if(!is_null($this->getUser()))
$response[$i]->is_member = (int) $clb->getSubscriptionStatus($this->getUser());
break;
} }
} }
} }
@ -215,4 +214,321 @@ final class Groups extends VKAPIRequestHandler
return $response; return $response;
} }
function search(string $q, int $offset = 0, int $count = 100)
{
$clubs = new ClubsRepo;
$array = [];
$find = $clubs->find($q);
foreach ($find as $group)
$array[] = $group->getId();
return (object) [
"count" => $find->size(),
"items" => $this->getById(implode(',', $array), "", "is_admin,is_member,is_advertiser,photo_50,photo_100,photo_200", $offset, $count)
/*
* As there is no thing as "fields" by the original documentation
* i'll just bake this param by the example shown here: https://dev.vk.com/method/groups.search
*/
];
}
function join(int $group_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$club = (new ClubsRepo)->get($group_id);
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
if($isMember == 0)
$club->toggleSubscription($this->getUser());
return 1;
}
function leave(int $group_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$club = (new ClubsRepo)->get($group_id);
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
if($isMember == 1)
$club->toggleSubscription($this->getUser());
return 1;
}
function create(string $title, string $description = "", string $type = "group", int $public_category = 1, int $public_subcategory = 1, int $subtype = 1)
{
$this->requireUser();
$this->willExecuteWriteAction();
$club = new Club;
$club->setName($title);
$club->setAbout($description);
$club->setOwner($this->getUser()->getId());
$club->save();
$club->toggleSubscription($this->getUser());
return $this->getById((string)$club->getId());
}
function edit(
int $group_id,
string $title = NULL,
string $description = NULL,
string $screen_name = NULL,
string $website = NULL,
int $wall = NULL,
int $topics = NULL,
int $adminlist = NULL,
int $topicsAboveWall = NULL,
int $hideFromGlobalFeed = NULL)
{
$this->requireUser();
$this->willExecuteWriteAction();
$club = (new ClubsRepo)->get($group_id);
if(!$club) $this->fail(203, "Club not found");
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;
$club->save();
return 1;
}
function getMembers(string $group_id, string $sort = "id_asc", int $offset = 0, int $count = 100, string $fields = "", string $filter = "any")
{
# bdate,can_post,can_see_all_posts,can_see_audio,can_write_private_message,city,common_count,connections,contacts,country,domain,education,has_mobile,last_seen,lists,online,online_mobile,photo_100,photo_200,photo_200_orig,photo_400_orig,photo_50,photo_max,photo_max_orig,relation,relatives,schools,sex,site,status,universities
$club = (new ClubsRepo)->get((int) $group_id);
if(!$club)
$this->fail(125, "Invalid group id");
$sorter = "follower ASC";
switch($sort) {
default:
case "time_asc":
case "id_asc":
$sorter = "follower ASC";
break;
case "time_desc":
case "id_desc":
$sorter = "follower DESC";
break;
}
$members = array_slice(iterator_to_array($club->getFollowers(1, $count, $sorter)), $offset);
$arr = (object) [
"count" => count($members),
"items" => array()];
$filds = explode(",", $fields);
$i = 0;
foreach($members as $member) {
if($i > $count) {
break;
}
$arr->items[] = (object) [
"id" => $member->getId(),
"name" => $member->getCanonicalName(),
];
foreach($filds as $fild) {
switch($fild) {
case "bdate":
$arr->items[$i]->bdate = $member->getBirthday()->format('%e.%m.%Y');
break;
case "can_post":
$arr->items[$i]->can_post = $club->canBeModifiedBy($member);
break;
case "can_see_all_posts":
$arr->items[$i]->can_see_all_posts = 1;
break;
case "can_see_audio":
$arr->items[$i]->can_see_audio = 0;
break;
case "can_write_private_message":
$arr->items[$i]->can_write_private_message = 0;
break;
case "common_count":
$arr->items[$i]->common_count = 420;
break;
case "connections":
$arr->items[$i]->connections = 1;
break;
case "contacts":
$arr->items[$i]->contacts = $member->getContactEmail();
break;
case "country":
$arr->items[$i]->country = 1;
break;
case "domain":
$arr->items[$i]->domain = "";
break;
case "education":
$arr->items[$i]->education = "";
break;
case "has_mobile":
$arr->items[$i]->has_mobile = false;
break;
case "last_seen":
$arr->items[$i]->last_seen = $member->getOnline()->timestamp();
break;
case "lists":
$arr->items[$i]->lists = "";
break;
case "online":
$arr->items[$i]->online = $member->isOnline();
break;
case "online_mobile":
$arr->items[$i]->online_mobile = $member->getOnlinePlatform() == "android" || $member->getOnlinePlatform() == "iphone" || $member->getOnlinePlatform() == "mobile";
break;
case "photo_100":
$arr->items[$i]->photo_100 = $member->getAvatarURL("tiny");
break;
case "photo_200":
$arr->items[$i]->photo_200 = $member->getAvatarURL("normal");
break;
case "photo_200_orig":
$arr->items[$i]->photo_200_orig = $member->getAvatarURL("normal");
break;
case "photo_400_orig":
$arr->items[$i]->photo_400_orig = $member->getAvatarURL("normal");
break;
case "photo_max":
$arr->items[$i]->photo_max = $member->getAvatarURL("original");
break;
case "photo_max_orig":
$arr->items[$i]->photo_max_orig = $member->getAvatarURL();
break;
case "relation":
$arr->items[$i]->relation = $member->getMaritalStatus();
break;
case "relatives":
$arr->items[$i]->relatives = 0;
break;
case "schools":
$arr->items[$i]->schools = 0;
break;
case "sex":
$arr->items[$i]->sex = $member->isFemale() ? 1 : 2;
break;
case "site":
$arr->items[$i]->site = $member->getWebsite();
break;
case "status":
$arr->items[$i]->status = $member->getStatus();
break;
case "universities":
$arr->items[$i]->universities = 0;
break;
}
}
$i++;
}
return $arr;
}
function getSettings(string $group_id)
{
$this->requireUser();
$club = (new ClubsRepo)->get((int)$group_id);
if(!$club || !$club->canBeModifiedBy($this->getUser()))
$this->fail(15, "You can't get settings of this group.");
$arr = (object) [
"title" => $club->getName(),
"description" => $club->getDescription() != NULL ? $club->getDescription() : "",
"address" => $club->getShortcode(),
"wall" => $club->canPost() == true ? 1 : 0,
"photos" => 1,
"video" => 0,
"audio" => 0,
"docs" => 0,
"topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0,
"wiki" => 0,
"messages" => 0,
"obscene_filter" => 0,
"obscene_stopwords" => 0,
"obscene_words" => "",
"access" => 1,
"subject" => 1,
"subject_list" => [
0 => "в",
1 => "опенвк",
2 => "нет",
3 => "категорий",
4 => "групп",
],
"rss" => "/club".$club->getId()."/rss",
"website" => $club->getWebsite(),
"age_limits" => 0,
"market" => [],
];
return $arr;
}
function isMember(string $group_id, int $user_id, string $user_ids = "", bool $extended = false)
{
$this->requireUser();
$id = $user_id != NULL ? $user_id : explode(",", $user_ids);
if($group_id < 0)
$this->fail(228, "Remove the minus from group_id");
$club = (new ClubsRepo)->get((int)$group_id);
$usver = (new UsersRepo)->get((int)$id);
if(!$club || $group_id == 0)
$this->fail(203, "Invalid club");
if(!$usver || $usver->isDeleted() || $user_id == 0)
$this->fail(30, "Invalid user");
if($extended == false) {
return $club->getSubscriptionStatus($usver) ? 1 : 0;
} else {
return (object)
[
"member" => $club->getSubscriptionStatus($usver) ? 1 : 0,
"request" => 0,
"invitation" => 0,
"can_invite" => 0,
"can_recall" => 0
];
}
}
function remove(int $group_id, int $user_id)
{
$this->requireUser();
$this->fail(501, "Not implemented");
}
} }

View file

@ -8,6 +8,7 @@ final class Likes extends VKAPIRequestHandler
function add(string $type, int $owner_id, int $item_id): object function add(string $type, int $owner_id, int $item_id): object
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
switch($type) { switch($type) {
case "post": case "post":
@ -28,6 +29,7 @@ final class Likes extends VKAPIRequestHandler
function delete(string $type, int $owner_id, int $item_id): object function delete(string $type, int $owner_id, int $item_id): object
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
switch($type) { switch($type) {
case "post": case "post":
@ -52,11 +54,7 @@ final class Likes extends VKAPIRequestHandler
case "post": case "post":
$user = (new UsersRepo)->get($user_id); $user = (new UsersRepo)->get($user_id);
if (is_null($user)) if (is_null($user))
return (object) [ $this->fail(100, "One of the parameters specified was missing or invalid: user not found");
"liked" => 0,
"copied" => 0,
"sex" => 0
];
$post = (new PostsRepo)->getPostById($owner_id, $item_id); $post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post)) if (is_null($post))

View file

@ -65,9 +65,15 @@ final class Messages extends VKAPIRequestHandler
]; ];
} }
function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1) 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)
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
if($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0)
{
$this->getUser()->updOnline($this->getPlatform());
}
if($chat_id !== -1) if($chat_id !== -1)
$this->fail(946, "Chats are not implemented"); $this->fail(946, "Chats are not implemented");
@ -117,6 +123,7 @@ final class Messages extends VKAPIRequestHandler
function delete(string $message_ids, int $spam = 0, int $delete_for_all = 0): object function delete(string $message_ids, int $spam = 0, int $delete_for_all = 0): object
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
$msgs = new MSGRepo; $msgs = new MSGRepo;
$ids = preg_split("%, ?%", $message_ids); $ids = preg_split("%, ?%", $message_ids);
@ -136,6 +143,7 @@ final class Messages extends VKAPIRequestHandler
function restore(int $message_id): int function restore(int $message_id): int
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
$msg = (new MSGRepo)->get($message_id); $msg = (new MSGRepo)->get($message_id);
if(!$msg) if(!$msg)
@ -247,6 +255,7 @@ final class Messages extends VKAPIRequestHandler
$user = (new USRRepo)->get((int) $peer); $user = (new USRRepo)->get((int) $peer);
if($user) {
$dialogue = new Correspondence($this->getUser(), $user); $dialogue = new Correspondence($this->getUser(), $user);
$iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false); $iterator = $dialogue->getMessages(Correspondence::CAP_BEHAVIOUR_START_MESSAGE_ID, 0, 1, 0, false);
$msg = $iterator[0]->unwrap(); // шоб удобнее было $msg = $iterator[0]->unwrap(); // шоб удобнее было
@ -274,6 +283,7 @@ final class Messages extends VKAPIRequestHandler
]; ];
$userslist[] = $user->getId(); $userslist[] = $user->getId();
} }
}
if($extended == 1) { if($extended == 1) {
$userslist = array_unique($userslist); $userslist = array_unique($userslist);

View file

@ -2,14 +2,20 @@
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\Posts as PostsRepo; use openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\Web\Models\Entities\User;
use openvk\VKAPI\Handlers\Wall; use openvk\VKAPI\Handlers\Wall;
final class Newsfeed extends VKAPIRequestHandler final class Newsfeed extends VKAPIRequestHandler
{ {
function get(string $fields = "", int $start_from = 0, int $offset = 0, int $count = 30, int $extended = 0) function get(string $fields = "", int $start_from = 0, int $start_time = 0, int $end_time = 0, int $offset = 0, int $count = 30, int $extended = 0, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0)
{ {
$this->requireUser(); $this->requireUser();
if($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0)
{
$this->getUser()->updOnline($this->getPlatform());
}
$id = $this->getUser()->getId(); $id = $this->getUser()->getId();
$subs = DatabaseConnection::i() $subs = DatabaseConnection::i()
->getContext() ->getContext()
@ -26,7 +32,9 @@ final class Newsfeed extends VKAPIRequestHandler
->select("id") ->select("id")
->where("wall IN (?)", $ids) ->where("wall IN (?)", $ids)
->where("deleted", 0) ->where("deleted", 0)
->where("id < (?)", empty($start_from) ? time()+1 : $start_from) ->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)
->order("created DESC"); ->order("created DESC");
$rposts = []; $rposts = [];
@ -35,6 +43,34 @@ final class Newsfeed extends VKAPIRequestHandler
$response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser()); $response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser());
$response->next_from = end(end($posts->page((int) ($offset + 1), $count))); // ну и костыли пиздец конечно) $response->next_from = end(end($posts->page((int) ($offset + 1), $count))); // ну и костыли пиздец конечно)
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)
{
$this->requireUser();
$queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND `posts`.`deleted` = 0";
if($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT)
$queryBase .= " AND `nsfw` = 0";
$start_from = empty($start_from) ? PHP_INT_MAX : $start_from;
$start_time = empty($start_time) ? 0 : $start_time;
$end_time = empty($end_time) ? PHP_INT_MAX : $end_time;
$posts = DatabaseConnection::i()->getConnection()->query("SELECT `posts`.`id` " . $queryBase . " AND `posts`.`id` <= " . $start_from . " AND " . $start_time . " <= `posts`.`created` AND `posts`.`created` <= " . $end_time . " ORDER BY `created` DESC LIMIT " . $count . " OFFSET " . $offset);
$rposts = [];
$ids = [];
foreach($posts as $post) {
$rposts[] = (new PostsRepo)->get($post->id)->getPrettyId();
$ids[] = $post->id;
}
$response = (new Wall)->getById(implode(',', $rposts), $extended, $fields, $this->getUser());
$response->next_from = end($ids);
return $response; return $response;
} }
} }

271
VKAPI/Handlers/Notes.php Normal file
View file

@ -0,0 +1,271 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\Notes as NotesRepo;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
use openvk\Web\Models\Repositories\Photos as PhotosRepo;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Entities\{Note, Comment};
final class Notes extends VKAPIRequestHandler
{
function add(string $title, string $text, int $privacy = 0, int $comment_privacy = 0, string $privacy_view = "", string $privacy_comment = "")
{
$this->requireUser();
$this->willExecuteWriteAction();
$note = new Note;
$note->setOwner($this->getUser()->getId());
$note->setCreated(time());
$note->setName($title);
$note->setSource($text);
$note->setEdited(time());
$note->save();
return $note->getVirtualId();
}
function createComment(string $note_id, int $owner_id, string $message, int $reply_to = 0, string $attachments = "")
{
$this->requireUser();
$this->willExecuteWriteAction();
$note = (new NotesRepo)->getNoteById((int)$owner_id, (int)$note_id);
if(!$note)
$this->fail(180, "Note not found");
if($note->isDeleted())
$this->fail(189, "Note is deleted");
if($note->getOwner()->isDeleted())
$this->fail(403, "Owner is deleted");
if(empty($message) && empty($attachments))
$this->fail(100, "Required parameter 'message' missing.");
$comment = new Comment;
$comment->setOwner($this->getUser()->getId());
$comment->setModel(get_class($note));
$comment->setTarget($note->getId());
$comment->setContent($message);
$comment->setCreated(time());
$comment->save();
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);
}
}
}
return $comment->getId();
}
function delete(string $note_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$note = (new NotesRepo)->get((int)$note_id);
if(!$note)
$this->fail(180, "Note not found");
if(!$note->canBeModifiedBy($this->getUser()))
$this->fail(15, "Access to note denied");
$note->delete();
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();
$this->willExecuteWriteAction();
$note = (new NotesRepo)->getNoteById($this->getUser()->getId(), (int)$note_id);
if(!$note)
$this->fail(180, "Note not found");
if($note->isDeleted())
$this->fail(189, "Note is deleted");
if(!$note->canBeModifiedBy($this->getUser()))
$this->fail(403, "No access");
!empty($title) ? $note->setName($title) : NULL;
!empty($text) ? $note->setSource($text) : NULL;
$note->setCached_Content(NULL);
$note->setEdited(time());
$note->save();
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();
$user = (new UsersRepo)->get($user_id);
if(!$user || $user->isDeleted())
$this->fail(15, "Invalid user");
if(empty($note_ids)) {
$notes = array_slice(iterator_to_array((new NotesRepo)->getUserNotes($user, 1, $count + $offset, $sort == 0 ? "ASC" : "DESC")), $offset);
$nodez = (object) [
"count" => (new NotesRepo)->getUserNotesCount((new UsersRepo)->get($user_id)),
"notes" => []
];
foreach($notes as $note) {
if($note->isDeleted()) continue;
$nodez->notes[] = $note->toVkApiStruct();
}
} else {
$notes = explode(',', $note_ids);
foreach($notes as $note)
{
$id = explode("_", $note);
$items = [];
$note = (new NotesRepo)->getNoteById((int)$id[0], (int)$id[1]);
if($note) {
$nodez->notes[] = $note->toVkApiStruct();
}
}
}
return $nodez;
}
function getById(int $note_id, int $owner_id, bool $need_wiki = false)
{
$this->requireUser();
$note = (new NotesRepo)->getNoteById($owner_id, $note_id);
if(!$note)
$this->fail(180, "Note not found");
if($note->isDeleted())
$this->fail(189, "Note is deleted");
if(!$note->getOwner() || $note->getOwner()->isDeleted())
$this->fail(177, "Owner does not exists");
return $note->toVkApiStruct();
}
function getComments(int $note_id, int $owner_id, int $sort = 1, int $offset = 0, int $count = 100)
{
$this->requireUser();
$note = (new NotesRepo)->getNoteById($owner_id, $note_id);
if(!$note)
$this->fail(180, "Note not found");
if($note->isDeleted())
$this->fail(189, "Note is deleted");
if(!$note->getOwner())
$this->fail(177, "Owner does not exists");
$arr = (object) [
"count" => $note->getCommentsCount(),
"comments" => []];
$comments = array_slice(iterator_to_array($note->getComments(1, $count + $offset)), $offset);
foreach($comments as $comment) {
$arr->comments[] = $comment->toVkApiStruct($this->getUser(), false, false, $note);
}
return $arr;
}
function getFriendsNotes(int $offset = 0, int $count = 0)
{
$this->fail(501, "Not implemented");
}
function restoreComment(int $comment_id = 0, int $owner_id = 0)
{
$this->fail(501, "Not implemented");
}
}

View file

@ -3,9 +3,12 @@ namespace openvk\VKAPI\Handlers;
use Nette\InvalidStateException; use Nette\InvalidStateException;
use Nette\Utils\ImageException; use Nette\Utils\ImageException;
use openvk\Web\Models\Entities\Photo; use openvk\Web\Models\Entities\{Photo, Album, Comment};
use openvk\Web\Models\Repositories\Albums; use openvk\Web\Models\Repositories\Albums;
use openvk\Web\Models\Repositories\Photos as PhotosRepo;
use openvk\Web\Models\Repositories\Clubs; use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
final class Photos extends VKAPIRequestHandler final class Photos extends VKAPIRequestHandler
{ {
@ -227,4 +230,504 @@ final class Photos extends VKAPIRequestHandler
"items" => $images, "items" => $images,
]; ];
} }
function createAlbum(string $title, int $group_id = 0, string $description = "", int $privacy = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
if($group_id != 0) {
$club = (new Clubs)->get((int) $group_id);
if(!$club || !$club->canBeModifiedBy($this->getUser())) {
$this->fail(20, "Invalid club");
}
}
$album = new Album;
$album->setOwner(isset($club) ? $club->getId() * -1 : $this->getUser()->getId());
$album->setName($title);
$album->setDescription($description);
$album->setCreated(time());
$album->save();
return $album->toVkApiStruct($this->getUser());
}
function editAlbum(int $album_id, int $owner_id, string $title, string $description = "", int $privacy = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id);
if(!$album || $album->isDeleted()) {
$this->fail(2, "Invalid album");
}
if(empty($title)) {
$this->fail(25, "Title is empty");
}
if($album->isCreatedBySystem()) {
$this->fail(40, "You can't change system album");
}
if(!$album->canBeModifiedBy($this->getUser())) {
$this->fail(2, "Access to album denied");
}
$album->setName($title);
$album->setDescription($description);
$album->save();
return $album->toVkApiStruct($this->getUser());
}
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 = [];
if(empty($album_ids)) {
if($owner_id > 0) {
$user = (new UsersRepo)->get($owner_id);
$res = [
"count" => (new Albums)->getUserAlbumsCount($user),
"items" => []
];
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.");
$albums = array_slice(iterator_to_array((new Albums)->getUserAlbums($user, 1, $count + $offset)), $offset);
foreach($albums as $album) {
if(!$need_system && $album->isCreatedBySystem()) continue;
$res["items"][] = $album->toVkApiStruct($this->getUser(), $need_covers, $photo_sizes);
}
}
else {
$club = (new Clubs)->get($owner_id * -1);
$res = [
"count" => (new Albums)->getClubAlbumsCount($club),
"items" => []
];
if(!$club)
$this->fail(2, "Invalid club");
$albums = array_slice(iterator_to_array((new Albums)->getClubAlbums($club, 1, $count + $offset)), $offset);
foreach($albums as $album) {
if(!$need_system && $album->isCreatedBySystem()) continue;
$res["items"][] = $album->toVkApiStruct($this->getUser(), $need_covers, $photo_sizes);
}
}
} else {
$albums = explode(',', $album_ids);
$res = [
"count" => sizeof($albums),
"items" => []
];
foreach($albums as $album)
{
$id = explode("_", $album);
$album = (new Albums)->getAlbumByOwnerAndId((int)$id[0], (int)$id[1]);
if($album && !$album->isDeleted()) {
if(!$need_system && $album->isCreatedBySystem()) continue;
$res["items"][] = $album->toVkApiStruct($this->getUser(), $need_covers, $photo_sizes);
}
}
}
return $res;
}
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) {
$this->fail(21, "Select user_id or group_id");
}
if($user_id > 0) {
$us = (new UsersRepo)->get($user_id);
if(!$us || $us->isDeleted()) {
$this->fail(21, "Invalid user");
}
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)
{
$cl = (new Clubs)->get($group_id);
if(!$cl) {
$this->fail(21, "Invalid club");
}
return (new Albums)->getClubAlbumsCount($cl);
}
}
function getById(string $photos, bool $extended = false, bool $photo_sizes = false)
{
$this->requireUser();
$this->willExecuteWriteAction();
$phts = explode(",", $photos);
$res = [];
foreach($phts as $phota) {
$ph = explode("_", $phota);
$photo = (new PhotosRepo)->getByOwnerAndVID((int)$ph[0], (int)$ph[1]);
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.");
}
$res[] = $photo->toVkApiStruct($photo_sizes, $extended);
}
return $res;
}
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()) {
$this->fail(21, "Invalid album");
}
$photos = array_slice(iterator_to_array($album->getPhotos(1, $count + $offset)), $offset);
$res["count"] = sizeof($photos);
foreach($photos as $photo) {
if(!$photo || $photo->isDeleted()) continue;
$res["items"][] = $photo->toVkApiStruct($photo_sizes, $extended);
}
} else {
$photos = explode(',', $photo_ids);
$res = [
"count" => sizeof($photos),
"items" => []
];
foreach($photos as $photo)
{
$id = explode("_", $photo);
$phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]);
if($phot && !$phot->isDeleted()) {
$res["items"][] = $phot->toVkApiStruct($photo_sizes, $extended);
}
}
}
return $res;
}
function deleteAlbum(int $album_id, int $group_id = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Albums)->get($album_id);
if(!$album || $album->canBeModifiedBy($this->getUser())) {
$this->fail(21, "Invalid album");
}
if($album->isDeleted()) {
$this->fail(22, "Album already deleted");
}
$album->delete();
return 1;
}
function edit(int $owner_id, int $photo_id, string $caption = "")
{
$this->requireUser();
$this->willExecuteWriteAction();
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id);
if(!$photo) {
$this->fail(21, "Invalid photo");
}
if($photo->isDeleted()) {
$this->fail(21, "Photo is deleted");
}
if(!empty($caption)) {
$photo->setDescription($caption);
$photo->save();
}
return 1;
}
function delete(int $owner_id, int $photo_id, string $photos = "")
{
$this->requireUser();
$this->willExecuteWriteAction();
if(empty($photos)) {
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $photo_id);
if($this->getUser()->getId() !== $photo->getOwner()->getId()) {
$this->fail(21, "You can't delete another's photo");
}
if(!$photo) {
$this->fail(21, "Invalid photo");
}
if($photo->isDeleted()) {
$this->fail(21, "Photo already deleted");
}
$photo->delete();
} else {
$photozs = explode(',', $photos);
foreach($photozs as $photo)
{
$id = explode("_", $photo);
$phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]);
if($this->getUser()->getId() !== $phot->getOwner()->getId()) {
$this->fail(21, "You can't delete another's photo");
}
if(!$phot) {
$this->fail(21, "Invalid photo");
}
if($phot->isDeleted()) {
$this->fail(21, "Photo already deleted");
}
$phot->delete();
}
}
return 1;
}
function getAllComments(int $owner_id, int $album_id, bool $need_likes = false, int $offset = 0, int $count = 100)
{
$this->fail(501, "Not implemented");
}
function deleteComment(int $comment_id, int $owner_id = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
$comment = (new CommentsRepo)->get($comment_id);
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");
}
$comment->delete();
return 1;
}
function createComment(int $owner_id, int $photo_id, string $message = "", string $attachments = "", bool $from_group = false)
{
$this->requireUser();
$this->willExecuteWriteAction();
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)
$this->fail(180, "Photo not found");
if($photo->isDeleted())
$this->fail(189, "Photo is deleted");
$comment = new Comment;
$comment->setOwner($this->getUser()->getId());
$comment->setModel(get_class($photo));
$comment->setTarget($photo->getId());
$comment->setContent($message);
$comment->setCreated(time());
$comment->save();
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);
}
}
}
return $comment->getId();
}
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) {
$this->fail(4, "This method doesn't works with clubs");
}
$user = (new UsersRepo)->get($owner_id);
if(!$user) {
$this->fail(4, "Invalid user");
}
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 = [];
foreach($photos as $photo) {
if(!$photo || $photo->isDeleted()) continue;
$res["items"][] = $photo->toVkApiStruct($photo_sizes, $extended);
}
return $res;
}
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) {
$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");
}
$res = [
"count" => sizeof($comms),
"items" => []
];
foreach($comms as $comment) {
$res["items"][] = $comment->toVkApiStruct($this->getUser(), $need_likes, $extended);
if($extended) {
if($comment->getOwner() instanceof \openvk\Web\Models\Entities\User) {
$res["profiles"][] = $comment->getOwner()->toVkApiStruct();
}
}
}
return $res;
}
} }

107
VKAPI/Handlers/Polls.php Executable file
View file

@ -0,0 +1,107 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Poll;
use openvk\Web\Models\Exceptions\AlreadyVotedException;
use openvk\Web\Models\Exceptions\InvalidOptionException;
use openvk\Web\Models\Exceptions\PollLockedException;
use openvk\Web\Models\Repositories\Polls as PollsRepo;
final class Polls extends VKAPIRequestHandler
{
function getById(int $poll_id, bool $extended = false, string $fields = "sex,screen_name,photo_50,photo_100,online_info,online")
{
$poll = (new PollsRepo)->get($poll_id);
if (!$poll)
$this->fail(100, "One of the parameters specified was missing or invalid: poll_id is incorrect");
$users = array();
$answers = array();
foreach($poll->getResults()->options as $answer) {
$answers[] = (object)[
"id" => $answer->id,
"rate" => $answer->pct,
"text" => $answer->name,
"votes" => $answer->votes
];
}
$userVote = array();
foreach($poll->getUserVote($this->getUser()) as $vote)
$userVote[] = $vote[0];
$response = [
"multiple" => $poll->isMultipleChoice(),
"end_date" => $poll->endsAt() == NULL ? 0 : $poll->endsAt()->timestamp(),
"closed" => $poll->hasEnded(),
"is_board" => false,
"can_edit" => false,
"can_vote" => $poll->canVote($this->getUser()),
"can_report" => false,
"can_share" => true,
"created" => 0,
"id" => $poll->getId(),
"owner_id" => $poll->getOwner()->getId(),
"question" => $poll->getTitle(),
"votes" => $poll->getVoterCount(),
"disable_unvote" => $poll->isRevotable(),
"anonymous" => $poll->isAnonymous(),
"answer_ids" => $userVote,
"answers" => $answers,
"author_id" => $poll->getOwner()->getId(),
];
if ($extended) {
$response["profiles"] = (new Users)->get(strval($poll->getOwner()->getId()), $fields, 0, 1);
/* Currently there is only one person that can be shown trough "Extended" param.
* As "friends" param will be implemented, "profiles" will show more users
*/
}
return (object) $response;
}
function addVote(int $poll_id, string $answers_ids)
{
$this->requireUser();
$this->willExecuteWriteAction();
$poll = (new PollsRepo)->get($poll_id);
if(!$poll)
$this->fail(251, "Invalid poll id");
try {
$poll->vote($this->getUser(), explode(",", $answers_ids));
return 1;
} catch(AlreadyVotedException $ex) {
return 0;
} catch(PollLockedException $ex) {
return 0;
} catch(InvalidOptionException $ex) {
$this->fail(8, "бдсм вибратор купить в киеве");
}
}
function deleteVote(int $poll_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$poll = (new PollsRepo)->get($poll_id);
if(!$poll)
$this->fail(251, "Invalid poll id");
try {
$poll->revokeVote($this->getUser());
return 1;
} catch(PollLockedException $ex) {
$this->fail(15, "Access denied: Poll is locked or isn't revotable");
} catch(InvalidOptionException $ex) {
$this->fail(8, "how.to. ook.bacon.in.microwova.");
}
}
}

35
VKAPI/Handlers/Status.php Normal file
View file

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Users as UsersRepo;
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();
}
}
function set(string $text, int $group_id = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
if($group_id > 0) {
$this->fail(501, "Group statuses are not implemented");
} else {
$this->getUser()->setStatus($text);
$this->getUser()->save();
return 1;
}
}
}

View file

@ -112,11 +112,31 @@ final class Users extends VKAPIRequestHandler
} }
break; break;
case "last_seen": case "last_seen":
if ($usr->onlineStatus() == 0) if ($usr->onlineStatus() == 0) {
$platform = $usr->getOnlinePlatform(true);
switch ($platform) {
case 'iphone':
$platform = 2;
break;
case 'android':
$platform = 4;
break;
case NULL:
$platform = 7;
break;
default:
$platform = 1;
break;
}
$response[$i]->last_seen = (object) [ $response[$i]->last_seen = (object) [
"platform" => 1, "platform" => $platform,
"time" => $usr->getOnline()->timestamp() "time" => $usr->getOnline()->timestamp()
]; ];
}
case "music": case "music":
$response[$i]->music = $usr->getFavoriteMusic(); $response[$i]->music = $usr->getFavoriteMusic();
break; break;
@ -135,6 +155,9 @@ final class Users extends VKAPIRequestHandler
case "interests": case "interests":
$response[$i]->interests = $usr->getInterests(); $response[$i]->interests = $usr->getInterests();
break; break;
case "rating":
$response[$i]->rating = $usr->getRating();
break;
} }
} }
@ -179,19 +202,94 @@ final class Users extends VKAPIRequestHandler
]; ];
} }
function search(string $q, string $fields = "", int $offset = 0, int $count = 100) function search(string $q,
string $fields = "",
int $offset = 0,
int $count = 100,
string $city = "",
string $hometown = "",
int $sex = 2,
int $status = 0, # это про marital status
bool $online = false,
# дальше идут параметры которых нету в vkapi но есть на сайте
string $profileStatus = "", # а это уже нормальный статус
int $sort = 0,
int $before = 0,
int $politViews = 0,
int $after = 0,
string $interests = "",
string $fav_music = "",
string $fav_films = "",
string $fav_shows = "",
string $fav_books = "",
string $fav_quotes = ""
)
{ {
$users = new UsersRepo; $users = new UsersRepo;
$sortg = "id ASC";
$nfilds = $fields;
switch($sort) {
case 0:
$sortg = "id DESC";
break;
case 1:
$sortg = "id ASC";
break;
case 2:
$sortg = "first_name DESC";
break;
case 3:
$sortg = "first_name ASC";
break;
case 4:
$sortg = "rating DESC";
if(!str_contains($nfilds, "rating")) {
$nfilds .= "rating";
}
break;
case 5:
$sortg = "rating DESC";
if(!str_contains($nfilds, "rating")) {
$nfilds .= "rating";
}
break;
}
$array = []; $array = [];
$find = $users->find($q);
$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,
];
$find = $users->find($q, $parameters, $sortg);
foreach ($find as $user) foreach ($find as $user)
$array[] = $user->getId(); $array[] = $user->getId();
return (object) [ return (object) [
"count" => $find->size(), "count" => $find->size(),
"items" => $this->get(implode(',', $array), $fields, $offset, $count) "items" => $this->get(implode(',', $array), $nfilds, $offset, $count)
]; ];
} }
} }

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\{Users, Clubs};
final class Utils extends VKAPIRequestHandler final class Utils extends VKAPIRequestHandler
{ {
@ -7,4 +8,39 @@ final class Utils extends VKAPIRequestHandler
{ {
return time(); return time();
} }
function resolveScreenName(string $screen_name): object
{
if(\Chandler\MVC\Routing\Router::i()->getMatchingRoute("/$screen_name")[0]->presenter !== "UnknownTextRouteStrategy") {
if(substr($screen_name, 0, strlen("id")) === "id") {
return (object) [
"object_id" => (int) substr($screen_name, strlen("id")),
"type" => "user"
];
} else if(substr($screen_name, 0, strlen("club")) === "club") {
return (object) [
"object_id" => (int) substr($screen_name, strlen("club")),
"type" => "group"
];
}
} else {
$user = (new Users)->getByShortURL($screen_name);
if($user) {
return (object) [
"object_id" => $user->getId(),
"type" => "user"
];
}
$club = (new Clubs)->getByShortURL($screen_name);
if($club) {
return (object) [
"object_id" => $club->getId(),
"type" => "group"
];
}
return (object) [];
}
}
} }

View file

@ -1,15 +1,19 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\VKAPI\Exceptions\APIErrorException; use openvk\VKAPI\Exceptions\APIErrorException;
use openvk\Web\Models\Entities\IP;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\IPs;
abstract class VKAPIRequestHandler abstract class VKAPIRequestHandler
{ {
protected $user; protected $user;
protected $platform;
function __construct(?User $user = NULL) function __construct(?User $user = NULL, ?string $platform = NULL)
{ {
$this->user = $user; $this->user = $user;
$this->platform = $platform;
} }
protected function fail(int $code, string $message): void protected function fail(int $code, string $message): void
@ -22,6 +26,11 @@ abstract class VKAPIRequestHandler
return $this->user; return $this->user;
} }
protected function getPlatform(): ?string
{
return $this->platform;
}
protected function userAuthorized(): bool protected function userAuthorized(): bool
{ {
return !is_null($this->getUser()); return !is_null($this->getUser());
@ -32,4 +41,19 @@ abstract class VKAPIRequestHandler
if(!$this->userAuthorized()) if(!$this->userAuthorized())
$this->fail(5, "User authorization failed: no access_token passed."); $this->fail(5, "User authorization failed: no access_token passed.");
} }
protected function willExecuteWriteAction(): void
{
$ip = (new IPs)->get(CONNECTING_IP);
$res = $ip->rateLimit();
if(!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) {
if($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) {
$this->user->ban("User account has been suspended for breaking API terms of service", false);
$this->fail(18, "User account has been suspended due to repeated violation of API rate limits.");
}
$this->fail(29, "You have been rate limited.");
}
}
} }

57
VKAPI/Handlers/Video.php Executable file
View file

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
use openvk\Web\Models\Entities\Video as VideoEntity;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Entities\Comment;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
final class Video extends VKAPIRequestHandler
{
function get(int $owner_id, string $videos, int $offset = 0, int $count = 30, int $extended = 0): object
{
$this->requireUser();
if ($videos) {
$vids = explode(',', $videos);
foreach($vids as $vid)
{
$id = explode("_", $vid);
$items = [];
$video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1]));
if($video) {
$items[] = $video->getApiStructure();
}
}
return (object) [
"count" => count($items),
"items" => $items
];
} else {
if ($owner_id > 0)
$user = (new UsersRepo)->get($owner_id);
else
$this->fail(1, "Not implemented");
$videos = (new VideosRepo)->getByUser($user, $offset + 1, $count);
$videosCount = (new VideosRepo)->getUserVideosCount($user);
$items = [];
foreach ($videos as $video) {
$items[] = $video->getApiStructure();
}
return (object) [
"count" => $videosCount,
"items" => $items
];
}
}
}

View file

@ -9,11 +9,17 @@ use openvk\Web\Models\Entities\Post;
use openvk\Web\Models\Repositories\Posts as PostsRepo; use openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\Web\Models\Entities\Comment; use openvk\Web\Models\Entities\Comment;
use openvk\Web\Models\Repositories\Comments as CommentsRepo; use openvk\Web\Models\Repositories\Comments as CommentsRepo;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Repositories\Photos as PhotosRepo;
use openvk\Web\Models\Entities\Video;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
final class Wall extends VKAPIRequestHandler final class Wall extends VKAPIRequestHandler
{ {
function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 30, int $extended = 0): object function get(int $owner_id, string $domain = "", int $offset = 0, int $count = 30, int $extended = 0): object
{ {
$this->requireUser();
$posts = new PostsRepo; $posts = new PostsRepo;
$items = []; $items = [];
@ -21,10 +27,17 @@ final class Wall extends VKAPIRequestHandler
$groups = []; $groups = [];
$cnt = $posts->getPostCountOnUserWall($owner_id); $cnt = $posts->getPostCountOnUserWall($owner_id);
if ($owner_id > 0)
$wallOnwer = (new UsersRepo)->get($owner_id); $wallOnwer = (new UsersRepo)->get($owner_id);
else
$wallOnwer = (new ClubsRepo)->get($owner_id * -1);
if(!$wallOnwer || $wallOnwer->isDeleted() || $wallOnwer->isDeleted()) if ($owner_id > 0)
if(!$wallOnwer || $wallOnwer->isDeleted())
$this->fail(18, "User was deleted or banned"); $this->fail(18, "User was deleted or banned");
else
if(!$wallOnwer)
$this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls
foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) { foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
@ -37,12 +50,16 @@ final class Wall extends VKAPIRequestHandler
continue; continue;
$attachments[] = $this->getApiPhoto($attachment); $attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
foreach($attachment->getChildren() as $repostAttachment) { foreach($attachment->getChildren() as $repostAttachment) {
if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) {
if($attachment->isDeleted()) if($repostAttachment->isDeleted())
continue; continue;
$repostAttachments[] = $this->getApiPhoto($repostAttachment); $repostAttachments[] = $this->getApiPhoto($repostAttachment);
@ -50,6 +67,22 @@ final class Wall extends VKAPIRequestHandler
} }
} }
if ($attachment->isPostedOnBehalfOfGroup())
$groups[] = $attachment->getOwner()->getId();
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[] = [ $repost[] = [
"id" => $attachment->getVirtualId(), "id" => $attachment->getVirtualId(),
"owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(),
@ -58,13 +91,22 @@ final class Wall extends VKAPIRequestHandler
"post_type" => "post", "post_type" => "post",
"text" => $attachment->getText(false), "text" => $attachment->getText(false),
"attachments" => $repostAttachments, "attachments" => $repostAttachments,
"post_source" => [ "post_source" => $post_source,
"type" => "vk"
],
]; ];
} }
} }
$post_source = [];
if($post->getPlatform(true) === NULL) {
$post_source = (object)["type" => "vk"];
} else {
$post_source = (object)[
"type" => "api",
"platform" => $post->getPlatform(true)
];
}
$items[] = (object)[ $items[] = (object)[
"id" => $post->getVirtualId(), "id" => $post->getVirtualId(),
"from_id" => $from_id, "from_id" => $from_id,
@ -80,7 +122,7 @@ final class Wall extends VKAPIRequestHandler
"is_archived" => false, "is_archived" => false,
"is_pinned" => $post->isPinned(), "is_pinned" => $post->isPinned(),
"attachments" => $attachments, "attachments" => $attachments,
"post_source" => (object)["type" => "vk"], "post_source" => $post_source,
"comments" => (object)[ "comments" => (object)[
"count" => $post->getCommentsCount(), "count" => $post->getCommentsCount(),
"can_post" => 1 "can_post" => 1
@ -124,7 +166,8 @@ final class Wall extends VKAPIRequestHandler
"screen_name" => $user->getShortCode(), "screen_name" => $user->getShortCode(),
"photo_50" => $user->getAvatarUrl(), "photo_50" => $user->getAvatarUrl(),
"photo_100" => $user->getAvatarUrl(), "photo_100" => $user->getAvatarUrl(),
"online" => $user->isOnline() "online" => $user->isOnline(),
"verified" => $user->isVerified()
]; ];
} }
@ -139,6 +182,7 @@ final class Wall extends VKAPIRequestHandler
"photo_50" => $group->getAvatarUrl(), "photo_50" => $group->getAvatarUrl(),
"photo_100" => $group->getAvatarUrl(), "photo_100" => $group->getAvatarUrl(),
"photo_200" => $group->getAvatarUrl(), "photo_200" => $group->getAvatarUrl(),
"verified" => $group->isVerified()
]; ];
} }
@ -178,6 +222,10 @@ final class Wall extends VKAPIRequestHandler
foreach($post->getChildren() as $attachment) { foreach($post->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment); $attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $user);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
@ -191,6 +239,22 @@ final class Wall extends VKAPIRequestHandler
} }
} }
if ($attachment->isPostedOnBehalfOfGroup())
$groups[] = $attachment->getOwner()->getId();
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[] = [ $repost[] = [
"id" => $attachment->getVirtualId(), "id" => $attachment->getVirtualId(),
"owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(), "owner_id" => $attachment->isPostedOnBehalfOfGroup() ? $attachment->getOwner()->getId() * -1 : $attachment->getOwner()->getId(),
@ -199,13 +263,22 @@ final class Wall extends VKAPIRequestHandler
"post_type" => "post", "post_type" => "post",
"text" => $attachment->getText(false), "text" => $attachment->getText(false),
"attachments" => $repostAttachments, "attachments" => $repostAttachments,
"post_source" => [ "post_source" => $post_source,
"type" => "vk"
],
]; ];
} }
} }
$post_source = [];
if($post->getPlatform(true) === NULL) {
$post_source = (object)["type" => "vk"];
} else {
$post_source = (object)[
"type" => "api",
"platform" => $post->getPlatform(true)
];
}
$items[] = (object)[ $items[] = (object)[
"id" => $post->getVirtualId(), "id" => $post->getVirtualId(),
"from_id" => $from_id, "from_id" => $from_id,
@ -220,7 +293,7 @@ final class Wall extends VKAPIRequestHandler
"can_archive" => false, # TODO MAYBE "can_archive" => false, # TODO MAYBE
"is_archived" => false, "is_archived" => false,
"is_pinned" => $post->isPinned(), "is_pinned" => $post->isPinned(),
"post_source" => (object)["type" => "vk"], "post_source" => $post_source,
"attachments" => $attachments, "attachments" => $attachments,
"comments" => (object)[ "comments" => (object)[
"count" => $post->getCommentsCount(), "count" => $post->getCommentsCount(),
@ -267,7 +340,8 @@ final class Wall extends VKAPIRequestHandler
"screen_name" => $user->getShortCode(), "screen_name" => $user->getShortCode(),
"photo_50" => $user->getAvatarUrl(), "photo_50" => $user->getAvatarUrl(),
"photo_100" => $user->getAvatarUrl(), "photo_100" => $user->getAvatarUrl(),
"online" => $user->isOnline() "online" => $user->isOnline(),
"verified" => $user->isVerified()
]; ];
} }
@ -282,6 +356,7 @@ final class Wall extends VKAPIRequestHandler
"photo_50" => $group->getAvatarUrl(), "photo_50" => $group->getAvatarUrl(),
"photo_100" => $group->getAvatarUrl(), "photo_100" => $group->getAvatarUrl(),
"photo_200" => $group->getAvatarUrl(), "photo_200" => $group->getAvatarUrl(),
"verified" => $group->isVerified()
]; ];
} }
@ -296,9 +371,10 @@ final class Wall extends VKAPIRequestHandler
]; ];
} }
function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0): object function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0, string $attachments = ""): object
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
$owner_id = intval($owner_id); $owner_id = intval($owner_id);
@ -333,27 +409,7 @@ final class Wall extends VKAPIRequestHandler
if($signed == 1) if($signed == 1)
$flags |= 0b01000000; $flags |= 0b01000000;
# TODO: Compatible implementation of this if(empty($message) && empty($attachments))
try {
$photo = NULL;
$video = NULL;
if($_FILES["photo"]["error"] === UPLOAD_ERR_OK) {
$album = NULL;
if(!$anon && $owner_id > 0 && $owner_id === $this->getUser()->getId())
$album = (new AlbumsRepo)->getUserWallAlbum($wallOwner);
$photo = Photo::fastMake($this->getUser()->getId(), $message, $_FILES["photo"], $album, $anon);
}
if($_FILES["video"]["error"] === UPLOAD_ERR_OK)
$video = Video::fastMake($this->getUser()->getId(), $message, $_FILES["video"], $anon);
} catch(\DomainException $ex) {
$this->fail(-156, "The media file is corrupted");
} catch(ISE $ex) {
$this->fail(-156, "The media file is corrupted or too large ");
}
if(empty($message) && !$photo && !$video)
$this->fail(100, "Required parameter 'message' missing."); $this->fail(100, "Required parameter 'message' missing.");
try { try {
@ -363,16 +419,56 @@ final class Wall extends VKAPIRequestHandler
$post->setCreated(time()); $post->setCreated(time());
$post->setContent($message); $post->setContent($message);
$post->setFlags($flags); $post->setFlags($flags);
$post->setApi_Source_Name($this->getPlatform());
$post->save(); $post->save();
} catch(\LogicException $ex) { } catch(\LogicException $ex) {
$this->fail(100, "One of the parameters specified was missing or invalid"); $this->fail(100, "One of the parameters specified was missing or invalid");
} }
if(!is_null($photo)) if(!empty($attachments)) {
$post->attach($photo); $attachmentsArr = explode(",", $attachments);
# Аттачи такого вида: [тип][id владельца]_[id вложения]
# Пример: photo1_1
if(!is_null($video)) if(sizeof($attachmentsArr) > 10)
$post->attach($video); $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);
}
}
}
if($wall > 0 && $wall !== $this->user->identity->getId()) if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
@ -380,8 +476,9 @@ final class Wall extends VKAPIRequestHandler
return (object)["post_id" => $post->getVirtualId()]; return (object)["post_id" => $post->getVirtualId()];
} }
function repost(string $object, string $message = "") { function repost(string $object, string $message = "", int $group_id = 0) {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
$postArray; $postArray;
if(preg_match('/wall((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0) if(preg_match('/wall((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0)
@ -392,13 +489,27 @@ final class Wall extends VKAPIRequestHandler
$nPost = new Post; $nPost = new Post;
$nPost->setOwner($this->user->getId()); $nPost->setOwner($this->user->getId());
if($group_id > 0) {
$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);
} else {
$nPost->setWall($this->user->getId()); $nPost->setWall($this->user->getId());
}
$nPost->setContent($message); $nPost->setContent($message);
$nPost->setApi_Source_Name($this->getPlatform());
$nPost->save(); $nPost->save();
$nPost->attach($post); $nPost->attach($post);
if($post->getOwner(false)->getId() !== $this->user->getId() && !($post->getOwner() instanceof Club)) if($post->getOwner(false)->getId() !== $this->user->getId() && !($post->getOwner() instanceof Club))
(new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit(); (new RepostNotification($post->getOwner(false), $post, $this->user))->emit();
return (object) [ return (object) [
"success" => 1, // 👍 "success" => 1, // 👍
@ -408,6 +519,7 @@ final class Wall extends VKAPIRequestHandler
]; ];
} }
function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $offset = 0, int $count = 10, string $fields = "sex,screen_name,photo_50,photo_100,online_info,online", string $sort = "asc", bool $extended = false) { function getComments(int $owner_id, int $post_id, bool $need_likes = true, int $offset = 0, int $count = 10, string $fields = "sex,screen_name,photo_50,photo_100,online_info,online", string $sort = "asc", bool $extended = false) {
$this->requireUser(); $this->requireUser();
@ -420,14 +532,28 @@ final class Wall extends VKAPIRequestHandler
$profiles = []; $profiles = [];
foreach($comments as $comment) { foreach($comments as $comment) {
$owner = $comment->getOwner();
$oid = $owner->getId();
if($owner instanceof Club)
$oid *= -1;
$attachments = [];
foreach($comment->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment);
}
}
$item = [ $item = [
"id" => $comment->getId(), "id" => $comment->getId(),
"from_id" => $comment->getOwner()->getId(), "from_id" => $oid,
"date" => $comment->getPublicationTime()->timestamp(), "date" => $comment->getPublicationTime()->timestamp(),
"text" => $comment->getText(false), "text" => $comment->getText(false),
"post_id" => $post->getVirtualId(), "post_id" => $post->getVirtualId(),
"owner_id" => $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(), "owner_id" => $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(),
"parents_stack" => [], "parents_stack" => [],
"attachments" => $attachments,
"thread" => [ "thread" => [
"count" => 0, "count" => 0,
"items" => [], "items" => [],
@ -448,6 +574,9 @@ final class Wall extends VKAPIRequestHandler
$items[] = $item; $items[] = $item;
if($extended == true) if($extended == true)
$profiles[] = $comment->getOwner()->getId(); $profiles[] = $comment->getOwner()->getId();
$attachments = null;
// Reset $attachments to not duplicate prikols
} }
$response = [ $response = [
@ -474,6 +603,14 @@ final class Wall extends VKAPIRequestHandler
$profiles = []; $profiles = [];
$attachments = [];
foreach($comment->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment);
}
}
$item = [ $item = [
"id" => $comment->getId(), "id" => $comment->getId(),
"from_id" => $comment->getOwner()->getId(), "from_id" => $comment->getOwner()->getId(),
@ -482,6 +619,7 @@ final class Wall extends VKAPIRequestHandler
"post_id" => $comment->getTarget()->getVirtualId(), "post_id" => $comment->getTarget()->getVirtualId(),
"owner_id" => $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(), "owner_id" => $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(),
"parents_stack" => [], "parents_stack" => [],
"attachments" => $attachments,
"likes" => [ "likes" => [
"can_like" => 1, "can_like" => 1,
"count" => $comment->getLikesCount(), "count" => $comment->getLikesCount(),
@ -512,16 +650,25 @@ final class Wall extends VKAPIRequestHandler
$response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []); $response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []);
} }
return $response; return $response;
} }
function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0) { 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); $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 || $post->isDeleted()) $this->fail(100, "Invalid post");
if($post->getTargetWall() < 0) if($post->getTargetWall() < 0)
$club = (new ClubsRepo)->get(abs($post->getTargetWall())); $club = (new ClubsRepo)->get(abs($post->getTargetWall()));
if(empty($message) && empty($attachments)) {
$this->fail(100, "Required parameter 'message' missing.");
}
$flags = 0; $flags = 0;
if($from_group != 0 && !is_null($club) && $club->canBeModifiedBy($this->user)) if($from_group != 0 && !is_null($club) && $club->canBeModifiedBy($this->user))
$flags |= 0b10000000; $flags |= 0b10000000;
@ -539,6 +686,49 @@ final class Wall extends VKAPIRequestHandler
$this->fail(1, "ошибка про то что коммент большой слишком"); $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);
}
}
}
if($post->getOwner()->getId() !== $this->user->getId()) if($post->getOwner()->getId() !== $this->user->getId())
if(($owner = $post->getOwner()) instanceof User) if(($owner = $post->getOwner()) instanceof User)
(new CommentNotification($owner, $comment, $post, $this->user))->emit(); (new CommentNotification($owner, $comment, $post, $this->user))->emit();
@ -551,6 +741,7 @@ final class Wall extends VKAPIRequestHandler
function deleteComment(int $comment_id) { function deleteComment(int $comment_id) {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction();
$comment = (new CommentsRepo)->get($comment_id); $comment = (new CommentsRepo)->get($comment_id);
if(!$comment) $this->fail(100, "One of the parameters specified was missing or invalid");; if(!$comment) $this->fail(100, "One of the parameters specified was missing or invalid");;
@ -570,10 +761,50 @@ final class Wall extends VKAPIRequestHandler
"date" => $attachment->getPublicationTime()->timestamp(), "date" => $attachment->getPublicationTime()->timestamp(),
"id" => $attachment->getVirtualId(), "id" => $attachment->getVirtualId(),
"owner_id" => $attachment->getOwner()->getId(), "owner_id" => $attachment->getOwner()->getId(),
"sizes" => array_values($attachment->getVkApiSizes()), "sizes" => !is_null($attachment->getVkApiSizes()) ? array_values($attachment->getVkApiSizes()) : NULL,
"text" => "", "text" => "",
"has_tags" => false "has_tags" => false
] ]
]; ];
} }
private function getApiPoll($attachment, $user) {
$answers = array();
foreach($attachment->getResults()->options as $answer) {
$answers[] = (object)[
"id" => $answer->id,
"rate" => $answer->pct,
"text" => $answer->name,
"votes" => $answer->votes
];
}
$userVote = array();
foreach($attachment->getUserVote($user) as $vote)
$userVote[] = $vote[0];
return [
"type" => "poll",
"poll" => [
"multiple" => $attachment->isMultipleChoice(),
"end_date" => $attachment->endsAt() == NULL ? 0 : $attachment->endsAt()->timestamp(),
"closed" => $attachment->hasEnded(),
"is_board" => false,
"can_edit" => false,
"can_vote" => $attachment->canVote($user),
"can_report" => false,
"can_share" => true,
"created" => 0,
"id" => $attachment->getId(),
"owner_id" => $attachment->getOwner()->getId(),
"question" => $attachment->getTitle(),
"votes" => $attachment->getVoterCount(),
"disable_unvote" => $attachment->isRevotable(),
"anonymous" => $attachment->isAnonymous(),
"answer_ids" => $userVote,
"answers" => $answers,
"author_id" => $attachment->getOwner()->getId(),
]
];
}
} }

View file

@ -1,10 +1,12 @@
# VK API Compatability layer for OpenVK # VK API Compatability layer for OpenVK
This directory contains VK api handlers, structures and relared This directory contains VK API handlers, structures and relared
exceptions. It is still a work-in-progress functionality. exceptions. It is still a work-in-progress functionality.
**Note**: requests to api are routed through **Note**: requests to API are routed through
openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers. openvk.Web.Presenters.VKAPIPresenter, this dir contains only handlers.
[Documentation for API clients](https://docs.openvk.uk/openvk_engine/api/description/)
## Implementing API methods ## Implementing API methods
VK API methods have names like this: `example.test`. To implement a VK API methods have names like this: `example.test`. To implement a

View file

@ -23,6 +23,11 @@ class APIToken extends RowModel
return $this->getId() . "-" . chunk_split($this->getSecret(), 8, "-") . "jill"; return $this->getId() . "-" . chunk_split($this->getSecret(), 8, "-") . "jill";
} }
function getPlatform(): ?string
{
return $this->getRecord()->platform;
}
function isRevoked(): bool function isRevoked(): bool
{ {
return $this->isDeleted(); return $this->isDeleted();

View file

@ -66,4 +66,31 @@ class Album extends MediaCollection
{ {
return $this->has($photo); return $this->has($photo);
} }
function toVkApiStruct(?User $user = NULL, bool $need_covers = false, bool $photo_sizes = false): object
{
$res = (object) [];
$res->id = $this->getPrettyId();
$res->thumb_id = !is_null($this->getCoverPhoto()) ? $this->getCoverPhoto()->getPrettyId() : 0;
$res->owner_id = $this->getOwner()->getId();
$res->title = $this->getName();
$res->description = $this->getDescription();
$res->created = $this->getCreationTime()->timestamp();
$res->updated = $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL;
$res->size = $this->size();
$res->privacy_comment = 1;
$res->upload_by_admins_only = 1;
$res->comments_disabled = 0;
$res->can_upload = $this->canBeModifiedBy($user); # thisUser недоступен в entities
if($need_covers) {
$res->thumb_src = $this->getCoverURL();
if($photo_sizes) {
$res->sizes = !is_null($this->getCoverPhoto()) ? $this->getCoverPhoto()->getVkApiSizes() : NULL;
}
}
return $res;
}
} }

View file

@ -262,12 +262,12 @@ class Club extends RowModel
return $subbed && ($this->getOpennesStatus() === static::CLOSED ? $this->isSubscriptionAccepted($user) : true); return $subbed && ($this->getOpennesStatus() === static::CLOSED ? $this->isSubscriptionAccepted($user) : true);
} }
function getFollowersQuery(): GroupedSelection function getFollowersQuery(string $sort = "follower ASC"): GroupedSelection
{ {
$query = $this->getRecord()->related("subscriptions.target"); $query = $this->getRecord()->related("subscriptions.target");
if($this->getOpennesStatus() === static::OPEN) { if($this->getOpennesStatus() === static::OPEN) {
$query = $query->where("model", "openvk\\Web\\Models\\Entities\\Club"); $query = $query->where("model", "openvk\\Web\\Models\\Entities\\Club")->order($sort);
} else { } else {
return false; return false;
} }
@ -280,9 +280,9 @@ class Club extends RowModel
return sizeof($this->getFollowersQuery()); return sizeof($this->getFollowersQuery());
} }
function getFollowers(int $page = 1): \Traversable function getFollowers(int $page = 1, int $perPage = 6, string $sort = "follower ASC"): \Traversable
{ {
$rels = $this->getFollowersQuery()->page($page, 6); $rels = $this->getFollowersQuery($sort)->page($page, $perPage);
foreach($rels as $rel) { foreach($rels as $rel) {
$rel = (new Users)->get($rel->follower); $rel = (new Users)->get($rel->follower);
@ -360,5 +360,35 @@ class Club extends RowModel
return $this->getRecord()->alert; return $this->getRecord()->alert;
} }
function toVkApiStruct(?User $user = NULL): object
{
$res = [];
$res->id = $this->getId();
$res->name = $this->getName();
$res->screen_name = $this->getShortCode();
$res->is_closed = 0;
$res->deactivated = NULL;
$res->is_admin = $this->canBeModifiedBy($user);
if($this->canBeModifiedBy($user)) {
$res->admin_level = 3;
}
$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;
}
use Traits\TBackDrops;
use Traits\TSubscribable; use Traits\TSubscribable;
} }

View file

@ -2,6 +2,7 @@
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Repositories\Clubs; use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Note};
class Comment extends Post class Comment extends Post
{ {
@ -52,4 +53,36 @@ class Comment extends Post
$this->getTarget() instanceof Post && $this->getTarget()->getTargetWall() < 0 && (new Clubs)->get(abs($this->getTarget()->getTargetWall()))->canBeModifiedBy($user) || $this->getTarget() instanceof Post && $this->getTarget()->getTargetWall() < 0 && (new Clubs)->get(abs($this->getTarget()->getTargetWall()))->canBeModifiedBy($user) ||
$this->getTarget() instanceof Topic && $this->getTarget()->canBeModifiedBy($user); $this->getTarget() instanceof Topic && $this->getTarget()->canBeModifiedBy($user);
} }
function toVkApiStruct(?User $user = NULL, bool $need_likes = false, bool $extended = false, ?Note $note = NULL): object
{
$res = (object) [];
$res->id = $this->getId();
$res->from_id = $this->getOwner()->getId();
$res->date = $this->getPublicationTime()->timestamp();
$res->text = $this->getText();
$res->attachments = [];
$res->parents_stack = [];
if(!is_null($note)) {
$res->uid = $this->getOwner()->getId();
$res->nid = $note->getId();
$res->oid = $note->getOwner()->getId();
}
foreach($this->getChildren() as $attachment) {
if($attachment->isDeleted())
continue;
$res->attachments[] = $attachment->toVkApiStruct();
}
if($need_likes) {
$res->count = $this->getLikesCount();
$res->user_likes = (int)$this->hasLikeFrom($user);
$res->can_like = 1;
}
return $res;
}
} }

View file

@ -118,4 +118,23 @@ class Note extends Postable
{ {
return $this->getRecord()->source; return $this->getRecord()->source;
} }
function toVkApiStruct(): object
{
$res = (object) [];
$res->id = $this->getId();
$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->privacy_view = 1;
$res->can_comment = 1;
$res->text_wiki = "r";
return $res;
}
} }

View file

@ -8,6 +8,6 @@ final class CommentNotification extends Notification
function __construct(User $recipient, Comment $comment, $postable, User $commenter) function __construct(User $recipient, Comment $comment, $postable, User $commenter)
{ {
parent::__construct($recipient, $postable, $commenter, time(), ovk_proc_strtr($comment->getText(), 10)); parent::__construct($recipient, $postable, $commenter, time(), ovk_proc_strtr(strip_tags($comment->getText()), 400));
} }
} }

View file

@ -1,13 +1,14 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Notifications; namespace openvk\Web\Models\Entities\Notifications;
use openvk\Web\Models\Entities\Postable;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
final class MentionNotification extends Notification final class MentionNotification extends Notification
{ {
protected $actionCode = 4; protected $actionCode = 4;
function __construct(User $recipient, User $target, User $mentioner) function __construct(User $recipient, Postable $discussionHost, $mentioner, string $quote = "")
{ {
parent::__construct($recipient, $target, $mentioner, time(), ""); parent::__construct($recipient, $mentioner, $discussionHost, time(), $quote);
} }
} }

View file

@ -30,6 +30,11 @@ class Notification
return (int) json_decode(file_get_contents(__DIR__ . "/../../../../data/modelCodes.json"), true)[get_class($model)]; return (int) json_decode(file_get_contents(__DIR__ . "/../../../../data/modelCodes.json"), true)[get_class($model)];
} }
function reverseModelOrder(): bool
{
return false;
}
function getActionCode(): int function getActionCode(): int
{ {
return $this->actionCode; return $this->actionCode;

View file

@ -1,6 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use MessagePack\MessagePack; use MessagePack\MessagePack;
use Nette\Utils\ImageException;
use Nette\Utils\UnknownImageFileException;
use openvk\Web\Models\Entities\Album; use openvk\Web\Models\Entities\Album;
use openvk\Web\Models\Repositories\Albums; use openvk\Web\Models\Repositories\Albums;
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
@ -14,47 +16,61 @@ class Photo extends Media
const ALLOWED_SIDE_MULTIPLIER = 7; const ALLOWED_SIDE_MULTIPLIER = 7;
private function resizeImage(string $filename, string $outputDir, \SimpleXMLElement $size): array /**
* @throws \ImagickException
* @throws ImageException
* @throws UnknownImageFileException
*/
private function resizeImage(\Imagick $image, string $outputDir, \SimpleXMLElement $size): array
{ {
$res = [false]; $res = [false];
$image = Image::fromFile($filename);
$requiresProportion = ((string) $size["requireProp"]) != "none"; $requiresProportion = ((string) $size["requireProp"]) != "none";
if($requiresProportion) { if($requiresProportion) {
$props = explode(":", (string) $size["requireProp"]); $props = explode(":", (string) $size["requireProp"]);
$px = (int) $props[0]; $px = (int) $props[0];
$py = (int) $props[1]; $py = (int) $props[1];
if(($image->getWidth() / $image->getHeight()) > ($px / $py)) { if(($image->getImageWidth() / $image->getImageHeight()) > ($px / $py)) {
# For some weird reason using resize with EXACT flag causes system to consume an unholy amount of RAM $height = (int) ceil(($px * $image->getImageWidth()) / $py);
$image->crop(0, 0, "100%", (int) ceil(($px * $image->getWidth()) / $py)); $image->cropImage($image->getImageWidth(), $height, 0, 0);
$res[0] = true; $res[0] = true;
} }
} }
if(isset($size["maxSize"])) { if(isset($size["maxSize"])) {
$maxSize = (int) $size["maxSize"]; $maxSize = (int) $size["maxSize"];
$image->resize($maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT); $sizes = Image::calculateSize($image->getImageWidth(), $image->getImageHeight(), $maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT);
$image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1);
} else if(isset($size["maxResolution"])) { } else if(isset($size["maxResolution"])) {
$resolution = explode("x", (string) $size["maxResolution"]); $resolution = explode("x", (string) $size["maxResolution"]);
$image->resize((int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT); $sizes = Image::calculateSize(
$image->getImageWidth(), $image->getImageHeight(), (int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT
);
$image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1);
} else { } else {
throw new \RuntimeException("Malformed size description: " . (string) $size["id"]); throw new \RuntimeException("Malformed size description: " . (string) $size["id"]);
} }
$res[1] = $image->getWidth(); $res[1] = $image->getImageWidth();
$res[2] = $image->getHeight(); $res[2] = $image->getImageHeight();
if($res[1] <= 300 || $res[2] <= 300) if($res[1] <= 300 || $res[2] <= 300)
$image->save("$outputDir/" . (string) $size["id"] . ".gif"); $image->writeImage("$outputDir/$size[id].gif");
else else
$image->save("$outputDir/" . (string) $size["id"] . ".jpeg"); $image->writeImage("$outputDir/$size[id].jpeg");
imagedestroy($image->getImageResource()); $res[3] = true;
$image->destroy();
unset($image); unset($image);
return $res; return $res;
} }
private function saveImageResizedCopies(string $filename, string $hash): void private function saveImageResizedCopies(?\Imagick $image, string $filename, string $hash): void
{ {
if(!$image) {
$image = new \Imagick;
$image->readImage($filename);
}
$dir = dirname($this->pathFromHash($hash)); $dir = dirname($this->pathFromHash($hash));
$dir = "$dir/$hash" . "_cropped"; $dir = "$dir/$hash" . "_cropped";
if(!is_dir($dir)) { if(!is_dir($dir)) {
@ -67,8 +83,13 @@ class Photo extends Media
throw new \RuntimeException("Could not load photosizes.xml!"); throw new \RuntimeException("Could not load photosizes.xml!");
$sizesMeta = []; $sizesMeta = [];
if(OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["photoSaving"] === "quick") {
foreach($sizes->Size as $size) foreach($sizes->Size as $size)
$sizesMeta[(string) $size["id"]] = $this->resizeImage($filename, $dir, $size); $sizesMeta[(string)$size["id"]] = [false, false, false, false];
} else {
foreach($sizes->Size as $size)
$sizesMeta[(string)$size["id"]] = $this->resizeImage(clone $image, $dir, $size);
}
$sizesMeta = MessagePack::pack($sizesMeta); $sizesMeta = MessagePack::pack($sizesMeta);
$this->stateChanges("sizes", $sizesMeta); $this->stateChanges("sizes", $sizesMeta);
@ -76,13 +97,19 @@ class Photo extends Media
protected function saveFile(string $filename, string $hash): bool protected function saveFile(string $filename, string $hash): bool
{ {
$image = Image::fromFile($filename); $image = new \Imagick;
if(($image->height >= ($image->width * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($image->width >= ($image->height * Photo::ALLOWED_SIDE_MULTIPLIER))) $image->readImage($filename);
$h = $image->getImageHeight();
$w = $image->getImageWidth();
if(($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER)))
throw new ISE("Invalid layout: image is too wide/short"); throw new ISE("Invalid layout: image is too wide/short");
$image->resize(8192, 4320, Image::SHRINK_ONLY | Image::FIT); $sizes = Image::calculateSize(
$image->save($this->pathFromHash($hash), 92, Image::JPEG); $image->getImageWidth(), $image->getImageHeight(), 8192, 4320, Image::SHRINK_ONLY | Image::FIT
$this->saveImageResizedCopies($filename, $hash); );
$image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1);
$image->writeImage($this->pathFromHash($hash));
$this->saveImageResizedCopies($image, $filename, $hash);
return true; return true;
} }
@ -115,7 +142,7 @@ class Photo extends Media
if(!$sizes || $forceUpdate) { if(!$sizes || $forceUpdate) {
if($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) { if($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) {
$hash = $this->getRecord()->hash; $hash = $this->getRecord()->hash;
$this->saveImageResizedCopies($this->pathFromHash($hash), $hash); $this->saveImageResizedCopies(NULL, $this->pathFromHash($hash), $hash);
$this->save(); $this->save();
return $this->getSizes(); return $this->getSizes();
@ -127,6 +154,16 @@ class Photo extends Media
$res = []; $res = [];
$sizes = MessagePack::unpack($sizes); $sizes = MessagePack::unpack($sizes);
foreach($sizes as $id => $meta) { foreach($sizes as $id => $meta) {
if(isset($meta[3]) && !$meta[3]) {
$res[$id] = (object) [
"url" => ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/photos/thumbnails/" . $this->getId() . "_$id.jpeg",
"width" => NULL,
"height" => NULL,
"crop" => NULL
];
continue;
}
$url = $this->getURL(); $url = $this->getURL();
$url = str_replace(".$this->fileExtension", "_cropped/$id.", $url); $url = str_replace(".$this->fileExtension", "_cropped/$id.", $url);
$url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg"; $url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg";
@ -150,6 +187,47 @@ class Photo extends Media
return $res; return $res;
} }
function forceSize(string $sizeName): bool
{
$hash = $this->getRecord()->hash;
$sizes = MessagePack::unpack($this->getRecord()->sizes);
$size = $sizes[$sizeName] ?? false;
if(!$size)
return $size;
if(!isset($size[3]) || $size[3] === true)
return true;
$path = $this->pathFromHash($hash);
$dir = dirname($this->pathFromHash($hash));
$dir = "$dir/$hash" . "_cropped";
if(!is_dir($dir)) {
@unlink($dir);
mkdir($dir);
}
$sizeMetas = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml");
if(!$sizeMetas)
throw new \RuntimeException("Could not load photosizes.xml!");
$sizeInfo = NULL;
foreach($sizeMetas->Size as $size)
if($size["id"] == $sizeName)
$sizeInfo = $size;
if(!$sizeInfo)
return false;
$pic = new \Imagick;
$pic->readImage($path);
$sizes[$sizeName] = $this->resizeImage($pic, $dir, $sizeInfo);
$this->stateChanges("sizes", MessagePack::pack($sizes));
$this->save();
return $sizes[$sizeName][3];
}
function getVkApiSizes(): ?array function getVkApiSizes(): ?array
{ {
$res = []; $res = [];
@ -205,22 +283,31 @@ class Photo extends Media
return [$x, $y]; return [$x, $y];
} }
function getPageURL(): string
{
if($this->isAnonymous())
return "/photos/" . base_convert((string) $this->getId(), 10, 32);
return "/photo" . $this->getPrettyId();
}
function getAlbum(): ?Album function getAlbum(): ?Album
{ {
return (new Albums)->getAlbumByPhotoId($this); return (new Albums)->getAlbumByPhotoId($this);
} }
function toVkApiStruct(): object function toVkApiStruct(bool $photo_sizes = true, bool $extended = false): object
{ {
$res = (object) []; $res = (object) [];
$res->id = $res->pid = $this->getId(); $res->id = $res->pid = $this->getId();
$res->owner_id = $res->user_id = $this->getOwner()->getId()->getId(); $res->owner_id = $res->user_id = $this->getOwner()->getId();
$res->aid = $res->album_id = NULL; $res->aid = $res->album_id = NULL;
$res->width = $this->getDimensions()[0]; $res->width = $this->getDimensions()[0];
$res->height = $this->getDimensions()[1]; $res->height = $this->getDimensions()[1];
$res->date = $res->created = $this->getPublicationTime()->timestamp(); $res->date = $res->created = $this->getPublicationTime()->timestamp();
if($photo_sizes) {
$res->sizes = $this->getVkApiSizes(); $res->sizes = $this->getVkApiSizes();
$res->src_small = $res->photo_75 = $this->getURLBySizeId("miniscule"); $res->src_small = $res->photo_75 = $this->getURLBySizeId("miniscule");
$res->src = $res->photo_130 = $this->getURLBySizeId("tiny"); $res->src = $res->photo_130 = $this->getURLBySizeId("tiny");
@ -229,6 +316,15 @@ class Photo extends Media
$res->src_xxbig = $res->photo_1280 = $this->getURLBySizeId("larger"); $res->src_xxbig = $res->photo_1280 = $this->getURLBySizeId("larger");
$res->src_xxxbig = $res->photo_2560 = $this->getURLBySizeId("original"); $res->src_xxxbig = $res->photo_2560 = $this->getURLBySizeId("original");
$res->src_original = $res->url = $this->getURLBySizeId("UPLOADED_MAXRES"); $res->src_original = $res->url = $this->getURLBySizeId("UPLOADED_MAXRES");
}
if($extended) {
$res->likes = $this->getLikesCount(); # их нету но пусть будут
$res->comments = $this->getCommentsCount();
$res->tags = 0;
$res->can_comment = 1;
$res->can_repost = 0;
}
return $res; return $res;
} }

View file

@ -0,0 +1,295 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Util\DateTime;
use \UnexpectedValueException;
use Nette\InvalidStateException;
use openvk\Web\Models\Repositories\Users;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Exceptions\PollLockedException;
use openvk\Web\Models\Exceptions\AlreadyVotedException;
use openvk\Web\Models\Exceptions\InvalidOptionException;
class Poll extends Attachable
{
protected $tableName = "polls";
private $choicesToPersist = [];
function getTitle(): string
{
return $this->getRecord()->title;
}
function getMetaDescription(): string
{
$props = [];
$props[] = tr($this->isAnonymous() ? "poll_anon" : "poll_public");
if($this->isMultipleChoice()) $props[] = tr("poll_multi");
if(!$this->isRevotable()) $props[] = tr("poll_lock");
if(!is_null($this->endsAt())) $props[] = tr("poll_until", $this->endsAt());
return implode("", $props);
}
function getOwner(): User
{
return (new Users)->get($this->getRecord()->owner);
}
function getOptions(): array
{
$options = $this->getRecord()->related("poll_options.poll");
$res = [];
foreach($options as $opt)
$res[$opt->id] = $opt->name;
return $res;
}
function getUserVote(User $user): ?array
{
$ctx = DatabaseConnection::i()->getContext();
$votedOpts = $ctx->table("poll_votes")
->where(["user" => $user->getId(), "poll" => $this->getId()]);
if($votedOpts->count() == 0)
return NULL;
$res = [];
foreach($votedOpts as $votedOpt) {
$option = $ctx->table("poll_options")->get($votedOpt->option);
$res[] = [$option->id, $option->name];
}
return $res;
}
function getVoters(int $optionId, int $page = 1, ?int $perPage = NULL): array
{
$res = [];
$ctx = DatabaseConnection::i()->getContext();
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$voters = $ctx->table("poll_votes")->where(["poll" => $this->getId(), "option" => $optionId]);
foreach($voters->page($page, $perPage) as $vote)
$res[] = (new Users)->get($vote->user);
return $res;
}
function getVoterCount(?int $optionId = NULL): int
{
$votes = DatabaseConnection::i()->getContext()->table("poll_votes");
if(!$optionId)
return $votes->select("COUNT(DISTINCT user) AS c")->where("poll", $this->getId())->fetch()->c;
return $votes->where(["poll" => $this->getId(), "option" => $optionId])->count();
}
function getResults(?User $user = NULL): object
{
$ctx = DatabaseConnection::i()->getContext();
$voted = NULL;
if(!is_null($user))
$voted = $this->getUserVote($user);
$result = (object) [];
$result->totalVotes = $this->getVoterCount();
$unsOptions = [];
foreach($this->getOptions() as $id => $title) {
$option = (object) [];
$option->id = $id;
$option->name = $title;
$option->votes = $this->getVoterCount($id);
$option->pct = $result->totalVotes == 0 ? 0 : min(100, floor(($option->votes / $result->totalVotes) * 100));
$option->voters = $this->getVoters($id, 1, 10);
if(!$user || !$voted)
$option->voted = NULL;
else
$option->voted = in_array([$id, $title], $voted);
$unsOptions[$id] = $option;
}
$optionsC = sizeof($unsOptions);
$sOptions = $unsOptions;
usort($sOptions, function($a, $b) { return $a->votes <=> $b->votes; });
for($i = 0; $i < $optionsC; $i++)
$unsOptions[$id]->rate = $optionsC - $i - 1;
$result->options = array_values($unsOptions);
return $result;
}
function isAnonymous(): bool
{
return (bool) $this->getRecord()->is_anonymous;
}
function isMultipleChoice(): bool
{
return (bool) $this->getRecord()->allows_multiple;
}
function isRevotable(): bool
{
return (bool) $this->getRecord()->can_revote;
}
function endsAt(): ?DateTime
{
if(!$this->getRecord()->until)
return NULL;
return new DateTime($this->getRecord()->until);
}
function hasEnded(): bool
{
if($this->getRecord()->ended)
return true;
if(!is_null($this->getRecord()->until))
return time() >= $this->getRecord()->until;
return false;
}
function hasVoted(User $user): bool
{
return !is_null($this->getUserVote($user));
}
function canVote(User $user): bool
{
return !$this->hasEnded() && !$this->hasVoted($user);
}
function vote(User $user, array $optionIds): void
{
if($this->hasEnded())
throw new PollLockedException;
if($this->hasVoted($user))
throw new AlreadyVotedException;
$optionIds = array_map(function($x) { return (int) $x; }, array_unique($optionIds));
$validOpts = array_keys($this->getOptions());
if(empty($optionIds) || (sizeof($optionIds) > 1 && !$this->isMultipleChoice()))
throw new UnexpectedValueException;
if(sizeof(array_diff($optionIds, $validOpts)) > 0)
throw new InvalidOptionException;
foreach($optionIds as $opt) {
DatabaseConnection::i()->getContext()->table("poll_votes")->insert([
"user" => $user->getId(),
"poll" => $this->getId(),
"option" => $opt,
]);
}
}
function revokeVote(User $user): void
{
if(!$this->isRevotable())
throw new PollLockedException;
$this->getRecord()->related("poll_votes.poll")
->where("user", $user->getId())->delete();
}
function setOwner(User $owner): void
{
$this->stateChanges("owner", $owner->getId());
}
function setEndDate(int $timestamp): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("until", $timestamp);
}
function setEnded(): void
{
$this->stateChanges("ended", 1);
}
function setOptions(array $options): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
if(sizeof($options) > ovkGetQuirk("polls.max-opts"))
throw new TooMuchOptionsException;
$this->choicesToPersist = $options;
}
function setRevotability(bool $canReVote): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("can_revote", $canReVote);
}
function setAnonymity(bool $anonymous): void
{
$this->stateChanges("is_anonymous", $anonymous);
}
function setMultipleChoice(bool $mc): void
{
$this->stateChanges("allows_multiple", $mc);
}
function importXML(User $owner, string $xml): void
{
$xml = simplexml_load_string($xml);
$this->setOwner($owner);
$this->setTitle($xml["title"] ?? "Untitled");
$this->setMultipleChoice(($xml["multiple"] ?? "no") == "yes");
$this->setAnonymity(($xml["anonymous"] ?? "no") == "yes");
$this->setRevotability(($xml["locked"] ?? "no") == "no");
if(ctype_digit((string) ($xml["duration"] ?? "")))
$this->setEndDate(time() + ((86400 * (int) $xml["duration"])));
$options = [];
foreach($xml->options->option as $opt)
$options[] = (string) $opt;
if(empty($options))
throw new UnexpectedValueException;
$this->setOptions($options);
}
static function import(User $owner, string $xml): Poll
{
$poll = new Poll;
$poll->importXML($owner, $xml);
$poll->save();
return $poll;
}
function save(): void
{
if(empty($this->choicesToPersist))
throw new InvalidStateException;
parent::save();
foreach($this->choicesToPersist as $option) {
DatabaseConnection::i()->getContext()->table("poll_options")->insert([
"poll" => $this->getId(),
"name" => $option,
]);
}
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Clubs; use openvk\Web\Models\Repositories\{Clubs, Users};
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Notifications\LikeNotification; use openvk\Web\Models\Entities\Notifications\LikeNotification;
@ -56,6 +56,15 @@ class Post extends Postable
return $this->getRecord()->wall; return $this->getRecord()->wall;
} }
function getWallOwner()
{
$w = $this->getRecord()->wall;
if($w < 0)
return (new Clubs)->get(abs($w));
return (new Users)->get($w);
}
function getRepostCount(): int function getRepostCount(): int
{ {
return sizeof( return sizeof(
@ -87,7 +96,12 @@ class Post extends Postable
function isDeactivationMessage(): bool function isDeactivationMessage(): bool
{ {
return ($this->getRecord()->flags & 0b00100000) > 0; return (($this->getRecord()->flags & 0b00100000) > 0) && ($this->getRecord()->owner > 0);
}
function isUpdateAvatarMessage(): bool
{
return (($this->getRecord()->flags & 0b00010000) > 0) && ($this->getRecord()->owner > 0);
} }
function isExplicit(): bool function isExplicit(): bool
@ -105,6 +119,63 @@ class Post extends Postable
return $this->getOwner(false)->getId(); return $this->getOwner(false)->getId();
} }
function getPlatform(bool $forAPI = false): ?string
{
$platform = $this->getRecord()->api_source_name;
if($forAPI) {
switch ($platform) {
case 'openvk_refresh_android':
case 'openvk_legacy_android':
return 'android';
break;
case 'openvk_ios':
case 'openvk_legacy_ios':
return 'iphone';
break;
case 'vika_touch': // кика хохотач ахахахаххахахахахах
case 'vk4me':
return 'mobile';
break;
case NULL:
return NULL;
break;
default:
return 'api';
break;
}
} else {
return $platform;
}
}
function getPlatformDetails(): array
{
$clients = simplexml_load_file(OPENVK_ROOT . "/data/clients.xml");
foreach($clients as $client) {
if($client['tag'] == $this->getPlatform()) {
return [
"tag" => $client['tag'],
"name" => $client['name'],
"url" => $client['url'],
"img" => $client['img']
];
break;
}
}
return [
"tag" => $this->getPlatform(),
"name" => NULL,
"url" => NULL,
"img" => NULL
];
}
function pin(): void function pin(): void
{ {
DB::i() DB::i()

View file

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\RowModel;
class SupportAgent extends RowModel
{
protected $tableName = "support_names";
function getAgentId(): int
{
return $this->getRecord()->agent;
}
function getName(): ?string
{
return $this->getRecord()->name;
}
function getCanonicalName(): string
{
return $this->getName();
}
function getAvatarURL(): ?string
{
return $this->getRecord()->icon;
}
function isShowNumber(): int
{
return $this->getRecord()->numerate;
}
function getRealName(): string
{
return (new Users)->get($this->getAgentId())->getCanonicalName();
}
}

View file

@ -42,7 +42,7 @@ class TicketComment extends RowModel
$alias = $this->getSupportAlias(); $alias = $this->getSupportAlias();
if(!$alias) if(!$alias)
return OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["supportName"] . "" . $this->getAgentNumber(); return tr("helpdesk_agent") . " #" . $this->getAgentNumber();
$name = $alias->getName(); $name = $alias->getName();
if($alias->shouldAppendNumber()) if($alias->shouldAppendNumber())

View file

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Traits;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Repositories\Photos;
trait TBackDrops {
function getBackDropPictureURLs(): ?array
{
$photo1 = $this->getRecord()->backdrop_1;
$photo2 = $this->getRecord()->backdrop_2;
if(is_null($photo1) && is_null($photo2))
return NULL;
$photo1obj = $photo2obj = NULL;
if(!is_null($photo1))
$photo1obj = (new Photos)->get($photo1);
if(!is_null($photo2))
$photo2obj = (new Photos)->get($photo2);
if(is_null($photo1obj) && is_null($photo2obj))
return NULL;
return [
is_null($photo1obj) ? "" : $photo1obj->getURL(),
is_null($photo2obj) ? "" : $photo2obj->getURL(),
];
}
function setBackDropPictures(?Photo $first, ?Photo $second): void
{
if(!is_null($first))
$this->stateChanges("backdrop_1", $first->getId());
if(!is_null($second))
$this->stateChanges("backdrop_2", $second->getId());
}
function unsetBackDropPictures(): void
{
$this->stateChanges("backdrop_1", NULL);
$this->stateChanges("backdrop_2", NULL);
}
}

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Traits; namespace openvk\Web\Models\Entities\Traits;
use openvk\Web\Models\Repositories\{Users, Clubs};
use Wkhooy\ObsceneCensorRus; use Wkhooy\ObsceneCensorRus;
trait TRichText trait TRichText
@ -35,9 +36,9 @@ trait TRichText
"%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%", "%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%",
(function (array $matches): string { (function (array $matches): string {
$href = str_replace("#", "&num;", $matches[1]); $href = str_replace("#", "&num;", $matches[1]);
$href = rawurlencode(str_replace(";", "&#59;", $matches[1])); $href = rawurlencode(str_replace(";", "&#59;", $href));
$link = str_replace("#", "&num;", $matches[3]); $link = str_replace("#", "&num;", $matches[3]);
$link = str_replace(";", "&#59;", $matches[3]); $link = str_replace(";", "&#59;", $link);
$rel = $this->isAd() ? "sponsored" : "ugc"; $rel = $this->isAd() ? "sponsored" : "ugc";
return "<a href='/away.php?to=$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]); return "<a href='/away.php?to=$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]);
@ -48,7 +49,63 @@ trait TRichText
private function removeZalgo(string $text): string private function removeZalgo(string $text): string
{ {
return preg_replace("%[\x{0300}-\x{036F}]{3,}%Xu", "<EFBFBD>", $text); return preg_replace("%\p{M}{3,}%Xu", "", $text);
}
function resolveMentions(array $skipUsers = []): \Traversable
{
$contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content";
$text = $this->getRecord()->{$contentColumn};
$text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%Xu", "[$1|$2]", $text);
$text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text);
$resolvedUsers = $skipUsers;
$resolvedClubs = [];
preg_match_all("%\[([A-Za-z0-9]++)\|((?:[\p{L&}\p{Lo} 0-9@]\p{Mn}?)++)\]%Xu", $text, $links, PREG_PATTERN_ORDER);
foreach($links[1] as $link) {
if(preg_match("%^id([0-9]++)$%", $link, $match)) {
$uid = (int) $match[1];
if(in_array($uid, $resolvedUsers))
continue;
$resolvedUsers[] = $uid;
$maybeUser = (new Users)->get($uid);
if($maybeUser)
yield $maybeUser;
} else if(preg_match("%^(?:club|public|event)([0-9]++)$%", $link, $match)) {
$cid = (int) $match[1];
if(in_array($cid, $resolvedClubs))
continue;
$resolvedClubs[] = $cid;
$maybeClub = (new Clubs)->get($cid);
if($maybeClub)
yield $maybeClub;
} else {
$maybeUser = (new Users)->getByShortURL($link);
if($maybeUser) {
$uid = $maybeUser->getId();
if(in_array($uid, $resolvedUsers))
continue;
else
$resolvedUsers[] = $uid;
yield $maybeUser;
continue;
}
$maybeClub = (new Clubs)->getByShortURL($link);
if($maybeClub) {
$cid = $maybeClub->getId();
if(in_array($cid, $resolvedClubs))
continue;
else
$resolvedClubs[] = $cid;
yield $maybeClub;
}
}
}
} }
function getText(bool $html = true): string function getText(bool $html = true): string
@ -59,7 +116,6 @@ trait TRichText
$proc = iconv_strlen($this->getRecord()->{$contentColumn}) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"]; $proc = iconv_strlen($this->getRecord()->{$contentColumn}) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"];
if($html) { if($html) {
if($proc) { if($proc) {
$rel = $this->isAd() ? "sponsored" : "ugc";
$text = $this->formatLinks($text); $text = $this->formatLinks($text);
$text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%Xu", "[$1|$2]", $text); $text = preg_replace("%@([A-Za-z0-9]++) \(((?:[\p{L&}\p{Lo} 0-9]\p{Mn}?)++)\)%Xu", "[$1|$2]", $text);
$text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text); $text = preg_replace("%([\n\r\s]|^)(@([A-Za-z0-9]++))%Xu", "$1[$3|@$3]", $text);

View file

@ -5,7 +5,7 @@ use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime; use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Gifts, Notifications, Blacklists}; use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Photos, Gifts, Notifications, Blacklists};
use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Models\Exceptions\InvalidUserNameException;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -148,8 +148,9 @@ class User extends RowModel
function getFirstName(bool $pristine = false): string function getFirstName(bool $pristine = false): string
{ {
$name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE)); $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->first_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine) $tsn = tr("__transNames");
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE); if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine)
return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE);
else else
return $name; return $name;
} }
@ -157,8 +158,9 @@ class User extends RowModel
function getLastName(bool $pristine = false): string function getLastName(bool $pristine = false): string
{ {
$name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE)); $name = ($this->isDeleted() && !$this->isDeactivated() ? "DELETED" : mb_convert_case($this->getRecord()->last_name, MB_CASE_TITLE));
if((($ts = tr("__transNames")) !== "@__transNames") && !$pristine) $tsn = tr("__transNames");
return mb_convert_case(transliterator_transliterate($ts, $name), MB_CASE_TITLE); if(( $tsn !== "@__transNames" && !empty($tsn) ) && !$pristine)
return mb_convert_case(transliterator_transliterate($tsn, $name), MB_CASE_TITLE);
else else
return $name; return $name;
} }
@ -538,12 +540,15 @@ class User extends RowModel
return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1])); return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1]));
} }
function getClubs(int $page = 1, bool $admin = false): \Traversable function getClubs(int $page = 1, bool $admin = false, int $count = OPENVK_DEFAULT_PER_PAGE, bool $offset = false): \Traversable
{ {
if(!$offset)
$page = ($page - 1) * $count;
if($admin) { if($admin) {
$id = $this->getId(); $id = $this->getId();
$query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?"; $query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?";
$query .= " LIMIT " . OPENVK_DEFAULT_PER_PAGE . " OFFSET " . ($page - 1) * OPENVK_DEFAULT_PER_PAGE; $query .= " LIMIT " . $count . " OFFSET " . $page;
$sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id); $sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id);
foreach($sel as $target) { foreach($sel as $target) {
@ -553,7 +558,7 @@ class User extends RowModel
yield $target; yield $target;
} }
} else { } else {
$sel = $this->getRecord()->related("subscriptions.follower")->page($page, OPENVK_DEFAULT_PER_PAGE); $sel = $this->getRecord()->related("subscriptions.follower")->limit($count, $page);
foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) { foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) {
$target = (new Clubs)->get($target->target); $target = (new Clubs)->get($target->target);
if(!$target) continue; if(!$target) continue;
@ -749,6 +754,63 @@ class User extends RowModel
return time() - $this->getRecord()->online <= 300; return time() - $this->getRecord()->online <= 300;
} }
function getOnlinePlatform(bool $forAPI = false): ?string
{
$platform = $this->getRecord()->client_name;
if($forAPI) {
switch ($platform) {
case 'openvk_refresh_android':
case 'openvk_legacy_android':
return 'android';
break;
case 'openvk_ios':
case 'openvk_legacy_ios':
return 'iphone';
break;
case 'vika_touch': // кика хохотач ахахахаххахахахахах
case 'vk4me':
return 'mobile';
break;
case NULL:
return NULL;
break;
default:
return 'api';
break;
}
} else {
return $platform;
}
}
function getOnlinePlatformDetails(): array
{
$clients = simplexml_load_file(OPENVK_ROOT . "/data/clients.xml");
foreach($clients as $client) {
if($client['tag'] == $this->getOnlinePlatform()) {
return [
"tag" => $client['tag'],
"name" => $client['name'],
"url" => $client['url'],
"img" => $client['img']
];
break;
}
}
return [
"tag" => $this->getOnlinePlatform(),
"name" => NULL,
"url" => NULL,
"img" => NULL
];
}
function prefersNotToSeeRating(): bool function prefersNotToSeeRating(): bool
{ {
return !((bool) $this->getRecord()->show_rating); return !((bool) $this->getRecord()->show_rating);
@ -950,6 +1012,15 @@ class User extends RowModel
return true; return true;
} }
function updOnline(string $platform): bool
{
$this->setOnline(time());
$this->setClient_name($platform);
$this->save();
return true;
}
function changeEmail(string $email): void function changeEmail(string $email): void
{ {
DatabaseConnection::i()->getContext()->table("ChandlerUsers") DatabaseConnection::i()->getContext()->table("ChandlerUsers")
@ -1047,5 +1118,23 @@ class User extends RowModel
return true; return true;
} }
function toVkApiStruct(): object
{
$res = (object) [];
$res->id = $this->getId();
$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
return $res;
}
use Traits\TBackDrops;
use Traits\TSubscribable; use Traits\TSubscribable;
} }

View file

@ -13,7 +13,7 @@ class Video extends Media
const TYPE_EMBED = 1; const TYPE_EMBED = 1;
protected $tableName = "videos"; protected $tableName = "videos";
protected $fileExtension = "ogv"; protected $fileExtension = "mp4";
protected $processingPlaceholder = "video/rendering"; protected $processingPlaceholder = "video/rendering";
@ -30,7 +30,7 @@ class Video extends Media
throw new \DomainException("$filename does not contain any video streams"); throw new \DomainException("$filename does not contain any video streams");
$durations = []; $durations = [];
preg_match('%duration=([0-9\.]++)%', $streams, $durations); preg_match_all('%duration=([0-9\.]++)%', $streams, $durations);
if(sizeof($durations[1]) === 0) if(sizeof($durations[1]) === 0)
throw new \DomainException("$filename does not contain any meaningful video streams"); throw new \DomainException("$filename does not contain any meaningful video streams");
@ -104,7 +104,7 @@ class Video extends Media
if(!$this->isProcessed()) if(!$this->isProcessed())
return "/assets/packages/static/openvk/video/rendering.apng"; return "/assets/packages/static/openvk/video/rendering.apng";
return preg_replace("%\.[A-z]++$%", ".gif", $this->getURL()); return preg_replace("%\.[A-z0-9]++$%", ".gif", $this->getURL());
} else { } else {
return $this->getVideoDriver()->getThumbnailURL(); return $this->getVideoDriver()->getThumbnailURL();
} }
@ -115,6 +115,61 @@ class Video extends Media
return $this->getRecord()->owner; return $this->getRecord()->owner;
} }
function getApiStructure(): object
{
return (object)[
"type" => "video",
"video" => [
"can_comment" => 1,
"can_like" => 0, // we don't h-have wikes in videos
"can_repost" => 0,
"can_subscribe" => 1,
"can_add_to_faves" => 0,
"can_add" => 0,
"comments" => $this->getCommentsCount(),
"date" => $this->getPublicationTime()->timestamp(),
"description" => $this->getDescription(),
"duration" => 0, // я хуй знает как получить длину видео
"image" => [
[
"url" => $this->getThumbnailURL(),
"width" => 320,
"height" => 240,
"with_padding" => 1
]
],
"width" => 640,
"height" => 480,
"id" => $this->getVirtualId(),
"owner_id" => $this->getOwner()->getId(),
"user_id" => $this->getOwner()->getId(),
"title" => $this->getName(),
"is_favorite" => false,
"player" => $this->getURL(),
"files" => [
"mp4_480" => $this->getURL()
],
"added" => 0,
"repeat" => 0,
"type" => "video",
"views" => 0,
"likes" => [
"count" => 0,
"user_likes" => 0
],
"reposts" => [
"count" => 0,
"user_reposted" => 0
]
]
];
}
function toVkApiStruct(): object
{
return $this->getApiStructure();
}
function setLink(string $link): string function setLink(string $link): string
{ {
if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/youtube.txt"), $link, $matches)) { if(preg_match(file_get_contents(__DIR__ . "/../VideoDrivers/regex/youtube.txt"), $link, $matches)) {
@ -145,11 +200,14 @@ class Video extends Media
$this->save(); $this->save();
} }
static function fastMake(int $owner, string $description = "", array $file, bool $unlisted = true, bool $anon = false): Video static function fastMake(int $owner, string $name = "Unnamed Video.ogv", string $description = "", array $file, bool $unlisted = true, bool $anon = false): Video
{ {
if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
exit(VIDEOS_FRIENDLY_ERROR);
$video = new Video; $video = new Video;
$video->setOwner($owner); $video->setOwner($owner);
$video->setName("Unnamed Video.ogv"); $video->setName(ovk_proc_strtr($name, 61));
$video->setDescription(ovk_proc_strtr($description, 300)); $video->setDescription(ovk_proc_strtr($description, 300));
$video->setAnonymous($anon); $video->setAnonymous($anon);
$video->setCreated(time()); $video->setCreated(time());

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class AlreadyVotedException extends \RuntimeException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class InvalidOptionException extends \UnexpectedValueException
{
}

View file

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
use Nette\InvalidStateException;
final class PollLockedException extends InvalidStateException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class TooMuchOptionsException extends \UnexpectedValueException
{
}

View file

@ -123,4 +123,14 @@ class Albums
return $dbalbum->collection ? $this->get($dbalbum->collection) : null; return $dbalbum->collection ? $this->get($dbalbum->collection) : null;
} }
function getAlbumByOwnerAndId(int $owner, int $id)
{
$album = $this->albums->where([
"owner" => $owner,
"id" => $id
])->fetch();
return new Album($album);
}
} }

View file

@ -66,4 +66,12 @@ class Applications
{ {
return sizeof($this->appRels->where("user", $user->getId())); return sizeof($this->appRels->where("user", $user->getId()));
} }
function find(string $query, array $pars = [], string $sort = "id"): Util\EntityStream
{
$query = "%$query%";
$result = $this->apps->where("CONCAT_WS(' ', name, description) LIKE ?", $query)->where("enabled", 1);
return new Util\EntityStream("Application", $result->order("$sort"));
}
} }

View file

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Entities\User;
use Chandler\Security\User as ChandlerUser;
class ChandlerGroups
{
private $context;
private $groups;
public function __construct()
{
$this->context = DB::i()->getContext();
$this->groups = $this->context->table("ChandlerGroups");
$this->members = $this->context->table("ChandlerACLRelations");
$this->perms = $this->context->table("ChandlerACLGroupsPermissions");
}
function get(string $UUID): ?ActiveRow
{
return $this->groups->where("id", $UUID)->fetch();
}
function getList(): \Traversable
{
foreach($this->groups as $group) yield $group;
}
function getMembersById(string $UUID): \Traversable
{
foreach($this->members->where("group", $UUID) as $member)
yield (new Users)->getByChandlerUser(
new ChandlerUser($this->context->table("ChandlerUsers")->where("id", $member->user)->fetch())
);
}
function getUsersMemberships(string $UUID): \Traversable
{
foreach($this->members->where("user", $UUID) as $member) yield $member;
}
function getPermissionsById(string $UUID): \Traversable
{
foreach($this->perms->where("group", $UUID) as $perm) yield $perm;
}
}

View file

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Entities\User;
use Chandler\Security\User as ChandlerUser;
class ChandlerUsers
{
private $context;
private $users;
public function __construct()
{
$this->context = DB::i()->getContext();
$this->users = $this->context->table("ChandlerUsers");
}
private function toUser(?ActiveRow $ar): ?ChandlerUser
{
return is_null($ar) ? NULL : (new User($ar))->getChandlerUser();
}
function get(int $id): ?ChandlerUser
{
return (new Users)->get($id)->getChandlerUser();
}
function getById(string $UUID): ?ChandlerUser
{
return new ChandlerUser($this->users->where("id", $UUID)->fetch());
}
function getList(int $page = 1): \Traversable
{
foreach($this->users as $user)
yield new ChandlerUser($user);
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories; namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\{Club, Manager};
use openvk\Web\Models\Repositories\Aliases; use openvk\Web\Models\Repositories\{Aliases, Users};
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -9,11 +9,13 @@ class Clubs
{ {
private $context; private $context;
private $clubs; private $clubs;
private $coadmins;
function __construct() function __construct()
{ {
$this->context = DatabaseConnection::i()->getContext(); $this->context = DatabaseConnection::i()->getContext();
$this->clubs = $this->context->table("groups"); $this->clubs = $this->context->table("groups");
$this->coadmins = $this->context->table("group_coadmins");
} }
private function toClub(?ActiveRow $ar): ?Club private function toClub(?ActiveRow $ar): ?Club
@ -41,12 +43,12 @@ class Clubs
return $this->toClub($this->clubs->get($id)); return $this->toClub($this->clubs->get($id));
} }
function find(string $query, int $page = 1, ?int $perPage = NULL): \Traversable function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable
{ {
$query = "%$query%"; $query = "%$query%";
$result = $this->clubs->where("name LIKE ? OR about LIKE ?", $query, $query); $result = $this->clubs->where("name LIKE ? OR about LIKE ?", $query, $query);
return new Util\EntityStream("Club", $result); return new Util\EntityStream("Club", $result->order($sort));
} }
function getCount(): int function getCount(): int
@ -71,5 +73,25 @@ class Clubs
*/ */
} }
function getWriteableClubs(int $id): \Traversable
{
$result = $this->clubs->where("owner", $id);
$coadmins = $this->coadmins->where("user", $id);
foreach($result as $entry) {
yield new Club($entry);
}
foreach($coadmins as $coadmin) {
$cl = new Manager($coadmin);
yield $cl->getClub();
}
}
function getWriteableClubsCount(int $id): int
{
return sizeof($this->clubs->where("owner", $id)) + sizeof($this->coadmins->where("user", $id));
}
use \Nette\SmartObject; use \Nette\SmartObject;
} }

View file

@ -59,4 +59,35 @@ class Comments
"deleted" => false, "deleted" => false,
])); ]));
} }
function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream
{
$query = "%$query%";
$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);
if($nnparamsCount > 0) {
foreach($notNullParams 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"));
}
} }

View file

@ -26,10 +26,10 @@ class Notes
return $this->toNote($this->notes->get($id)); return $this->toNote($this->notes->get($id));
} }
function getUserNotes(User $user, int $page = 1, ?int $perPage = NULL): \Traversable function getUserNotes(User $user, int $page = 1, ?int $perPage = NULL, string $sort = "DESC"): \Traversable
{ {
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->order("created DESC")->page($page, $perPage) as $album) foreach($this->notes->where("owner", $user->getId())->where("deleted", 0)->order("created $sort")->page($page, $perPage) as $album)
yield new Note($album); yield new Note($album);
} }

View file

@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories; namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Photo; use openvk\Web\Models\Entities\{Photo, User};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
class Photos class Photos
@ -32,4 +32,15 @@ class Photos
return new Photo($photo); return new Photo($photo);
} }
function getEveryUserPhoto(User $user): \Traversable
{
$photos = $this->photos->where([
"owner" => $user->getId()
]);
foreach($photos as $photo) {
yield new Photo($photo);
}
}
} }

View file

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Poll;
class Polls
{
private $polls;
function __construct()
{
$this->polls = DatabaseConnection::i()->getContext()->table("polls");
}
function get(int $id): ?Poll
{
$poll = $this->polls->get($id);
if(!$poll)
return NULL;
return new Poll($poll);
}
}

View file

@ -100,6 +100,38 @@ class Posts
} }
function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream
{
$query = "%$query%";
$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->posts->where("content LIKE ?", $query)->where("deleted", 0);
$nnparamsCount = sizeof($notNullParams);
if($nnparamsCount > 0) {
foreach($notNullParams as $paramName => $paramValue) {
switch($paramName) {
case "before":
$result->where("created < ?", $paramValue);
break;
case "after":
$result->where("created > ?", $paramValue);
break;
}
}
}
return new Util\EntityStream("Post", $result->order("$sort"));
}
function getPostCountOnUserWall(int $user): int function getPostCountOnUserWall(int $user): int
{ {
return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])); return sizeof($this->posts->where(["wall" => $user, "deleted" => 0]));

View file

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\{User, SupportAgent};
class SupportAgents
{
private $context;
private $tickets;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->agents = $this->context->table("support_names");
}
private function toAgent(?ActiveRow $ar)
{
return is_null($ar) ? NULL : new SupportAgent($ar);
}
function get(int $id): ?SupportAgent
{
return $this->toAgent($this->agents->where("agent", $id)->fetch());
}
function isExists(int $id): bool
{
return !is_null($this->get($id));
}
}

View file

@ -28,5 +28,12 @@ class TicketComments
return NULL; return NULL;
} }
function getCountByAgent(int $agent_id, int $mark = NULL): int
{
$filter = ['user_id' => $agent_id, 'user_type' => 1];
$mark && $filter['mark'] = $mark;
return sizeof($this->comments->where($filter));
}
use \Nette\SmartObject; use \Nette\SmartObject;
} }

View file

@ -49,12 +49,88 @@ class Users
return $this->toUser($this->users->where("user", $user->getId())->fetch()); return $this->toUser($this->users->where("user", $user->getId())->fetch());
} }
function find(string $query): Util\EntityStream function find(string $query, array $pars = [], string $sort = "id DESC"): Util\EntityStream
{ {
$query = "%$query%"; $query = "%$query%";
$result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo, shortcode) LIKE ?", $query)->where("deleted", 0); $result = $this->users->where("CONCAT_WS(' ', first_name, last_name, pseudo, shortcode) LIKE ?", $query)->where("deleted", 0);
return new Util\EntityStream("User", $result); $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;
}
}
}
return new Util\EntityStream("User", $result->order($sort));
} }
function getStatistics(): object function getStatistics(): object

View file

@ -45,4 +45,36 @@ class Videos
{ {
return sizeof($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])); return sizeof($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0]));
} }
function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream
{
$query = "%$query%";
$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);
if($nnparamsCount > 0) {
foreach($notNullParams as $paramName => $paramValue) {
switch($paramName) {
case "before":
$result->where("created < ?", $paramValue);
break;
case "after":
$result->where("created > ?", $paramValue);
break;
}
}
}
return new Util\EntityStream("Video", $result->order("$sort"));
}
} }

View file

@ -14,5 +14,5 @@ abstract class VideoDriver
abstract function getURL(): string; abstract function getURL(): string;
abstract function getEmbed(): string; abstract function getEmbed(string $w = "600", string $h = "340"): string;
} }

View file

@ -13,13 +13,13 @@ final class YouTubeVideoDriver extends VideoDriver
return "https://youtu.be/$this->id"; return "https://youtu.be/$this->id";
} }
function getEmbed(): string function getEmbed(string $w = "600", string $h = "340"): string
{ {
return <<<CODE return <<<CODE
<iframe <iframe
width="600" width="$w"
height="340" height="$h"
src="https://www.youtube.com/embed/$this->id" src="https://www.youtube-nocookie.com/embed/$this->id"
frameborder="0" frameborder="0"
sandbox="allow-same-origin allow-scripts allow-popups" sandbox="allow-same-origin allow-scripts allow-popups"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"

View file

@ -13,8 +13,8 @@ Move-Item $file $temp
# video stub logic was implicitly deprecated, so we start processing at once # 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 -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif"
ffmpeg -i $temp -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -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=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2
Move-Item $temp2 "$dir$hashT/$hash.ogv" Move-Item $temp2 "$dir$hashT/$hash.mp4"
Remove-Item $temp Remove-Item $temp
Remove-Item $temp2 Remove-Item $temp2

View file

@ -1,14 +1,12 @@
tmpfile="$RANDOM-$(date +%s%N)" tmpfile="$RANDOM-$(date +%s%N)"
cp $2 "/tmp/vid_$tmpfile.bin" cp $2 "/tmp/vid_$tmpfile.bin"
cp ../files/video/rendering.apng $3${4:0:2}/$4.gif
cp ../files/video/rendering.ogv $3/${4:0:2}/$4.ogv
nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif 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 libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y "/tmp/ffmOi$tmpfile.ogv" 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"
rm -rf $3${4:0:2}/$4.ogv rm -rf $3${4:0:2}/$4.mp4
mv "/tmp/ffmOi$tmpfile.ogv" $3${4:0:2}/$4.ogv mv "/tmp/ffmOi$tmpfile.mp4" $3${4:0:2}/$4.mp4
rm -f "/tmp/ffmOi$tmpfile.ogv" rm -f "/tmp/ffmOi$tmpfile.mp4"
rm -f "/tmp/vid_$tmpfile.bin" rm -f "/tmp/vid_$tmpfile.bin"

View file

@ -10,6 +10,8 @@ SELECT DISTINCT id, class FROM
sender_id = ? sender_id = ?
AND AND
sender_type = ? sender_type = ?
AND
deleted = 0
) UNION ( ) UNION (
SELECT SELECT
sender_id AS id, sender_id AS id,
@ -20,6 +22,8 @@ SELECT DISTINCT id, class FROM
recipient_id = ? recipient_id = ?
AND AND
recipient_type = ? recipient_type = ?
AND
deleted = 0
) )
ORDER BY ORDER BY
time time

View file

@ -38,6 +38,9 @@ final class AboutPresenter extends OpenVKPresenter
function renderBB(): void function renderBB(): void
{} {}
function renderTour(): void
{}
function renderInvite(): void function renderInvite(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
@ -76,6 +79,9 @@ final class AboutPresenter extends OpenVKPresenter
$this->assertNoCSRF(); $this->assertNoCSRF();
setLanguage($_GET['lg']); setLanguage($_GET['lg']);
} }
if(!is_null($_GET['jReturnTo']))
$this->redirect(rawurldecode($_GET['jReturnTo']));
} }
function renderExportJSLanguage($lg = NULL): void function renderExportJSLanguage($lg = NULL): void
@ -135,6 +141,6 @@ final class AboutPresenter extends OpenVKPresenter
function renderDev(): void function renderDev(): void
{ {
$this->redirect("https://docs.openvk.su/"); $this->redirect("https://docs.openvk.uk/");
} }
} }

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink}; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts, BannedLinks}; use openvk\Web\Models\Repositories\{ChandlerGroups, ChandlerUsers, Users, Clubs, Vouchers, Gifts, BannedLinks};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter final class AdminPresenter extends OpenVKPresenter
@ -11,14 +11,16 @@ final class AdminPresenter extends OpenVKPresenter
private $vouchers; private $vouchers;
private $gifts; private $gifts;
private $bannedLinks; private $bannedLinks;
private $chandlerGroups;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks) function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups)
{ {
$this->users = $users; $this->users = $users;
$this->clubs = $clubs; $this->clubs = $clubs;
$this->vouchers = $vouchers; $this->vouchers = $vouchers;
$this->gifts = $gifts; $this->gifts = $gifts;
$this->bannedLinks = $bannedLinks; $this->bannedLinks = $bannedLinks;
$this->chandlerGroups = $chandlerGroups;
parent::__construct(); parent::__construct();
} }
@ -62,6 +64,8 @@ final class AdminPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
$this->template->user = $user; $this->template->user = $user;
$this->template->c_groups_list = (new ChandlerGroups)->getList();
$this->template->c_memberships = $this->chandlerGroups->getUsersMemberships($user->getChandlerGUID());
if($_SERVER["REQUEST_METHOD"] !== "POST") if($_SERVER["REQUEST_METHOD"] !== "POST")
return; return;
@ -78,8 +82,13 @@ final class AdminPresenter extends OpenVKPresenter
$user->changeEmail($this->postParam("email")); $user->changeEmail($this->postParam("email"));
if($user->onlineStatus() != $this->postParam("online")) $user->setOnline(intval($this->postParam("online"))); if($user->onlineStatus() != $this->postParam("online")) $user->setOnline(intval($this->postParam("online")));
$user->setVerified(empty($this->postParam("verify") ? 0 : 1)); $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);
}
$user->save(); $user->save();
break; break;
} }
} }
@ -447,4 +456,95 @@ final class AdminPresenter extends OpenVKPresenter
$this->redirect("/admin/bannedLinks"); $this->redirect("/admin/bannedLinks");
} }
function renderChandlerGroups(): void
{
$this->template->groups = (new ChandlerGroups)->getList();
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$req = "INSERT INTO `ChandlerGroups` (`name`) VALUES ('" . $this->postParam("name") . "')";
DatabaseConnection::i()->getConnection()->query($req);
}
function renderChandlerGroup(string $UUID): void
{
$DB = DatabaseConnection::i()->getConnection();
if(is_null($DB->query("SELECT * FROM `ChandlerGroups` WHERE `id` = '$UUID'")->fetch()))
$this->flashFail("err", tr("error"), tr("c_group_not_found"));
$this->template->group = (new ChandlerGroups)->get($UUID);
$this->template->mode = in_array(
$this->queryParam("act"),
[
"main",
"members",
"permissions",
"removeMember",
"removePermission",
"delete"
]) ? $this->queryParam("act") : "main";
$this->template->members = (new ChandlerGroups)->getMembersById($UUID);
$this->template->perms = (new ChandlerGroups)->getPermissionsById($UUID);
if($this->template->mode == "removeMember") {
$where = "`user` = '" . $this->queryParam("uid") . "' AND `group` = '$UUID'";
if(is_null($DB->query("SELECT * FROM `ChandlerACLRelations` WHERE " . $where)->fetch()))
$this->flashFail("err", tr("error"), tr("c_user_is_not_in_group"));
$DB->query("DELETE FROM `ChandlerACLRelations` WHERE " . $where);
$this->flashFail("succ", tr("changes_saved"), tr("c_user_removed_from_group"));
} elseif($this->template->mode == "removePermission") {
$where = "`model` = '" . trim(addslashes($this->queryParam("model"))) . "' AND `permission` = '". $this->queryParam("perm") ."' AND `group` = '$UUID'";
if(is_null($DB->query("SELECT * FROM `ChandlerACLGroupsPermissions WHERE $where`")))
$this->flashFail("err", tr("error"), tr("c_permission_not_found"));
$DB->query("DELETE FROM `ChandlerACLGroupsPermissions` WHERE $where");
$this->flashFail("succ", tr("changes_saved"), tr("c_permission_removed_from_group"));
} elseif($this->template->mode == "delete") {
$DB->query("DELETE FROM `ChandlerGroups` WHERE `id` = '$UUID'");
$DB->query("DELETE FROM `ChandlerACLGroupsPermissions` WHERE `group` = '$UUID'");
$DB->query("DELETE FROM `ChandlerACLRelations` WHERE `group` = '$UUID'");
$this->flashFail("succ", tr("changes_saved"), tr("c_group_removed"));
}
if ($_SERVER["REQUEST_METHOD"] !== "POST") return;
$req = "";
if($this->template->mode == "main")
if($this->postParam("delete"))
$req = "DELETE FROM `ChandlerGroups` WHERE `id`='$UUID'";
else
$req = "UPDATE `ChandlerGroups` SET `name`='". $this->postParam('name') ."' , `color`='". $this->postParam("color") ."' WHERE `id`='$UUID'";
if($this->template->mode == "members")
if($this->postParam("uid"))
if(!is_null($DB->query("SELECT * FROM `ChandlerACLRelations` WHERE `user` = '" . $this->postParam("uid") . "'")))
$this->flashFail("err", tr("error"), tr("c_user_is_already_in_group"));
$req = "INSERT INTO `ChandlerACLRelations` (`user`, `group`, `priority`) VALUES ('". $this->postParam("uid") ."', '$UUID', 32)";
if($this->template->mode == "permissions")
$req = "INSERT INTO `ChandlerACLGroupsPermissions` (`group`, `model`, `permission`, `context`) VALUES ('$UUID', '". trim(addslashes($this->postParam("model"))) ."', '". $this->postParam("permission") ."', 0)";
$DB->query($req);
$this->flashFail("succ", tr("changes_saved"));
}
function renderChandlerUser(string $UUID): void
{
if(!$UUID) $this->notFound();
$c_user = (new ChandlerUsers())->getById($UUID);
$user = $this->users->getByChandlerUser($c_user);
if(!$user) $this->notFound();
$this->redirect("/admin/users/id" . $user->getId());
}
} }

View file

@ -81,6 +81,10 @@ final class AuthPresenter extends OpenVKPresenter
if(!Validator::i()->emailValid($this->postParam("email"))) if(!Validator::i()->emailValid($this->postParam("email")))
$this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment")); $this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment"));
if(OPENVK_ROOT_CONF['openvk']['preferences']['security']['forceStrongPassword'])
if(!Validator::i()->passwordStrong($this->postParam("password")))
$this->flashFail("err", tr("error"), tr("error_weak_password"));
if (strtotime($this->postParam("birthday")) > time()) if (strtotime($this->postParam("birthday")) > time())
$this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment")); $this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment"));
@ -203,6 +207,9 @@ final class AuthPresenter extends OpenVKPresenter
function renderFinishRestoringPassword(): void function renderFinishRestoringPassword(): void
{ {
if(OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring'])
$this->notFound();
$request = $this->restores->getByToken(str_replace(" ", "+", $this->queryParam("key"))); $request = $this->restores->getByToken(str_replace(" ", "+", $this->queryParam("key")));
if(!$request || !$request->isStillValid()) { if(!$request || !$request->isStillValid()) {
$this->flash("err", tr("token_manipulation_error"), tr("token_manipulation_error_comment")); $this->flash("err", tr("token_manipulation_error"), tr("token_manipulation_error_comment"));
@ -237,6 +244,9 @@ final class AuthPresenter extends OpenVKPresenter
function renderRestore(): void function renderRestore(): void
{ {
if(OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring'])
$this->notFound();
if(!is_null($this->user)) if(!is_null($this->user))
$this->redirect($this->user->identity->getURL()); $this->redirect($this->user->identity->getURL());

View file

@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Comment, Photo, Video, User, Topic, Post}; use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post};
use openvk\Web\Models\Entities\Notifications\CommentNotification; use openvk\Web\Models\Entities\Notifications\CommentNotification;
use openvk\Web\Models\Repositories\{Comments, Clubs}; use openvk\Web\Models\Repositories\{Comments, Clubs};
@ -48,6 +48,9 @@ final class CommentPresenter extends OpenVKPresenter
else if($entity instanceof Topic) else if($entity instanceof Topic)
$club = $entity->getClub(); $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.");
$flags = 0; $flags = 0;
if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity)) if($this->postParam("as_group") === "on" && !is_null($club) && $club->canBeModifiedBy($this->user->identity))
$flags |= 0b10000000; $flags |= 0b10000000;
@ -74,7 +77,7 @@ final class CommentPresenter extends OpenVKPresenter
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"]); $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]);
} }
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); $this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");
@ -106,6 +109,15 @@ final class CommentPresenter extends OpenVKPresenter
if(($owner = $entity->getOwner()) instanceof User) if(($owner = $entity->getOwner()) instanceof User)
(new CommentNotification($owner, $comment, $entity, $this->user->identity))->emit(); (new CommentNotification($owner, $comment, $entity, $this->user->identity))->emit();
$excludeMentions = [$this->user->identity->getId()];
if(($owner = $entity->getOwner()) instanceof User)
$excludeMentions[] = $owner->getId();
$mentions = iterator_to_array($comment->resolveMentions($excludeMentions));
foreach($mentions as $mentionee)
if($mentionee instanceof User)
(new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit();
$this->flashFail("succ", "Комментарий добавлен", "Ваш комментарий появится на странице."); $this->flashFail("succ", "Комментарий добавлен", "Ваш комментарий появится на странице.");
} }

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Club, Photo}; use openvk\Web\Models\Entities\{Club, Photo, Post};
use Nette\InvalidStateException;
use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; 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};
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
@ -191,7 +192,7 @@ final class GroupPresenter extends OpenVKPresenter
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
$club = $this->clubs->get($id); $club = $this->clubs->get($id);
if(!$club->canBeModifiedBy($this->user->identity)) if(!$club || !$club->canBeModifiedBy($this->user->identity))
$this->notFound(); $this->notFound();
else else
$this->template->club = $club; $this->template->club = $club;
@ -250,6 +251,88 @@ final class GroupPresenter extends OpenVKPresenter
} }
} }
function renderSetAvatar(int $id)
{
$photo = new Photo;
$club = $this->clubs->get($id);
if($_SERVER["REQUEST_METHOD"] === "POST" && $_FILES["ava"]["error"] === UPLOAD_ERR_OK) {
try {
$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->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", "Неизвестная ошибка", "Не удалось сохранить фотографию.");
}
}
$this->returnJson([
"url" => $photo->getURL(),
"id" => $photo->getPrettyId()
]);
}
function renderEditBackdrop(int $id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$club = $this->clubs->get($id);
if(!$club || !$club->canBeModifiedBy($this->user->identity))
$this->notFound();
else
$this->template->club = $club;
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
if($this->postParam("subact") === "remove") {
$club->unsetBackDropPictures();
$club->save();
$this->flashFail("succ", tr("backdrop_succ_rem"), tr("backdrop_succ_desc")); # will exit
}
$pic1 = $pic2 = NULL;
try {
if($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE)
$pic1 = Photo::fastMake($this->user->id, "Profile backdrop (system)", $_FILES["backdrop1"]);
if($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE)
$pic2 = Photo::fastMake($this->user->id, "Profile backdrop (system)", $_FILES["backdrop2"]);
} catch(InvalidStateException $e) {
$this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media"));
}
if($pic1 == $pic2 && is_null($pic1))
$this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media"));
$club->setBackDropPictures($pic1, $pic2);
$club->save();
$this->flashFail("succ", tr("backdrop_succ"), tr("backdrop_succ_desc"));
}
function renderStatistics(int $id): void function renderStatistics(int $id): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();

View file

@ -58,6 +58,11 @@ final class MessengerPresenter extends OpenVKPresenter
if(!$correspondent) if(!$correspondent)
$this->notFound(); $this->notFound();
if(!$this->user->identity->getPrivacyPermission('messages.write', $correspondent))
{
$this->flash("err", tr("warning"), tr("user_may_not_reply"));
}
$this->template->selId = $sel; $this->template->selId = $sel;
$this->template->correspondent = $correspondent; $this->template->correspondent = $correspondent;
} }

View file

@ -254,6 +254,7 @@ abstract class OpenVKPresenter extends SimplePresenter
$cacheTime = 0; # Force no cache $cacheTime = 0; # Force no cache
if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) {
$this->user->identity->setOnline(time()); $this->user->identity->setOnline(time());
$this->user->identity->setClient_name(NULL);
$this->user->identity->save(); $this->user->identity->save();
} }
@ -284,8 +285,12 @@ abstract class OpenVKPresenter extends SimplePresenter
parent::onBeforeRender(); parent::onBeforeRender();
$whichbrowser = new WhichBrowser\Parser(getallheaders()); $whichbrowser = new WhichBrowser\Parser(getallheaders());
$featurephonetheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultFeaturePhoneTheme"];
$mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"]; $mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"];
if($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL)
if($featurephonetheme && $this->isOldThing($whichbrowser) && Session::i()->get("_tempTheme") == NULL) {
$this->setSessionTheme($featurephonetheme);
} elseif($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL)
$this->setSessionTheme($mobiletheme); $this->setSessionTheme($mobiletheme);
$theme = NULL; $theme = NULL;
@ -318,4 +323,33 @@ abstract class OpenVKPresenter extends SimplePresenter
header("Content-Length: $size"); header("Content-Length: $size");
exit($payload); exit($payload);
} }
protected function isOldThing($whichbrowser) {
if($whichbrowser->isOs('Series60') ||
$whichbrowser->isOs('Series40') ||
$whichbrowser->isOs('Series80') ||
$whichbrowser->isOs('Windows CE') ||
$whichbrowser->isOs('Windows Mobile') ||
$whichbrowser->isOs('Nokia Asha Platform') ||
$whichbrowser->isOs('UIQ') ||
$whichbrowser->isEngine('NetFront') || // PSP and other japanese portable systems
$whichbrowser->isOs('Android') ||
$whichbrowser->isOs('iOS') ||
$whichbrowser->isBrowser('Internet Explorer', '<=', '8')) {
// yeah, it's old, but ios and android are?
if($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '<=', '9'))
return true;
elseif($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '>', '9'))
return false;
if($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '<=', '5'))
return true;
elseif($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '>', '5'))
return false;
return true;
} else {
return false;
}
}
} }

View file

@ -196,6 +196,18 @@ final class PhotosPresenter extends OpenVKPresenter
$this->renderPhoto($photo->getOwner(true)->getId(), $photo->getVirtualId()); $this->renderPhoto($photo->getOwner(true)->getId(), $photo->getVirtualId());
} }
function renderThumbnail($id, $size): void
{
$photo = $this->photos->get($id);
if(!$photo || $photo->isDeleted())
$this->notFound();
if(!$photo->forceSize($size))
chandler_http_panic(588, "Gone", "This thumbnail cannot be generated due to server misconfiguration");
$this->redirect($photo->getURLBySizeId($size), 8);
}
function renderEditPhoto(int $ownerId, int $photoId): void function renderEditPhoto(int $ownerId, int $photoId): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();

View file

@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Poll;
use openvk\Web\Models\Repositories\Polls;
final class PollPresenter extends OpenVKPresenter
{
private $polls;
function __construct(Polls $polls)
{
$this->polls = $polls;
parent::__construct();
}
function renderView(int $id): void
{
$poll = $this->polls->get($id);
if(!$poll)
$this->notFound();
$this->template->id = $poll->getId();
$this->template->title = $poll->getTitle();
$this->template->isAnon = $poll->isAnonymous();
$this->template->multiple = $poll->isMultipleChoice();
$this->template->unlocked = $poll->isRevotable();
$this->template->until = $poll->endsAt();
$this->template->votes = $poll->getVoterCount();
$this->template->meta = $poll->getMetaDescription();
$this->template->ended = $ended = $poll->hasEnded();
if((is_null($this->user) || $poll->canVote($this->user->identity)) && !$ended) {
$this->template->options = $poll->getOptions();
$this->template->_template = "Poll/Poll.xml";
return;
}
if(is_null($this->user)) {
$this->template->voted = false;
$this->template->results = $poll->getResults();
} else {
$this->template->voted = $poll->hasVoted($this->user->identity);
$this->template->results = $poll->getResults($this->user->identity);
}
$this->template->_template = "Poll/PollResults.xml";
}
function renderVoters(int $pollId): void
{
$poll = $this->polls->get($pollId);
if(!$poll)
$this->notFound();
if($poll->isAnonymous())
$this->flashFail("err", tr("forbidden"), tr("poll_err_anonymous"));
$options = $poll->getOptions();
$option = (int) base_convert($this->queryParam("option"), 32, 10);
if(!in_array($option, array_keys($options)))
$this->notFound();
$page = (int) ($this->queryParam("p") ?? 1);
$voters = $poll->getVoters($option, $page);
$this->template->pollId = $pollId;
$this->template->options = $options;
$this->template->option = [$option, $options[$option]];
$this->template->tabs = $options;
$this->template->iterator = $voters;
$this->template->count = $poll->getVoterCount($option);
$this->template->page = $page;
}
}

View file

@ -1,18 +1,28 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{User, Club}; use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs}; use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
final class SearchPresenter extends OpenVKPresenter final class SearchPresenter extends OpenVKPresenter
{ {
private $users; private $users;
private $clubs; private $clubs;
private $posts;
private $comments;
private $videos;
private $apps;
private $notes;
function __construct(Users $users, Clubs $clubs) function __construct(Users $users, Clubs $clubs)
{ {
$this->users = $users; $this->users = $users;
$this->clubs = $clubs; $this->clubs = $clubs;
$this->posts = new Posts;
$this->comments = new Comments;
$this->videos = new Videos;
$this->apps = new Applications;
$this->notes = new Notes;
parent::__construct(); parent::__construct();
} }
@ -21,6 +31,8 @@ final class SearchPresenter extends OpenVKPresenter
{ {
$query = $this->queryParam("query") ?? ""; $query = $this->queryParam("query") ?? "";
$type = $this->queryParam("type") ?? "users"; $type = $this->queryParam("type") ?? "users";
$sorter = $this->queryParam("sort") ?? "id";
$invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC";
$page = (int) ($this->queryParam("p") ?? 1); $page = (int) ($this->queryParam("p") ?? 1);
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -29,10 +41,57 @@ final class SearchPresenter extends OpenVKPresenter
# https://youtu.be/pSAWM5YuXx8 # https://youtu.be/pSAWM5YuXx8
$repos = [ "groups" => "clubs", "users" => "users" ]; $repos = [
"groups" => "clubs",
"users" => "users",
"posts" => "posts",
"comments" => "comments",
"videos" => "videos",
"audios" => "posts",
"apps" => "apps",
"notes" => "notes"
];
switch($sorter) {
default:
case "id":
$sort = "id " . $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."); $repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type.");
$results = $this->{$repo}->find($query); $results = $this->{$repo}->find($query, $parameters, $sort);
$iterator = $results->page($page); $iterator = $results->page($page);
$count = $results->size(); $count = $results->size();

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Ticket, TicketComment}; use openvk\Web\Models\Entities\{SupportAgent, Ticket, TicketComment};
use openvk\Web\Models\Repositories\{Tickets, Users, TicketComments}; use openvk\Web\Models\Repositories\{Tickets, Users, TicketComments, SupportAgents};
use openvk\Web\Util\Telegram; use openvk\Web\Util\Telegram;
use Chandler\Session\Session; use Chandler\Session\Session;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -342,4 +342,58 @@ final class SupportPresenter extends OpenVKPresenter
$user->save(); $user->save();
$this->returnJson([ "success" => true ]); $this->returnJson([ "success" => true ]);
} }
function renderAgent(int $id): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$support_names = new SupportAgents;
if(!$support_names->isExists($id))
$this->template->mode = "edit";
$this->template->agent_id = $id;
$this->template->mode = in_array($this->queryParam("act"), ["info", "edit"]) ? $this->queryParam("act") : "info";
$this->template->agent = $support_names->get($id) ?? NULL;
$this->template->counters = [
"all" => (new TicketComments)->getCountByAgent($id),
"good" => (new TicketComments)->getCountByAgent($id, 1),
"bad" => (new TicketComments)->getCountByAgent($id, 2)
];
if($id != $this->user->identity->getId())
if ($support_names->isExists($id))
$this->template->mode = "info";
else
$this->redirect("/support/agent" . $this->user->identity->getId());
}
function renderEditAgent(int $id): void
{
$this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0);
$this->assertNoCSRF();
$support_names = new SupportAgents;
$agent = $support_names->get($id);
if($agent)
if($agent->getAgentId() != $this->user->identity->getId()) $this->flashFail("err", tr("error"), tr("forbidden"));
if ($support_names->isExists($id)) {
$agent = $support_names->get($id);
$agent->setName($this->postParam("name") ?? tr("helpdesk_agent"));
$agent->setNumerate((int) $this->postParam("number") ?? NULL);
$agent->setIcon($this->postParam("avatar"));
$agent->save();
$this->flashFail("succ", "Успех", "Профиль отредактирован.");
} else {
$agent = new SupportAgent;
$agent->setAgent($this->user->identity->getId());
$agent->setName($this->postParam("name") ?? tr("helpdesk_agent"));
$agent->setNumerate((int) $this->postParam("number") ?? NULL);
$agent->setIcon($this->postParam("avatar"));
$agent->save();
$this->flashFail("succ", "Успех", "Профиль создан. Теперь пользователи видят Ваши псевдоним и аватарку вместо стандартных аватарки и номера.");
}
}
} }

View file

@ -84,6 +84,9 @@ final class TopicsPresenter extends OpenVKPresenter
if($this->postParam("as_group") === "on" && $club->canBeModifiedBy($this->user->identity)) if($this->postParam("as_group") === "on" && $club->canBeModifiedBy($this->user->identity))
$flags |= 0b10000000; $flags |= 0b10000000;
if($_FILES["_vid_attachment"] && OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
$this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator.");
$topic = new Topic; $topic = new Topic;
$topic->setGroup($club->getId()); $topic->setGroup($club->getId());
$topic->setOwner($this->user->id); $topic->setOwner($this->user->id);
@ -105,7 +108,7 @@ final class TopicsPresenter extends OpenVKPresenter
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"]); $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]);
} }
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик."); $this->flash("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use Nette\InvalidStateException;
use openvk\Web\Util\Sms; use openvk\Web\Util\Sms;
use openvk\Web\Themes\Themepacks; use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification}; use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification};
@ -43,7 +44,7 @@ final class UserPresenter extends OpenVKPresenter
} }
if(!$user || $user->isDeleted()) { if(!$user || $user->isDeleted()) {
if($user->isDeactivated()) { if(!is_null($user) && $user->isDeactivated()) {
$this->template->_template = "User/deactivated.xml"; $this->template->_template = "User/deactivated.xml";
$this->template->user = $user; $this->template->user = $user;
@ -52,6 +53,7 @@ final class UserPresenter extends OpenVKPresenter
} }
} else { } else {
$this->template->albums = (new Albums)->getUserAlbums($user); $this->template->albums = (new Albums)->getUserAlbums($user);
$this->template->avatarAlbum = (new Albums)->getUserAvatarAlbum($user);
$this->template->albumsCount = (new Albums)->getUserAlbumsCount($user); $this->template->albumsCount = (new Albums)->getUserAlbumsCount($user);
$this->template->videos = (new Videos)->getByUser($user, 1, 2); $this->template->videos = (new Videos)->getByUser($user, 1, 2);
$this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->videosCount = (new Videos)->getUserVideosCount($user);
@ -225,6 +227,30 @@ final class UserPresenter extends OpenVKPresenter
$user->setFav_Books(empty($this->postParam("fav_books")) ? NULL : ovk_proc_strtr($this->postParam("fav_books"), 300)); $user->setFav_Books(empty($this->postParam("fav_books")) ? NULL : ovk_proc_strtr($this->postParam("fav_books"), 300));
$user->setFav_Quote(empty($this->postParam("fav_quote")) ? NULL : ovk_proc_strtr($this->postParam("fav_quote"), 300)); $user->setFav_Quote(empty($this->postParam("fav_quote")) ? NULL : ovk_proc_strtr($this->postParam("fav_quote"), 300));
$user->setAbout(empty($this->postParam("about")) ? NULL : ovk_proc_strtr($this->postParam("about"), 300)); $user->setAbout(empty($this->postParam("about")) ? NULL : ovk_proc_strtr($this->postParam("about"), 300));
} elseif($_GET["act"] === "backdrop") {
if($this->postParam("subact") === "remove") {
$user->unsetBackDropPictures();
$user->save();
$this->flashFail("succ", tr("backdrop_succ_rem"), tr("backdrop_succ_desc")); # will exit
}
$pic1 = $pic2 = NULL;
try {
if($_FILES["backdrop1"]["error"] !== UPLOAD_ERR_NO_FILE)
$pic1 = Photo::fastMake($user->getId(), "Profile backdrop (system)", $_FILES["backdrop1"]);
if($_FILES["backdrop2"]["error"] !== UPLOAD_ERR_NO_FILE)
$pic2 = Photo::fastMake($user->getId(), "Profile backdrop (system)", $_FILES["backdrop2"]);
} catch(InvalidStateException $e) {
$this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media"));
}
if($pic1 == $pic2 && is_null($pic1))
$this->flashFail("err", tr("backdrop_error_title"), tr("backdrop_error_no_media"));
$user->setBackDropPictures($pic1, $pic2);
$user->save();
$this->flashFail("succ", tr("backdrop_succ"), tr("backdrop_succ_desc"));
} elseif($_GET['act'] === "status") { } elseif($_GET['act'] === "status") {
if(mb_strlen($this->postParam("status")) > 255) { if(mb_strlen($this->postParam("status")) > 255) {
$statusLength = (string) mb_strlen($this->postParam("status")); $statusLength = (string) mb_strlen($this->postParam("status"));
@ -252,7 +278,7 @@ final class UserPresenter extends OpenVKPresenter
} }
$this->template->mode = in_array($this->queryParam("act"), [ $this->template->mode = in_array($this->queryParam("act"), [
"main", "contacts", "interests", "avatar" "main", "contacts", "interests", "avatar", "backdrop"
]) ? $this->queryParam("act") ]) ? $this->queryParam("act")
: "main"; : "main";
@ -293,7 +319,7 @@ final class UserPresenter extends OpenVKPresenter
$this->redirect($user->getURL()); $this->redirect($user->getURL());
} }
function renderSetAvatar(): void function renderSetAvatar()
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -314,8 +340,26 @@ final class UserPresenter extends OpenVKPresenter
$album->setEdited(time()); $album->setEdited(time());
$album->save(); $album->save();
$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) {
$this->returnJson([
"url" => $photo->getURL(),
"id" => $photo->getPrettyId()
]);
} else {
$this->flashFail("succ", tr("photo_saved"), tr("photo_saved_comment")); $this->flashFail("succ", tr("photo_saved"), tr("photo_saved_comment"));
} }
}
function renderSettings(): void function renderSettings(): void
{ {

View file

@ -195,19 +195,24 @@ final class VKAPIPresenter extends OpenVKPresenter
$identity = NULL; $identity = NULL;
} else { } else {
$token = (new APITokens)->getByCode($this->requestParam("access_token")); $token = (new APITokens)->getByCode($this->requestParam("access_token"));
if(!$token) if(!$token) {
$identity = NULL; $identity = NULL;
else } else {
$identity = $token->getUser(); $identity = $token->getUser();
$platform = $token->getPlatform();
} }
} }
}
if(!is_null($identity) && $identity->isBanned())
$this->fail(18, "User account is deactivated", $object, $method);
$object = ucfirst(strtolower($object)); $object = ucfirst(strtolower($object));
$handlerClass = "openvk\\VKAPI\\Handlers\\$object"; $handlerClass = "openvk\\VKAPI\\Handlers\\$object";
if(!class_exists($handlerClass)) if(!class_exists($handlerClass))
$this->badMethod($object, $method); $this->badMethod($object, $method);
$handler = new $handlerClass($identity); $handler = new $handlerClass($identity, $platform);
if(!is_callable([$handler, $method])) if(!is_callable([$handler, $method]))
$this->badMethod($object, $method); $this->badMethod($object, $method);
@ -274,8 +279,11 @@ final class VKAPIPresenter extends OpenVKPresenter
$this->fail(28, "Invalid 2FA code", "internal", "acquireToken"); $this->fail(28, "Invalid 2FA code", "internal", "acquireToken");
} }
$platform = $this->requestParam("client_name");
$token = new APIToken; $token = new APIToken;
$token->setUser($user); $token->setUser($user);
$token->setPlatform(is_null($platform) ? "api" : $platform);
$token->save(); $token->save();
$payload = json_encode([ $payload = json_encode([

View file

@ -61,6 +61,9 @@ final class VideosPresenter extends OpenVKPresenter
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
$this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator.");
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!empty($this->postParam("name"))) { if(!empty($this->postParam("name"))) {
$video = new Video; $video = new Video;

View file

@ -1,7 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Post, Photo, Video, Club, User}; use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification}; 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\Repositories\{Posts, Users, Clubs, Albums};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
@ -44,9 +45,6 @@ final class WallPresenter extends OpenVKPresenter
function renderWall(int $user, bool $embedded = false): void function renderWall(int $user, bool $embedded = false): void
{ {
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
$canPost = false; $canPost = false;
@ -66,6 +64,9 @@ final class WallPresenter extends OpenVKPresenter
if ($embedded == true) $this->template->_template = "components/wall.xml"; if ($embedded == true) $this->template->_template = "components/wall.xml";
$this->template->oObj = $owner; $this->template->oObj = $owner;
if($user < 0)
$this->template->club = $owner;
$this->template->owner = $user; $this->template->owner = $user;
$this->template->canPost = $canPost; $this->template->canPost = $canPost;
$this->template->count = $this->posts->getPostCountOnUserWall($user); $this->template->count = $this->posts->getPostCountOnUserWall($user);
@ -88,9 +89,6 @@ final class WallPresenter extends OpenVKPresenter
function renderRSS(int $user): void function renderRSS(int $user): void
{ {
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
$canPost = false; $canPost = false;
@ -231,6 +229,9 @@ final class WallPresenter extends OpenVKPresenter
if(!$canPost) if(!$canPost)
$this->flashFail("err", tr("not_enough_permissions"), tr("not_enough_permissions_comment")); $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"]; $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) { if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) {
$manager = $wallOwner->getManager($this->user->identity); $manager = $wallOwner->getManager($this->user->identity);
@ -259,16 +260,26 @@ final class WallPresenter extends OpenVKPresenter
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon); $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon); $video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
}
} catch(\DomainException $ex) { } catch(\DomainException $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted")); $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large")); $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
} }
if(empty($this->postParam("text")) && !$photo && !$video) try {
$poll = NULL;
$xml = $this->postParam("poll");
if (!is_null($xml) && $xml != "none")
$poll = Poll::import($this->user->identity, $xml);
} catch(TooMuchOptionsException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("poll_err_to_much_options"));
} catch(\UnexpectedValueException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), "Poll format invalid");
}
if(empty($this->postParam("text")) && !$photo && !$video && !$poll)
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try { try {
@ -291,9 +302,21 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($video)) if(!is_null($video))
$post->attach($video); $post->attach($video);
if(!is_null($poll))
$post->attach($poll);
if($wall > 0 && $wall !== $this->user->identity->getId()) if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
$excludeMentions = [$this->user->identity->getId()];
if($wall > 0)
$excludeMentions[] = $wall;
$mentions = iterator_to_array($post->resolveMentions($excludeMentions));
foreach($mentions as $mentionee)
if($mentionee instanceof User)
(new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit();
$this->redirect($wallOwner->getURL()); $this->redirect($wallOwner->getURL());
} }
@ -343,21 +366,52 @@ final class WallPresenter extends OpenVKPresenter
$this->assertNoCSRF(); $this->assertNoCSRF();
$post = $this->posts->getPostById($wall, $post_id); $post = $this->posts->getPostById($wall, $post_id);
if(!$post || $post->isDeleted()) $this->notFound();
if(!$post || $post->isDeleted())
$this->notFound();
$where = $this->postParam("type") ?? "wall";
$groupId = NULL;
$flags = 0;
if($where == "group")
$groupId = $this->postParam("groupId");
if(!is_null($this->user)) { if(!is_null($this->user)) {
$nPost = new Post; $nPost = new Post;
if($where == "wall") {
$nPost->setOwner($this->user->id); $nPost->setOwner($this->user->id);
$nPost->setWall($this->user->id); $nPost->setWall($this->user->id);
} elseif($where == "group") {
$nPost->setOwner($this->user->id);
$club = (new Clubs)->get((int)$groupId);
if(!$club || !$club->canBeModifiedBy($this->user->identity))
$this->notFound();
if($this->postParam("asGroup") == 1)
$flags |= 0b10000000;
if($this->postParam("signed") == 1)
$flags |= 0b01000000;
$nPost->setWall($groupId * -1);
}
$nPost->setContent($this->postParam("text")); $nPost->setContent($this->postParam("text"));
$nPost->setFlags($flags);
$nPost->save(); $nPost->save();
$nPost->attach($post); $nPost->attach($post);
if($post->getOwner(false)->getId() !== $this->user->identity->getId() && !($post->getOwner() instanceof Club)) if($post->getOwner(false)->getId() !== $this->user->identity->getId() && !($post->getOwner() instanceof Club))
(new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit(); (new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit();
}; };
$this->returnJson(["wall_owner" => $this->user->identity->getId()]); $this->returnJson([
"wall_owner" => $where == "wall" ? $this->user->identity->getId() : $groupId * -1
]);
} }
function renderDelete(int $wall, int $post_id): void function renderDelete(int $wall, int $post_id): void

View file

@ -17,62 +17,19 @@
{script "js/l10n.js"} {script "js/l10n.js"}
{script "js/openvk.cls.js"} {script "js/openvk.cls.js"}
{css "js/node_modules/tippy.js/dist/backdrop.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"}
{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"}
{if $isTimezoned == NULL} {if $isTimezoned == NULL}
{script "js/timezone.js"} {script "js/timezone.js"}
{/if} {/if}
{ifset $thisUser} {include "_includeCSS.xml"}
{if $thisUser->getNsfwTolerance() < 2}
{css "css/nsfw-posts.css"}
{/if}
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
{if $isXmas}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
{if $thisUser->getStyleAvatar() == 1}
{css "css/avatar.1.css"}
{/if}
{if $thisUser->getStyleAvatar() == 2}
{css "css/avatar.2.css"}
{/if}
{if $thisUser->hasMicroblogEnabled() == 1}
{css "css/microblog.css"}
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/ifset}
{ifset headIncludes} {ifset headIncludes}
{include headIncludes} {include headIncludes}
@ -92,6 +49,12 @@
<div class="notifications_global_wrap"></div> <div class="notifications_global_wrap"></div>
<div class="dimmer"></div> <div class="dimmer"></div>
{if isset($backdrops) && !is_null($backdrops)}
<div id="backdrop" style="background-image: url('{$backdrops[0]|noescape}'), url('{$backdrops[1]|noescape}');">
<div id="backdropDripper"></div>
</div>
{/if}
<div class="toTop"> <div class="toTop">
⬆ {_to_top} ⬆ {_to_top}
</div> </div>
@ -107,28 +70,60 @@
<a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a> <a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a>
</div> </div>
{else} {else}
<div class="link"> <div class="link dec">
<a href="/">{_header_home}</a> <a href="/">{_header_home}</a>
</div> </div>
<div class="link"> <div class="link dec">
<a href="/search?type=groups">{_header_groups}</a> <a href="/search?type=groups">{_header_groups}</a>
</div> </div>
<div class="link"> <div class="link dec">
<a href="/search">{_header_search}</a> <a href="/search">{_header_search}</a>
</div> </div>
<div class="link"> <div class="link dec">
<a href="/invite">{_header_invite}</a> <a href="/invite">{_header_invite}</a>
</div> </div>
<div class="link"> <div class="link dec">
<a href="/support">{_header_help} <b n:if="$ticketAnsweredCount > 0">({$ticketAnsweredCount})</b></a> <a href="/support">{_header_help} <b n:if="$ticketAnsweredCount > 0">({$ticketAnsweredCount})</b></a>
</div> </div>
<div class="link"> <div class="link dec">
<a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a> <a href="/logout?hash={urlencode($csrfToken)}">{_header_log_out}</a>
</div> </div>
<div class="link"> {var $atSearch = str_contains($_SERVER['REQUEST_URI'], "/search")}
<form action="/search" method="get"> <div id="srch" class="{if $atSearch}nodivider{else}link{/if}">
<input type="search" name="query" placeholder="{_header_search}" style="height: 20px;background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" title="{_header_search} [Alt+Shift+F]" accesskey="f" />
{if !$atSearch}
<form action="/search" method="get" id="searcher" style="position:relative;">
<input id="searchInput" onfocus="expandSearch()" onblur="decreaseSearch()" class="sr" type="search" name="query" placeholder="{_header_search}" style="height: 20px;background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" title="{_header_search} [Alt+Shift+F]" accesskey="f" />
<select name="type" class="whatFind" style="display:none;top: 0px;">
<option value="users">{_s_by_people}</option>
<option value="groups">{_s_by_groups}</option>
<option value="posts">{_s_by_posts}</option>
<option value="comments">{_s_by_comments}</option>
<option value="videos">{_s_by_videos}</option>
<option value="apps">{_s_by_apps}</option>
</select>
</form> </form>
{else}
<form action="/search" method="get" id="searcher" style="margin-top: -1px;position:relative;">
<input id="searchInput" value="{$_GET['query'] ?? ''}" type="search" class="sr" name="query" placeholder="{_header_search}" style="height: 20px; background-color: #fff; padding-left: 6px;width: 555px;" title="{_header_search} [Alt+Shift+F]" accesskey="f" />
<select name="type" class="whatFind">
<option value="users" {if str_contains($_SERVER['REQUEST_URI'], "type=users")}selected{/if}>{_s_by_people}</option>
<option value="groups" {if str_contains($_SERVER['REQUEST_URI'], "type=groups")}selected{/if}>{_s_by_groups}</option>
<option value="posts" {if str_contains($_SERVER['REQUEST_URI'], "type=posts")}selected{/if}>{_s_by_posts}</option>
<option value="comments" {if str_contains($_SERVER['REQUEST_URI'], "type=comments")}selected{/if}>{_s_by_comments}</option>
<option value="videos" {if str_contains($_SERVER['REQUEST_URI'], "type=videos")}selected{/if}>{_s_by_videos}</option>
<option value="apps" {if str_contains($_SERVER['REQUEST_URI'], "type=apps")}selected{/if}>{_s_by_apps}</option>
</select>
<button class="searchBtn"><span style="color:white;font-weight: 600;font-size:12px;">{_header_search}</span></button>
</form>
<script>
let els = document.querySelectorAll("div.dec")
for(const element of els)
{
element.style.display = "none"
}
</script>
{/if}
</div> </div>
{/if} {/if}
{else} {else}
@ -181,7 +176,7 @@
{var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')} {var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')}
<div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div> <div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div>
<a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a> <a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a>
<a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">Helpdesk <a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">{_helpdesk}
{if $helpdeskTicketNotAnsweredCount > 0} {if $helpdeskTicketNotAnsweredCount > 0}
(<b>{$helpdeskTicketNotAnsweredCount}</b>) (<b>{$helpdeskTicketNotAnsweredCount}</b>)
{/if} {/if}
@ -206,7 +201,7 @@
</a> </a>
<div class="floating_sidebar"> <div class="floating_sidebar">
<a class="minilink" href="/friends{$thisUser->getId()}"> <a id="minilink-friends" class="minilink" href="/friends{$thisUser->getId()}">
<object type="internal/link" n:if="$thisUser->getFollowersCount() > 0"> <object type="internal/link" n:if="$thisUser->getFollowersCount() > 0">
<div class="counter"> <div class="counter">
+{$thisUser->getFollowersCount()} +{$thisUser->getFollowersCount()}
@ -214,10 +209,10 @@
</object> </object>
<img src="/assets/packages/static/openvk/img/friends.svg"> <img src="/assets/packages/static/openvk/img/friends.svg">
</a> </a>
<a class="minilink" href="/albums{$thisUser->getId()}"> <a id="minilink-albums" class="minilink" href="/albums{$thisUser->getId()}">
<img src="/assets/packages/static/openvk/img/photos.svg"> <img src="/assets/packages/static/openvk/img/photos.svg">
</a> </a>
<a class="minilink" href="/im"> <a id="minilink-messenger" class="minilink" href="/im">
<object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0"> <object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0">
<div class="counter"> <div class="counter">
+{$thisUser->getUnreadMessagesCount()} +{$thisUser->getUnreadMessagesCount()}
@ -225,10 +220,10 @@
</object> </object>
<img src="/assets/packages/static/openvk/img/messages.svg"> <img src="/assets/packages/static/openvk/img/messages.svg">
</a> </a>
<a class="minilink" href="/groups{$thisUser->getId()}"> <a id="minilink-groups" class="minilink" href="/groups{$thisUser->getId()}">
<img src="/assets/packages/static/openvk/img/groups.svg"> <img src="/assets/packages/static/openvk/img/groups.svg">
</a> </a>
<a class="minilink" href="/notifications"> <a id="minilink-notifications" class="minilink" href="/notifications">
<object type="internal/link" n:if="$thisUser->getNotificationsCount() > 0"> <object type="internal/link" n:if="$thisUser->getNotificationsCount() > 0">
<div class="counter"> <div class="counter">
+{$thisUser->getNotificationsCount()} +{$thisUser->getNotificationsCount()}
@ -255,9 +250,9 @@
<input id="password" type="password" name="password" required /> <input id="password" type="password" name="password" required />
<input type="hidden" name="jReturnTo" value="{$_SERVER['REQUEST_URI']}" /> <input type="hidden" name="jReturnTo" value="{$_SERVER['REQUEST_URI']}" />
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_log_in}" class="button" style="display: inline-block;" /> <input type="submit" value="{_log_in}" class="button" style="display: inline-block; font-family: Tahoma" />
<a href="/reg" class="button" style="display: inline-block;">{_registration}</a><br><br> <a href="/reg"><input type="button" value="{_registration}" class="button" style="font-family: Tahoma" /></a><br><br>
<a href="/restore">{_forgot_password}</a> {if !OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring']}<a href="/restore">{_forgot_password}</a>{/if}
</form> </form>
{/ifset} {/ifset}
</div> </div>
@ -303,12 +298,21 @@
<div class="navigation_footer"> <div class="navigation_footer">
<a href="/about" class="link">{_footer_about_instance}</a> <a href="/about" class="link">{_footer_about_instance}</a>
<a href="/terms" class="link">{_footer_rules}</a>
<a href="/blog" class="link">{_footer_blog}</a> <a href="/blog" class="link">{_footer_blog}</a>
<a href="/support" class="link">{_footer_help}</a> <a href="/support" class="link">{_footer_help}</a>
<a href="/dev" target="_blank" class="link">{_footer_developers}</a> <a href="/dev" target="_blank" class="link">{_footer_developers}</a>
<a href="/language" class="link">{_footer_choose_language}</a>
<a href="/privacy" class="link">{_footer_privacy}</a> <a href="/privacy" class="link">{_footer_privacy}</a>
</div> </div>
<p>
{var $currentUrl = $_SERVER["REQUEST_URI"]}
{foreach array_slice(getLanguages(), 0, 3) as $language}
<a href="/language?lg={$language['code']}&hash={urlencode($csrfToken)}&jReturnTo={php echo rawurlencode($currentUrl)}" rel="nofollow" title="{$language['native_name']}" class="link">
<img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif" alt="{$language['native_name']}">
</a>
{/foreach}
<a href="/language" class="link">all languages &raquo;</a>
</p>
<p>OpenVK <a href="/about:openvk">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {$dbVersion}</p> <p>OpenVK <a href="/about:openvk">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {$dbVersion}</p>
<p n:ifcontent> <p n:ifcontent>
{php echo OPENVK_ROOT_CONF["openvk"]["appearance"]["motd"]} {php echo OPENVK_ROOT_CONF["openvk"]["appearance"]["motd"]}
@ -323,8 +327,11 @@
{script "js/messagebox.js"} {script "js/messagebox.js"}
{script "js/notifications.js"} {script "js/notifications.js"}
{script "js/scroll.js"} {script "js/scroll.js"}
{script "js/player.js"}
{script "js/al_wall.js"} {script "js/al_wall.js"}
{script "js/al_api.js"} {script "js/al_api.js"}
{script "js/al_mentions.js"}
{script "js/al_polls.js"}
{ifset $thisUser} {ifset $thisUser}
{script "js/al_notifs.js"} {script "js/al_notifs.js"}
@ -337,6 +344,8 @@
</script> </script>
{/if} {/if}
<script>bsdnHydrate();</script>
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']" async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}" src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script> <script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']" async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}" src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script>
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['piwik']['enable']"> <script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['piwik']['enable']">

View file

@ -74,6 +74,6 @@
<h4>{_rules}</h4> <h4>{_rules}</h4>
<div style="margin-top: 16px;"> <div style="margin-top: 16px;">
{presenter "openvk!Support->knowledgeBaseArticle", "rules"} {tr("about_watch_rules", "/terms")|noescape}
</div> </div>
{/block} {/block}

View file

@ -7,6 +7,9 @@
{block content} {block content}
{presenter "openvk!Support->knowledgeBaseArticle", "about"} {presenter "openvk!Support->knowledgeBaseArticle", "about"}
<a href="/tour"><div class="tour" onmouseover="this.style.backgroundColor='#FCFBF5'" onmouseout="this.style.backgroundColor='#F9F6E7'" style="background-color: rgb(249, 246, 231);"><b>{_tour_title}</b><div>{_tour_promo}</div></div></a><br>
<center> <center>
<a class="button" style="margin-right: 5px;cursor: pointer;" href="/login">{_log_in}</a> <a class="button" style="margin-right: 5px;cursor: pointer;" href="/login">{_log_in}</a>
<a class="button" style="cursor: pointer;" href="/reg">{_registration}</a> <a class="button" style="cursor: pointer;" href="/reg">{_registration}</a>

View file

@ -40,7 +40,7 @@
{var $result = preg_match("/(.+)\((.+)\)/", $language['native_name'], $name)} {var $result = preg_match("/(.+)\((.+)\)/", $language['native_name'], $name)}
<a href="language?lg={$language['code']}&hash={urlencode($csrfToken)}" class="link_new" rel="nofollow"> <a href="language?lg={$language['code']}&hash={urlencode($csrfToken)}" class="link_new" rel="nofollow">
<center><img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif"></center> <center><img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif" alt="{$language['native_name']}"></center>
<br> <br>
{if $result == 1} {if $result == 1}
{$name[1]} {$name[1]}

View file

@ -0,0 +1,515 @@
{extends "../@layout.xml"}
{block title}{_tour_title}{/block}
{block header}
{_tour_title}
{/block}
{block content}
{css "css/tour.css"}
<div id="tour">
<div class="rightNav">
<h1>{_tour_title}</h1>
<div class="rightLinks">
<div class="tab">
<button class="tablinks" onclick="eurotour(event, 'start')" id="defaultOpen"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/1.svg" width="16" height="16"></div>{_tour_section_1|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'profile')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/2.svg" width="16" height="16"></div>{_tour_section_2|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'photos')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/3.svg" width="16" height="16"></div>{_tour_section_3|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'search')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/4.svg" width="16" height="16"></div>{_tour_section_4|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'videos')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/5.svg" width="16" height="16"></div>{_tour_section_5|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'audios')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/6.svg" width="16" height="16"></div>{_tour_section_6|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'news')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/7.svg" width="16" height="16"></div>{_tour_section_7|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'news_global')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/8.svg" width="16" height="16"></div>{_tour_section_8|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'groups')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/9.svg" width="16" height="16"></div>{_tour_section_9|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'events')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/10.svg" width="16" height="16"></div>{_tour_section_10|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'themes')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/11.svg" width="16" height="16"></div>{_tour_section_11|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'customization')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/12.svg" width="16" height="16"></div>{_tour_section_12|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'vouchers')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/13.svg" width="16" height="16"></div>{_tour_section_13|noescape}</button>
<button class="tablinks" onclick="eurotour(event, 'mobile')"><div class="tabicon"><img src="assets/packages/static/openvk/img/icons/14.svg" width="16" height="16"></div>{_tour_section_14|noescape}</button>
</div>
</div>
<div class="rightNav" n:if="!isset($thisUser)">
<h1>{_reg_title|noescape}</h1>
<div class="rightLinks">
<div>{_reg_text|noescape}</div>
</div>
<h1>{_ifnotlike_title|noescape}</h1>
<div class="rightLinks">
<div>{_ifnotlike_text|noescape}</div>
</div>
</div>
</div>
<div id="start" class="tabcontent">
<h2>{_tour_section_1_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_1_text_1|noescape}</span></li>
<li><span>{_tour_section_1_text_2|noescape}</span></li>
<li><span>{_tour_section_1_text_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/reg.png" width="440">
<p class="big">{_tour_section_1_bottom_text_1|noescape}</p>
<div style="margin-top:10px; padding-left:175px">
</div>
<br>
</div>
<div id="profile" class="tabcontent">
<h2>{_tour_section_2_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_2_text_1_1|noescape}</span></li>
<li><span>{_tour_section_2_text_1_2|noescape}</span></li>
<li><span>{_tour_section_2_text_1_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/profile.png" width="440">
<p class="big">{_tour_section_2_bottom_text_1|noescape}</p>
<h2>{_tour_section_2_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_2_text_2_1|noescape}</span></li>
<li><span>{_tour_section_2_text_2_2|noescape}</span></li>
<li><span>{_tour_section_2_text_2_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/privacy.png" width="440">
<h2>{_tour_section_2_title_3|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_2_text_3_1|noescape}</span></li>
<li><span>{_tour_section_2_text_3_2|noescape}</span></li>
</ul>
<center><img src="assets/packages/static/openvk/img/tour/adres_ff.jpg"></center>
<ul class="listing">
<li><span>{_tour_section_2_text_3_3|noescape}</span></li>
<li><span>{_tour_section_2_text_3_4|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/adres3.png" width="440">
<center><img src="assets/packages/static/openvk/img/tour/adres_ff_tohru.jpg"></center>
<br>
<i><p class="big">{_tour_section_2_bottom_text_2|noescape}</p></p></i>
<h2>{_tour_section_2_title_4|noescape}</h2>
<img src="assets/packages/static/openvk/img/tour/wall.png" width="440">
<br>
</div>
<div id="photos" class="tabcontent">
<h2>{_tour_section_3_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_3_text_1|noescape}</span></li>
<li><span>{_tour_section_3_text_2|noescape}</span></li>
<li><span>{_tour_section_3_text_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/photos.png" width="440">
<img src="assets/packages/static/openvk/img/tour/photos2.png" width="440">
<p class="big">{_tour_section_3_bottom_text_1|noescape}</p>
<br>
</div>
<div id="search" class="tabcontent">
<h2>{_tour_section_4_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_4_text_1|noescape}</span></li>
<li><span>{_tour_section_4_text_2|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/search.png" width="440">
<ul class="listing">
<li><span>{_tour_section_4_text_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/search2.png" width="440">
<h2>{_tour_section_4_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_4_text_4|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/search_h.png" width="440">
<br>
</div>
<div id="videos" class="tabcontent">
<h2>{_tour_section_5_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_5_text_1|noescape}</span></li>
<li><span>{_tour_section_5_text_2|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/videos.png" width="440">
<p class="big">{_tour_section_5_bottom_text_1|noescape}</p>
<img src="assets/packages/static/openvk/img/tour/videos_a.png" width="440">
<h2>{_tour_section_5_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_5_text_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/videos_y.png" width="440">
<img src="assets/packages/static/openvk/img/tour/videos_w.png" width="440">
<br>
</div>
<div id="audios" class="tabcontent">
<h2>{_tour_section_6_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_6_text_1|noescape}</span></li>
</ul>
<br>
</div>
<div id="news" class="tabcontent">
<h2>{_tour_section_7_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_7_text_1|noescape}</span></li>
<li><span>{_tour_section_7_text_2|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/local_news.png" width="440">
<p class="big">{_tour_section_7_bottom_text_1|noescape}</p>
<br>
</div>
<div id="news_global" class="tabcontent">
<h2>{_tour_section_8_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_8_text_1|noescape}</span></li>
<li><span>{_tour_section_8_text_2|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/news_global.png" width="440">
<p class="big">{_tour_section_8_bottom_text_1|noescape}</p>
<img src="assets/packages/static/openvk/img/tour/poll.png" width="440">
<p class="big">{_tour_section_8_bottom_text_2|noescape}</p>
<br>
</div>
<div id="groups" class="tabcontent">
<h2>{_tour_section_9_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_9_text_1|noescape}</span></li>
<li><span>{_tour_section_9_text_2|noescape}</span></li>
<li><span>{_tour_section_9_text_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/groups.png" width="440">
<p class="big">{_tour_section_9_bottom_text_1|noescape}</p>
<img src="assets/packages/static/openvk/img/tour/groups_view.png" width="440">
<p class="big">{_tour_section_9_bottom_text_2|noescape}</p>
<h2>{_tour_section_9_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_9_text_2_1|noescape}</span></li>
<li><span>{_tour_section_9_text_2_2|noescape}</span></li>
<li><span>{_tour_section_9_text_2_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/groups_admin.png" width="440">
<p class="big">{_tour_section_9_bottom_text_3|noescape}</p>
<br>
</div>
<div id="events" class="tabcontent">
<h2>{_tour_section_10_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_10_text_1|noescape}</span></li>
</ul>
<br>
</div>
<div id="themes" class="tabcontent">
<h2>{_tour_section_11_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_11_text_1|noescape}</span></li>
<li><span>{_tour_section_11_text_2|noescape}</span></li>
<li><span>{_tour_section_11_text_3|noescape}</span></li>
</ul>
<center><img src="assets/packages/static/openvk/img/tour/theme_picker.png"></center>
<p class="big">{_tour_section_11_bottom_text_1|noescape}</p><br>
<img src="assets/packages/static/openvk/img/tour/theme3.png" width="460">
<table cellspacing="5" border="0">
<tbody>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/theme1.png" style="float:left;" width="220"></td>
<td><img src="assets/packages/static/openvk/img/tour/theme2.png" style="float:right;" width="220"></td>
</tr>
</tbody>
</table>
<br>
<center>{_tour_section_11_wordart|noescape}</center>
<br>
</div>
<div id="customization" class="tabcontent">
<h2>{_tour_section_12_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_12_text_1|noescape}</span></li>
<li><span>{_tour_section_12_text_2|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/backdrop.png" width="440">
<p class="big">{_tour_section_12_bottom_text_1|noescape}</p><br>
<ul class="listing">
<li><span>{_tour_section_12_text_3|noescape}</span></li>
</ul>
<br>
<table cellspacing="5" border="0">
<tbody>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/backdrop_ex.png" width="440"></td>
</tr>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/backdrop_ex1.png" width="440"></td>
</tr>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/backdrop_ex2.png" width="440"></td>
</tr>
</tbody>
</table>
<p class="big">{_tour_section_12_bottom_text_2|noescape}</p><br>
<p class="big">{_tour_section_12_bottom_text_3|noescape}</p>
<h2>{_tour_section_12_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_12_text_2_1|noescape}</span></li>
<li><span>{_tour_section_12_text_2_2|noescape}</span></li>
</ul>
<center><img src="assets/packages/static/openvk/img/tour/avatar_picker.png"></center><br>
<table cellspacing="5" border="0">
<tbody>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/avatars_def.png" style="float:left;" width="220"></td>
<td><img src="assets/packages/static/openvk/img/tour/avatars_round.png" style="float:right;" width="220"></td>
</tr>
</tbody>
</table><br>
<img src="assets/packages/static/openvk/img/tour/avatars_quad.png" width="440">
<h2>{_tour_section_12_title_3|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_12_text_3_1|noescape}</span></li>
<li><span>{_tour_section_12_text_3_2|noescape}</span></li>
</ul>
<table cellspacing="5" border="0">
<tbody>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/leftmenu.png" style="float:left;" width="220"></td>
<td><img src="assets/packages/static/openvk/img/tour/leftmenu2.png" style="float:right;" width="220"></td>
</tr>
</tbody>
</table>
<h2>{_tour_section_12_title_4|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_12_text_4_1|noescape}</span></li>
<li><span>{_tour_section_12_text_4_2|noescape}</span></li>
<li><span>{_tour_section_12_text_4_3|noescape}</span></li>
</ul>
<center><img src="assets/packages/static/openvk/img/tour/wall_pick.png"></center><br>
<table cellspacing="5" border="0">
<tbody>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/wall_old.png" style="float:left;" width="220">
<br> <p class="big"{_tour_section_12_bottom_text_4|noescape}</p></td>
<td><img src="assets/packages/static/openvk/img/tour/wall_new.png" style="float:right;" width="220">
<br><p class="big">{_tour_section_12_bottom_text_5|noescape}</p></td>
</tr>
</tbody>
</table>
<br>
<br>
</div>
<div id="vouchers" class="tabcontent">
<h2>{_tour_section_13_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_13_text_1|noescape}</span></li>
<li><span>{_tour_section_13_text_2|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/vouchers.png" width="440">
<img src="assets/packages/static/openvk/img/tour/vouchers_type.png" width="440">
<p class="big">{_tour_section_13_bottom_text_1|noescape}</p><br>
<ul class="listing">
<li><span>{_tour_section_13_text_3|noescape}</span></li>
<li><span>{_tour_section_13_text_4|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/vouchers_ok.png" width="440">
<p class="big">{_tour_section_13_bottom_text_2|noescape}</p><br>
<p class="big">{_tour_section_13_bottom_text_3|noescape}</p><br>
<br>
<br>
</div>
<div id="mobile" class="tabcontent">
<h2>{_tour_section_14_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_14_text_1|noescape}</span></li>
<li><span>{_tour_section_14_text_2|noescape}</span></li>
<li><span>{_tour_section_14_text_3|noescape}</span></li>
</ul>
<table cellspacing="5" border="0">
<tbody>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/app1.png" style="float:left;" width="210"></td>
<td><img src="assets/packages/static/openvk/img/tour/app2.png" style="float:right;" width="210"></td>
</tr>
</tbody>
</table>
<table cellspacing="5" border="0">
<tbody>
<tr>
<td><img src="assets/packages/static/openvk/img/tour/app3.png" width="425"></td>
</tr>
</tbody>
</table>
<p class="big">{_tour_section_14_bottom_text_1|noescape}</p><br>
<h2>{_tour_section_14_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_14_text_2_1|noescape}</span></li>
<li><span>{_tour_section_14_text_2_2|noescape}</span></li>
<li><span>{_tour_section_14_text_2_3|noescape}</span></li>
</ul>
<img src="assets/packages/static/openvk/img/tour/app4.jpeg" width="440">
<br>
<p class="big" n:if="!isset($thisUser)">{_tour_section_14_bottom_text_2|noescape}</p><br>
<p class="big" n:if="isset($thisUser)">{_tour_section_14_bottom_text_3|noescape}</p><br>
<div style="margin-top:10px; padding-left:175px" n:if="!isset($thisUser)">
<a class="button" href="/reg">{_tour_reg|noescape}</a>
</div>
<br>
</div>
<!--
.__ .__ __ .__ .___.__ __
| |__ ____ |__|/ |_ ___________ ___.__. |__| __| _/|__|/ |_ ____ ___ __ _____ ____ _____ ____ _____ ______
| | \_/ __ \| \ __\/ __ \_ __ < | | | |/ __ | | \ __\/ __ \ \ \/ / \__ \ / \\__ \ / \\__ \ / ___/
| Y \ ___/| || | \ ___/| | \/\___ | | / /_/ | | || | \ ___/ \ / / __ \| | \/ __ \| | \/ __ \_\___ \
|___| /\___ >__||__| \___ >__| / ____| |__\____ | |__||__| \___ > \_/ (____ /___| (____ /___| (____ /____ >
\/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/
.__ .__ __ __
_____ ___.__. _____| | |__|__ __ _____/ |_ ______ _____ ___ ___/ |_ ___________
/ < | |/ ___/ | | \ \/ // __ \ __\/ ___/ \__ \\ \/ /\ __\/ _ \_ __ \
| Y Y \___ |\___ \| |_| |\ /\ ___/| | \___ \ / __ \\ / | | ( <_> ) | \/
|__|_| / ____/____ >____/__| \_/ \___ >__| /____ > (____ /\_/ |__| \____/|__|
\/\/ \/ \/ \/ \/ -->
{script "js/tour.js"}
{/block}

View file

@ -411,7 +411,7 @@
<tr> <tr>
<td class="e"> <td class="e">
Vladimir Barinov (veselcraft) and Konstantin Kichulkin (kosfurler)<br/> Vladimir Barinov (veselcraft) and Konstantin Kichulkin (kosfurler)<br/>
OpenVK is a free open-source software that "cosplays" (or imitates) older versions of russian website VKontakte. VKontakte belongs to Pavel Durov and VK Group. OpenVK is a free open source software that "cosplays" (or imitates) older versions of a Russian social network called VKontakte. VKontakte belongs to Pavel Durov and VK Group.
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -430,7 +430,7 @@
Native name Native name
</td> </td>
<td> <td>
Author Author(s)
</td> </td>
</tr> </tr>
{foreach $languages as $language} {foreach $languages as $language}
@ -458,7 +458,7 @@
</tr> </tr>
<tr> <tr>
<td class="e">Initial hosting</td> <td class="e">Initial hosting</td>
<td class="v">Ilya Prokopenko (dsrev) and Celestora</td> <td class="v">Lumaeris and Celestora</td>
</tr> </tr>
<tr> <tr>
<td class="e">Initial bug-tracker hosting</td> <td class="e">Initial bug-tracker hosting</td>
@ -492,7 +492,7 @@
<td> <td>
kovaltim, Vladimir Lapskiy (0x7d5), Alexander Minkin (WerySkok), Polina Katunina (RousPhaul), veth, kovaltim, Vladimir Lapskiy (0x7d5), Alexander Minkin (WerySkok), Polina Katunina (RousPhaul), veth,
Egor Shevchenko, Vadim Korovin (yuni), Ash Defenders, Egor Shevchenko, Vadim Korovin (yuni), Ash Defenders,
Pavel Silaev, Dmitriy Daemon, Ilya Prokopenko (dsrev), Pavel Silaev, Dmitriy Daemon, Lumaeris,
cmed404 and unknown tester, who disappeared shortly after trying to upload post with cat. cmed404 and unknown tester, who disappeared shortly after trying to upload post with cat.
</td> </td>
</tr> </tr>

View file

@ -6,6 +6,7 @@
<style> <style>
{var $css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")} {var $css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")}
{str_replace("fonts/", "/assets/packages/static/openvk/js/node_modules/@atlassian/aui/dist/aui/fonts/", $css)|noescape} {str_replace("fonts/", "/assets/packages/static/openvk/js/node_modules/@atlassian/aui/dist/aui/fonts/", $css)|noescape}
{file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping-darkmode.css")|noescape}
</style> </style>
<title>{include title} - {_admin} {$instance_name}</title> <title>{include title} - {_admin} {$instance_name}</title>
</head> </head>
@ -21,12 +22,49 @@
</a> </a>
</h1> </h1>
</div> </div>
<div n:if="$search ?? false" class="aui-header-secondary"> <div class="aui-header-secondary">
<ul class="aui-nav"> <ul class="aui-nav">
<li n:if="$search ?? false">
<form class="aui-quicksearch dont-default-focus ajs-dirty-warning-exempt"> <form class="aui-quicksearch dont-default-focus ajs-dirty-warning-exempt">
<input id="quickSearchInput" autocomplete="off" class="search" type="text" placeholder="{include searchTitle}" value="{$_GET['q'] ?? ''}" name="q" accesskey="Q" /> <input id="quickSearchInput" autocomplete="off" class="search" type="text" placeholder="{include searchTitle}" value="{$_GET['q'] ?? ''}" name="q" accesskey="Q" />
<input type="hidden" value=1 name=p /> <input type="hidden" value=1 name=p />
</form> </form>
</li>
<li>
<aui-toggle id="switch-theme" label="Toggle dark mode"></aui-toggle>
<script>
const toggle = document.getElementById("switch-theme");
let currentTheme = localStorage.getItem("ovkadmin-theme");
if (currentTheme == null) {
const preferDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
let theme = "light";
if (preferDarkScheme.matches) {
theme = "dark";
document.body.classList.add("aui-theme-dark");
}
localStorage.setItem("ovkadmin-theme", theme);
}
if (currentTheme == "dark") {
document.body.classList.add("aui-theme-dark");
}
toggle.addEventListener("click", function() {
document.body.classList.toggle("aui-theme-dark");
let theme = "light";
if (document.body.classList.contains("aui-theme-dark")) {
theme = "dark";
}
localStorage.setItem("ovkadmin-theme", theme);
});
</script>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -34,7 +72,7 @@
</header> </header>
<div class="aui-page-panel"> <div class="aui-page-panel">
<div class="aui-page-panel-inner"> <div class="aui-page-panel-inner">
<div class="aui-page-panel-nav" style="background-color: #fff;"> <div class="aui-page-panel-nav">
<nav class="aui-navgroup aui-navgroup-vertical"> <nav class="aui-navgroup aui-navgroup-vertical">
<div class="aui-navgroup-inner"> <div class="aui-navgroup-inner">
<div class="aui-navgroup-primary"> <div class="aui-navgroup-primary">
@ -60,6 +98,14 @@
<a href="/admin/bannedLinks">{_admin_banned_links}</a> <a href="/admin/bannedLinks">{_admin_banned_links}</a>
</li> </li>
</ul> </ul>
<div class="aui-nav-heading">
<strong>Chandler</strong>
</div>
<ul class="aui-nav">
<li>
<a href="/admin/chandler/groups">{_c_groups}</a>
</li>
</ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>{_admin_services}</strong> <strong>{_admin_services}</strong>
</div> </div>

View file

@ -0,0 +1,177 @@
{extends "@layout.xml"}
{block title}
{$group->name}
{/block}
{block heading}
<a href="/admin/chandler/groups">{_c_groups}</a>
» {$group->name}
{/block}
{block content}
{var $isMain = $mode === 'main'}
{var $isPermissions = $mode === 'permissions'}
{var $isMembers = $mode === 'members'}
{if $isMain}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li class="aui-nav-selected"><a href="?act=main">{_admin_tab_main}</a></li>
<li><a href="?act=permissions">{_c_group_permissions}</a></li>
<li><a href="?act=members">{_c_group_members}</a></li>
</ul>
</div>
</div>
</nav>
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text medium-field" type="text" id="id" disabled value="{$group->id}" />
</div>
<div class="field-group">
<label for="first_name">{_name}</label>
<input class="text medium-field" type="text" id="name" name="name" value="{$group->name}" />
</div>
<div class="field-group">
<label for="first_name">{_c_color}</label>
<input class="text medium-field" type="text" id="color" name="color" value="{$group->color}" />
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
{elseif $isMembers}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li><a href="?act=main">{_admin_tab_main}</a></li>
<li><a href="?act=permissions">{_c_group_permissions}</a></li>
<li class="aui-nav-selected"><a href="?act=members">{_c_group_members}</a></li>
<li>
<form class="aui" method="POST" style="display: flex;">
<div class="field-group">
<label for="uid">UID</label>
<input class="text" type="text" id="uid" name="uid" />
</div>
<div style="margin: 5px;">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_add}">
</div>
</div>
</form>
</li>
</ul>
</div>
</div>
</nav>
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>UUID</th>
<th>{_admin_name}</th>
<th>{_gender}</th>
<th>{_admin_shortcode}</th>
<th>{_registration_date}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$members as $user">
<td>{$user->getId()}</td>
<td>{$user->getChandlerGUID()}</td>
<td>
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$user->getAvatarUrl('miniscule')}" alt="{$user->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>{$user->isFemale() ? tr("female") : tr("male")}</td>
<td>{$user->getShortCode() ?? "(" . tr("none") . ")"}</td>
<td>{$user->getRegistrationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}?act=removeMember&uid={$user->getChandlerGUID()}">
<span class="aui-icon aui-icon-small aui-iconfont-delete">{_delete}</span>
</a>
<a class="aui-button aui-button-primary" href="/admin/users/id{$user->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
{if $thisUser->getChandlerUser()->can("substitute")->model('openvk\Web\Models\Entities\User')->whichBelongsTo(0)}
<a class="aui-button" href="/setSID/{$user->getChandlerUser()->getId()}?hash={rawurlencode($csrfToken)}">
<span class="aui-icon aui-icon-small aui-iconfont-sign-in">{_admin_loginas}</span>
</a>
{/if}
</td>
</tr>
</tbody>
</table>
</table>
</div>
{elseif $isPermissions}
<div class="aui-tabs horizontal-tabs">
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav">
<li><a href="?act=main">{_admin_tab_main}</a></li>
<li class="aui-nav-selected"><a href="?act=permissions">{_c_group_permissions}</a></li>
<li><a href="?act=members">{_c_group_members}</a></li>
<li>
<form class="aui" method="POST" style="display: flex;">
<div class="field-group">
<label for="model">{_c_model}</label>
<input class="text" type="text" id="model" name="model" />
<input class="text" type="text" id="permission" name="permission" />
<label for="action">{_c_permission}</label>
</div>
<div style="margin: 5px;">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_add}">
</div>
</div>
</form>
</li>
</ul>
</div>
</div>
</nav>
<table class="aui aui-table-list">
<thead>
<tr>
<th>{_c_model}</th>
<th>{_c_permission}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$perms as $perm">
<td>{$perm->model}</td>
<td>{$perm->permission}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$perm->group}?act=removePermission&model={$perm->model}&perm={$perm->permission}">
<span class="aui-icon aui-icon-small aui-iconfont-delete">{_edit}</span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
{/if}
{/block}

View file

@ -0,0 +1,59 @@
{extends "@layout.xml"}
{block title}
{_c_groups}
{/block}
{block heading}
{_c_groups}
{/block}
{block content}
<form class="aui" method="POST">
<div class="field-group" style="margin-left: -65px;">
<label for="uid">{_admin_title}</label>
<div style="display: flex;">
<input class="text" type="text" id="name" name="name" />
<div class="buttons" style="margin-left: 5px;">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_add}">
</div>
</div>
</div>
</form>
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>{_admin_title}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$groups as $group">
<td>
<a href="/admin/chandler/groups/{$group->id}">{$group->id}</a>
</td>
<td>
<span class="aui-lozenge aui-lozenge-subtle" style="text-transform: none;">
{$group->name}
</span>
</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}?act=permissions">
<span class="aui-icon aui-icon-small aui-iconfont-book">{_c_permissions}</span>
</a>
<a class="aui-button aui-button-primary" href="/admin/chandler/groups/{$group->id}?act=members">
<span class="aui-icon aui-icon-small aui-iconfont-group">{_members}</span>
</a>
<a class="aui-button aui-button-secondary" href="/admin/chandler/groups/{$group->id}?act=delete">
<span class="aui-icon aui-icon-small aui-iconfont-delete">{_delete}</span>
</a>
</td>
</tr>
</tbody>
</table>
{/block}

View file

@ -68,6 +68,43 @@
<option value="2" {if $user->onlineStatus() == 2}selected{/if}>{_admin_user_online_deceased}</option> <option value="2" {if $user->onlineStatus() == 2}selected{/if}>{_admin_user_online_deceased}</option>
</select> </select>
</div> </div>
<hr/>
<h2>{_c_groups}</h2>
<div>
<div class="field-group">
<label for="add-to-group">{_c_add_to_group}</label>
<select class="select" name="add-to-group">
<option n:foreach="$c_groups_list as $group" value="{$group->id}">
{$group->name}
</option>
</select>
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$c_memberships as $membership">
<td>
<a href="/admin/chandler/groups/{$membership->group}?act=members">{$membership->group}</a>
</td>
<td>
<a
class="aui-icon aui-icon-small aui-iconfont-cross"
href="/admin/chandler/groups/{$membership->group}?act=removeMember&uid={$user->getChandlerGUID()}"
style="margin: 0 50%;"
>
{_c_remove_from_group}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="buttons-container"> <div class="buttons-container">
<div class="buttons"> <div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />

View file

@ -21,6 +21,7 @@
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>UUID</th>
<th>{_admin_name}</th> <th>{_admin_name}</th>
<th>{_gender}</th> <th>{_gender}</th>
<th>{_admin_shortcode}</th> <th>{_admin_shortcode}</th>
@ -31,6 +32,7 @@
<tbody> <tbody>
<tr n:foreach="$users as $user"> <tr n:foreach="$users as $user">
<td>{$user->getId()}</td> <td>{$user->getId()}</td>
<td>{$user->getChandlerGUID()}</td>
<td> <td>
<span class="aui-avatar aui-avatar-xsmall"> <span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner"> <span class="aui-avatar-inner">

View file

@ -34,7 +34,7 @@
<input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" /> <input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" />
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="rating">{_admin_voucher_rating}</label> <label for="rating">{_admin_voucher_rating_number}</label>
<input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" /> <input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" />
</div> </div>
<div class="field-group"> <div class="field-group">

View file

@ -9,21 +9,47 @@
{/block} {/block}
{block content} {block content}
<p> <h4 style="margin-left: 100px; margin-right: 100px;">{_access_recovery}</h4>
<table cellspacing="10" cellpadding="0" border="0" align="center" width="70%">
<tbody>
<tr>
<td>
{_access_recovery_info_2} {_access_recovery_info_2}
</p> </td>
</tr>
</tbody>
</table>
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<label for="password">{_new_password}: </label> <table cellspacing="7" cellpadding="0" width="55%" border="0" align="center">
<tbody>
<tr>
<td class="regform-left">
<span class="nobold">{_new_password}: </span>
</td>
<td class="regform-right">
<input id="password" type="password" name="password" required /> <input id="password" type="password" name="password" required />
<br/><br/> </td>
</tr>
{if $is2faEnabled} {if $is2faEnabled}
<label for="code">{_"2fa_code_2"}: </label> <tr style="text-align: right;">
<input id="code" type="text" name="code" required /> <td class="regform-left">
<br/><br/> <span class="nobold">{_"2fa_code_2"}: </span>
</td>
<td class="regform-right">
<input id="password" type="password" name="password" required />
</td>
</tr>
{/if} {/if}
<tr>
<td>
</td>
</tr>
</tbody>
</table>
<center>
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_reset_password}" class="button" style="float: right;" /> <input type="submit" value="{_reset_password}" class="button" />
</center>
</form> </form>
{/block} {/block}

View file

@ -7,35 +7,36 @@
{block content} {block content}
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center"> <h4 style="margin-left: 100px; margin-right: 100px;">{_log_in}</h4>
<table cellspacing="7" cellpadding="0" width="46%" border="0" align="center">
<tbody> <tbody>
<tr> <tr style="text-align: right;">
<td> <td>
<span>{_email}: </span> <span class="nobold">{_email}: </span>
</td> </td>
<td> <td style="width:191px;">
<input type="text" name="login" required /> <input type="text" name="login" required />
</td> </td>
</tr> </tr>
<tr> <tr style="text-align: right;">
<td> <td>
<span>{_password}: </span> <span class="nobold">{_password}: </span>
</td> </td>
<td> <td>
<input type="password" name="password" required /> <input type="password" name="password" required />
</td> </td>
</tr> </tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_log_in}" class="button" />
<a href="/reg">{_registration}</a>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<center>
<!-- div style="margin-bottom: 8px;">
<input type="checkbox" name="someone_pc" value=""/>
<label for="someone_pc" class="nobold">{_not_your_pc}?</label><br>
</div -->
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_log_in}" class="button" />
<a href="/reg" class="button" style="display: inline-block;">{_registration}</a><br><br>
<a href="/restore">{_forgot_password}</a>
</center>
</form> </form>
{/block} {/block}

View file

@ -6,33 +6,40 @@
{/block} {/block}
{block content} {block content}
<p> <h4 style="margin-left: 100px; margin-right: 100px;">{_two_factor_authentication}</h4>
{_two_factor_authentication_login} <table cellspacing="10" cellpadding="0" border="0" align="center" width="70%">
</p>
<form method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center">
<tbody> <tbody>
<tr> <tr>
<td> <td>
<span>{_code}: </span> {_two_factor_authentication_login}
</td>
<td>
<input type="text" name="code" autocomplete="off" required />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="login" value="{$login}" />
<input type="hidden" name="password" value="{$password}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_log_in}" class="button" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<form method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center">
<tbody>
<tr style="text-align: right;">
<td>
<span class="nobold">{_code}: </span>
</td>
<td>
<input type="text" name="code" autocomplete="off" required autofocus />
</td>
</tr>
<tr>
<td>
</td>
</tr>
</tbody>
</table>
<center>
<input type="hidden" name="login" value="{$login}" />
<input type="hidden" name="password" value="{$password}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_log_in}" class="button" />
</center>
</form> </form>
{/block} {/block}

Some files were not shown because too many files have changed in this diff Show more