Merge branch 'master' into close_profiles

This commit is contained in:
lalka2018 2023-11-18 16:53:12 +03:00 committed by GitHub
commit 8a8b44e869
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
236 changed files with 16758 additions and 1508 deletions

View file

@ -30,7 +30,7 @@ If you want, you can add your instance to the list above so that people can regi
1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler)
* PHP 8.1 is supported too, however it was not tested carefully, so be aware. * PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed.
2. Install MySQL-compatible database. 2. Install MySQL-compatible database.

View file

@ -30,7 +30,7 @@ _[English](README.md)_
1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler)
* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает). * PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется.
2. Установите MySQL-совместимую базу данных. 2. Установите MySQL-совместимую базу данных.

92
ServiceAPI/Photos.php Normal file
View file

@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Photos as PhotosRepo, Albums, Clubs};
class Photos implements Handler
{
protected $user;
protected $photos;
function __construct(?User $user)
{
$this->user = $user;
$this->photos = new PhotosRepo;
}
function getPhotos(int $page = 1, int $album = 0, callable $resolve, callable $reject)
{
if($album == 0) {
$photos = $this->photos->getEveryUserPhoto($this->user, $page, 24);
$count = $this->photos->getUserPhotosCount($this->user);
} else {
$album = (new Albums)->get($album);
if(!$album || $album->isDeleted())
$reject(55, "Invalid .");
if($album->getOwner() instanceof User) {
if($album->getOwner()->getId() != $this->user->getId())
$reject(555, "Access to album denied");
} else {
if(!$album->getOwner()->canBeModifiedBy($this->user))
$reject(555, "Access to album denied");
}
$photos = $album->getPhotos($page, 24);
$count = $album->size();
}
$arr = [
"count" => $count,
"items" => [],
];
foreach($photos as $photo) {
$res = json_decode(json_encode($photo->toVkApiStruct()), true);
$arr["items"][] = $res;
}
$resolve($arr);
}
function getAlbums(int $club, callable $resolve, callable $reject)
{
$albumsRepo = (new Albums);
$count = $albumsRepo->getUserAlbumsCount($this->user);
$albums = $albumsRepo->getUserAlbums($this->user, 1, $count);
$arr = [
"count" => $count,
"items" => [],
];
foreach($albums as $album) {
$res = ["id" => $album->getId(), "name" => $album->getName()];
$arr["items"][] = $res;
}
if($club > 0) {
$cluber = (new Clubs)->get($club);
if(!$cluber || !$cluber->canBeModifiedBy($this->user))
$reject(1337, "Invalid (club), or you can't modify him");
$clubCount = (new Albums)->getClubAlbumsCount($cluber);
$clubAlbums = (new Albums)->getClubAlbums($cluber, 1, $clubCount);
foreach($clubAlbums as $albumr) {
$res = ["id" => $albumr->getId(), "name" => $albumr->getName()];
$arr["items"][] = $res;
}
$arr["count"] = $arr["count"] + $clubCount;
}
$resolve($arr);
}
}

156
ServiceAPI/Video.php Normal file
View file

@ -0,0 +1,156 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\{User, Post};
use openvk\Web\Models\Repositories\{Videos, Comments, Clubs};
use Chandler\MVC\Routing\Router;
class Video implements Handler
{
protected $user;
protected $videos;
protected $comments;
protected $groups;
function __construct(?User $user)
{
$this->user = $user;
$this->videos = new Videos;
$this->comments = new Comments;
$this->groups = new Clubs;
}
function getVideo(int $id, callable $resolve, callable $reject)
{
$video = $this->videos->get($id);
if(!$video || $video->isDeleted()) {
$reject(2, "Video does not exists");
}
if(method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user)) {
$reject(4, "Access to video denied");
}
if(!$video->getOwner()->getPrivacyPermission('videos.read', $this->user)) {
$reject(8, "Access to video denied: this user chose to hide his videos");
}
$prevVideo = NULL;
$nextVideo = NULL;
$lastVideo = $this->videos->getLastVideo($video->getOwner());
if($video->getVirtualId() - 1 != 0) {
for($i = $video->getVirtualId(); $i != 0; $i--) {
$maybeVideo = (new Videos)->getByOwnerAndVID($video->getOwner()->getId(), $i);
if(!is_null($maybeVideo) && !$maybeVideo->isDeleted() && $maybeVideo->getId() != $video->getId()) {
if(method_exists($maybeVideo, "canBeViewedBy") && !$maybeVideo->canBeViewedBy($this->user)) {
continue;
}
$prevVideo = $maybeVideo;
break;
}
}
}
if(is_null($lastVideo) || $lastVideo->getId() == $video->getId()) {
$nextVideo = NULL;
} else {
for($i = $video->getVirtualId(); $i <= $lastVideo->getVirtualId(); $i++) {
$maybeVideo = (new Videos)->getByOwnerAndVID($video->getOwner()->getId(), $i);
if(!is_null($maybeVideo) && !$maybeVideo->isDeleted() && $maybeVideo->getId() != $video->getId()) {
if(method_exists($maybeVideo, "canBeViewedBy") && !$maybeVideo->canBeViewedBy($this->user)) {
continue;
}
$nextVideo = $maybeVideo;
break;
}
}
}
$res = [
"id" => $video->getId(),
"title" => $video->getName(),
"owner" => $video->getOwner()->getId(),
"commentsCount" => $video->getCommentsCount(),
"description" => $video->getDescription(),
"type" => $video->getType(),
"name" => $video->getOwner()->getCanonicalName(),
"pretty_id" => $video->getPrettyId(),
"virtual_id" => $video->getVirtualId(),
"published" => (string)$video->getPublicationTime(),
"likes" => $video->getLikesCount(),
"has_like" => $video->hasLikeFrom($this->user),
"author" => $video->getOwner()->getCanonicalName(),
"canBeEdited" => $video->getOwner()->getId() == $this->user->getId(),
"isProcessing" => $video->getType() == 0 && $video->getURL() == "/assets/packages/static/openvk/video/rendering.mp4",
"prevVideo" => !is_null($prevVideo) ? $prevVideo->getId() : null,
"nextVideo" => !is_null($nextVideo) ? $nextVideo->getId() : null,
];
if($video->getType() == 1) {
$res["embed"] = $video->getVideoDriver()->getEmbed();
} else {
$res["url"] = $video->getURL();
}
$resolve($res);
}
function shareVideo(int $owner, int $vid, int $type, string $message, int $club, bool $signed, bool $asGroup, callable $resolve, callable $reject)
{
$video = $this->videos->getByOwnerAndVID($owner, $vid);
if(!$video || $video->isDeleted()) {
$reject(16, "Video does not exists");
}
if(method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user)) {
$reject(32, "Access to video denied");
}
if(!$video->getOwner()->getPrivacyPermission('videos.read', $this->user)) {
$reject(8, "Access to video denied: this user chose to hide his videos");
}
$flags = 0;
$nPost = new Post;
$nPost->setOwner($this->user->getId());
if($type == 0) {
$nPost->setWall($this->user->getId());
} else {
$club = $this->groups->get($club);
if(!$club || $club->isDeleted() || !$club->canBeModifiedBy($this->user)) {
$reject(64, "Can't do repost to this club");
}
if($asGroup)
$flags |= 0b10000000;
if($signed)
$flags |= 0b01000000;
$nPost->setWall($club->getId() * -1);
}
$nPost->setContent($message);
$nPost->setFlags($flags);
$nPost->save();
$nPost->attach($video);
$res = [
"id" => $nPost->getId(),
"pretty_id" => $nPost->getPrettyId(),
];
$resolve($res);
}
}

View file

@ -2,7 +2,7 @@
namespace openvk\ServiceAPI; namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\Post; use openvk\Web\Models\Entities\Post;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Posts, Notes}; use openvk\Web\Models\Repositories\{Posts, Notes, Videos};
class Wall implements Handler class Wall implements Handler
{ {
@ -15,13 +15,17 @@ class Wall implements Handler
$this->user = $user; $this->user = $user;
$this->posts = new Posts; $this->posts = new Posts;
$this->notes = new Notes; $this->notes = new Notes;
$this->videos = new Videos;
} }
function getPost(int $id, callable $resolve, callable $reject): void function getPost(int $id, callable $resolve, callable $reject): void
{ {
$post = $this->posts->get($id); $post = $this->posts->get($id);
if(!$post || $post->isDeleted()) if(!$post || $post->isDeleted())
$reject("No post with id=$id"); $reject(53, "No post with id=$id");
if($post->getSuggestionType() != 0)
$reject(25, "Can't get suggested post");
if(!$post->canBeViewedBy($this->user)) if(!$post->canBeViewedBy($this->user))
$reject(12, "Access denied"); $reject(12, "Access denied");
@ -98,4 +102,45 @@ class Wall implements Handler
$resolve($arr); $resolve($arr);
} }
function getVideos(int $page = 1, callable $resolve, callable $reject)
{
$videos = $this->videos->getByUser($this->user, $page, 8);
$count = $this->videos->getUserVideosCount($this->user);
$arr = [
"count" => $count,
"items" => [],
];
foreach($videos as $video) {
$res = json_decode(json_encode($video->toVkApiStruct($this->user)), true);
$res["video"]["author_name"] = $video->getOwner()->getCanonicalName();
$arr["items"][] = $res;
}
$resolve($arr);
}
function searchVideos(int $page = 1, string $query, callable $resolve, callable $reject)
{
$dbc = $this->videos->find($query);
$videos = $dbc->page($page, 8);
$count = $dbc->size();
$arr = [
"count" => $count,
"items" => [],
];
foreach($videos as $video) {
$res = json_decode(json_encode($video->toVkApiStruct($this->user)), true);
$res["video"]["author_name"] = $video->getOwner()->getCanonicalName();
$arr["items"][] = $res;
}
$resolve($arr);
}
} }

View file

@ -1,22 +1,788 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Audio as AEntity;
use openvk\Web\Models\Entities\Playlist;
use openvk\Web\Models\Repositories\Audios;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Util\EntityStream;
final class Audio extends VKAPIRequestHandler final class Audio extends VKAPIRequestHandler
{ {
function get(): object private function toSafeAudioStruct(?AEntity $audio, ?string $hash = NULL, bool $need_user = false): object
{ {
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; if(!$audio)
$this->fail(0404, "Audio not found");
else if(!$audio->canBeViewedBy($this->getUser()))
$this->fail(201, "Access denied to audio(" . $audio->getPrettyId() . ")");
# рофлан ебало
$privApi = $hash && $GLOBALS["csrfCheck"];
$audioObj = $audio->toVkApiStruct($this->getUser());
if(!$privApi) {
$audioObj->manifest = false;
$audioObj->keys = false;
}
if($need_user) {
$user = (new \openvk\Web\Models\Repositories\Users)->get($audio->getOwner()->getId());
$audioObj->user = (object) [
"id" => $user->getId(),
"photo" => $user->getAvatarUrl(),
"name" => $user->getCanonicalName(),
"name_gen" => $user->getCanonicalName(),
];
}
return $audioObj;
}
private function streamToResponse(EntityStream $es, int $offset, int $count, ?string $hash = NULL): object
{
$items = [];
foreach($es->offsetLimit($offset, $count) as $audio) {
$items[] = $this->toSafeAudioStruct($audio, $hash);
}
return (object) [
"count" => sizeof($items),
"items" => $items,
];
}
private function validateGenre(?string& $genre_str, ?int $genre_id): void
{
if(!is_null($genre_str)) {
if(!in_array($genre_str, AEntity::genres))
$this->fail(8, "Invalid genre_str");
} else if(!is_null($genre_id)) {
$genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL;
if(!$genre_str)
$this->fail(8, "Invalid genre ID $genre_id");
}
}
private function audioFromAnyId(string $id): ?AEntity
{
$descriptor = explode("_", $id);
if(sizeof($descriptor) === 1) {
if(ctype_digit($descriptor[0])) {
$audio = (new Audios)->get((int) $descriptor[0]);
} else {
$aid = base64_decode($descriptor[0], true);
if(!$aid)
$this->fail(8, "Invalid audio $id");
$audio = (new Audios)->get((int) $aid);
}
} else if(sizeof($descriptor) === 2) {
$audio = (new Audios)->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]);
} else {
$this->fail(8, "Invalid audio $id");
}
return $audio;
}
function getById(string $audios, ?string $hash = NULL, int $need_user = 0): object
{
$this->requireUser();
$audioIds = array_unique(explode(",", $audios));
if(sizeof($audioIds) === 1) {
$audio = $this->audioFromAnyId($audioIds[0]);
return (object) [ return (object) [
"count" => 1, "count" => 1,
"items" => [(object) [ "items" => [
"id" => 1, $this->toSafeAudioStruct($audio, $hash, (bool) $need_user),
"owner_id" => 1, ],
"artist" => "В ОВК ПОКА НЕТ МУЗЫКИ", ];
"title" => "ЖДИТЕ :)))", } else if(sizeof($audioIds) > 6000) {
"duration" => 22, $this->fail(1980, "Can't get more than 6000 audios at once");
"url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3" }
]]
$audios = [];
foreach($audioIds as $id)
$audios[] = $this->getById($id, $hash)->items[0];
return (object) [
"count" => sizeof($audios),
"items" => $audios,
]; ];
} }
function isLagtrain(string $audio_id): int
{
$this->requireUser();
$audio = $this->audioFromAnyId($audio_id);
if(!$audio)
$this->fail(0404, "Audio not found");
# Possible information disclosure risks are acceptable :D
return (int) (strpos($audio->getName(), "Lagtrain") !== false);
}
// TODO stub
function getRecommendations(): object
{
return (object) [
"count" => 0,
"items" => [],
];
}
function getPopular(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object
{
$this->requireUser();
$this->validateGenre($genre_str, $genre_id);
$results = (new Audios)->getGlobal(Audios::ORDER_POPULAR, $genre_str);
return $this->streamToResponse($results, $offset, $count, $hash);
}
function getFeed(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object
{
$this->requireUser();
$this->validateGenre($genre_str, $genre_id);
$results = (new Audios)->getGlobal(Audios::ORDER_NEW, $genre_str);
return $this->streamToResponse($results, $offset, $count, $hash);
}
function search(string $q, int $auto_complete = 0, int $lyrics = 0, int $performer_only = 0, int $sort = 2, int $search_own = 0, int $offset = 0, int $count = 30, ?string $hash = NULL): object
{
$this->requireUser();
if(($auto_complete + $search_own) != 0)
$this->fail(10, "auto_complete and search_own are not supported");
else if($count > 300 || $count < 1)
$this->fail(8, "count is invalid: $count");
$results = (new Audios)->search($q, $sort, (bool) $performer_only, (bool) $lyrics);
return $this->streamToResponse($results, $offset, $count, $hash);
}
function getCount(int $owner_id, int $uploaded_only = 0): int
{
$this->requireUser();
if($owner_id < 0) {
$owner_id *= -1;
$group = (new Clubs)->get($owner_id);
if(!$group)
$this->fail(0404, "Group not found");
return (new Audios)->getClubCollectionSize($group);
}
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
if(!$user)
$this->fail(0404, "User not found");
if(!$user->getPrivacyPermission("audios.read", $this->getUser()))
$this->fail(15, "Access denied");
if($uploaded_only) {
return DatabaseConnection::i()->getContext()->table("audios")
->where([
"deleted" => false,
"owner" => $owner_id,
])->count();
}
return (new Audios)->getUserCollectionSize($user);
}
function get(int $owner_id = 0, int $album_id = 0, string $audio_ids = '', int $need_user = 1, int $offset = 0, int $count = 100, int $uploaded_only = 0, int $need_seed = 0, ?string $shuffle_seed = NULL, int $shuffle = 0, ?string $hash = NULL): object
{
$this->requireUser();
$shuffleSeed = NULL;
$shuffleSeedStr = NULL;
if($shuffle == 1) {
if(!$shuffle_seed) {
if($need_seed == 1) {
$shuffleSeed = openssl_random_pseudo_bytes(6);
$shuffleSeedStr = base64_encode($shuffleSeed);
$shuffleSeed = hexdec(bin2hex($shuffleSeed));
} else {
$hOffset = ((int) date("i") * 60) + (int) date("s");
$thisHour = time() - $hOffset;
$shuffleSeed = $thisHour + $this->getUser()->getId();
$shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed)));
}
} else {
$shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed)));
$shuffleSeedStr = $shuffle_seed;
}
}
if($album_id != 0) {
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "album_id invalid");
else if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Can't open this album for reading");
$songs = [];
$list = $album->getAudios($offset, $count, $shuffleSeed);
foreach($list as $song)
$songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1);
$response = (object) [
"count" => sizeof($songs),
"items" => $songs,
];
if(!is_null($shuffleSeed))
$response->shuffle_seed = $shuffleSeedStr;
return $response;
}
if(!empty($audio_ids)) {
$audio_ids = explode(",", $audio_ids);
if(!$audio_ids)
$this->fail(10, "Audio::get@L0d186:explode(string): Unknown error");
else if(sizeof($audio_ids) < 1)
$this->fail(8, "Invalid audio_ids syntax");
if(!is_null($shuffleSeed))
$audio_ids = knuth_shuffle($audio_ids, $shuffleSeed);
$obj = $this->getById(implode(",", $audio_ids), $hash, $need_user);
if(!is_null($shuffleSeed))
$obj->shuffle_seed = $shuffleSeedStr;
return $obj;
}
$dbCtx = DatabaseConnection::i()->getContext();
if($uploaded_only == 1) {
if($owner_id <= 0)
$this->fail(8, "uploaded_only can only be used with owner_id > 0");
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
if(!$user)
$this->fail(0602, "Invalid user");
if(!$user->getPrivacyPermission("audios.read", $this->getUser()))
$this->fail(15, "Access denied: this user chose to hide his audios");
if(!is_null($shuffleSeed)) {
$audio_ids = [];
$query = $dbCtx->table("audios")->select("virtual_id")->where([
"owner" => $owner_id,
"deleted" => 0,
]);
foreach($query as $res)
$audio_ids[] = $res->virtual_id;
$audio_ids = knuth_shuffle($audio_ids, $shuffleSeed);
$audio_ids = array_slice($audio_ids, $offset, $count);
$audio_q = ""; # audio.getById query
foreach($audio_ids as $aid)
$audio_q .= ",$owner_id" . "_$aid";
$obj = $this->getById(substr($audio_q, 1), $hash, $need_user);
$obj->shuffle_seed = $shuffleSeedStr;
return $obj;
}
$res = (new Audios)->getByUploader((new \openvk\Web\Models\Repositories\Users)->get($owner_id));
return $this->streamToResponse($res, $offset, $count, $hash, $need_user);
}
$query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id);
if(!is_null($shuffleSeed)) {
$audio_ids = [];
foreach($query as $aid)
$audio_ids[] = $aid->audio;
$audio_ids = knuth_shuffle($audio_ids, $shuffleSeed);
$audio_ids = array_slice($audio_ids, $offset, $count);
$audio_q = "";
foreach($audio_ids as $aid)
$audio_q .= ",$aid";
$obj = $this->getById(substr($audio_q, 1), $hash, $need_user);
$obj->shuffle_seed = $shuffleSeedStr;
return $obj;
}
$items = [];
if($owner_id > 0) {
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
if(!$user)
$this->fail(50, "Invalid user");
if(!$user->getPrivacyPermission("audios.read", $this->getUser()))
$this->fail(15, "Access denied: this user chose to hide his audios");
}
$audios = (new Audios)->getByEntityID($owner_id, $offset, $count);
foreach($audios as $audio)
$items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1);
return (object) [
"count" => sizeof($items),
"items" => $items,
];
}
function getLyrics(int $lyrics_id): object
{
$this->requireUser();
$audio = (new Audios)->get($lyrics_id);
if(!$audio || !$audio->getLyrics())
$this->fail(0404, "Not found");
if(!$audio->canBeViewedBy($this->getUser()))
$this->fail(201, "Access denied to lyrics");
return (object) [
"lyrics_id" => $lyrics_id,
"text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()),
];
}
function beacon(int $aid, ?int $gid = NULL): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$audio = (new Audios)->get($aid);
if(!$audio)
$this->fail(0404, "Not Found");
else if(!$audio->canBeViewedBy($this->getUser()))
$this->fail(201, "Insufficient permissions to listen this audio");
$group = NULL;
if(!is_null($gid)) {
$group = (new Clubs)->get($gid);
if(!$group)
$this->fail(0404, "Not Found");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203, "Insufficient rights to this group");
}
return (int) $audio->listen($group ?? $this->getUser());
}
function setBroadcast(string $audio, string $target_ids): array
{
$this->requireUser();
[$owner, $aid] = explode("_", $audio);
$song = (new Audios)->getByOwnerAndVID((int) $owner, (int) $aid);
$ids = [];
foreach(explode(",", $target_ids) as $id) {
$id = (int) $id;
if($id > 0) {
if ($id != $this->getUser()->getId()) {
$this->fail(600, "Can't listen on behalf of $id");
} else {
$ids[] = $id;
$this->beacon($song->getId());
continue;
}
}
$group = (new Clubs)->get($id * -1);
if(!$group)
$this->fail(0404, "Not Found");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203,"Insufficient rights to this group");
$ids[] = $id;
$this->beacon($song ? $song->getId() : 0, $id * -1);
}
return $ids;
}
function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = NULL): object
{
$this->requireUser();
if(!in_array($filter, ["all", "friends", "groups"]))
$this->fail(8, "Invalid filter $filter");
$broadcastList = $this->getUser()->getBroadcastList($filter);
$items = [];
foreach($broadcastList as $res) {
$struct = $res->toVkApiStruct();
$status = $res->getCurrentAudioStatus();
$struct->status_audio = $status ? $this->toSafeAudioStruct($status) : NULL;
$items[] = $struct;
}
return (object) [
"count" => sizeof($items),
"items" => $items,
];
}
function edit(int $owner_id, int $audio_id, ?string $artist = NULL, ?string $title = NULL, ?string $text = NULL, ?int $genre_id = NULL, ?string $genre_str = NULL, int $no_search = 0): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id);
if(!$audio)
$this->fail(0404, "Not Found");
else if(!$audio->canBeModifiedBy($this->getUser()))
$this->fail(201, "Insufficient permissions to edit this audio");
if(!is_null($genre_id)) {
$genre = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL;
if(!$genre)
$this->fail(8, "Invalid genre ID $genre_id");
$audio->setGenre($genre);
} else if(!is_null($genre_str)) {
if(!in_array($genre_str, AEntity::genres))
$this->fail(8, "Invalid genre ID $genre_str");
$audio->setGenre($genre_str);
}
$lyrics = 0;
if(!is_null($text)) {
$audio->setLyrics($text);
$lyrics = $audio->getId();
}
if(!is_null($artist))
$audio->setPerformer($artist);
if(!is_null($title))
$audio->setName($title);
$audio->setSearchability(!((bool) $no_search));
$audio->setEdited(time());
$audio->save();
return $lyrics;
}
function add(int $audio_id, int $owner_id, ?int $group_id = NULL, ?int $album_id = NULL): string
{
$this->requireUser();
$this->willExecuteWriteAction();
if(!is_null($album_id))
$this->fail(10, "album_id not implemented");
// TODO get rid of dups
$to = $this->getUser();
if(!is_null($group_id)) {
$group = (new Clubs)->get($group_id);
if(!$group)
$this->fail(0404, "Invalid group_id");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203, "Insufficient rights to this group");
$to = $group;
}
$audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id);
if(!$audio)
$this->fail(0404, "Not found");
else if(!$audio->canBeViewedBy($this->getUser()))
$this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)");
try {
$audio->add($to);
} catch(\OverflowException $ex) {
$this->fail(300, "Album is full");
}
return $audio->getPrettyId();
}
function delete(int $audio_id, int $owner_id, ?int $group_id = NULL): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$from = $this->getUser();
if(!is_null($group_id)) {
$group = (new Clubs)->get($group_id);
if(!$group)
$this->fail(0404, "Invalid group_id");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203, "Insufficient rights to this group");
$from = $group;
}
$audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id);
if(!$audio)
$this->fail(0404, "Not found");
$audio->remove($from);
return 1;
}
function restore(int $audio_id, int $owner_id, ?int $group_id = NULL, ?string $hash = NULL): object
{
$this->requireUser();
$vid = $this->add($audio_id, $owner_id, $group_id);
return $this->getById($vid, $hash)->items[0];
}
function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object
{
$this->requireUser();
$owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id;
$playlists = [];
if($owner_id > 0 && $owner_id != $this->getUser()->getId()) {
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
if(!$user->getPrivacyPermission("audios.read", $this->getUser()))
$this->fail(50, "Access to playlists denied");
}
foreach((new Audios)->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) {
if(!$playlist->canBeViewedBy($this->getUser())) {
if($drop_private == 1)
continue;
$playlists[] = NULL;
continue;
}
$playlists[] = $playlist->toVkApiStruct($this->getUser());
}
return (object) [
"count" => sizeof($playlists),
"items" => $playlists,
];
}
function searchAlbums(string $query, int $offset = 0, int $limit = 25, int $drop_private = 0): object
{
$this->requireUser();
$playlists = [];
$search = (new Audios)->searchPlaylists($query)->offsetLimit($offset, $limit);
foreach($search as $playlist) {
if(!$playlist->canBeViewedBy($this->getUser())) {
if($drop_private == 0)
$playlists[] = NULL;
continue;
}
$playlists[] = $playlist->toVkApiStruct($this->getUser());
}
return (object) [
"count" => sizeof($playlists),
"items" => $playlists,
];
}
function addAlbum(string $title, ?string $description = NULL, int $group_id = 0): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$group = NULL;
if($group_id != 0) {
$group = (new Clubs)->get($group_id);
if(!$group)
$this->fail(0404, "Invalid group_id");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this group");
}
$album = new Playlist;
$album->setName($title);
if(!is_null($group))
$album->setOwner($group_id * -1);
else
$album->setOwner($this->getUser()->getId());
if(!is_null($description))
$album->setDescription($description);
$album->save();
if(!is_null($group))
$album->bookmark($group);
else
$album->bookmark($this->getUser());
return $album->getId();
}
function editAlbum(int $album_id, ?string $title = NULL, ?string $description = NULL): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
if(!is_null($title))
$album->setName($title);
if(!is_null($description))
$album->setDescription($description);
$album->setEdited(time());
$album->save();
return (int) !(!$title && !$description);
}
function deleteAlbum(int $album_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
$album->delete();
return 1;
}
function moveToAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
$audios = [];
$audio_ids = array_unique(explode(",", $audio_ids));
if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000)
$this->fail(8, "audio_ids must contain at least 1 audio and at most 1000");
foreach($audio_ids as $audio_id) {
$audio = $this->audioFromAnyId($audio_id);
if(!$audio)
continue;
else if(!$audio->canBeViewedBy($this->getUser()))
continue;
$audios[] = $audio;
}
if(sizeof($audios) < 1)
return 0;
$res = 1;
try {
foreach ($audios as $audio)
$res = min($res, (int) $album->add($audio));
} catch(\OutOfBoundsException $ex) {
return 0;
}
return $res;
}
function removeFromAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
$audios = [];
$audio_ids = array_unique(explode(",", $audio_ids));
if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000)
$this->fail(8, "audio_ids must contain at least 1 audio and at most 1000");
foreach($audio_ids as $audio_id) {
$audio = $this->audioFromAnyId($audio_id);
if(!$audio)
continue;
else if($audio->canBeViewedBy($this->getUser()))
continue;
$audios[] = $audio;
}
if(sizeof($audios) < 1)
return 0;
foreach($audios as $audio)
$album->remove($audio);
return 1;
}
function copyToAlbum(int $album_id, string $audio_ids): int
{
return $this->moveToAlbum($album_id, $audio_ids);
}
function bookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($id);
if(!$album)
$this->fail(0404, "Not found");
if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Access error");
return (int) $album->bookmark($this->getUser());
}
function unBookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($id);
if(!$album)
$this->fail(0404, "Not found");
if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Access error");
return (int) $album->unbookmark($this->getUser());
}
} }

View file

@ -4,7 +4,7 @@ use openvk\Web\Models\Repositories\Users as UsersRepo;
final class Friends extends VKAPIRequestHandler final class Friends extends VKAPIRequestHandler
{ {
function get(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 100): object
{ {
$i = 0; $i = 0;
$offset++; $offset++;
@ -14,6 +14,10 @@ final class Friends extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
if ($user_id == 0) {
$user_id = $this->getUser()->getId();
}
$user = $users->get($user_id); $user = $users->get($user_id);
if(!$user || $user->isDeleted()) if(!$user || $user->isDeleted())
@ -25,7 +29,7 @@ final class Friends extends VKAPIRequestHandler
if(!$user->canBeViewedBy($this->getUser())) if(!$user->canBeViewedBy($this->getUser()))
$this->fail(15, "Access denied"); $this->fail(15, "Access denied");
foreach($users->get($user_id)->getFriends($offset, $count) as $friend) { foreach($user->getFriends($offset, $count) as $friend) {
$friends[$i] = $friend->getId(); $friends[$i] = $friend->getId();
$i++; $i++;
} }

View file

@ -2,16 +2,17 @@
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\Repositories\Posts as PostsRepo;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\Club;
final class Groups extends VKAPIRequestHandler final class Groups extends VKAPIRequestHandler
{ {
function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 6, bool $online = false): object function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 6, bool $online = false, string $filter = "groups"): object
{ {
$this->requireUser(); $this->requireUser();
if($user_id == 0) { if($user_id == 0) {
foreach($this->getUser()->getClubs($offset, false, $count, true) as $club) foreach($this->getUser()->getClubs($offset, $filter == "admin", $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $this->getUser()->getClubCount(); $clbsCount = $this->getUser()->getClubCount();
} else { } else {
@ -27,7 +28,7 @@ final class Groups extends VKAPIRequestHandler
if(!$user->getPrivacyPermission('groups.read', $this->getUser())) if(!$user->getPrivacyPermission('groups.read', $this->getUser()))
$this->fail(15, "Access denied: this user chose to hide his groups."); $this->fail(15, "Access denied: this user chose to hide his groups.");
foreach($user->getClubs($offset, false, $count, true) as $club) foreach($user->getClubs($offset, $filter == "admin", $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $user->getClubCount(); $clbsCount = $user->getClubCount();
@ -86,6 +87,19 @@ final class Groups extends VKAPIRequestHandler
break; break;
case "members_count": case "members_count":
$rClubs[$i]->members_count = $usr->getFollowersCount(); $rClubs[$i]->members_count = $usr->getFollowersCount();
break;
case "can_suggest":
$rClubs[$i]->can_suggest = !$usr->canBeModifiedBy($this->getUser()) && $usr->getWallType() == 2;
break;
# unstandard feild
case "suggested_count":
if($usr->getWallType() != 2) {
$rClubs[$i]->suggested_count = NULL;
break;
}
$rClubs[$i]->suggested_count = $usr->getSuggestedPostsCount($this->getUser());
break; break;
} }
} }
@ -194,6 +208,18 @@ final class Groups extends VKAPIRequestHandler
case "description": case "description":
$response[$i]->description = $clb->getDescription(); $response[$i]->description = $clb->getDescription();
break; break;
case "can_suggest":
$response[$i]->can_suggest = !$clb->canBeModifiedBy($this->getUser()) && $clb->getWallType() == 2;
break;
# unstandard feild
case "suggested_count":
if($clb->getWallType() != 2) {
$response[$i]->suggested_count = NULL;
break;
}
$response[$i]->suggested_count = $clb->getSuggestedPostsCount($this->getUser());
break;
case "contacts": case "contacts":
$contacts; $contacts;
$contactTmp = $clb->getManagers(1, true); $contactTmp = $clb->getManagers(1, true);
@ -294,11 +320,12 @@ final class Groups extends VKAPIRequestHandler
string $description = NULL, string $description = NULL,
string $screen_name = NULL, string $screen_name = NULL,
string $website = NULL, string $website = NULL,
int $wall = NULL, int $wall = -1,
int $topics = NULL, int $topics = NULL,
int $adminlist = NULL, int $adminlist = NULL,
int $topicsAboveWall = NULL, int $topicsAboveWall = NULL,
int $hideFromGlobalFeed = NULL) int $hideFromGlobalFeed = NULL,
int $audio = NULL)
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -309,17 +336,31 @@ final class Groups extends VKAPIRequestHandler
if(!$club || !$club->canBeModifiedBy($this->getUser())) $this->fail(15, "You can't modify this group."); 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."); if(!empty($screen_name) && !$club->setShortcode($screen_name)) $this->fail(103, "Invalid shortcode.");
!is_null($title) ? $club->setName($title) : NULL; !empty($title) ? $club->setName($title) : NULL;
!is_null($description) ? $club->setAbout($description) : NULL; !empty($description) ? $club->setAbout($description) : NULL;
!is_null($screen_name) ? $club->setShortcode($screen_name) : NULL; !empty($screen_name) ? $club->setShortcode($screen_name) : NULL;
!is_null($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; !empty($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;
try {
$wall != -1 ? $club->setWall($wall) : NULL;
} catch(\Exception $e) {
$this->fail(50, "Invalid wall value");
}
!empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL;
!empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL;
!empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL;
!empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL;
in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : NULL;
try {
$club->save(); $club->save();
} catch(\TypeError $e) {
$this->fail(15, "Nothing changed");
} catch(\Exception $e) {
$this->fail(18, "An unknown error occurred: maybe you set an incorrect value?");
}
return 1; return 1;
} }
@ -376,7 +417,7 @@ final class Groups extends VKAPIRequestHandler
$arr->items[$i]->can_see_all_posts = 1; $arr->items[$i]->can_see_all_posts = 1;
break; break;
case "can_see_audio": case "can_see_audio":
$arr->items[$i]->can_see_audio = 0; $arr->items[$i]->can_see_audio = 1;
break; break;
case "can_write_private_message": case "can_write_private_message":
$arr->items[$i]->can_write_private_message = 0; $arr->items[$i]->can_write_private_message = 0;
@ -472,10 +513,10 @@ final class Groups extends VKAPIRequestHandler
"title" => $club->getName(), "title" => $club->getName(),
"description" => $club->getDescription() != NULL ? $club->getDescription() : "", "description" => $club->getDescription() != NULL ? $club->getDescription() : "",
"address" => $club->getShortcode(), "address" => $club->getShortcode(),
"wall" => $club->canPost() == true ? 1 : 0, "wall" => $club->getWallType(), # отличается от вкшных но да ладно
"photos" => 1, "photos" => 1,
"video" => 0, "video" => 0,
"audio" => 0, "audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0,
"docs" => 0, "docs" => 0,
"topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0, "topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0,
"wiki" => 0, "wiki" => 0,

View file

@ -2,6 +2,11 @@
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Repositories\Users as UsersRepo; use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\Posts as PostsRepo; use openvk\Web\Models\Repositories\Posts as PostsRepo;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Repositories\Photos as PhotosRepo;
use openvk\Web\Models\Repositories\Notes as NotesRepo;
final class Likes extends VKAPIRequestHandler final class Likes extends VKAPIRequestHandler
{ {
@ -10,23 +15,44 @@ final class Likes extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
$postable = NULL;
switch($type) { switch($type) {
case "post": case "post":
$post = (new PostsRepo)->getPostById($owner_id, $item_id); $post = (new PostsRepo)->getPostById($owner_id, $item_id);
if(is_null($post)) $postable = $post;
$this->fail(100, "One of the parameters specified was missing or invalid: object not found"); break;
case "comment":
if(!$post->canBeViewedBy($this->getUser())) $comment = (new CommentsRepo)->get($item_id);
$this->fail(15, "Access denied"); $postable = $comment;
break;
$post->setLike(true, $this->getUser()); case "video":
$video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id);
return (object) [ $postable = $video;
"likes" => $post->getLikesCount() break;
]; case "photo":
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $photo;
break;
case "note":
$note = (new NotesRepo)->getNoteById($owner_id, $item_id);
$postable = $note;
break;
default: default:
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
} }
if(is_null($postable) || $postable->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
if(method_exists($postable, "canBeViewedBy") && !$postable->canBeViewedBy($this->getUser() ?? NULL)) {
$this->fail(2, "Access to postable denied");
}
$postable->setLike(true, $this->getUser());
return (object) [
"likes" => $postable->getLikesCount()
];
} }
function delete(string $type, int $owner_id, int $item_id): object function delete(string $type, int $owner_id, int $item_id): object
@ -34,47 +60,144 @@ final class Likes extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
$postable = NULL;
switch($type) { switch($type) {
case "post": case "post":
$post = (new PostsRepo)->getPostById($owner_id, $item_id); $post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post)) $postable = $post;
$this->fail(100, "One of the parameters specified was missing or invalid: object not found"); break;
case "comment":
if(!$post->canBeViewedBy($this->getUser())) $comment = (new CommentsRepo)->get($item_id);
$this->fail(15, "Access denied"); $postable = $comment;
break;
$post->setLike(false, $this->getUser()); case "video":
return (object) [ $video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id);
"likes" => $post->getLikesCount() $postable = $video;
]; break;
case "photo":
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $photo;
break;
case "note":
$note = (new NotesRepo)->getNoteById($owner_id, $item_id);
$postable = $note;
break;
default: default:
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
} }
if(is_null($postable) || $postable->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
if(method_exists($postable, "canBeViewedBy") && !$postable->canBeViewedBy($this->getUser() ?? NULL)) {
$this->fail(2, "Access to postable denied");
}
if(!is_null($postable)) {
$postable->setLike(false, $this->getUser());
return (object) [
"likes" => $postable->getLikesCount()
];
}
} }
function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object function isLiked(int $user_id, string $type, int $owner_id, int $item_id): object
{ {
$this->requireUser(); $this->requireUser();
switch($type) {
case "post":
$user = (new UsersRepo)->get($user_id); $user = (new UsersRepo)->get($user_id);
if (is_null($user))
if(is_null($user) || $user->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: user not found"); $this->fail(100, "One of the parameters specified was missing or invalid: user not found");
if(method_exists($user, "canBeViewedBy") && !$user->canBeViewedBy($this->getUser())) {
$this->fail(1984, "Access denied: you can't see this user");
}
$postable = NULL;
switch($type) {
case "post":
$post = (new PostsRepo)->getPostById($owner_id, $item_id); $post = (new PostsRepo)->getPostById($owner_id, $item_id);
if (is_null($post)) $postable = $post;
$this->fail(100, "One of the parameters specified was missing or invalid: object not found"); break;
case "comment":
if(!$post->canBeViewedBy($this->getUser())) $comment = (new CommentsRepo)->get($item_id);
$this->fail(15, "Access denied"); $postable = $comment;
break;
return (object) [ case "video":
"liked" => (int) $post->hasLikeFrom($user), $video = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id);
"copied" => 0 # TODO: handle this $postable = $video;
]; break;
case "photo":
$photo = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id);
$postable = $photo;
break;
case "note":
$note = (new NotesRepo)->getNoteById($owner_id, $item_id);
$postable = $note;
break;
default: default:
$this->fail(100, "One of the parameters specified was missing or invalid: incorrect type"); $this->fail(100, "One of the parameters specified was missing or invalid: incorrect type");
} }
if(is_null($postable) || $postable->isDeleted())
$this->fail(100, "One of the parameters specified was missing or invalid: object not found");
if(!$postable->canBeViewedBy($this->getUser())) {
$this->fail(665, "Access to postable denied");
}
return (object) [
"liked" => (int) $postable->hasLikeFrom($user),
"copied" => 0
];
}
function getList(string $type, int $owner_id, int $item_id, bool $extended = false, int $offset = 0, int $count = 10, bool $skip_own = false)
{
$this->requireUser();
$object = NULL;
switch($type) {
case "post":
$object = (new PostsRepo)->getPostById($owner_id, $item_id);
break;
case "comment":
$object = (new CommentsRepo)->get($item_id);
break;
case "photo":
$object = (new PhotosRepo)->getByOwnerAndVID($owner_id, $item_id);
break;
case "video":
$object = (new VideosRepo)->getByOwnerAndVID($owner_id, $item_id);
break;
default:
$this->fail(58, "Invalid type");
break;
}
if(!$object || $object->isDeleted())
$this->fail(56, "Invalid postable");
$res = (object)[
"count" => $object->getLikesCount(),
"items" => []
];
$likers = array_slice(iterator_to_array($object->getLikers(1, $offset + $count)), $offset);
foreach($likers as $liker) {
if($skip_own && $liker->getId() == $this->getUser()->getId())
continue;
if(!$extended)
$res->items[] = $liker->getId();
else
$res->items[] = $liker->toVkApiStruct();
}
return $res;
} }
} }

View file

@ -65,7 +65,8 @@ 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, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0) function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0,
string $attachment = "") # интересно почему не attachments
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -79,7 +80,8 @@ final class Messages extends VKAPIRequestHandler
$this->fail(946, "Chats are not implemented"); $this->fail(946, "Chats are not implemented");
else if($sticker_id !== -1) else if($sticker_id !== -1)
$this->fail(-151, "Stickers are not implemented"); $this->fail(-151, "Stickers are not implemented");
else if(empty($message))
if(empty($message) && empty($attachment))
$this->fail(100, "Message text is empty or invalid"); $this->fail(100, "Message text is empty or invalid");
# lol recursion # lol recursion
@ -117,6 +119,21 @@ final class Messages extends VKAPIRequestHandler
if(!$msg) if(!$msg)
$this->fail(950, "Internal error"); $this->fail(950, "Internal error");
else else
if(!empty($attachment)) {
$attachs = parseAttachments($attachment);
# Работают только фотки, остальное просто не будет отображаться.
if(sizeof($attachs) >= 10)
$this->fail(15, "Too many attachments");
foreach($attachs as $attach) {
if($attach && !$attach->isDeleted() && $attach->getOwner()->getId() == $this->getUser()->getId())
$msg->attach($attach);
else
$this->fail(52, "One of the attachments is invalid");
}
}
return $msg->getId(); return $msg->getId();
} }
@ -393,4 +410,49 @@ final class Messages extends VKAPIRequestHandler
return $res; return $res;
} }
function edit(int $message_id, string $message = "", string $attachment = "", int $peer_id = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
$msg = (new MSGRepo)->get($message_id);
if(empty($message) && empty($attachment))
$this->fail(100, "Required parameter 'message' missing.");
if(!$msg || $msg->isDeleted())
$this->fail(102, "Invalid message");
if($msg->getSender()->getId() != $this->getUser()->getId())
$this->fail(15, "Access to message denied");
if(!empty($message))
$msg->setContent($message);
$msg->setEdited(time());
$msg->save(true);
if(!empty($attachment)) {
$attachs = parseAttachments($attachment);
$newAttachmentsCount = sizeof($attachs);
$postsAttachments = iterator_to_array($msg->getChildren());
if(sizeof($postsAttachments) >= 10)
$this->fail(15, "Message have too many attachments");
if(($newAttachmentsCount + sizeof($postsAttachments)) > 10)
$this->fail(158, "Message will have too many attachments");
foreach($attachs as $attach) {
if($attach && !$attach->isDeleted() && $attach->getOwner()->getId() == $this->getUser()->getId())
$msg->attach($attach);
else
$this->fail(52, "One of the attachments is invalid");
}
}
return 1;
}
} }

View file

@ -52,7 +52,7 @@ final class Newsfeed extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
$queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)";
$queryBase .= "WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0"; # AND `posts`.`suggested` = 0 $queryBase .= "WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0";
if($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT) if($this->getUser()->getNsfwTolerance() === User::NSFW_INTOLERANT)
$queryBase .= " AND `nsfw` = 0"; $queryBase .= " AND `nsfw` = 0";

View file

@ -121,21 +121,6 @@ final class Notes extends VKAPIRequestHandler
return 1; 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 = "") 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->requireUser();
@ -162,25 +147,6 @@ final class Notes extends VKAPIRequestHandler
return 1; 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) function get(int $user_id, string $note_ids = "", int $offset = 0, int $count = 10, int $sort = 0)
{ {
$this->requireUser(); $this->requireUser();
@ -217,7 +183,7 @@ final class Notes extends VKAPIRequestHandler
$items = []; $items = [];
$note = (new NotesRepo)->getNoteById((int)$id[0], (int)$id[1]); $note = (new NotesRepo)->getNoteById((int)$id[0], (int)$id[1]);
if($note) { if($note && !$note->isDeleted()) {
$nodez->notes[] = $note->toVkApiStruct(); $nodez->notes[] = $note->toVkApiStruct();
} }
} }

View file

@ -0,0 +1,83 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\{Notifications as Notifs, Clubs, Users};
final class Notifications extends VKAPIRequestHandler
{
function get(int $count = 10,
string $from = "",
int $offset = 0,
string $start_from = "",
string $filters = "",
int $start_time = 0,
int $end_time = 0,
int $archived = 0)
{
$this->requireUser();
$res = (object)[
"items" => [],
"profiles" => [],
"groups" => [],
"last_viewed" => $this->getUser()->getNotificationOffset()
];
if($count > 100)
$this->fail(125, "Count is too big");
if(!eventdb())
$this->fail(1289, "EventDB is disabled on this instance");
$notifs = array_slice(iterator_to_array((new Notifs)->getNotificationsByUser($this->getUser(), $this->getUser()->getNotificationOffset(), (bool)$archived, 1, $offset + $count)), $offset);
$tmpProfiles = [];
foreach($notifs as $notif) {
$sxModel = $notif->getModel(1);
if(!method_exists($sxModel, "getAvatarUrl"))
$sxModel = $notif->getModel(0);
$tmpProfiles[] = $sxModel instanceof Club ? $sxModel->getId() * -1 : $sxModel->getId();
$res->items[] = $notif->toVkApiStruct();
}
foreach(array_unique($tmpProfiles) as $id) {
if($id > 0) {
$sxModel = (new Users)->get($id);
$result = (object)[
"uid" => $sxModel->getId(),
"first_name" => $sxModel->getFirstName(),
"last_name" => $sxModel->getLastName(),
"photo" => $sxModel->getAvatarUrl(),
"photo_medium_rec" => $sxModel->getAvatarUrl("tiny"),
"screen_name" => $sxModel->getShortCode()
];
$res->profiles[] = $result;
} else {
$sxModel = (new Clubs)->get(abs($id));
$result = $sxModel->toVkApiStruct($this->getUser());
$res->groups[] = $result;
}
}
return $res;
}
function markAsViewed()
{
$this->requireUser();
$this->willExecuteWriteAction();
try {
$this->getUser()->updateNotificationOffset();
$this->getUser()->save();
} catch(\Throwable $e) {
return 0;
}
return 1;
}
}

View file

@ -104,4 +104,67 @@ final class Polls extends VKAPIRequestHandler
$this->fail(8, "how.to. ook.bacon.in.microwova."); $this->fail(8, "how.to. ook.bacon.in.microwova.");
} }
} }
function getVoters(int $poll_id, int $answer_ids, int $offset = 0, int $count = 6)
{
$this->requireUser();
$poll = (new PollsRepo)->get($poll_id);
if(!$poll)
$this->fail(251, "Invalid poll");
if($poll->isAnonymous())
$this->fail(251, "Access denied: poll is anonymous.");
$voters = array_slice($poll->getVoters($answer_ids, 1, $offset + $count), $offset);
$res = (object)[
"answer_id" => $answer_ids,
"users" => []
];
foreach($voters as $voter)
$res->users[] = $voter->toVkApiStruct();
return $res;
}
function create(string $question, string $add_answers, bool $disable_unvote = false, bool $is_anonymous = false, bool $is_multiple = false, int $end_date = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
$options = json_decode($add_answers);
if(!$options || empty($options))
$this->fail(62, "Invalid options");
if(sizeof($options) > ovkGetQuirk("polls.max-opts"))
$this->fail(51, "Too many options");
$poll = new Poll;
$poll->setOwner($this->getUser());
$poll->setTitle($question);
$poll->setMultipleChoice($is_multiple);
$poll->setAnonymity($is_anonymous);
$poll->setRevotability(!$disable_unvote);
$poll->setOptions($options);
if($end_date > time()) {
if($end_date > time() + (DAY * 365))
$this->fail(89, "End date is too big");
$poll->setEndDate($end_date);
}
$poll->save();
return $this->getById($poll->getId());
}
function edit()
{
#todo
return 1;
}
} }

View file

@ -8,13 +8,23 @@ final class Status extends VKAPIRequestHandler
function get(int $user_id = 0, int $group_id = 0) function get(int $user_id = 0, int $group_id = 0)
{ {
$this->requireUser(); $this->requireUser();
if($user_id == 0 && $group_id == 0) {
return $this->getUser()->getStatus(); if($user_id == 0 && $group_id == 0)
} else { $user_id = $this->getUser()->getId();
if($group_id > 0) if($group_id > 0)
$this->fail(501, "Group statuses are not implemented"); $this->fail(501, "Group statuses are not implemented");
else else {
return (new UsersRepo)->get($user_id)->getStatus(); $user = (new UsersRepo)->get($user_id);
$audioStatus = $user->getCurrentAudioStatus();
if($audioStatus) {
return [
"status" => $user->getStatus(),
"audio" => $audioStatus->toVkApiStruct(),
];
}
return $user->getStatus();
} }
} }

View file

@ -1,7 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\{User, Report};
use openvk\Web\Models\Repositories\Users as UsersRepo; use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Repositories\Reports;
final class Users extends VKAPIRequestHandler final class Users extends VKAPIRequestHandler
{ {
@ -36,8 +37,8 @@ final class Users extends VKAPIRequestHandler
} else if($usr->isBanned()) { } else if($usr->isBanned()) {
$response[$i] = (object)[ $response[$i] = (object)[
"id" => $usr->getId(), "id" => $usr->getId(),
"first_name" => $usr->getFirstName(), "first_name" => $usr->getFirstName(true),
"last_name" => $usr->getLastName(), "last_name" => $usr->getLastName(true),
"deactivated" => "banned", "deactivated" => "banned",
"ban_reason" => $usr->getBanReason() "ban_reason" => $usr->getBanReason()
]; ];
@ -46,8 +47,8 @@ final class Users extends VKAPIRequestHandler
} else { } else {
$response[$i] = (object)[ $response[$i] = (object)[
"id" => $usr->getId(), "id" => $usr->getId(),
"first_name" => $usr->getFirstName(), "first_name" => $usr->getFirstName(true),
"last_name" => $usr->getLastName(), "last_name" => $usr->getLastName(true),
"is_closed" => $usr->isClosed(), "is_closed" => $usr->isClosed(),
"can_access_closed" => (bool)$usr->canBeViewedBy($this->getUser()), "can_access_closed" => (bool)$usr->canBeViewedBy($this->getUser()),
]; ];
@ -95,6 +96,12 @@ final class Users extends VKAPIRequestHandler
case "status": case "status":
if($usr->getStatus() != NULL) if($usr->getStatus() != NULL)
$response[$i]->status = $usr->getStatus(); $response[$i]->status = $usr->getStatus();
$audioStatus = $usr->getCurrentAudioStatus();
if($audioStatus)
$response[$i]->status_audio = $audioStatus->toVkApiStruct();
break; break;
case "screen_name": case "screen_name":
if($usr->getShortCode() != NULL) if($usr->getShortCode() != NULL)
@ -189,6 +196,18 @@ final class Users extends VKAPIRequestHandler
$response[$i]->interests = $usr->getInterests(); $response[$i]->interests = $usr->getInterests();
break; break;
case "quotes":
$response[$i]->interests = $usr->getFavoriteQuote();
break;
case "email":
$response[$i]->interests = $usr->getEmail();
break;
case "telegram":
$response[$i]->interests = $usr->getTelegram();
break;
case "about":
$response[$i]->interests = $usr->getDescription();
break;
case "rating": case "rating":
$response[$i]->rating = $usr->getRating(); $response[$i]->rating = $usr->getRating();
break; break;
@ -328,4 +347,26 @@ final class Users extends VKAPIRequestHandler
"items" => $this->get(implode(',', $array), $nfilds, $offset, $count) "items" => $this->get(implode(',', $array), $nfilds, $offset, $count)
]; ];
} }
function report(int $user_id, string $type = "spam", string $comment = "")
{
$this->requireUser();
$this->willExecuteWriteAction();
if($user_id == $this->getUser()->getId())
$this->fail(12, "Can't report yourself.");
if(sizeof(iterator_to_array((new Reports)->getDuplicates("user", $user_id, NULL, $this->getUser()->getId()))) > 0)
return 1;
$report = new Report;
$report->setUser_id($this->getUser()->getId());
$report->setTarget_id($user_id);
$report->setType("user");
$report->setReason($comment);
$report->setCreated(time());
$report->save();
return 1;
}
} }

View file

@ -28,7 +28,7 @@ abstract class VKAPIRequestHandler
protected function getPlatform(): ?string protected function getPlatform(): ?string
{ {
return $this->platform; return $this->platform ?? "";
} }
protected function userAuthorized(): bool protected function userAuthorized(): bool

View file

@ -11,11 +11,11 @@ use openvk\Web\Models\Repositories\Comments as CommentsRepo;
final class Video extends VKAPIRequestHandler final class Video extends VKAPIRequestHandler
{ {
function get(int $owner_id, string $videos, int $offset = 0, int $count = 30, int $extended = 0): object function get(int $owner_id, string $videos = "", int $offset = 0, int $count = 30, int $extended = 0): object
{ {
$this->requireUser(); $this->requireUser();
if ($videos) { if(!empty($videos)) {
$vids = explode(',', $videos); $vids = explode(',', $videos);
foreach($vids as $vid) foreach($vids as $vid)
@ -26,7 +26,7 @@ final class Video extends VKAPIRequestHandler
$video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1])); $video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1]));
if($video) { if($video) {
$items[] = $video->getApiStructure(); $items[] = $video->getApiStructure($this->getUser());
} }
} }
@ -40,6 +40,10 @@ final class Video extends VKAPIRequestHandler
else else
$this->fail(1, "Not implemented"); $this->fail(1, "Not implemented");
if(!$user->getPrivacyPermission('videos.read', $this->getUser())) {
$this->fail(20, "Access denied: this user chose to hide his videos");
}
if(!$user || $user->isDeleted()) if(!$user || $user->isDeleted())
$this->fail(14, "Invalid user"); $this->fail(14, "Invalid user");
@ -54,7 +58,7 @@ final class Video extends VKAPIRequestHandler
$items = []; $items = [];
foreach ($videos as $video) { foreach ($videos as $video) {
$items[] = $video->getApiStructure(); $items[] = $video->getApiStructure($this->getUser());
} }
return (object) [ return (object) [

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers; namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\Notifications\{WallPostNotification, RepostNotification, CommentNotification}; use openvk\Web\Models\Entities\Notifications\{PostAcceptedNotification, WallPostNotification, NewSuggestedPostsNotification, RepostNotification, CommentNotification};
use openvk\Web\Models\Repositories\Users as UsersRepo; use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo; use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
@ -15,10 +15,12 @@ use openvk\Web\Models\Entities\Video;
use openvk\Web\Models\Repositories\Videos as VideosRepo; use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Entities\Note; use openvk\Web\Models\Entities\Note;
use openvk\Web\Models\Repositories\Notes as NotesRepo; use openvk\Web\Models\Repositories\Notes as NotesRepo;
use openvk\Web\Models\Repositories\Polls as PollsRepo;
use openvk\Web\Models\Repositories\Audios as AudiosRepo;
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, string $filter = "all"): object
{ {
$this->requireUser(); $this->requireUser();
@ -27,7 +29,7 @@ final class Wall extends VKAPIRequestHandler
$items = []; $items = [];
$profiles = []; $profiles = [];
$groups = []; $groups = [];
$cnt = $posts->getPostCountOnUserWall($owner_id); $cnt = 0;
if ($owner_id > 0) if ($owner_id > 0)
$wallOnwer = (new UsersRepo)->get($owner_id); $wallOnwer = (new UsersRepo)->get($owner_id);
@ -44,7 +46,47 @@ final class Wall extends VKAPIRequestHandler
if(!$wallOnwer) if(!$wallOnwer)
$this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls $this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls
foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) { $iteratorv;
switch($filter) {
case "all":
$iteratorv = $posts->getPostsFromUsersWall($owner_id, 1, $count, $offset);
$cnt = $posts->getPostCountOnUserWall($owner_id);
break;
case "owner":
$iteratorv = $posts->getOwnersPostsFromWall($owner_id, 1, $count, $offset);
$cnt = $posts->getOwnersCountOnUserWall($owner_id);
break;
case "others":
$iteratorv = $posts->getOthersPostsFromWall($owner_id, 1, $count, $offset);
$cnt = $posts->getOthersCountOnUserWall($owner_id);
break;
case "postponed":
$this->fail(42, "Postponed posts are not implemented.");
break;
case "suggests":
if($owner_id < 0) {
if($wallOnwer->getWallType() != 2)
$this->fail(125, "Group's wall type is open or closed");
if($wallOnwer->canBeModifiedBy($this->getUser())) {
$iteratorv = $posts->getSuggestedPosts($owner_id * -1, 1, $count, $offset);
$cnt = $posts->getSuggestedPostsCount($owner_id * -1);
} else {
$iteratorv = $posts->getSuggestedPostsByUser($owner_id * -1, $this->getUser()->getId(), 1, $count, $offset);
$cnt = $posts->getSuggestedPostsCountByUser($owner_id * -1, $this->getUser()->getId());
}
} else {
$this->fail(528, "Suggested posts avaiable only at groups");
}
break;
default:
$this->fail(254, "Invalid filter");
break;
}
foreach($iteratorv 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();
$attachments = []; $attachments = [];
@ -58,9 +100,14 @@ final class Wall extends VKAPIRequestHandler
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) { } else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $this->getUser()); $attachments[] = $this->getApiPoll($attachment, $this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure(); $attachments[] = $attachment->getApiStructure($this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct(); $attachments[] = $attachment->toVkApiStruct();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()),
];
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
@ -100,6 +147,13 @@ final class Wall extends VKAPIRequestHandler
"attachments" => $repostAttachments, "attachments" => $repostAttachments,
"post_source" => $post_source, "post_source" => $post_source,
]; ];
if ($attachment->getVirtualId() > 0)
$profiles[] = $attachment->getVirtualId();
else
$groups[] = $attachment->getVirtualId();
if($post->isSigned())
$profiles[] = $attachment->getOwner()->getId();
} }
} }
@ -114,15 +168,26 @@ final class Wall extends VKAPIRequestHandler
]; ];
} }
$postType = "post";
$signerId = NULL;
if($post->getSuggestionType() != 0)
$postType = "suggest";
if($post->isSigned()) {
$actualAuthor = $post->getOwner(false);
$signerId = $actualAuthor->getId();
}
$items[] = (object)[ $items[] = (object)[
"id" => $post->getVirtualId(), "id" => $post->getVirtualId(),
"from_id" => $from_id, "from_id" => $from_id,
"owner_id" => $post->getTargetWall(), "owner_id" => $post->getTargetWall(),
"date" => $post->getPublicationTime()->timestamp(), "date" => $post->getPublicationTime()->timestamp(),
"post_type" => "post", "post_type" => $postType,
"text" => $post->getText(false), "text" => $post->getText(false),
"copy_history" => $repost, "copy_history" => $repost,
"can_edit" => 0, # TODO "can_edit" => $post->canBeEditedBy($this->getUser()),
"can_delete" => $post->canBeDeletedBy($this->getUser()), "can_delete" => $post->canBeDeletedBy($this->getUser()),
"can_pin" => $post->canBePinnedBy($this->getUser()), "can_pin" => $post->canBePinnedBy($this->getUser()),
"can_archive" => false, # TODO MAYBE "can_archive" => false, # TODO MAYBE
@ -131,6 +196,7 @@ final class Wall extends VKAPIRequestHandler
"is_explicit" => $post->isExplicit(), "is_explicit" => $post->isExplicit(),
"attachments" => $attachments, "attachments" => $attachments,
"post_source" => $post_source, "post_source" => $post_source,
"signer_id" => $signerId,
"comments" => (object)[ "comments" => (object)[
"count" => $post->getCommentsCount(), "count" => $post->getCommentsCount(),
"can_post" => 1 "can_post" => 1
@ -152,6 +218,9 @@ final class Wall extends VKAPIRequestHandler
else else
$groups[] = $from_id * -1; $groups[] = $from_id * -1;
if($post->isSigned())
$profiles[] = $post->getOwner(false)->getId();
$attachments = NULL; # free attachments so it will not clone everythingg $attachments = NULL; # free attachments so it will not clone everythingg
} }
@ -237,9 +306,14 @@ final class Wall extends VKAPIRequestHandler
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) { } else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $user); $attachments[] = $this->getApiPoll($attachment, $user);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure(); $attachments[] = $attachment->getApiStructure($this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct(); $attachments[] = $attachment->toVkApiStruct();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser())
];
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
@ -279,6 +353,13 @@ final class Wall extends VKAPIRequestHandler
"attachments" => $repostAttachments, "attachments" => $repostAttachments,
"post_source" => $post_source, "post_source" => $post_source,
]; ];
if ($attachment->getVirtualId() > 0)
$profiles[] = $attachment->getVirtualId();
else
$groups[] = $attachment->getVirtualId();
if($post->isSigned())
$profiles[] = $attachment->getOwner()->getId();
} }
} }
@ -293,15 +374,27 @@ final class Wall extends VKAPIRequestHandler
]; ];
} }
# TODO: $post->getVkApiType()
$postType = "post";
$signerId = NULL;
if($post->getSuggestionType() != 0)
$postType = "suggest";
if($post->isSigned()) {
$actualAuthor = $post->getOwner(false);
$signerId = $actualAuthor->getId();
}
$items[] = (object)[ $items[] = (object)[
"id" => $post->getVirtualId(), "id" => $post->getVirtualId(),
"from_id" => $from_id, "from_id" => $from_id,
"owner_id" => $post->getTargetWall(), "owner_id" => $post->getTargetWall(),
"date" => $post->getPublicationTime()->timestamp(), "date" => $post->getPublicationTime()->timestamp(),
"post_type" => "post", "post_type" => $postType,
"text" => $post->getText(false), "text" => $post->getText(false),
"copy_history" => $repost, "copy_history" => $repost,
"can_edit" => 0, # TODO "can_edit" => $post->canBeEditedBy($this->getUser()),
"can_delete" => $post->canBeDeletedBy($user), "can_delete" => $post->canBeDeletedBy($user),
"can_pin" => $post->canBePinnedBy($user), "can_pin" => $post->canBePinnedBy($user),
"can_archive" => false, # TODO MAYBE "can_archive" => false, # TODO MAYBE
@ -309,6 +402,7 @@ final class Wall extends VKAPIRequestHandler
"is_pinned" => $post->isPinned(), "is_pinned" => $post->isPinned(),
"is_explicit" => $post->isExplicit(), "is_explicit" => $post->isExplicit(),
"post_source" => $post_source, "post_source" => $post_source,
"signer_id" => $signerId,
"attachments" => $attachments, "attachments" => $attachments,
"comments" => (object)[ "comments" => (object)[
"count" => $post->getCommentsCount(), "count" => $post->getCommentsCount(),
@ -331,6 +425,9 @@ final class Wall extends VKAPIRequestHandler
else else
$groups[] = $from_id * -1; $groups[] = $from_id * -1;
if($post->isSigned())
$profiles[] = $post->getOwner(false)->getId();
$attachments = NULL; # free attachments so it will not clone everything $attachments = NULL; # free attachments so it will not clone everything
$repost = NULL; # same $repost = NULL; # same
} }
@ -386,7 +483,7 @@ final class Wall extends VKAPIRequestHandler
]; ];
} }
function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0, string $attachments = ""): object function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0, string $attachments = "", int $post_id = 0): object
{ {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -407,6 +504,46 @@ final class Wall extends VKAPIRequestHandler
if($canPost == false) $this->fail(15, "Access denied"); if($canPost == false) $this->fail(15, "Access denied");
if($post_id > 0) {
if($owner_id > 0)
$this->fail(62, "Suggested posts available only at groups");
$post = (new PostsRepo)->getPostById($owner_id, $post_id, true);
if(!$post || $post->isDeleted())
$this->fail(32, "Invald post");
if($post->getSuggestionType() == 0)
$this->fail(20, "Post is not suggested");
if($post->getSuggestionType() == 2)
$this->fail(16, "Post is declined");
if(!$post->canBePinnedBy($this->getUser()))
$this->fail(51, "Access denied");
$author = $post->getOwner();
$flags = 0;
$flags |= 0b10000000;
if($signed == 1)
$flags |= 0b01000000;
$post->setSuggested(0);
$post->setCreated(time());
$post->setFlags($flags);
if(!empty($message) && iconv_strlen($message) > 0)
$post->setContent($message);
$post->save();
if($author->getId() != $this->getUser()->getId())
(new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit();
return (object)["post_id" => $post->getVirtualId()];
}
$anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
if($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) { if($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) {
$manager = $wallOwner->getManager($this->getUser()); $manager = $wallOwner->getManager($this->getUser());
@ -435,11 +572,16 @@ final class Wall extends VKAPIRequestHandler
$post->setContent($message); $post->setContent($message);
$post->setFlags($flags); $post->setFlags($flags);
$post->setApi_Source_Name($this->getPlatform()); $post->setApi_Source_Name($this->getPlatform());
if($owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2)
$post->setSuggested(1);
$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");
} }
# TODO use parseAttachments
if(!empty($attachments)) { if(!empty($attachments)) {
$attachmentsArr = explode(",", $attachments); $attachmentsArr = explode(",", $attachments);
# Аттачи такого вида: [тип][id владельца]_[id вложения] # Аттачи такого вида: [тип][id владельца]_[id вложения]
@ -448,6 +590,10 @@ final class Wall extends VKAPIRequestHandler
if(sizeof($attachmentsArr) > 10) if(sizeof($attachmentsArr) > 10)
$this->fail(50, "Error: too many attachments"); $this->fail(50, "Error: too many attachments");
preg_match_all("/poll/m", $attachments, $matches, PREG_SET_ORDER, 0);
if(sizeof($matches) > 1)
$this->fail(85, "Error: too many polls");
foreach($attachmentsArr as $attac) { foreach($attachmentsArr as $attac) {
$attachmentType = NULL; $attachmentType = NULL;
@ -457,6 +603,11 @@ final class Wall extends VKAPIRequestHandler
$attachmentType = "video"; $attachmentType = "video";
elseif(str_contains($attac, "note")) elseif(str_contains($attac, "note"))
$attachmentType = "note"; $attachmentType = "note";
elseif(str_contains($attac, "poll"))
$attachmentType = "poll";
elseif(str_contains($attac, "audio"))
$attachmentType = "audio";
else else
$this->fail(205, "Unknown attachment type"); $this->fail(205, "Unknown attachment type");
@ -470,28 +621,40 @@ final class Wall extends VKAPIRequestHandler
if($attachmentType == "photo") { if($attachmentType == "photo") {
$attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted()) if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Photo does not exists"); $this->fail(100, "Invalid photo");
if($attacc->getOwner()->getId() != $this->getUser()->getId()) if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser()))
$this->fail(43, "You do not have access to this photo"); $this->fail(43, "Access to photo denied");
$post->attach($attacc); $post->attach($attacc);
} elseif($attachmentType == "video") { } elseif($attachmentType == "video") {
$attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted()) if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Video does not exists"); $this->fail(100, "Video does not exists");
if($attacc->getOwner()->getId() != $this->getUser()->getId()) if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser()))
$this->fail(43, "You do not have access to this video"); $this->fail(43, "Access to video denied");
$post->attach($attacc); $post->attach($attacc);
} elseif($attachmentType == "note") { } elseif($attachmentType == "note") {
$attacc = (new NotesRepo)->getNoteById($attachmentOwner, $attachmentId); $attacc = (new NotesRepo)->getNoteById($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted()) if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Note does not exist"); $this->fail(100, "Note does not exist");
if($attacc->getOwner()->getId() != $this->getUser()->getId()) if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser()))
$this->fail(43, "You do not have access to this note"); $this->fail(11, "Access to note denied");
if($attacc->getOwner()->getPrivacySetting("notes.read") < 1) $post->attach($attacc);
$this->fail(11, "You can't attach note to post, because your notes list is closed. Change it in privacy settings in web-version."); } elseif($attachmentType == "poll") {
$attacc = (new PollsRepo)->get($attachmentId);
if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Poll does not exist");
if($attacc->getOwner()->getId() != $this->getUser()->getId())
$this->fail(43, "You do not have access to this poll");
$post->attach($attacc);
} elseif($attachmentType == "audio") {
$attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Audio does not exist");
$post->attach($attacc); $post->attach($attacc);
} }
@ -501,6 +664,22 @@ final class Wall extends VKAPIRequestHandler
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();
if($owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2) {
$suggsCount = (new PostsRepo)->getSuggestedPostsCount($wallOwner->getId());
if($suggsCount % 10 == 0) {
$managers = $wallOwner->getManagers();
$owner = $wallOwner->getOwner();
(new NewSuggestedPostsNotification($owner, $wallOwner))->emit();
foreach($managers as $manager) {
(new NewSuggestedPostsNotification($manager->getUser(), $wallOwner))->emit();
}
}
return (object)["post_id" => "on_view"];
}
return (object)["post_id" => $post->getVirtualId()]; return (object)["post_id" => $post->getVirtualId()];
} }
@ -578,6 +757,11 @@ final class Wall extends VKAPIRequestHandler
$attachments[] = $this->getApiPhoto($attachment); $attachments[] = $this->getApiPhoto($attachment);
} elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct(); $attachments[] = $attachment->toVkApiStruct();
} elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()),
];
} }
} }
@ -650,6 +834,11 @@ final class Wall extends VKAPIRequestHandler
foreach($comment->getChildren() as $attachment) { foreach($comment->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);
} elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()),
];
} }
} }
@ -697,7 +886,7 @@ final class Wall extends VKAPIRequestHandler
return $response; return $response;
} }
function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0, string $attachments = "") { function createComment(int $owner_id, int $post_id, string $message = "", int $from_group = 0, string $attachments = "") {
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -744,6 +933,8 @@ final class Wall extends VKAPIRequestHandler
$attachmentType = "photo"; $attachmentType = "photo";
elseif(str_contains($attac, "video")) elseif(str_contains($attac, "video"))
$attachmentType = "video"; $attachmentType = "video";
elseif(str_contains($attac, "audio"))
$attachmentType = "audio";
else else
$this->fail(205, "Unknown attachment type"); $this->fail(205, "Unknown attachment type");
@ -758,16 +949,22 @@ final class Wall extends VKAPIRequestHandler
$attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); $attacc = (new PhotosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted()) if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Photo does not exists"); $this->fail(100, "Photo does not exists");
if($attacc->getOwner()->getId() != $this->getUser()->getId()) if(!$attacc->getOwner()->getPrivacyPermission('photos.read', $this->getUser()))
$this->fail(43, "You do not have access to this photo"); $this->fail(11, "Access to photo denied");
$comment->attach($attacc); $comment->attach($attacc);
} elseif($attachmentType == "video") { } elseif($attachmentType == "video") {
$attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); $attacc = (new VideosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted()) if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Video does not exists"); $this->fail(100, "Video does not exists");
if($attacc->getOwner()->getId() != $this->getUser()->getId()) if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser()))
$this->fail(43, "You do not have access to this video"); $this->fail(11, "Access to video denied");
$comment->attach($attacc);
} elseif($attachmentType == "audio") {
$attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Audio does not exist");
$comment->attach($attacc); $comment->attach($attacc);
} }
@ -798,11 +995,124 @@ final class Wall extends VKAPIRequestHandler
return 1; return 1;
} }
function delete(int $owner_id, int $post_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$post = (new PostsRepo)->getPostById($owner_id, $post_id, true);
if(!$post || $post->isDeleted())
$this->fail(583, "Invalid post");
$wallOwner = $post->getWallOwner();
if($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->getUser()) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0)
$this->fail(12, "Access denied: you can't delete your accepted post.");
if($post->getOwnerPost() == $this->getUser()->getId() || $post->getTargetWall() == $this->getUser()->getId() || $owner_id < 0 && $wallOwner->canBeModifiedBy($this->getUser())) {
$post->unwire();
$post->delete();
return 1;
} else {
$this->fail(15, "Access denied");
}
}
function edit(int $owner_id, int $post_id, string $message = "", string $attachments = "") {
$this->requireUser();
$this->willExecuteWriteAction();
$post = (new PostsRepo)->getPostById($owner_id, $post_id);
if(!$post || $post->isDeleted())
$this->fail(102, "Invalid post");
if(empty($message) && empty($attachments))
$this->fail(100, "Required parameter 'message' missing.");
if(!$post->canBeEditedBy($this->getUser()))
$this->fail(7, "Access to editing denied");
if(!empty($message))
$post->setContent($message);
$post->setEdited(time());
$post->save(true);
# todo добавить такое в веб версию
if(!empty($attachments)) {
$attachs = parseAttachments($attachments);
$newAttachmentsCount = sizeof($attachs);
$postsAttachments = iterator_to_array($post->getChildren());
if(sizeof($postsAttachments) >= 10)
$this->fail(15, "Post have too many attachments");
if(($newAttachmentsCount + sizeof($postsAttachments)) > 10)
$this->fail(158, "Post will have too many attachments");
foreach($attachs as $attach) {
if($attach && !$attach->isDeleted())
$post->attach($attach);
else
$this->fail(52, "One of the attachments is invalid");
}
}
return ["post_id" => $post->getVirtualId()];
}
function editComment(int $comment_id, int $owner_id = 0, string $message = "", string $attachments = "") {
$this->requireUser();
$this->willExecuteWriteAction();
$comment = (new CommentsRepo)->get($comment_id);
if(empty($message) && empty($attachments))
$this->fail(100, "Required parameter 'message' missing.");
if(!$comment || $comment->isDeleted())
$this->fail(102, "Invalid comment");
if(!$comment->canBeEditedBy($this->getUser()))
$this->fail(15, "Access to editing comment denied");
if(!empty($message))
$comment->setContent($message);
$comment->setEdited(time());
$comment->save(true);
if(!empty($attachments)) {
$attachs = parseAttachments($attachments);
$newAttachmentsCount = sizeof($attachs);
$postsAttachments = iterator_to_array($comment->getChildren());
if(sizeof($postsAttachments) >= 10)
$this->fail(15, "Post have too many attachments");
if(($newAttachmentsCount + sizeof($postsAttachments)) > 10)
$this->fail(158, "Post will have too many attachments");
foreach($attachs as $attach) {
if($attach && !$attach->isDeleted())
$comment->attach($attach);
else
$this->fail(52, "One of the attachments is invalid");
}
}
return 1;
}
private function getApiPhoto($attachment) { private function getApiPhoto($attachment) {
return [ return [
"type" => "photo", "type" => "photo",
"photo" => [ "photo" => [
"album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : NULL, "album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : 0,
"date" => $attachment->getPublicationTime()->timestamp(), "date" => $attachment->getPublicationTime()->timestamp(),
"id" => $attachment->getVirtualId(), "id" => $attachment->getVirtualId(),
"owner_id" => $attachment->getOwner()->getId(), "owner_id" => $attachment->getOwner()->getId(),

View file

@ -48,7 +48,7 @@ class APIToken extends RowModel
$this->delete(); $this->delete();
} }
function save(): void function save(?bool $log = false): void
{ {
if(is_null($this->getRecord())) if(is_null($this->getRecord()))
$this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36))); $this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36)));

View file

@ -0,0 +1,469 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Util\Shell\Exceptions\UnknownCommandException;
use openvk\Web\Util\Shell\Shell;
/**
* @method setName(string)
* @method setPerformer(string)
* @method setLyrics(string)
* @method setExplicit(bool)
*/
class Audio extends Media
{
protected $tableName = "audios";
protected $fileExtension = "mpd";
# Taken from winamp :D
const genres = [
'Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou'
];
# Taken from: https://web.archive.org/web/20220322153107/https://dev.vk.com/reference/objects/audio-genres
const vkGenres = [
"Rock" => 1,
"Pop" => 2,
"Rap" => 3,
"Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK
"Easy Listening" => 4,
"House" => 5,
"Dance" => 5,
"Instrumental" => 6,
"Metal" => 7,
"Alternative" => 21,
"Dubstep" => 8,
"Jazz" => 1001,
"Blues" => 1001,
"Drum & Bass" => 10,
"Trance" => 11,
"Chanson" => 12,
"Ethnic" => 13,
"Acoustic" => 14,
"Vocal" => 14,
"Reggae" => 15,
"Classical" => 16,
"Indie Pop" => 17,
"Speech" => 19,
"Disco" => 22,
"Other" => 18,
];
private function fileLength(string $filename): int
{
if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
throw new \Exception();
$error = NULL;
$streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error);
if($error !== 0)
throw new \DomainException("$filename is not recognized as media container");
else if(empty($streams) || ctype_space($streams))
throw new \DomainException("$filename does not contain any audio streams");
$vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error);
# check if audio has cover (attached_pic)
preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover);
if(!empty($vstreams) && !ctype_space($vstreams) && ((int)($hasCover[1]) !== 1))
throw new \DomainException("$filename is a video");
$durations = [];
preg_match_all('%duration=([0-9\.]++)%', $streams, $durations);
if(sizeof($durations[1]) === 0)
throw new \DomainException("$filename does not contain any meaningful audio streams");
$length = 0;
foreach($durations[1] as $duration) {
$duration = floatval($duration);
if($duration < 1.0 || $duration > 65536.0)
throw new \DomainException("$filename does not contain any meaningful audio streams");
else
$length = max($length, $duration);
}
return (int) round($length, 0, PHP_ROUND_HALF_EVEN);
}
/**
* @throws \Exception
*/
protected function saveFile(string $filename, string $hash): bool
{
$duration = $this->fileLength($filename);
$kid = openssl_random_pseudo_bytes(16);
$key = openssl_random_pseudo_bytes(16);
$tok = openssl_random_pseudo_bytes(28);
$ss = ceil($duration / 15);
$this->stateChanges("kid", $kid);
$this->stateChanges("key", $key);
$this->stateChanges("token", $tok);
$this->stateChanges("segment_size", $ss);
$this->stateChanges("length", $duration);
try {
$args = [
str_replace("enabled", "available", OPENVK_ROOT),
str_replace("enabled", "available", $this->getBaseDir()),
$hash,
$filename,
bin2hex($kid),
bin2hex($key),
bin2hex($tok),
$ss,
];
if(Shell::isPowershell()) {
Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args)
->start();
} else {
Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk
->start(); // idk, not tested :")
}
# Wait until processAudio will consume the file
$start = time();
while(file_exists($filename))
if(time() - $start > 5)
throw new \RuntimeException("Timed out waiting FFMPEG");
} catch(UnknownCommandException $ucex) {
exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR);
}
return true;
}
function getTitle(): string
{
return $this->getRecord()->name;
}
function getPerformer(): string
{
return $this->getRecord()->performer;
}
function getName(): string
{
return $this->getPerformer() . "" . $this->getTitle();
}
function getGenre(): ?string
{
return $this->getRecord()->genre;
}
function getLyrics(): ?string
{
return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL;
}
function getLength(): int
{
return $this->getRecord()->length;
}
function getFormattedLength(): string
{
$len = $this->getLength();
$mins = floor($len / 60);
$secs = $len - ($mins * 60);
return (
str_pad((string) $mins, 2, "0", STR_PAD_LEFT)
. ":" .
str_pad((string) $secs, 2, "0", STR_PAD_LEFT)
);
}
function getSegmentSize(): float
{
return $this->getRecord()->segment_size;
}
function getListens(): int
{
return $this->getRecord()->listens;
}
function getOriginalURL(bool $force = false): string
{
$disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force;
if(!$this->isAvailable() || $disallowed)
return ovk_scheme(true)
. $_SERVER["HTTP_HOST"] . ":"
. $_SERVER["HTTP_PORT"]
. "/assets/packages/static/openvk/audio/nomusic.mp3";
$key = bin2hex($this->getRecord()->token);
return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3";
}
function getURL(?bool $force = false): string
{
if ($this->isWithdrawn()) return "";
return parent::getURL();
}
function getKeys(): array
{
$keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key);
return $keys;
}
function isAnonymous(): bool
{
return false;
}
function isExplicit(): bool
{
return (bool) $this->getRecord()->explicit;
}
function isWithdrawn(): bool
{
return (bool) $this->getRecord()->withdrawn;
}
function isUnlisted(): bool
{
return (bool) $this->getRecord()->unlisted;
}
# NOTICE may flush model to DB if it was just processed
function isAvailable(): bool
{
if($this->getRecord()->processed)
return true;
# throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage
if(time() - $this->getRecord()->checked < 5)
return false;
try {
$fragments = str_replace(".mpd", "_fragments", $this->getFileName());
$original = "original_" . bin2hex($this->getRecord()->token) . ".mp3";
if(file_exists("$fragments/$original")) {
# Original gets uploaded after fragments
$this->stateChanges("processed", 0x01);
return true;
}
} finally {
$this->stateChanges("checked", time());
$this->save();
}
return false;
}
function isInLibraryOf($entity): bool
{
return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([
"entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1),
"audio" => $this->getId(),
])) != 0;
}
function add($entity): bool
{
if($this->isInLibraryOf($entity))
return false;
$entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1);
$audioRels = DatabaseConnection::i()->getContext()->table("audio_relations");
if(sizeof($audioRels->where("entity", $entityId)) > 65536)
throw new \OverflowException("Can't have more than 65536 audios in a playlist");
$audioRels->insert([
"entity" => $entityId,
"audio" => $this->getId(),
]);
return true;
}
function remove($entity): bool
{
if(!$this->isInLibraryOf($entity))
return false;
DatabaseConnection::i()->getContext()->table("audio_relations")->where([
"entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1),
"audio" => $this->getId(),
])->delete();
return true;
}
function listen($entity, Playlist $playlist = NULL): bool
{
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
$lastListen = $listensTable->where([
"entity" => $entity->getRealId(),
"audio" => $this->getId(),
])->order("index DESC")->fetch();
if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) {
$listensTable->insert([
"entity" => $entity->getRealId(),
"audio" => $this->getId(),
"time" => time(),
"playlist" => $playlist ? $playlist->getId() : NULL,
]);
if($entity instanceof User) {
$this->stateChanges("listens", ($this->getListens() + 1));
$this->save();
if($playlist) {
$playlist->incrementListens();
$playlist->save();
}
}
$entity->setLast_played_track($this->getId());
$entity->save();
return true;
}
$lastListen->update([
"time" => time(),
]);
return false;
}
/**
* Returns compatible with VK API 4.x, 5.x structure.
*
* Always sets album(_id) to NULL at this time.
* If genre is not present in VK genre list, fallbacks to "Other".
* The url and manifest properties will be set to false if the audio can't be played (processing, removed).
*
* Aside from standard VK properties, this method will also return some OVK extended props:
* 1. added - Is in the library of $user?
* 2. editable - Can be edited by $user?
* 3. withdrawn - Removed due to copyright request?
* 4. ready - Can be played at this time?
* 5. genre_str - Full name of genre, NULL if it's undefined
* 6. manifest - URL to MPEG-DASH manifest
* 7. keys - ClearKey DRM keys
* 8. explicit - Marked as NSFW?
* 9. searchable - Can be found via search?
* 10. unique_id - Unique ID of audio
*
* @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to nomusic.mp3,
* unless $forceURLExposure is set to true.
*
* @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models.
*
* @param ?User $user user, relative to whom "added", "editable" will be set
* @param bool $forceURLExposure force set "url" regardless of config
*/
function toVkApiStruct(?User $user = NULL, bool $forceURLExposure = false): object
{
$obj = (object) [];
$obj->unique_id = base64_encode((string) $this->getId());
$obj->id = $obj->aid = $this->getVirtualId();
$obj->artist = $this->getPerformer();
$obj->title = $this->getTitle();
$obj->duration = $this->getLength();
$obj->album_id = $obj->album = NULL; # i forgor to implement
$obj->url = false;
$obj->manifest = false;
$obj->keys = false;
$obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match
$obj->genre_str = $this->getGenre();
$obj->owner_id = $this->getOwner()->getId();
if($this->getOwner() instanceof Club)
$obj->owner_id *= -1;
$obj->lyrics = NULL;
if(!is_null($this->getLyrics()))
$obj->lyrics = $this->getId();
$obj->added = $user && $this->isInLibraryOf($user);
$obj->editable = $user && $this->canBeModifiedBy($user);
$obj->searchable = !$this->isUnlisted();
$obj->explicit = $this->isExplicit();
$obj->withdrawn = $this->isWithdrawn();
$obj->ready = $this->isAvailable() && !$obj->withdrawn;
if($obj->ready) {
$obj->url = $this->getOriginalURL($forceURLExposure);
$obj->manifest = $this->getURL();
$obj->keys = $this->getKeys();
}
return $obj;
}
function setOwner(int $oid): void
{
# WARNING: API implementation won't be able to handle groups like that, don't remove
if($oid <= 0)
throw new \OutOfRangeException("Only users can be owners of audio!");
$this->stateChanges("owner", $oid);
}
function setGenre(string $genre): void
{
if(!in_array($genre, Audio::genres)) {
$this->stateChanges("genre", NULL);
return;
}
$this->stateChanges("genre", $genre);
}
function setCopyrightStatus(bool $withdrawn = true): void {
$this->stateChanges("withdrawn", $withdrawn);
}
function setSearchability(bool $searchable = true): void {
$this->stateChanges("unlisted", !$searchable);
}
function setToken(string $tok): void {
throw new \LogicException("Changing keys is not supported.");
}
function setKid(string $kid): void {
throw new \LogicException("Changing keys is not supported.");
}
function setKey(string $key): void {
throw new \LogicException("Changing keys is not supported.");
}
function setLength(int $len): void {
throw new \LogicException("Changing length is not supported.");
}
function setSegment_Size(int $len): void {
throw new \LogicException("Changing length is not supported.");
}
function delete(bool $softly = true): void
{
$ctx = DatabaseConnection::i()->getContext();
$ctx->table("audio_relations")->where("audio", $this->getId())
->delete();
$ctx->table("audio_listens")->where("audio", $this->getId())
->delete();
$ctx->table("playlist_relations")->where("media", $this->getId())
->delete();
parent::delete($softly);
}
}

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Models\Entities;
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\{User, Manager}; use openvk\Web\Models\Entities\{User, Manager};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Managers}; use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Managers, Posts};
use Nette\Database\Table\{ActiveRow, GroupedSelection}; use Nette\Database\Table\{ActiveRow, GroupedSelection};
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
use Chandler\Security\User as ChandlerUser; use Chandler\Security\User as ChandlerUser;
@ -24,6 +24,10 @@ class Club extends RowModel
const SUBSCRIBED = 1; const SUBSCRIBED = 1;
const REQUEST_SENT = 2; const REQUEST_SENT = 2;
const WALL_CLOSED = 0;
const WALL_OPEN = 1;
const WALL_LIMITED = 2;
function getId(): int function getId(): int
{ {
return $this->getRecord()->id; return $this->getRecord()->id;
@ -46,6 +50,11 @@ class Club extends RowModel
return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size); return is_null($avPhoto) ? "$serverUrl/assets/packages/static/openvk/img/camera_200.png" : $avPhoto->getURLBySizeId($size);
} }
function getWallType(): int
{
return $this->getRecord()->wall;
}
function getAvatarLink(): string function getAvatarLink(): string
{ {
$avPhoto = $this->getAvatarPhoto(); $avPhoto = $this->getAvatarPhoto();
@ -183,6 +192,14 @@ class Club extends RowModel
return true; return true;
} }
function setWall(int $type)
{
if($type > 2 || $type < 0)
throw new \LogicException("Invalid wall");
$this->stateChanges("wall", $type);
}
function isSubscriptionAccepted(User $user): bool function isSubscriptionAccepted(User $user): bool
{ {
return !is_null($this->getRecord()->related("subscriptions.follower")->where([ return !is_null($this->getRecord()->related("subscriptions.follower")->where([
@ -224,7 +241,7 @@ class Club extends RowModel
"shape" => "spline", "shape" => "spline",
"color" => "#597da3", "color" => "#597da3",
], ],
"name" => $unique ? "Полный охват" : "Все просмотры", "name" => $unique ? tr("full_coverage") : tr("all_views"),
], ],
"subs" => [ "subs" => [
"x" => array_reverse(range(1, 7)), "x" => array_reverse(range(1, 7)),
@ -235,7 +252,7 @@ class Club extends RowModel
"color" => "#b05c91", "color" => "#b05c91",
], ],
"fill" => "tozeroy", "fill" => "tozeroy",
"name" => $unique ? "Охват подписчиков" : "Просмотры подписчиков", "name" => $unique ? tr("subs_coverage") : tr("subs_views"),
], ],
"viral" => [ "viral" => [
"x" => array_reverse(range(1, 7)), "x" => array_reverse(range(1, 7)),
@ -246,7 +263,7 @@ class Club extends RowModel
"color" => "#4d9fab", "color" => "#4d9fab",
], ],
"fill" => "tozeroy", "fill" => "tozeroy",
"name" => $unique ? "Виральный охват" : "Виральные просмотры", "name" => $unique ? tr("viral_coverage") : tr("viral_views"),
], ],
]; ];
} }
@ -292,6 +309,21 @@ class Club extends RowModel
} }
} }
function getSuggestedPostsCount(User $user = NULL)
{
$count = 0;
if(is_null($user))
return NULL;
if($this->canBeModifiedBy($user))
$count = (new Posts)->getSuggestedPostsCount($this->getId());
else
$count = (new Posts)->getSuggestedPostsCountByUser($this->getId(), $user->getId());
return $count;
}
function getManagers(int $page = 1, bool $ignoreHidden = false): \Traversable function getManagers(int $page = 1, bool $ignoreHidden = false): \Traversable
{ {
$rels = $this->getRecord()->related("group_coadmins.club")->page($page, 6); $rels = $this->getRecord()->related("group_coadmins.club")->page($page, 6);
@ -377,35 +409,59 @@ class Club extends RowModel
return $this->getRecord()->alert; return $this->getRecord()->alert;
} }
function getRealId(): int
{
return $this->getId() * -1;
}
function isEveryoneCanUploadAudios(): bool
{
return (bool) $this->getRecord()->everyone_can_upload_audios;
}
function canUploadAudio(?User $user): bool
{
if(!$user)
return NULL;
return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user);
}
function getAudiosCollectionSize()
{
return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this);
}
function toVkApiStruct(?User $user = NULL): object function toVkApiStruct(?User $user = NULL): object
{ {
$res = []; $res = (object) [];
$res->id = $this->getId(); $res->id = $this->getId();
$res->name = $this->getName(); $res->name = $this->getName();
$res->screen_name = $this->getShortCode(); $res->screen_name = $this->getShortCode();
$res->is_closed = 0; $res->is_closed = 0;
$res->deactivated = NULL; $res->deactivated = NULL;
$res->is_admin = $this->canBeModifiedBy($user); $res->is_admin = $user && $this->canBeModifiedBy($user);
if($this->canBeModifiedBy($user)) { if($user && $this->canBeModifiedBy($user)) {
$res->admin_level = 3; $res->admin_level = 3;
} }
$res->is_member = $this->getSubscriptionStatus($user) ? 1 : 0; $res->is_member = $user && $this->getSubscriptionStatus($user) ? 1 : 0;
$res->type = "group"; $res->type = "group";
$res->photo_50 = $this->getAvatarUrl("miniscule"); $res->photo_50 = $this->getAvatarUrl("miniscule");
$res->photo_100 = $this->getAvatarUrl("tiny"); $res->photo_100 = $this->getAvatarUrl("tiny");
$res->photo_200 = $this->getAvatarUrl("normal"); $res->photo_200 = $this->getAvatarUrl("normal");
$res->can_create_topic = $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); $res->can_create_topic = $user && $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0);
$res->can_post = $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); $res->can_post = $user && $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0);
return (object) $res; return $res;
} }
use Traits\TBackDrops; use Traits\TBackDrops;
use Traits\TSubscribable; use Traits\TSubscribable;
use Traits\TAudioStatuses;
} }

View file

@ -11,7 +11,7 @@ class Comment extends Post
function getPrettyId(): string function getPrettyId(): string
{ {
return $this->getRecord()->id; return (string)$this->getRecord()->id;
} }
function getVirtualId(): int function getVirtualId(): int
@ -75,7 +75,11 @@ class Comment extends Post
if($attachment->isDeleted()) if($attachment->isDeleted())
continue; continue;
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$res->attachments[] = $attachment->toVkApiStruct(); $res->attachments[] = $attachment->toVkApiStruct();
} else if($attachment instanceof \openvk\Web\Models\Entities\Video) {
$res->attachments[] = $attachment->toVkApiStruct($this->getUser());
}
} }
if($need_likes) { if($need_likes) {
@ -99,4 +103,25 @@ class Comment extends Post
return $this->getTarget()->canBeViewedBy($user); return $this->getTarget()->canBeViewedBy($user);
} }
function toNotifApiStruct()
{
$res = (object)[];
$res->id = $this->getId();
$res->owner_id = $this->getOwner()->getId();
$res->date = $this->getPublicationTime()->timestamp();
$res->text = $this->getText(false);
$res->post = NULL; # todo
return $res;
}
function canBeEditedBy(?User $user = NULL): bool
{
if(!$user)
return false;
return $user->getId() == $this->getOwner(false)->getId();
}
} }

View file

@ -131,7 +131,7 @@ class Correspondence
*/ */
function getPreviewMessage(): ?Message function getPreviewMessage(): ?Message
{ {
$messages = $this->getMessages(1, NULL, 1); $messages = $this->getMessages(1, NULL, 1, 0);
return $messages[0] ?? NULL; return $messages[0] ?? NULL;
} }

View file

@ -105,7 +105,7 @@ class IP extends RowModel
$this->stateChanges("ip", $ip); $this->stateChanges("ip", $ip);
} }
function save($log): void function save(?bool $log = false): void
{ {
if(is_null($this->getRecord())) if(is_null($this->getRecord()))
$this->stateChanges("first_seen", time()); $this->stateChanges("first_seen", time());

View file

@ -121,14 +121,14 @@ abstract class Media extends Postable
$this->stateChanges("hash", $hash); $this->stateChanges("hash", $hash);
} }
function save(): void function save(?bool $log = false): void
{ {
if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) { if(!is_null($this->processingPlaceholder) && is_null($this->getRecord())) {
$this->stateChanges("processed", 0); $this->stateChanges("processed", 0);
$this->stateChanges("last_checked", time()); $this->stateChanges("last_checked", time());
} }
parent::save(); parent::save($log);
} }
function delete(bool $softly = true): void function delete(bool $softly = true): void

View file

@ -17,7 +17,17 @@ abstract class MediaCollection extends RowModel
protected $specialNames = []; protected $specialNames = [];
private $relations; protected $relations;
/**
* Maximum amount of items Collection can have
*/
const MAX_ITEMS = INF;
/**
* Maximum amount of Collections with same "owner" allowed
*/
const MAX_COUNT = INF;
function __construct(?ActiveRow $ar = NULL) function __construct(?ActiveRow $ar = NULL)
{ {
@ -71,9 +81,12 @@ abstract class MediaCollection extends RowModel
abstract function getCoverURL(): ?string; abstract function getCoverURL(): ?string;
function fetch(int $page = 1, ?int $perPage = NULL): \Traversable function fetchClassic(int $offset = 0, ?int $limit = NULL): \Traversable
{ {
$related = $this->getRecord()->related("$this->relTableName.collection")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("media ASC"); $related = $this->getRecord()->related("$this->relTableName.collection")
->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset)
->order("media ASC");
foreach($related as $rel) { foreach($related as $rel) {
$media = $rel->ref($this->entityTableName, "media"); $media = $rel->ref($this->entityTableName, "media");
if(!$media) if(!$media)
@ -83,6 +96,14 @@ abstract class MediaCollection extends RowModel
} }
} }
function fetch(int $page = 1, ?int $perPage = NULL): \Traversable
{
$page = max(1, $page);
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
return $this->fetchClassic($perPage * ($page - 1), $perPage);
}
function size(): int function size(): int
{ {
return sizeof($this->getRecord()->related("$this->relTableName.collection")); return sizeof($this->getRecord()->related("$this->relTableName.collection"));
@ -119,6 +140,10 @@ abstract class MediaCollection extends RowModel
if($this->has($entity)) if($this->has($entity))
return false; return false;
if(self::MAX_ITEMS != INF)
if(sizeof($this->relations->where("collection", $this->getId())) > self::MAX_ITEMS)
throw new \OutOfBoundsException("Collection is full");
$this->relations->insert([ $this->relations->insert([
"collection" => $this->getId(), "collection" => $this->getId(),
"media" => $entity->getId(), "media" => $entity->getId(),
@ -127,14 +152,14 @@ abstract class MediaCollection extends RowModel
return true; return true;
} }
function remove(RowModel $entity): void function remove(RowModel $entity): bool
{ {
$this->entitySuitable($entity); $this->entitySuitable($entity);
$this->relations->where([ return $this->relations->where([
"collection" => $this->getId(), "collection" => $this->getId(),
"media" => $entity->getId(), "media" => $entity->getId(),
])->delete(); ])->delete() > 0;
} }
function has(RowModel $entity): bool function has(RowModel $entity): bool
@ -149,5 +174,32 @@ abstract class MediaCollection extends RowModel
return !is_null($rel); return !is_null($rel);
} }
function save(?bool $log = false): void
{
$thisTable = DatabaseConnection::i()->getContext()->table($this->tableName);
if(self::MAX_COUNT != INF)
if(isset($this->changes["owner"]))
if(sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT)
throw new \OutOfBoundsException("Maximum amount of collections");
if(is_null($this->getRecord()))
if(!isset($this->changes["created"]))
$this->stateChanges("created", time());
else
$this->stateChanges("edited", time());
parent::save($log);
}
function delete(bool $softly = true): void
{
if(!$softly) {
$this->relations->where("collection", $this->getId())
->delete();
}
parent::delete($softly);
}
use Traits\TOwnable; use Traits\TOwnable;
} }

View file

@ -123,7 +123,11 @@ class Message extends RowModel
], ],
]; ];
} else { } else {
throw new \Exception("Unknown attachment type: " . get_class($attachment)); $attachments[] = [
"type" => "unknown"
];
# throw new \Exception("Unknown attachment type: " . get_class($attachment));
} }
} }

View file

@ -133,14 +133,14 @@ class Note extends Postable
$res = (object) []; $res = (object) [];
$res->type = "note"; $res->type = "note";
$res->id = $this->getId(); $res->id = $this->getVirtualId();
$res->owner_id = $this->getOwner()->getId(); $res->owner_id = $this->getOwner()->getId();
$res->title = $this->getName(); $res->title = $this->getName();
$res->text = $this->getText(); $res->text = $this->getText();
$res->date = $this->getPublicationTime()->timestamp(); $res->date = $this->getPublicationTime()->timestamp();
$res->comments = $this->getCommentsCount(); $res->comments = $this->getCommentsCount();
$res->read_comments = $this->getCommentsCount(); $res->read_comments = $this->getCommentsCount();
$res->view_url = "/note".$this->getOwner()->getId()."_".$this->getId(); $res->view_url = "/note".$this->getOwner()->getId()."_".$this->getVirtualId();
$res->privacy_view = 1; $res->privacy_view = 1;
$res->can_comment = 1; $res->can_comment = 1;
$res->text_wiki = "r"; $res->text_wiki = "r";

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Notifications;
use openvk\Web\Models\Entities\{User, Club};
final class NewSuggestedPostsNotification extends Notification
{
protected $actionCode = 7;
function __construct(User $owner, Club $group)
{
parent::__construct($owner, $owner, $group, time(), "");
}
}

View file

@ -132,4 +132,138 @@ QUERY;
return true; return true;
} }
function getVkApiInfo()
{
$origin_m = $this->encodeType($this->originModel);
$target_m = $this->encodeType($this->targetModel);
$info = [
"type" => "",
"parent" => NULL,
"feedback" => NULL,
];
switch($this->getActionCode()) {
case 0:
$info["type"] = "like_post";
$info["parent"] = $this->getModel(0)->toNotifApiStruct();
$info["feedback"] = $this->getModel(1)->toVkApiStruct();
break;
case 1:
$info["type"] = "copy_post";
$info["parent"] = $this->getModel(0)->toNotifApiStruct();
$info["feedback"] = NULL; # todo
break;
case 2:
switch($origin_m) {
case 19:
$info["type"] = "comment_video";
$info["parent"] = $this->getModel(0)->toNotifApiStruct();
$info["feedback"] = NULL; # айди коммента не сохраняется в бд( ну пиздец блять
break;
case 13:
$info["type"] = "comment_photo";
$info["parent"] = $this->getModel(0)->toNotifApiStruct();
$info["feedback"] = NULL;
break;
# unstandart (vk forgor about notes)
case 10:
$info["type"] = "comment_note";
$info["parent"] = $this->getModel(0)->toVkApiStruct();
$info["feedback"] = NULL;
break;
case 14:
$info["type"] = "comment_post";
$info["parent"] = $this->getModel(0)->toNotifApiStruct();
$info["feedback"] = NULL;
break;
# unused (users don't have topics bruh)
case 21:
$info["type"] = "comment_topic";
$info["parent"] = $this->getModel(0)->toVkApiStruct(0, 90);
break;
default:
$info["type"] = "comment_unknown";
break;
}
break;
case 3:
$info["type"] = "wall";
$info["feedback"] = $this->getModel(0)->toNotifApiStruct();
break;
case 4:
switch($target_m) {
case 14:
$info["type"] = "mention";
$info["feedback"] = $this->getModel(1)->toNotifApiStruct();
break;
case 19:
$info["type"] = "mention_comment_video";
$info["parent"] = $this->getModel(1)->toNotifApiStruct();
break;
case 13:
$info["type"] = "mention_comment_photo";
$info["parent"] = $this->getModel(1)->toNotifApiStruct();
break;
# unstandart
case 10:
$info["type"] = "mention_comment_note";
$info["parent"] = $this->getModel(1)->toVkApiStruct();
break;
case 21:
$info["type"] = "mention_comments";
break;
default:
$info["type"] = "mention_comment_unknown";
break;
}
break;
case 5:
$info["type"] = "make_you_admin";
$info["parent"] = $this->getModel(0)->toVkApiStruct($this->getModel(1));
break;
# Нужно доделать после мержа #935
case 6:
$info["type"] = "wall_publish";
break;
# В вк не было такого уведомления, так что unstandart
case 7:
$info["type"] = "new_posts_in_club";
break;
# В вк при передаче подарков приходит сообщение, а не уведомление, так что unstandart
case 9601:
$info["type"] = "sent_gift";
$info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1));
break;
case 9602:
$info["type"] = "voices_transfer";
$info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1));
break;
case 9603:
$info["type"] = "up_rating";
$info["parent"] = $this->getModel(1)->toVkApiStruct($this->getModel(1));
$info["parent"]->count = $this->getData();
break;
default:
$info["type"] = NULL;
break;
}
return $info;
}
function toVkApiStruct()
{
$res = (object)[];
$info = $this->getVkApiInfo();
$res->type = $info["type"];
$res->date = $this->getDateTime()->timestamp();
$res->parent = $info["parent"];
$res->feedback = $info["feedback"];
$res->reply = NULL; # Ответы на комментарии не реализованы
return $res;
}
} }

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Notifications;
use openvk\Web\Models\Entities\{User, Club, Post};
final class PostAcceptedNotification extends Notification
{
protected $actionCode = 6;
function __construct(User $author, Post $post, Club $group)
{
parent::__construct($author, $post, $group, time(), "");
}
}

View file

@ -54,11 +54,11 @@ class PasswordReset extends RowModel
} }
} }
function save(): void function save(?bool $log = false): void
{ {
$this->stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46))); $this->stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46)));
$this->stateChanges("timestamp", time()); $this->stateChanges("timestamp", time());
parent::save(); parent::save($log);
} }
} }

View file

@ -360,4 +360,20 @@ class Photo extends Media
return $photo; return $photo;
} }
function toNotifApiStruct()
{
$res = (object)[];
$res->id = $this->getVirtualId();
$res->owner_id = $this->getOwner()->getId();
$res->aid = 0;
$res->src = $this->getURLBySizeId("tiny");
$res->src_big = $this->getURLBySizeId("normal");
$res->src_small = $this->getURLBySizeId("miniscule");
$res->text = $this->getDescription();
$res->created = $this->getPublicationTime()->timestamp();
return $res;
}
} }

View file

@ -0,0 +1,256 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection;
use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\Repositories\Audios;
use openvk\Web\Models\Repositories\Photos;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Photo;
/**
* @method setName(string $name)
* @method setDescription(?string $desc)
*/
class Playlist extends MediaCollection
{
protected $tableName = "playlists";
protected $relTableName = "playlist_relations";
protected $entityTableName = "audios";
protected $entityClassName = 'openvk\Web\Models\Entities\Audio';
protected $allowDuplicates = false;
private $importTable;
const MAX_COUNT = 1000;
const MAX_ITEMS = 10000;
function __construct(?ActiveRow $ar = NULL)
{
parent::__construct($ar);
$this->importTable = DatabaseConnection::i()->getContext()->table("playlist_imports");
}
function getCoverURL(string $size = "normal"): ?string
{
$photo = (new Photos)->get((int) $this->getRecord()->cover_photo_id);
return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURLBySizeId($size);
}
function getLength(): int
{
return $this->getRecord()->length;
}
function getAudios(int $offset = 0, ?int $limit = NULL, ?int $shuffleSeed = NULL): \Traversable
{
if(!$shuffleSeed) {
foreach ($this->fetchClassic($offset, $limit) as $e)
yield $e; # No, I can't return, it will break with []
return;
}
$ids = [];
foreach($this->relations->select("media AS i")->where("collection", $this->getId()) as $rel)
$ids[] = $rel->i;
$ids = knuth_shuffle($ids, $shuffleSeed);
$ids = array_slice($ids, $offset, $limit ?? OPENVK_DEFAULT_PER_PAGE);
foreach($ids as $id)
yield (new Audios)->get($id);
}
function add(RowModel $audio): bool
{
if($res = parent::add($audio)) {
$this->stateChanges("length", $this->getRecord()->length + $audio->getLength());
$this->save();
}
return $res;
}
function remove(RowModel $audio): bool
{
if($res = parent::remove($audio)) {
$this->stateChanges("length", $this->getRecord()->length - $audio->getLength());
$this->save();
}
return $res;
}
function isBookmarkedBy(RowModel $entity): bool
{
$id = $entity->getId();
if($entity instanceof Club)
$id *= -1;
return !is_null($this->importTable->where([
"entity" => $id,
"playlist" => $this->getId(),
])->fetch());
}
function bookmark(RowModel $entity): bool
{
if($this->isBookmarkedBy($entity))
return false;
$id = $entity->getId();
if($entity instanceof Club)
$id *= -1;
if($this->importTable->where("entity", $id)->count() > self::MAX_COUNT)
throw new \OutOfBoundsException("Maximum amount of playlists");
$this->importTable->insert([
"entity" => $id,
"playlist" => $this->getId(),
]);
return true;
}
function unbookmark(RowModel $entity): bool
{
$id = $entity->getId();
if($entity instanceof Club)
$id *= -1;
$count = $this->importTable->where([
"entity" => $id,
"playlist" => $this->getId(),
])->delete();
return $count > 0;
}
function getDescription(): ?string
{
return $this->getRecord()->description;
}
function getDescriptionHTML(): ?string
{
return htmlspecialchars($this->getRecord()->description, ENT_DISALLOWED | ENT_XHTML);
}
function getListens()
{
return $this->getRecord()->listens;
}
function toVkApiStruct(?User $user = NULL): object
{
$oid = $this->getOwner()->getId();
if($this->getOwner() instanceof Club)
$oid *= -1;
return (object) [
"id" => $this->getId(),
"owner_id" => $oid,
"title" => $this->getName(),
"description" => $this->getDescription(),
"size" => $this->size(),
"length" => $this->getLength(),
"created" => $this->getCreationTime()->timestamp(),
"modified" => $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL,
"accessible" => $this->canBeViewedBy($user),
"editable" => $this->canBeModifiedBy($user),
"bookmarked" => $this->isBookmarkedBy($user),
"listens" => $this->getListens(),
"cover_url" => $this->getCoverURL(),
];
}
function setLength(): void
{
throw new \LogicException("Can't set length of playlist manually");
}
function resetLength(): bool
{
$this->stateChanges("length", 0);
return true;
}
function delete(bool $softly = true): void
{
$ctx = DatabaseConnection::i()->getContext();
$ctx->table("playlist_imports")->where("playlist", $this->getId())
->delete();
parent::delete($softly);
}
function hasAudio(Audio $audio): bool
{
$ctx = DatabaseConnection::i()->getContext();
return !is_null($ctx->table("playlist_relations")->where([
"collection" => $this->getId(),
"media" => $audio->getId()
])->fetch());
}
function getCoverPhotoId(): ?int
{
return $this->getRecord()->cover_photo_id;
}
function canBeModifiedBy(User $user): bool
{
if(!$user)
return false;
if($this->getOwner() instanceof User)
return $user->getId() == $this->getOwner()->getId();
else
return $this->getOwner()->canBeModifiedBy($user);
}
function getLengthInMinutes(): int
{
return (int)round($this->getLength() / 60, PHP_ROUND_HALF_DOWN);
}
function fastMakeCover(int $owner, array $file)
{
$cover = new Photo;
$cover->setOwner($owner);
$cover->setDescription("Playlist cover image");
$cover->setFile($file);
$cover->setCreated(time());
$cover->save();
$this->setCover_photo_id($cover->getId());
return $cover;
}
function getURL(): string
{
return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId();
}
function incrementListens()
{
$this->stateChanges("listens", ($this->getListens() + 1));
}
function getMetaDescription(): string
{
$length = $this->getLengthInMinutes();
$props = [];
$props[] = tr("audios_count", $this->size());
$props[] = "<span id='listensCount'>" . tr("listens_count", $this->getListens()) . "</span>";
if($length > 0) $props[] = tr("minutes_count", $length);
$props[] = tr("created_playlist") . " " . $this->getPublicationTime();
# if($this->getEditTime()) $props[] = tr("updated_playlist") . " " . $this->getEditTime();
return implode("", $props);
}
}

View file

@ -4,7 +4,7 @@ use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Util\DateTime; use openvk\Web\Util\DateTime;
use \UnexpectedValueException; use \UnexpectedValueException;
use Nette\InvalidStateException; use Nette\InvalidStateException;
use openvk\Web\Models\Repositories\Users; use openvk\Web\Models\Repositories\{Users, Posts};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Exceptions\PollLockedException; use openvk\Web\Models\Exceptions\PollLockedException;
use openvk\Web\Models\Exceptions\AlreadyVotedException; use openvk\Web\Models\Exceptions\AlreadyVotedException;
@ -165,7 +165,7 @@ class Poll extends Attachable
function canVote(User $user): bool function canVote(User $user): bool
{ {
return !$this->hasEnded() && !$this->hasVoted($user); return !$this->hasEnded() && !$this->hasVoted($user) && !is_null($this->getAttachedPost()) && $this->getAttachedPost()->getSuggestionType() == 0;
} }
function vote(User $user, array $optionIds): void function vote(User $user, array $optionIds): void
@ -290,12 +290,12 @@ class Poll extends Attachable
} }
function save(): void function save(?bool $log = false): void
{ {
if(empty($this->choicesToPersist)) if(empty($this->choicesToPersist))
throw new InvalidStateException; throw new InvalidStateException;
parent::save(); parent::save($log);
foreach($this->choicesToPersist as $option) { foreach($this->choicesToPersist as $option) {
DatabaseConnection::i()->getContext()->table("poll_options")->insert([ DatabaseConnection::i()->getContext()->table("poll_options")->insert([
"poll" => $this->getId(), "poll" => $this->getId(),
@ -303,4 +303,17 @@ class Poll extends Attachable
]); ]);
} }
} }
function getAttachedPost()
{
$post = DatabaseConnection::i()->getContext()->table("attachments")
->where(
["attachable_type" => static::class,
"attachable_id" => $this->getId()])->fetch();
if(!is_null($post->target_id))
return (new Posts)->get($post->target_id);
else
return NULL;
}
} }

View file

@ -207,6 +207,9 @@ class Post extends Postable
function canBeDeletedBy(User $user): bool function canBeDeletedBy(User $user): bool
{ {
if($this->getTargetWall() < 0 && !$this->getWallOwner()->canBeModifiedBy($user) && $this->getWallOwner()->getWallType() != 1 && $this->getSuggestionType() == 0)
return false;
return $this->getOwnerPost() === $user->getId() || $this->canBePinnedBy($user); return $this->getOwnerPost() === $user->getId() || $this->canBePinnedBy($user);
} }
@ -255,5 +258,47 @@ class Post extends Postable
return $this->getWallOwner()->canBeViewedBy($user); return $this->getWallOwner()->canBeViewedBy($user);
} }
function getSuggestionType()
{
return $this->getRecord()->suggested;
}
function toNotifApiStruct()
{
$res = (object)[];
$res->id = $this->getVirtualId();
$res->to_id = $this->getOwner() instanceof Club ? $this->getOwner()->getId() * -1 : $this->getOwner()->getId();
$res->from_id = $res->to_id;
$res->date = $this->getPublicationTime()->timestamp();
$res->text = $this->getText(false);
$res->attachments = []; # todo
$res->copy_owner_id = NULL; # todo
$res->copy_post_id = NULL; # todo
return $res;
}
function canBeEditedBy(?User $user = NULL): bool
{
if(!$user)
return false;
if($this->isDeactivationMessage() || $this->isUpdateAvatarMessage())
return false;
if($this->getTargetWall() > 0)
return $this->getPublicationTime()->timestamp() + WEEK > time() && $user->getId() == $this->getOwner(false)->getId();
else {
if($this->isPostedOnBehalfOfGroup())
return $this->getWallOwner()->canBeModifiedBy($user);
else
return $user->getId() == $this->getOwner(false)->getId();
}
return $user->getId() == $this->getOwner(false)->getId();
}
use Traits\TRichText; use Traits\TRichText;
} }

View file

@ -33,7 +33,7 @@ abstract class Postable extends Attachable
{ {
$oid = (int) $this->getRecord()->owner; $oid = (int) $this->getRecord()->owner;
if(!$real && $this->isAnonymous()) if(!$real && $this->isAnonymous())
$oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; $oid = (int) OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"];
$oid = abs($oid); $oid = abs($oid);
if($oid > 0) if($oid > 0)
@ -88,13 +88,14 @@ abstract class Postable extends Attachable
])->group("origin")); ])->group("origin"));
} }
# TODO add pagination function getLikers(int $page = 1, ?int $perPage = NULL): \Traversable
function getLikers(): \Traversable
{ {
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
$sel = DB::i()->getContext()->table("likes")->where([ $sel = DB::i()->getContext()->table("likes")->where([
"model" => static::class, "model" => static::class,
"target" => $this->getRecord()->id, "target" => $this->getRecord()->id,
]); ])->page($page, $perPage);
foreach($sel as $like) foreach($sel as $like)
yield (new Users)->get($like->origin); yield (new Users)->get($like->origin);
@ -130,11 +131,16 @@ abstract class Postable extends Attachable
"target" => $this->getRecord()->id, "target" => $this->getRecord()->id,
]; ];
if($liked) if($liked) {
if(!$this->hasLikeFrom($user)) {
DB::i()->getContext()->table("likes")->insert($searchData); DB::i()->getContext()->table("likes")->insert($searchData);
else }
} else {
if($this->hasLikeFrom($user)) {
DB::i()->getContext()->table("likes")->where($searchData)->delete(); DB::i()->getContext()->table("likes")->where($searchData)->delete();
} }
}
}
function hasLikeFrom(User $user): bool function hasLikeFrom(User $user): bool
{ {
@ -152,7 +158,7 @@ abstract class Postable extends Attachable
throw new ISE("Setting virtual id manually is forbidden"); throw new ISE("Setting virtual id manually is forbidden");
} }
function save(): void function save(?bool $log = false): void
{ {
$vref = $this->upperNodeReferenceColumnName; $vref = $this->upperNodeReferenceColumnName;
@ -167,11 +173,11 @@ abstract class Postable extends Attachable
$this->stateChanges("created", time()); $this->stateChanges("created", time());
$this->stateChanges("virtual_id", $pCount + 1); $this->stateChanges("virtual_id", $pCount + 1);
} else { } /*else {
$this->stateChanges("edited", time()); $this->stateChanges("edited", time());
} }*/
parent::save(); parent::save($log);
} }
use Traits\TAttachmentHost; use Traits\TAttachmentHost;

View file

@ -5,7 +5,7 @@ use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\Club;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Users, Posts, Photos, Videos, Clubs}; use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Audios, Users, Posts, Photos, Videos, Clubs};
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
use Nette\Database\Table\Selection; use Nette\Database\Table\Selection;
@ -74,6 +74,7 @@ class Report extends RowModel
else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId());
else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId());
else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId());
else if ($this->getContentType() == "audio") return (new Audios)->get($this->getContentId());
else return null; else return null;
} }

View file

@ -1,6 +1,7 @@
<?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\Entities\Attachable; use openvk\Web\Models\Entities\{Attachable, Photo};
use openvk\Web\Util\Makima\Makima;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
trait TAttachmentHost trait TAttachmentHost
@ -30,6 +31,46 @@ trait TAttachmentHost
} }
} }
function getChildrenWithLayout(int $w, int $h = -1): object
{
if($h < 0)
$h = $w;
$children = $this->getChildren();
$skipped = $photos = $result = [];
foreach($children as $child) {
if($child instanceof Photo) {
$photos[] = $child;
continue;
}
$skipped[] = $child;
}
$height = "unset";
$width = $w;
if(sizeof($photos) < 2) {
if(isset($photos[0]))
$result[] = ["100%", "unset", $photos[0], "unset"];
} else {
$mak = new Makima($photos);
$layout = $mak->computeMasonryLayout($w, $h);
$height = $layout->height;
$width = $layout->width;
for($i = 0; $i < sizeof($photos); $i++) {
$tile = $layout->tiles[$i];
$result[] = [$tile->width . "px", $tile->height . "px", $photos[$i], "left"];
}
}
return (object) [
"width" => $width . "px",
"height" => $height . "px",
"tiles" => $result,
"extras" => $skipped,
];
}
function attach(Attachable $attachment): void function attach(Attachable $attachment): void
{ {
DatabaseConnection::i()->getContext() DatabaseConnection::i()->getContext()

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Traits;
use openvk\Web\Models\Repositories\Audios;
use Chandler\Database\DatabaseConnection;
trait TAudioStatuses
{
function isBroadcastEnabled(): bool
{
if($this->getRealId() < 0) return true;
return (bool) $this->getRecord()->audio_broadcast_enabled;
}
function getCurrentAudioStatus()
{
if(!$this->isBroadcastEnabled()) return NULL;
$audioId = $this->getRecord()->last_played_track;
if(!$audioId) return NULL;
$audio = (new Audios)->get($audioId);
if(!$audio || $audio->isDeleted())
return NULL;
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
$lastListen = $listensTable->where([
"entity" => $this->getRealId(),
"audio" => $audio->getId(),
"time >" => (time() - $audio->getLength()) - 10,
])->fetch();
if($lastListen)
return $audio;
return NULL;
}
}

View file

@ -4,7 +4,7 @@ use morphos\Gender;
use openvk\Web\Themes\{Themepack, Themepacks}; 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, Audio};
use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos}; use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos};
use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Models\Exceptions\InvalidUserNameException;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
@ -190,7 +190,7 @@ class User extends RowModel
function getMorphedName(string $case = "genitive", bool $fullName = true): string function getMorphedName(string $case = "genitive", bool $fullName = true): string
{ {
$name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName(); $name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName();
if(!preg_match("%^[А-яё\-]+$%", $name)) if(!preg_match("%[А-яё\-]+$%", $name))
return $name; # name is probably not russian return $name; # name is probably not russian
$inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE); $inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE);
@ -348,10 +348,11 @@ class User extends RowModel
return $this->getRecord()->marital_status; return $this->getRecord()->marital_status;
} }
function getLocalizedMaritalStatus(): string function getLocalizedMaritalStatus(?bool $prefix = false): string
{ {
$status = $this->getMaritalStatus(); $status = $this->getMaritalStatus();
$string = "relationship_$status"; $string = "relationship_$status";
if ($prefix) $string .= "_prefix";
if($this->isFemale()) { if($this->isFemale()) {
$res = tr($string . "_fem"); $res = tr($string . "_fem");
if($res != ("@" . $string . "_fem")) if($res != ("@" . $string . "_fem"))
@ -361,6 +362,17 @@ class User extends RowModel
return tr($string); return tr($string);
} }
function getMaritalStatusUser(): ?User
{
if (!$this->getRecord()->marital_status_user) return NULL;
return (new Users)->get($this->getRecord()->marital_status_user);
}
function getMaritalStatusUserPrefix(): ?string
{
return $this->getLocalizedMaritalStatus(true);
}
function getContactEmail(): ?string function getContactEmail(): ?string
{ {
return $this->getRecord()->email_contact; return $this->getRecord()->email_contact;
@ -455,6 +467,7 @@ class User extends RowModel
"length" => 1, "length" => 1,
"mappings" => [ "mappings" => [
"photos", "photos",
"audios",
"videos", "videos",
"messages", "messages",
"notes", "notes",
@ -462,6 +475,7 @@ class User extends RowModel
"news", "news",
"links", "links",
"poster", "poster",
"apps",
], ],
])->get($id); ])->get($id);
} }
@ -481,6 +495,7 @@ class User extends RowModel
"friends.add", "friends.add",
"wall.write", "wall.write",
"messages.write", "messages.write",
"audios.read",
], ],
])->get($id); ])->get($id);
} }
@ -719,8 +734,8 @@ class User extends RowModel
for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) { for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) {
$codes[] = [ $codes[] = [
owner => $this->getId(), "owner" => $this->getId(),
code => random_int(10000000, 99999999) "code" => random_int(10000000, 99999999)
]; ];
} }
@ -1009,6 +1024,7 @@ class User extends RowModel
"friends.add", "friends.add",
"wall.write", "wall.write",
"messages.write", "messages.write",
"audios.read",
], ],
])->set($id, $status)->toInteger()); ])->set($id, $status)->toInteger());
} }
@ -1019,6 +1035,7 @@ class User extends RowModel
"length" => 1, "length" => 1,
"mappings" => [ "mappings" => [
"photos", "photos",
"audios",
"videos", "videos",
"messages", "messages",
"notes", "notes",
@ -1026,6 +1043,7 @@ class User extends RowModel
"news", "news",
"links", "links",
"poster", "poster",
"apps",
], ],
])->set($id, (int) $status)->toInteger(); ])->set($id, (int) $status)->toInteger();
@ -1268,6 +1286,11 @@ class User extends RowModel
return (bool) $this->getProfileType(); return (bool) $this->getProfileType();
} }
function getRealId()
{
return $this->getId();
}
function toVkApiStruct(?User $user = NULL): object function toVkApiStruct(?User $user = NULL): object
{ {
$res = (object) []; $res = (object) [];
@ -1291,6 +1314,46 @@ class User extends RowModel
return $res; return $res;
} }
function getAudiosCollectionSize()
{
return (new \openvk\Web\Models\Repositories\Audios)->getUserCollectionSize($this);
}
function getBroadcastList(string $filter = "friends", bool $shuffle = false)
{
$dbContext = DatabaseConnection::i()->getContext();
$entityIds = [];
$query = $dbContext->table("subscriptions")->where("follower", $this->getRealId());
if($filter != "all")
$query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User"));
foreach($query as $_rel) {
$entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target;
}
if($shuffle) {
$shuffleSeed = openssl_random_pseudo_bytes(6);
$shuffleSeed = hexdec(bin2hex($shuffleSeed));
$entityIds = knuth_shuffle($entityIds, $shuffleSeed);
}
$entityIds = array_slice($entityIds, 0, 10);
$returnArr = [];
foreach($entityIds as $id) {
$entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id));
if($id > 0 && $entit->isDeleted()) continue;
$returnArr[] = $entit;
}
return $returnArr;
}
use Traits\TBackDrops; use Traits\TBackDrops;
use Traits\TSubscribable; use Traits\TSubscribable;
use Traits\TAudioStatuses;
} }

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 openvk\Web\Util\Shell\Shell; use openvk\Web\Util\Shell\Shell;
use openvk\Web\Util\Shell\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException}; use openvk\Web\Util\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException};
use openvk\Web\Models\VideoDrivers\VideoDriver; use openvk\Web\Models\VideoDrivers\VideoDriver;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
@ -115,15 +115,15 @@ class Video extends Media
return $this->getRecord()->owner; return $this->getRecord()->owner;
} }
function getApiStructure(): object function getApiStructure(?User $user = NULL): object
{ {
$fromYoutube = $this->getType() == Video::TYPE_EMBED; $fromYoutube = $this->getType() == Video::TYPE_EMBED;
return (object)[ $res = (object)[
"type" => "video", "type" => "video",
"video" => [ "video" => [
"can_comment" => 1, "can_comment" => 1,
"can_like" => 0, // we don't h-have wikes in videos "can_like" => 1, // we don't h-have wikes in videos
"can_repost" => 0, "can_repost" => 1,
"can_subscribe" => 1, "can_subscribe" => 1,
"can_add_to_faves" => 0, "can_add_to_faves" => 0,
"can_add" => 0, "can_add" => 0,
@ -155,21 +155,26 @@ class Video extends Media
"repeat" => 0, "repeat" => 0,
"type" => "video", "type" => "video",
"views" => 0, "views" => 0,
"likes" => [
"count" => 0,
"user_likes" => 0
],
"reposts" => [ "reposts" => [
"count" => 0, "count" => 0,
"user_reposted" => 0 "user_reposted" => 0
] ]
] ]
]; ];
if(!is_null($user)) {
$res->video["likes"] = [
"count" => $this->getLikesCount(),
"user_likes" => $this->hasLikeFrom($user)
];
} }
function toVkApiStruct(): object return $res;
}
function toVkApiStruct(?User $user): object
{ {
return $this->getApiStructure(); return $this->getApiStructure($user);
} }
function setLink(string $link): string function setLink(string $link): string
@ -233,4 +238,23 @@ class Video extends Media
return $this->getOwner()->canBeViewedBy($user); return $this->getOwner()->canBeViewedBy($user);
} }
} }
function toNotifApiStruct()
{
$fromYoutube = $this->getType() == Video::TYPE_EMBED;
$res = (object)[];
$res->id = $this->getVirtualId();
$res->owner_id = $this->getOwner()->getId();
$res->title = $this->getName();
$res->description = $this->getDescription();
$res->duration = "22";
$res->link = "/video".$this->getOwner()->getId()."_".$this->getVirtualId();
$res->image = $this->getThumbnailURL();
$res->date = $this->getPublicationTime()->timestamp();
$res->views = 0;
$res->player = !$fromYoutube ? $this->getURL() : $this->getVideoDriver()->getURL();
return $res;
}
} }

View file

@ -131,6 +131,6 @@ class Albums
"id" => $id "id" => $id
])->fetch(); ])->fetch();
return new Album($album); return $album ? new Album($album) : NULL;
} }
} }

View file

@ -0,0 +1,296 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Audio;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Entities\Playlist;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Util\EntityStream;
class Audios
{
private $context;
private $audios;
private $rels;
private $playlists;
private $playlistImports;
private $playlistRels;
const ORDER_NEW = 0;
const ORDER_POPULAR = 1;
const VK_ORDER_NEW = 0;
const VK_ORDER_LENGTH = 1;
const VK_ORDER_POPULAR = 2;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->audios = $this->context->table("audios");
$this->rels = $this->context->table("audio_relations");
$this->playlists = $this->context->table("playlists");
$this->playlistImports = $this->context->table("playlist_imports");
$this->playlistRels = $this->context->table("playlist_relations");
}
function get(int $id): ?Audio
{
$audio = $this->audios->get($id);
if(!$audio)
return NULL;
return new Audio($audio);
}
function getPlaylist(int $id): ?Playlist
{
$playlist = $this->playlists->get($id);
if(!$playlist)
return NULL;
return new Playlist($playlist);
}
function getByOwnerAndVID(int $owner, int $vId): ?Audio
{
$audio = $this->audios->where([
"owner" => $owner,
"virtual_id" => $vId,
])->fetch();
if(!$audio) return NULL;
return new Audio($audio);
}
function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist
{
$playlist = $this->playlists->where([
"owner" => $owner,
"id" => $vId,
])->fetch();
if(!$playlist) return NULL;
return new Playlist($playlist);
}
function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable
{
$limit ??= OPENVK_DEFAULT_PER_PAGE;
$iter = $this->rels->where("entity", $entity)->limit($limit, $offset)->order("index DESC");
foreach($iter as $rel) {
$audio = $this->get($rel->audio);
if(!$audio || $audio->isDeleted()) {
$deleted++;
continue;
}
yield $audio;
}
}
function getPlaylistsByEntityId(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable
{
$limit ??= OPENVK_DEFAULT_PER_PAGE;
$iter = $this->playlistImports->where("entity", $entity)->limit($limit, $offset);
foreach($iter as $rel) {
$playlist = $this->getPlaylist($rel->playlist);
if(!$playlist || $playlist->isDeleted()) {
$deleted++;
continue;
}
yield $playlist;
}
}
function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
{
return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted);
}
function getRandomThreeAudiosByEntityId(int $id): Array
{
$iter = $this->rels->where("entity", $id);
$ids = [];
foreach($iter as $it)
$ids[] = $it->audio;
$shuffleSeed = openssl_random_pseudo_bytes(6);
$shuffleSeed = hexdec(bin2hex($shuffleSeed));
$ids = knuth_shuffle($ids, $shuffleSeed);
$ids = array_slice($ids, 0, 3);
$audios = [];
foreach($ids as $id) {
$audio = $this->get((int)$id);
if(!$audio || $audio->isDeleted())
continue;
$audios[] = $audio;
}
return $audios;
}
function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
{
return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted);
}
function getPlaylistsByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
{
return $this->getPlaylistsByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted);
}
function getPlaylistsByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
{
return $this->getPlaylistsByEntityId($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted);
}
function getCollectionSizeByEntityId(int $id): int
{
return sizeof($this->rels->where("entity", $id));
}
function getUserCollectionSize(User $user): int
{
return sizeof($this->rels->where("entity", $user->getId()));
}
function getClubCollectionSize(Club $club): int
{
return sizeof($this->rels->where("entity", $club->getId() * -1));
}
function getUserPlaylistsCount(User $user): int
{
return sizeof($this->playlistImports->where("entity", $user->getId()));
}
function getClubPlaylistsCount(Club $club): int
{
return sizeof($this->playlistImports->where("entity", $club->getId() * -1));
}
function getByUploader(User $user): EntityStream
{
$search = $this->audios->where([
"owner" => $user->getId(),
"deleted" => 0,
]);
return new EntityStream("Audio", $search);
}
function getGlobal(int $order, ?string $genreId = NULL): EntityStream
{
$search = $this->audios->where([
"deleted" => 0,
"unlisted" => 0,
"withdrawn" => 0,
])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC");
if(!is_null($genreId))
$search = $search->where("genre", $genreId);
return new EntityStream("Audio", $search);
}
function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream
{
$columns = $performerOnly ? "performer" : "performer, name";
$order = (["created", "length", "listens"][$sortMode] ?? "") . " DESC";
$search = $this->audios->where([
"unlisted" => 0,
"deleted" => 0,
])->where("MATCH ($columns) AGAINST (? WITH QUERY EXPANSION)", $query)->order($order);
if($withLyrics)
$search = $search->where("lyrics IS NOT NULL");
return new EntityStream("Audio", $search);
}
function searchPlaylists(string $query): EntityStream
{
$search = $this->playlists->where([
"deleted" => 0,
])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query);
return new EntityStream("Playlist", $search);
}
function getNew(): EntityStream
{
return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25));
}
function getPopular(): EntityStream
{
return new EntityStream("Audio", $this->audios->where("listens > 0")->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("listens DESC")->limit(25));
}
function isAdded(int $user_id, int $audio_id): bool
{
return !is_null($this->rels->where([
"entity" => $user_id,
"audio" => $audio_id
])->fetch());
}
function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable
{
$query = "%$query%";
$result = $this->audios->where([
"unlisted" => 0,
"deleted" => 0,
]);
$notNullParams = [];
foreach($pars as $paramName => $paramValue)
if($paramName != "before" && $paramName != "after" && $paramName != "only_performers")
$paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL;
else
$paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL;
$nnparamsCount = sizeof($notNullParams);
if($notNullParams["only_performers"] == "1") {
$result->where("performer LIKE ?", $query);
} else {
$result->where("name LIKE ? OR performer LIKE ?", $query, $query);
}
if($nnparamsCount > 0) {
foreach($notNullParams as $paramName => $paramValue) {
switch($paramName) {
case "before":
$result->where("created < ?", $paramValue);
break;
case "after":
$result->where("created > ?", $paramValue);
break;
case "with_lyrics":
$result->where("lyrics IS NOT NULL");
break;
}
}
}
return new Util\EntityStream("Audio", $result->order($sort));
}
function findPlaylists(string $query, int $page = 1, ?int $perPage = NULL): \Traversable
{
$query = "%$query%";
$result = $this->playlists->where("name LIKE ?", $query);
return new Util\EntityStream("Playlist", $result);
}
}

View file

@ -45,4 +45,9 @@ class ChandlerGroups
{ {
foreach($this->perms->where("group", $UUID) as $perm) yield $perm; foreach($this->perms->where("group", $UUID) as $perm) yield $perm;
} }
function isUserAMember(string $GID, string $UID): bool
{
return ($this->context->query("SELECT * FROM `ChandlerACLRelations` WHERE `group` = ? AND `user` = ?", $GID, $UID)) !== NULL;
}
} }

View file

@ -42,4 +42,10 @@ class Gifts
foreach($cats as $cat) foreach($cats as $cat)
yield new GiftCategory($cat); yield new GiftCategory($cat);
} }
function getCategoriesCount(): int
{
$cats = $this->cats->where("deleted", false);
return $cats->count();
}
} }

View file

@ -52,7 +52,6 @@ class Messages
$query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql"); $query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql");
DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql")); DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql"));
$count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt; $count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt;
bdump($count);
return $count; return $count;
} }
} }

View file

@ -33,14 +33,26 @@ class Photos
return new Photo($photo); return new Photo($photo);
} }
function getEveryUserPhoto(User $user): \Traversable function getEveryUserPhoto(User $user, int $page = 1, ?int $perPage = NULL): \Traversable
{ {
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$photos = $this->photos->where([ $photos = $this->photos->where([
"owner" => $user->getId() "owner" => $user->getId(),
]); "deleted" => 0
])->order("id DESC");
foreach($photos as $photo) { foreach($photos->page($page, $perPage) as $photo) {
yield new Photo($photo); yield new Photo($photo);
} }
} }
function getUserPhotosCount(User $user)
{
$photos = $this->photos->where([
"owner" => $user->getId(),
"deleted" => 0
]);
return sizeof($photos);
}
} }

View file

@ -61,12 +61,57 @@ class Posts
"wall" => $user, "wall" => $user,
"pinned" => false, "pinned" => false,
"deleted" => false, "deleted" => false,
"suggested" => 0,
])->order("created DESC")->limit($perPage, $offset); ])->order("created DESC")->limit($perPage, $offset);
foreach($sel as $post) foreach($sel as $post)
yield new Post($post); yield new Post($post);
} }
function getOwnersPostsFromWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable
{
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
$offset ??= $perPage * ($page - 1);
$sel = $this->posts->where([
"wall" => $user,
"deleted" => false,
"suggested" => 0,
]);
if($user > 0)
$sel->where("owner", $user);
else
$sel->where("flags !=", 0);
$sel->order("created DESC")->limit($perPage, $offset);
foreach($sel as $post)
yield new Post($post);
}
function getOthersPostsFromWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable
{
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
$offset ??= $perPage * ($page - 1);
$sel = $this->posts->where([
"wall" => $user,
"deleted" => false,
"suggested" => 0,
]);
if($user > 0)
$sel->where("owner !=", $user);
else
$sel->where("flags", 0);
$sel->order("created DESC")->limit($perPage, $offset);
foreach($sel as $post)
yield new Post($post);
}
function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = NULL): \Traversable function getPostsByHashtag(string $hashtag, int $page = 1, ?int $perPage = NULL): \Traversable
{ {
$hashtag = "#$hashtag"; $hashtag = "#$hashtag";
@ -74,6 +119,7 @@ class Posts
->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag") ->where("MATCH (content) AGAINST (? IN BOOLEAN MODE)", "+$hashtag")
->where("deleted", 0) ->where("deleted", 0)
->order("created DESC") ->order("created DESC")
->where("suggested", 0)
->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE); ->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
foreach($sel as $post) foreach($sel as $post)
@ -85,14 +131,22 @@ class Posts
$hashtag = "#$hashtag"; $hashtag = "#$hashtag";
$sel = $this->posts $sel = $this->posts
->where("content LIKE ?", "%$hashtag%") ->where("content LIKE ?", "%$hashtag%")
->where("deleted", 0); ->where("deleted", 0)
->where("suggested", 0);
return sizeof($sel); return sizeof($sel);
} }
function getPostById(int $wall, int $post): ?Post function getPostById(int $wall, int $post, bool $forceSuggestion = false): ?Post
{ {
$post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post])->fetch(); $post = $this->posts->where(['wall' => $wall, 'virtual_id' => $post]);
if(!$forceSuggestion) {
$post->where("suggested", 0);
}
$post = $post->fetch();
if(!is_null($post)) if(!is_null($post))
return new Post($post); return new Post($post);
else else
@ -112,7 +166,7 @@ class Posts
else else
$paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL;
$result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0); $result = $this->posts->where("content LIKE ?", $query)->where("deleted", 0)->where("suggested", 0);
$nnparamsCount = sizeof($notNullParams); $nnparamsCount = sizeof($notNullParams);
if($nnparamsCount > 0) { if($nnparamsCount > 0) {
@ -134,7 +188,66 @@ class Posts
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, "suggested" => 0]));
}
function getOwnersCountOnUserWall(int $user): int
{
if($user > 0)
return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "owner" => $user]));
else
return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags !=", 0));
}
function getOthersCountOnUserWall(int $user): int
{
if($user > 0)
return sizeof($this->posts->where(["wall" => $user, "deleted" => 0])->where("owner !=", $user));
else
return sizeof($this->posts->where(["wall" => $user, "deleted" => 0, "suggested" => 0])->where("flags", 0));
}
function getSuggestedPosts(int $club, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable
{
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
$offset ??= $perPage * ($page - 1);
$sel = $this->posts
->where("deleted", 0)
->where("wall", $club * -1)
->order("created DESC")
->where("suggested", 1)
->limit($perPage, $offset);
foreach($sel as $post)
yield new Post($post);
}
function getSuggestedPostsCount(int $club)
{
return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1]));
}
function getSuggestedPostsByUser(int $club, int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable
{
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
$offset ??= $perPage * ($page - 1);
$sel = $this->posts
->where("deleted", 0)
->where("wall", $club * -1)
->where("owner", $user)
->order("created DESC")
->where("suggested", 1)
->limit($perPage, $offset);
foreach($sel as $post)
yield new Post($post);
}
function getSuggestedPostsCountByUser(int $club, int $user): int
{
return sizeof($this->posts->where(["wall" => $club * -1, "deleted" => 0, "suggested" => 1, "owner" => $user]));
} }
function getCount(): int function getCount(): int

View file

@ -77,4 +77,11 @@ class Videos
return new Util\EntityStream("Video", $result->order("$sort")); return new Util\EntityStream("Video", $result->order("$sort"));
} }
function getLastVideo(User $user)
{
$video = $this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->order("id DESC")->fetch();
return new Video($video);
}
} }

View file

@ -0,0 +1,39 @@
$ovkRoot = $args[0]
$storageDir = $args[1]
$fileHash = $args[2]
$hashPart = $fileHash.substring(0, 2)
$filename = $args[3]
$audioFile = [System.IO.Path]::GetTempFileName()
$temp = [System.IO.Path]::GetTempFileName()
$keyID = $args[4]
$key = $args[5]
$token = $args[6]
$seg = $args[7]
$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID"
$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree
Remove-Item $temp
Remove-Item $audioFile
New-Item -ItemType "directory" $temp
New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments')
New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments')
Set-Location -Path $temp
Move-Item $filename $audioFile
ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key `
-encryption_kid $keyID -map 0:a -vn -c:a aac -ar 44100 -seg_duration $seg `
-use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') `
-media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' `
"$fileHash.mpd"
ffmpeg -i $audioFile -vn -ar 44100 "original_$token.mp3"
Move-Item "original_$token.mp3" ($fileHash + '_fragments')
Get-ChildItem -Path ($fileHash + '_fragments/*') | Move-Item -Destination ("$storageDir/$hashPart/$fileHash" + '_fragments')
Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart"
cd ..
Remove-Item -Recurse $temp
Remove-Item $audioFile

View file

@ -0,0 +1,35 @@
ovkRoot=$1
storageDir=$2
fileHash=$3
hashPart=$(echo $fileHash | cut -c1-2)
filename=$4
audioFile=$(mktemp)
temp=$(mktemp -d)
keyID=$5
key=$6
token=$7
seg=$8
trap 'rm -f "$temp" "$audioFile"' EXIT
mkdir -p "$temp/$fileHash"_fragments
mkdir -p "$storageDir/$hashPart/$fileHash"_fragments
cd "$temp"
mv "$filename" "$audioFile"
ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \
-encryption_kid "$keyID" -map 0 -vn -c:a aac -ar 44100 -seg_duration "$seg" \
-use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \
-media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \
"$fileHash.mpd"
ffmpeg -i "$audioFile" -vn -ar 44100 "original_$token.mp3"
mv "original_$token.mp3" "$fileHash"_fragments
mv "$fileHash"_fragments "$storageDir/$hashPart"
mv "$fileHash.mpd" "$storageDir/$hashPart"
cd ..
rm -rf "$temp"
rm -f "$audioFile"

View file

@ -109,6 +109,10 @@ final class AboutPresenter extends OpenVKPresenter
. "# lack of rights to access the admin panel)\n\n" . "# lack of rights to access the admin panel)\n\n"
. "User-Agent: *\n" . "User-Agent: *\n"
. "Disallow: /albums/create\n" . "Disallow: /albums/create\n"
. "Disallow: /assets/packages/static/openvk/img/banned.jpg\n"
. "Disallow: /assets/packages/static/openvk/img/camera_200.png\n"
. "Disallow: /assets/packages/static/openvk/img/flags/\n"
. "Disallow: /assets/packages/static/openvk/img/oof.apng\n"
. "Disallow: /videos/upload\n" . "Disallow: /videos/upload\n"
. "Disallow: /invite\n" . "Disallow: /invite\n"
. "Disallow: /groups_create\n" . "Disallow: /groups_create\n"

View file

@ -3,7 +3,19 @@ namespace openvk\Web\Presenters;
use Chandler\Database\Log; use Chandler\Database\Log;
use Chandler\Database\Logs; use Chandler\Database\Logs;
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\{Bans, ChandlerGroups, ChandlerUsers, Photos, Posts, Users, Clubs, Videos, Vouchers, Gifts, BannedLinks}; use openvk\Web\Models\Repositories\{Audios,
ChandlerGroups,
ChandlerUsers,
Users,
Clubs,
Util\EntityStream,
Vouchers,
Gifts,
BannedLinks,
Bans,
Photos,
Posts,
Videos};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter final class AdminPresenter extends OpenVKPresenter
@ -14,9 +26,10 @@ final class AdminPresenter extends OpenVKPresenter
private $gifts; private $gifts;
private $bannedLinks; private $bannedLinks;
private $chandlerGroups; private $chandlerGroups;
private $audios;
private $logs; private $logs;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups) function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups, Audios $audios)
{ {
$this->users = $users; $this->users = $users;
$this->clubs = $clubs; $this->clubs = $clubs;
@ -24,6 +37,7 @@ final class AdminPresenter extends OpenVKPresenter
$this->gifts = $gifts; $this->gifts = $gifts;
$this->bannedLinks = $bannedLinks; $this->bannedLinks = $bannedLinks;
$this->chandlerGroups = $chandlerGroups; $this->chandlerGroups = $chandlerGroups;
$this->audios = $audios;
$this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs"); $this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs");
parent::__construct(); parent::__construct();
@ -44,6 +58,15 @@ final class AdminPresenter extends OpenVKPresenter
return $repo->find($query)->page($page, 20); return $repo->find($query)->page($page, 20);
} }
private function searchPlaylists(&$count)
{
$query = $this->queryParam("q") ?? "";
$page = (int) ($this->queryParam("p") ?? 1);
$count = $this->audios->findPlaylists($query)->size();
return $this->audios->findPlaylists($query)->page($page, 20);
}
function onStartup(): void function onStartup(): void
{ {
parent::onStartup(); parent::onStartup();
@ -87,9 +110,11 @@ final class AdminPresenter extends OpenVKPresenter
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")) { if($this->postParam("add-to-group")) {
if (!(new ChandlerGroups)->isUserAMember($user->getChandlerGUID(), $this->postParam("add-to-group"))) {
$query = "INSERT INTO `ChandlerACLRelations` (`user`, `group`) VALUES ('" . $user->getChandlerGUID() . "', '" . $this->postParam("add-to-group") . "')"; $query = "INSERT INTO `ChandlerACLRelations` (`user`, `group`) VALUES ('" . $user->getChandlerGUID() . "', '" . $this->postParam("add-to-group") . "')";
DatabaseConnection::i()->getConnection()->query($query); DatabaseConnection::i()->getConnection()->query($query);
} }
}
if($this->postParam("password")) { if($this->postParam("password")) {
$user->getChandlerUser()->updatePassword($this->postParam("password")); $user->getChandlerUser()->updatePassword($this->postParam("password"));
} }
@ -283,7 +308,7 @@ final class AdminPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
$gift->delete(); $gift->delete();
$this->flashFail("succ", "Gift moved successfully", "This gift will now be in <b>Recycle Bin</b>."); $this->flashFail("succ", tr("admin_gift_moved_successfully"), tr("admin_gift_moved_to_recycle"));
break; break;
case "copy": case "copy":
case "move": case "move":
@ -302,7 +327,7 @@ final class AdminPresenter extends OpenVKPresenter
$catTo->addGift($gift); $catTo->addGift($gift);
$name = $catTo->getName(); $name = $catTo->getName();
$this->flash("succ", "Gift moved successfully", "This gift will now be in <b>$name</b>."); $this->flash("succ", tr("admin_gift_moved_successfully"), "This gift will now be in <b>$name</b>.");
$this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/"); $this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/");
break; break;
default: default:
@ -333,10 +358,10 @@ final class AdminPresenter extends OpenVKPresenter
$gift->setUsages((int) $this->postParam("usages")); $gift->setUsages((int) $this->postParam("usages"));
if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) { if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) {
if(!$gift->setImage($_FILES["pic"]["tmp_name"])) if(!$gift->setImage($_FILES["pic"]["tmp_name"]))
$this->flashFail("err", "Не удалось сохранить подарок", "Изображение подарка кривое."); $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_bad_image"));
} else if($gen) { } else if($gen) {
# If there's no gift pic but it's newly created # If there's no gift pic but it's newly created
$this->flashFail("err", "Не удалось сохранить подарок", "Пожалуйста, загрузите изображение подарка."); $this->flashFail("err", tr("error_when_saving_gift"), tr("error_when_saving_gift_no_image"));
} }
$gift->save(); $gift->save();
@ -363,7 +388,7 @@ final class AdminPresenter extends OpenVKPresenter
if (str_contains($this->queryParam("reason"), "*")) if (str_contains($this->queryParam("reason"), "*"))
exit(json_encode([ "error" => "Incorrect reason" ])); exit(json_encode([ "error" => "Incorrect reason" ]));
$unban_time = strtotime($this->queryParam("date")) ?: NULL; $unban_time = strtotime($this->queryParam("date")) ?: "permanent";
$user = $this->users->get($id); $user = $this->users->get($id);
if(!$user) if(!$user)
@ -578,6 +603,54 @@ final class AdminPresenter extends OpenVKPresenter
$this->redirect("/admin/users/id" . $user->getId()); $this->redirect("/admin/users/id" . $user->getId());
} }
function renderMusic(): void
{
$this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios";
if ($this->template->mode === "audios")
$this->template->audios = $this->searchResults($this->audios, $this->template->count);
else
$this->template->playlists = $this->searchPlaylists($this->template->count);
}
function renderEditMusic(int $audio_id): void
{
$audio = $this->audios->get($audio_id);
$this->template->audio = $audio;
try {
$this->template->owner = $audio->getOwner()->getId();
} catch(\Throwable $e) {
$this->template->owner = 1;
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$audio->setName($this->postParam("name"));
$audio->setPerformer($this->postParam("performer"));
$audio->setLyrics($this->postParam("text"));
$audio->setGenre($this->postParam("genre"));
$audio->setOwner((int) $this->postParam("owner"));
$audio->setExplicit(!empty($this->postParam("explicit")));
$audio->setDeleted(!empty($this->postParam("deleted")));
$audio->setWithdrawn(!empty($this->postParam("withdrawn")));
$audio->save();
}
}
function renderEditPlaylist(int $playlist_id): void
{
$playlist = $this->audios->getPlaylist($playlist_id);
$this->template->playlist = $playlist;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$playlist->setName($this->postParam("name"));
$playlist->setDescription($this->postParam("description"));
$playlist->setCover_Photo_Id((int) $this->postParam("photo"));
$playlist->setOwner((int) $this->postParam("owner"));
$playlist->setDeleted(!empty($this->postParam("deleted")));
$playlist->save();
}
}
function renderLogs(): void function renderLogs(): void
{ {
$filter = []; $filter = [];

View file

@ -0,0 +1,696 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Audio;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Entities\Playlist;
use openvk\Web\Models\Repositories\Audios;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Users;
final class AudioPresenter extends OpenVKPresenter
{
private $audios;
protected $presenterName = "audios";
const MAX_AUDIO_SIZE = 25000000;
function __construct(Audios $audios)
{
$this->audios = $audios;
}
function renderPopular(): void
{
$this->renderList(NULL, "popular");
}
function renderNew(): void
{
$this->renderList(NULL, "new");
}
function renderList(?int $owner = NULL, ?string $mode = "list"): void
{
$this->template->_template = "Audio/List.xml";
$page = (int)($this->queryParam("p") ?? 1);
$audios = [];
if ($mode === "list") {
$entity = NULL;
if ($owner < 0) {
$entity = (new Clubs)->get($owner * -1);
if (!$entity || $entity->isBanned())
$this->redirect("/audios" . $this->user->id);
$audios = $this->audios->getByClub($entity, $page, 10);
$audiosCount = $this->audios->getClubCollectionSize($entity);
} else {
$entity = (new Users)->get($owner);
if (!$entity || $entity->isDeleted() || $entity->isBanned())
$this->redirect("/audios" . $this->user->id);
if(!$entity->getPrivacyPermission("audios.read", $this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$audios = $this->audios->getByUser($entity, $page, 10);
$audiosCount = $this->audios->getUserCollectionSize($entity);
}
if (!$entity)
$this->notFound();
$this->template->owner = $entity;
$this->template->ownerId = $owner;
$this->template->club = $owner < 0 ? $entity : NULL;
$this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id));
$this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity));
} else if ($mode === "new") {
$audios = $this->audios->getNew();
$audiosCount = $audios->size();
} else if ($mode === "playlists") {
if($owner < 0) {
$entity = (new Clubs)->get(abs($owner));
if (!$entity || $entity->isBanned())
$this->redirect("/playlists" . $this->user->id);
$playlists = $this->audios->getPlaylistsByClub($entity, $page, 10);
$playlistsCount = $this->audios->getClubPlaylistsCount($entity);
} else {
$entity = (new Users)->get($owner);
if (!$entity || $entity->isDeleted() || $entity->isBanned())
$this->redirect("/playlists" . $this->user->id);
if(!$entity->getPrivacyPermission("audios.read", $this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$playlists = $this->audios->getPlaylistsByUser($entity, $page, 9);
$playlistsCount = $this->audios->getUserPlaylistsCount($entity);
}
$this->template->playlists = iterator_to_array($playlists);
$this->template->playlistsCount = $playlistsCount;
$this->template->owner = $entity;
$this->template->ownerId = $owner;
$this->template->club = $owner < 0 ? $entity : NULL;
$this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id));
$this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity));
} else {
$audios = $this->audios->getPopular();
$audiosCount = $audios->size();
}
// $this->renderApp("owner=$owner");
if ($audios !== []) {
$this->template->audios = iterator_to_array($audios);
$this->template->audiosCount = $audiosCount;
}
$this->template->mode = $mode;
$this->template->page = $page;
if(in_array($mode, ["list", "new", "popular"]) && $this->user->identity)
$this->template->friendsAudios = $this->user->identity->getBroadcastList("all", true);
}
function renderEmbed(int $owner, int $id): void
{
$audio = $this->audios->getByOwnerAndVID($owner, $id);
if(!$audio) {
header("HTTP/1.1 404 Not Found");
exit("<b>" . tr("audio_embed_not_found") . ".</b>");
} else if($audio->isDeleted()) {
header("HTTP/1.1 410 Not Found");
exit("<b>" . tr("audio_embed_deleted") . ".</b>");
} else if($audio->isWithdrawn()) {
header("HTTP/1.1 451 Unavailable for legal reasons");
exit("<b>" . tr("audio_embed_withdrawn") . ".</b>");
} else if(!$audio->canBeViewedBy(NULL)) {
header("HTTP/1.1 403 Forbidden");
exit("<b>" . tr("audio_embed_forbidden") . ".</b>");
} else if(!$audio->isAvailable()) {
header("HTTP/1.1 425 Too Early");
exit("<b>" . tr("audio_embed_processing") . ".</b>");
}
$this->template->audio = $audio;
}
function renderUpload(): void
{
$this->assertUserLoggedIn();
$group = NULL;
$isAjax = $this->postParam("ajax", false) == 1;
if(!is_null($this->queryParam("gid"))) {
$gid = (int) $this->queryParam("gid");
$group = (new Clubs)->get($gid);
if(!$group)
$this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax);
if(!$group->canUploadAudio($this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax);
}
$this->template->group = $group;
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$upload = $_FILES["blob"];
if(isset($upload) && file_exists($upload["tmp_name"])) {
if($upload["size"] > self::MAX_AUDIO_SIZE)
$this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large"), null, $isAjax);
} else {
$err = !isset($upload) ? 65536 : $upload["error"];
$err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT);
$readableError = tr("error_generic");
switch($upload["error"]) {
default:
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$readableError = tr("file_too_big");
break;
case UPLOAD_ERR_PARTIAL:
$readableError = tr("file_loaded_partially");
break;
case UPLOAD_ERR_NO_FILE:
$readableError = tr("file_not_uploaded");
break;
case UPLOAD_ERR_NO_TMP_DIR:
$readableError = "Missing a temporary folder.";
break;
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
$readableError = "Failed to write file to disk. ";
break;
}
$this->flashFail("err", tr("error"), $readableError . " " . tr("error_code", $err), null, $isAjax);
}
$performer = $this->postParam("performer");
$name = $this->postParam("name");
$lyrics = $this->postParam("lyrics");
$genre = empty($this->postParam("genre")) ? "Other" : $this->postParam("genre");
$nsfw = ($this->postParam("explicit") ?? "off") === "on";
if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars
$this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, $isAjax);
$audio = new Audio;
$audio->setOwner($this->user->id);
$audio->setName($name);
$audio->setPerformer($performer);
$audio->setLyrics(empty($lyrics) ? NULL : $lyrics);
$audio->setGenre($genre);
$audio->setExplicit($nsfw);
try {
$audio->setFile($upload);
} catch(\DomainException $ex) {
$e = $ex->getMessage();
$this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large") . " $e.", null, $isAjax);
} catch(\RuntimeException $ex) {
$this->flashFail("err", tr("error"), tr("ffmpeg_timeout"), null, $isAjax);
} catch(\BadMethodCallException $ex) {
$this->flashFail("err", tr("error"), "Загрузка аудио под Linux на данный момент не реализована. Следите за обновлениями: <a href='https://github.com/openvk/openvk/pull/512/commits'>https://github.com/openvk/openvk/pull/512/commits</a>", null, $isAjax);
} catch(\Exception $ex) {
$this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax);
}
$audio->save();
$audio->add($group ?? $this->user->identity);
if(!$isAjax)
$this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId());
else {
$redirectLink = "/audios";
if(!is_null($group))
$redirectLink .= $group->getRealId();
else
$redirectLink .= $this->user->id;
$pagesCount = (int)ceil((new Audios)->getCollectionSizeByEntityId(isset($group) ? $group->getRealId() : $this->user->id) / 10);
$redirectLink .= "?p=".$pagesCount;
$this->returnJson([
"success" => true,
"redirect_link" => $redirectLink,
]);
}
}
function renderListen(int $id): void
{
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$this->assertNoCSRF();
if(is_null($this->user))
$this->returnJson(["success" => false]);
$audio = $this->audios->get($id);
if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) {
if(!empty($this->postParam("playlist"))) {
$playlist = (new Audios)->getPlaylist((int)$this->postParam("playlist"));
if(!$playlist || $playlist->isDeleted() || !$playlist->canBeViewedBy($this->user->identity) || !$playlist->hasAudio($audio))
$playlist = NULL;
}
$listen = $audio->listen($this->user->identity, $playlist);
$returnArr = ["success" => $listen];
if($playlist)
$returnArr["new_playlists_listens"] = $playlist->getListens();
$this->returnJson($returnArr);
}
$this->returnJson(["success" => false]);
} else {
$this->redirect("/");
}
}
function renderSearch(): void
{
$this->redirect("/search?type=audios");
}
function renderNewPlaylist(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
$owner = $this->user->id;
if ($this->requestParam("gid")) {
$club = (new Clubs)->get((int) abs((int)$this->requestParam("gid")));
if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity))
$this->redirect("/audios" . $this->user->id);
$owner = ($club->getId() * -1);
$this->template->club = $club;
}
$this->template->owner = $owner;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$title = $this->postParam("title");
$description = $this->postParam("description");
$audios = !empty($this->postParam("audios")) ? array_slice(explode(",", $this->postParam("audios")), 0, 1000) : [];
if(empty($title) || iconv_strlen($title) < 1)
$this->flashFail("err", tr("error"), tr("set_playlist_name"));
$playlist = new Playlist;
$playlist->setOwner($owner);
$playlist->setName(substr($title, 0, 125));
$playlist->setDescription(substr($description, 0, 2045));
if($_FILES["cover"]["error"] === UPLOAD_ERR_OK) {
if(!str_starts_with($_FILES["cover"]["type"], "image"))
$this->flashFail("err", tr("error"), tr("not_a_photo"));
try {
$playlist->fastMakeCover($this->user->id, $_FILES["cover"]);
} catch(\Throwable $e) {
$this->flashFail("err", tr("error"), tr("invalid_cover_photo"));
}
}
$playlist->save();
foreach($audios as $audio) {
$audio = $this->audios->get((int)$audio);
if(!$audio || $audio->isDeleted() || !$audio->canBeViewedBy($this->user->identity))
continue;
$playlist->add($audio);
}
$playlist->bookmark(isset($club) ? $club : $this->user->identity);
$this->redirect("/playlist" . $owner . "_" . $playlist->getId());
} else {
if(isset($club)) {
$this->template->audios = iterator_to_array($this->audios->getByClub($club, 1, 10));
$count = (new Audios)->getClubCollectionSize($club);
} else {
$this->template->audios = iterator_to_array($this->audios->getByUser($this->user->identity, 1, 10));
$count = (new Audios)->getUserCollectionSize($this->user->identity);
}
$this->template->pagesCount = ceil($count / 10);
}
}
function renderPlaylistAction(int $id) {
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
$this->assertNoCSRF();
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
$this->redirect("/");
}
$playlist = $this->audios->getPlaylist($id);
if(!$playlist || $playlist->isDeleted())
$this->flashFail("err", "error", tr("invalid_playlist"), null, true);
switch ($this->queryParam("act")) {
case "bookmark":
if(!$playlist->isBookmarkedBy($this->user->identity))
$playlist->bookmark($this->user->identity);
else
$this->flashFail("err", "error", tr("playlist_already_bookmarked"), null, true);
break;
case "unbookmark":
if($playlist->isBookmarkedBy($this->user->identity))
$playlist->unbookmark($this->user->identity);
else
$this->flashFail("err", "error", tr("playlist_not_bookmarked"), null, true);
break;
case "delete":
if($playlist->canBeModifiedBy($this->user->identity)) {
$tmOwner = $playlist->getOwner();
$playlist->delete();
} else
$this->flashFail("err", "error", tr("access_denied"), null, true);
$this->returnJson(["success" => true, "id" => $tmOwner->getRealId()]);
break;
default:
break;
}
$this->returnJson(["success" => true]);
}
function renderEditPlaylist(int $owner_id, int $virtual_id)
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id);
$page = (int)($this->queryParam("p") ?? 1);
if (!$playlist || $playlist->isDeleted() || !$playlist->canBeModifiedBy($this->user->identity))
$this->notFound();
$this->template->playlist = $playlist;
$this->template->page = $page;
$audios = iterator_to_array($playlist->fetch(1, $playlist->size()));
$this->template->audios = array_slice($audios, 0, 10);
$audiosIds = [];
foreach($audios as $aud)
$audiosIds[] = $aud->getId();
$this->template->audiosIds = implode(",", array_unique($audiosIds)) . ",";
$this->template->ownerId = $owner_id;
$this->template->owner = $playlist->getOwner();
$this->template->pagesCount = $pagesCount = ceil($playlist->size() / 10);
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$title = $this->postParam("title");
$description = $this->postParam("description");
$new_audios = !empty($this->postParam("audios")) ? explode(",", rtrim($this->postParam("audios"), ",")) : [];
if(empty($title) || iconv_strlen($title) < 1)
$this->flashFail("err", tr("error"), tr("set_playlist_name"));
$playlist->setName(ovk_proc_strtr($title, 125));
$playlist->setDescription(ovk_proc_strtr($description, 2045));
$playlist->setEdited(time());
$playlist->resetLength();
if($_FILES["new_cover"]["error"] === UPLOAD_ERR_OK) {
if(!str_starts_with($_FILES["new_cover"]["type"], "image"))
$this->flashFail("err", tr("error"), tr("not_a_photo"));
try {
$playlist->fastMakeCover($this->user->id, $_FILES["new_cover"]);
} catch(\Throwable $e) {
$this->flashFail("err", tr("error"), tr("invalid_cover_photo"));
}
}
$playlist->save();
DatabaseConnection::i()->getContext()->table("playlist_relations")->where([
"collection" => $playlist->getId()
])->delete();
foreach ($new_audios as $new_audio) {
$audio = (new Audios)->get((int)$new_audio);
if(!$audio || $audio->isDeleted())
continue;
$playlist->add($audio);
}
$this->redirect("/playlist".$playlist->getPrettyId());
}
function renderPlaylist(int $owner_id, int $virtual_id): void
{
$playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id);
$page = (int)($this->queryParam("p") ?? 1);
if (!$playlist || $playlist->isDeleted())
$this->notFound();
$this->template->playlist = $playlist;
$this->template->page = $page;
$this->template->audios = iterator_to_array($playlist->fetch($page, 10));
$this->template->ownerId = $owner_id;
$this->template->owner = $playlist->getOwner();
$this->template->isBookmarked = $this->user->identity && $playlist->isBookmarkedBy($this->user->identity);
$this->template->isMy = $this->user->identity && $playlist->getOwner()->getId() === $this->user->id;
$this->template->canEdit = $this->user->identity && $playlist->canBeModifiedBy($this->user->identity);
}
function renderAction(int $audio_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
$this->assertNoCSRF();
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
$this->redirect("/");
}
$audio = $this->audios->get($audio_id);
if(!$audio || $audio->isDeleted())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
switch ($this->queryParam("act")) {
case "add":
if($audio->isWithdrawn())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
if(!$audio->isInLibraryOf($this->user->identity))
$audio->add($this->user->identity);
else
$this->flashFail("err", "error", tr("do_have_audio"), null, true);
break;
case "remove":
if($audio->isInLibraryOf($this->user->identity))
$audio->remove($this->user->identity);
else
$this->flashFail("err", "error", tr("do_not_have_audio"), null, true);
break;
case "remove_club":
$club = (new Clubs)->get((int)$this->postParam("club"));
if(!$club || !$club->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "error", tr("access_denied"), null, true);
if($audio->isInLibraryOf($club))
$audio->remove($club);
else
$this->flashFail("err", "error", tr("group_hasnt_audio"), null, true);
break;
case "add_to_club":
$club = (new Clubs)->get((int)$this->postParam("club"));
if(!$club || !$club->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "error", tr("access_denied"), null, true);
if(!$audio->isInLibraryOf($club))
$audio->add($club);
else
$this->flashFail("err", "error", tr("group_has_audio"), null, true);
break;
case "delete":
if($audio->canBeModifiedBy($this->user->identity))
$audio->delete();
else
$this->flashFail("err", "error", tr("access_denied"), null, true);
break;
case "edit":
$audio = $this->audios->get($audio_id);
if (!$audio || $audio->isDeleted() || $audio->isWithdrawn())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
if ($audio->getOwner()->getId() !== $this->user->id)
$this->flashFail("err", "error", tr("access_denied"), null, true);
$performer = $this->postParam("performer");
$name = $this->postParam("name");
$lyrics = $this->postParam("lyrics");
$genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre");
$nsfw = (int)($this->postParam("explicit") ?? 0) === 1;
$unlisted = (int)($this->postParam("unlisted") ?? 0) === 1;
if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars
$this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, true);
$audio->setName($name);
$audio->setPerformer($performer);
$audio->setLyrics(empty($lyrics) ? NULL : $lyrics);
$audio->setGenre($genre);
$audio->setExplicit($nsfw);
$audio->setSearchability($unlisted);
$audio->setEdited(time());
$audio->save();
$this->returnJson(["success" => true, "new_info" => [
"name" => ovk_proc_strtr($audio->getTitle(), 40),
"performer" => ovk_proc_strtr($audio->getPerformer(), 40),
"lyrics" => nl2br($audio->getLyrics() ?? ""),
"lyrics_unformatted" => $audio->getLyrics() ?? "",
"explicit" => $audio->isExplicit(),
"genre" => $audio->getGenre(),
"unlisted" => $audio->isUnlisted(),
]]);
break;
default:
break;
}
$this->returnJson(["success" => true]);
}
function renderPlaylists(int $owner)
{
$this->renderList($owner, "playlists");
}
function renderApiGetContext()
{
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
$this->redirect("/");
}
$ctx_type = $this->postParam("context");
$ctx_id = (int)($this->postParam("context_entity"));
$page = (int)($this->postParam("page") ?? 1);
$perPage = 10;
switch($ctx_type) {
default:
case "entity_audios":
if($ctx_id >= 0) {
$entity = $ctx_id != 0 ? (new Users)->get($ctx_id) : $this->user->identity;
if(!$entity || !$entity->getPrivacyPermission("audios.read", $this->user->identity))
$this->flashFail("err", "Error", "Can't get queue", 80, true);
$audios = $this->audios->getByUser($entity, $page, $perPage);
$audiosCount = $this->audios->getUserCollectionSize($entity);
} else {
$entity = (new Clubs)->get(abs($ctx_id));
if(!$entity || $entity->isBanned())
$this->flashFail("err", "Error", "Can't get queue", 80, true);
$audios = $this->audios->getByClub($entity, $page, $perPage);
$audiosCount = $this->audios->getClubCollectionSize($entity);
}
break;
case "new_audios":
$audios = $this->audios->getNew();
$audiosCount = $audios->size();
break;
case "popular_audios":
$audios = $this->audios->getPopular();
$audiosCount = $audios->size();
break;
case "playlist_context":
$playlist = $this->audios->getPlaylist($ctx_id);
if (!$playlist || $playlist->isDeleted())
$this->flashFail("err", "Error", "Can't get queue", 80, true);
$audios = $playlist->fetch($page, 10);
$audiosCount = $playlist->size();
break;
case "search_context":
$stream = $this->audios->search($this->postParam("query"), 2, $this->postParam("type") === "by_performer");
$audios = $stream->page($page, 10);
$audiosCount = $stream->size();
break;
}
$pagesCount = ceil($audiosCount / $perPage);
# костылёк для получения плееров в пикере аудиозаписей
if((int)($this->postParam("returnPlayers")) === 1) {
$this->template->audios = $audios;
$this->template->page = $page;
$this->template->pagesCount = $pagesCount;
$this->template->count = $audiosCount;
return 0;
}
$audiosArr = [];
foreach($audios as $audio) {
$audiosArr[] = [
"id" => $audio->getId(),
"name" => $audio->getTitle(),
"performer" => $audio->getPerformer(),
"keys" => $audio->getKeys(),
"url" => $audio->getUrl(),
"length" => $audio->getLength(),
"available" => $audio->isAvailable(),
"withdrawn" => $audio->isWithdrawn(),
];
}
$resultArr = [
"success" => true,
"page" => $page,
"perPage" => $perPage,
"pagesCount" => $pagesCount,
"count" => $audiosCount,
"items" => $audiosArr,
];
$this->returnJson($resultArr);
}
}

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\{IP, User, PasswordReset, EmailVerification}; use openvk\Web\Models\Entities\{IP, User, PasswordReset, EmailVerification};
use openvk\Web\Models\Repositories\{Bans, IPs, Users, Restores, Verifications, Logs}; use openvk\Web\Models\Repositories\{Bans, IPs, Users, Restores, Verifications};
use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator; use openvk\Web\Util\Validator;
use Chandler\Session\Session; use Chandler\Session\Session;
@ -130,7 +130,6 @@ final class AuthPresenter extends OpenVKPresenter
} }
$this->authenticator->authenticate($chUser->getId()); $this->authenticator->authenticate($chUser->getId());
(new Logs)->create($user->getId(), "profiles", "openvk\\Web\\Models\\Entities\\User", 0, $user, $user, $_SERVER["REMOTE_ADDR"], $_SERVER["HTTP_USER_AGENT"]);
$this->redirect("/id" . $user->getId()); $this->redirect("/id" . $user->getId());
$user->save(); $user->save();
} }

View file

@ -18,6 +18,8 @@ final class BlobPresenter extends OpenVKPresenter
function renderFile(/*string*/ $dir, string $name, string $format) function renderFile(/*string*/ $dir, string $name, string $format)
{ {
header("Access-Control-Allow-Origin: *");
$dir = $this->getDirName($dir); $dir = $this->getDirName($dir);
$base = realpath(OPENVK_ROOT . "/storage/$dir"); $base = realpath(OPENVK_ROOT . "/storage/$dir");
$path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format"); $path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format");

View file

@ -2,7 +2,7 @@
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, 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, Videos, Photos, Audios};
final class CommentPresenter extends OpenVKPresenter final class CommentPresenter extends OpenVKPresenter
{ {
@ -58,9 +58,6 @@ final class CommentPresenter extends OpenVKPresenter
if ($entity instanceof Post && $entity->getWallOwner()->isBanned()) if ($entity instanceof Post && $entity->getWallOwner()->isBanned())
$this->flashFail("err", tr("error"), tr("forbidden")); $this->flashFail("err", tr("error"), tr("forbidden"));
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;
@ -70,31 +67,68 @@ final class CommentPresenter extends OpenVKPresenter
try { try {
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"]); $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"]);
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл изображения повреждён, слишком велик или одна сторона изображения в разы больше другой."); $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_when_publishing_comment_description"));
} }
} }
# TODO move to trait $photos = [];
try { if(!empty($this->postParam("photos"))) {
$photo = NULL; $un = rtrim($this->postParam("photos"), ",");
$video = NULL; $arr = explode(",", $un);
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
$album = NULL;
if($wall > 0 && $wall === $this->user->id)
$album = (new Albums)->getUserWallAlbum($wallOwner);
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album); if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$photo || $photo->isDeleted())
continue;
$photos[] = $photo;
}
}
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { $videos = [];
$video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"]);
if(!empty($this->postParam("videos"))) {
$un = rtrim($this->postParam("videos"), ",");
$arr = explode(",", $un);
if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$video || $video->isDeleted())
continue;
$videos[] = $video;
}
} }
} catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");
} }
if(empty($this->postParam("text")) && !$photo && !$video) $audios = [];
$this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий пустой или слишком большой.");
if(!empty($this->postParam("audios"))) {
$un = rtrim($this->postParam("audios"), ",");
$arr = explode(",", $un);
if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$audio || $audio->isDeleted())
continue;
$audios[] = $audio;
}
}
}
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1)
$this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty"));
try { try {
$comment = new Comment; $comment = new Comment;
@ -106,14 +140,18 @@ final class CommentPresenter extends OpenVKPresenter
$comment->setFlags($flags); $comment->setFlags($flags);
$comment->save(); $comment->save();
} catch (\LengthException $ex) { } catch (\LengthException $ex) {
$this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой."); $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_too_big"));
} }
if(!is_null($photo)) foreach($photos as $photo)
$comment->attach($photo); $comment->attach($photo);
if(!is_null($video)) if(sizeof($videos) > 0)
$comment->attach($video); foreach($videos as $vid)
$comment->attach($vid);
foreach($audios as $audio)
$comment->attach($audio);
if($entity->getOwner()->getId() !== $this->user->identity->getId()) if($entity->getOwner()->getId() !== $this->user->identity->getId())
if(($owner = $entity->getOwner()) instanceof User) if(($owner = $entity->getOwner()) instanceof User)
@ -128,7 +166,7 @@ final class CommentPresenter extends OpenVKPresenter
if($mentionee instanceof User) if($mentionee instanceof User)
(new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit(); (new MentionNotification($mentionee, $entity, $comment->getOwner(), strip_tags($comment->getText())))->emit();
$this->flashFail("succ", "Комментарий добавлен", "Ваш комментарий появится на странице."); $this->flashFail("succ", tr("comment_is_added"), tr("comment_is_added_desc"));
} }
function renderDeleteComment(int $id): void function renderDeleteComment(int $id): void
@ -139,15 +177,15 @@ final class CommentPresenter extends OpenVKPresenter
$comment = (new Comments)->get($id); $comment = (new Comments)->get($id);
if(!$comment) $this->notFound(); if(!$comment) $this->notFound();
if(!$comment->canBeDeletedBy($this->user->identity)) if(!$comment->canBeDeletedBy($this->user->identity))
$this->throwError(403, "Forbidden", "У вас недостаточно прав чтобы редактировать этот ресурс."); $this->throwError(403, "Forbidden", tr("error_access_denied"));
if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned()) if ($comment->getTarget() instanceof Post && $comment->getTarget()->getWallOwner()->isBanned())
$this->flashFail("err", tr("error"), tr("forbidden")); $this->flashFail("err", tr("error"), tr("forbidden"));
$comment->delete(); $comment->delete();
$this->flashFail( $this->flashFail(
"succ", "succ",
"Успешно", tr("success"),
"Этот комментарий больше не будет показыватся.<br/><a href='/al_comments/spam?$id'>Отметить как спам</a>?" tr("comment_will_not_appear")
); );
} }
} }

View file

@ -44,6 +44,7 @@ final class GiftsPresenter extends OpenVKPresenter
$this->template->user = $user; $this->template->user = $user;
$this->template->iterator = $cats; $this->template->iterator = $cats;
$this->template->count = $this->gifts->getCategoriesCount();
$this->template->_template = "Gifts/Menu.xml"; $this->template->_template = "Gifts/Menu.xml";
} }
@ -52,7 +53,7 @@ final class GiftsPresenter extends OpenVKPresenter
$user = $this->users->get((int) ($this->queryParam("user") ?? 0)); $user = $this->users->get((int) ($this->queryParam("user") ?? 0));
$cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
if(!$user || !$cat) if(!$user || !$cat)
$this->flashFail("err", "Не удалось подарить", "Пользователь или набор не существуют."); $this->flashFail("err", tr("error_when_gifting"), tr("error_user_not_exists"));
if(!$user->canBeViewedBy($this->user->identity)) if(!$user->canBeViewedBy($this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); $this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
@ -72,17 +73,17 @@ final class GiftsPresenter extends OpenVKPresenter
$gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0)); $gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0));
$cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0)); $cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
if(!$user || !$cat || !$gift || !$cat->hasGift($gift)) if(!$user || !$cat || !$gift || !$cat->hasGift($gift))
$this->flashFail("err", "Не удалось подарить", "Не удалось подтвердить права на подарок."); $this->flashFail("err", tr("error_when_gifting"), tr("error_no_rights_gifts"));
if(!$gift->canUse($this->user->identity)) if(!$gift->canUse($this->user->identity))
$this->flashFail("err", "Не удалось подарить", "У вас больше не осталось таких подарков."); $this->flashFail("err", tr("error_when_gifting"), tr("error_no_more_gifts"));
if(!$user->canBeViewedBy($this->user->identity ?? NULL)) if(!$user->canBeViewedBy($this->user->identity ?? NULL))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); $this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$coinsLeft = $this->user->identity->getCoins() - $gift->getPrice(); $coinsLeft = $this->user->identity->getCoins() - $gift->getPrice();
if($coinsLeft < 0) if($coinsLeft < 0)
$this->flashFail("err", "Не удалось подарить", "Ору нищ не пук."); $this->flashFail("err", tr("error_when_gifting"), tr("error_no_money"));
$this->template->_template = "Gifts/Confirm.xml"; $this->template->_template = "Gifts/Confirm.xml";
if($_SERVER["REQUEST_METHOD"] !== "POST") { if($_SERVER["REQUEST_METHOD"] !== "POST") {
@ -100,7 +101,7 @@ final class GiftsPresenter extends OpenVKPresenter
$user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous"))); $user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous")));
$gift->used(); $gift->used();
$this->flash("succ", "Подарок отправлен", "Вы отправили подарок <b>" . $user->getFirstName() . "</b> за " . $gift->getPrice() . " голосов."); $this->flash("succ", tr("gift_sent"), tr("gift_sent_desc", $user->getFirstName(), $gift->getPrice()));
$this->redirect($user->getURL()); $this->redirect($user->getURL());
} }

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Club, Photo, Post}; use openvk\Web\Models\Entities\{Club, Photo, Post};
use Nette\InvalidStateException; 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, Audios, Posts};
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
final class GroupPresenter extends OpenVKPresenter final class GroupPresenter extends OpenVKPresenter
@ -31,6 +31,15 @@ final class GroupPresenter extends OpenVKPresenter
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
$this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topics = (new Topics)->getLastTopics($club, 3);
$this->template->topicsCount = (new Topics)->getClubTopicsCount($club); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club);
$this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId());
$this->template->audiosCount = (new Audios)->getClubCollectionSize($club);
}
if(!is_null($this->user->identity) && $club->getWallType() == 2) {
if(!$club->canBeModifiedBy($this->user->identity))
$this->template->suggestedPostsCountByUser = (new Posts)->getSuggestedPostsCountByUser($club->getId(), $this->user->id);
else
$this->template->suggestedPostsCountByEveryone = (new Posts)->getSuggestedPostsCount($club->getId());
} }
$this->template->club = $club; $this->template->club = $club;
@ -54,7 +63,7 @@ final class GroupPresenter extends OpenVKPresenter
$club->save(); $club->save();
} catch(\PDOException $ex) { } catch(\PDOException $ex) {
if($ex->getCode() == 23000) if($ex->getCode() == 23000)
$this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору."); $this->flashFail("err", tr("error"), tr("error_on_server_side"));
else else
throw $ex; throw $ex;
} }
@ -62,7 +71,7 @@ final class GroupPresenter extends OpenVKPresenter
$club->toggleSubscription($this->user->identity); $club->toggleSubscription($this->user->identity);
$this->redirect("/club" . $club->getId()); $this->redirect("/club" . $club->getId());
}else{ }else{
$this->flashFail("err", "Ошибка", "Вы не ввели название группы."); $this->flashFail("err", tr("error"), tr("error_no_group_name"));
} }
} }
} }
@ -132,7 +141,7 @@ final class GroupPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) if(!$club->canBeModifiedBy($this->user->identity ?? NULL))
$this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
if(!is_null($hidden)) { if(!is_null($hidden)) {
if($club->getOwner()->getId() == $user->getId()) { if($club->getOwner()->getId() == $user->getId()) {
@ -150,9 +159,9 @@ final class GroupPresenter extends OpenVKPresenter
} }
if($hidden) { if($hidden) {
$this->flashFail("succ", "Операция успешна", "Теперь " . $user->getCanonicalName() . " будет показываться как обычный подписчик всем кроме других администраторов"); $this->flashFail("succ", tr("success_action"), tr("x_is_now_hidden", $user->getCanonicalName()));
} else { } else {
$this->flashFail("succ", "Операция успешна", "Теперь все будут знать про то что " . $user->getCanonicalName() . " - администратор"); $this->flashFail("succ", tr("success_action"), tr("x_is_now_showed", $user->getCanonicalName()));
} }
} elseif($removeComment) { } elseif($removeComment) {
if($club->getOwner()->getId() == $user->getId()) { if($club->getOwner()->getId() == $user->getId()) {
@ -164,11 +173,11 @@ final class GroupPresenter extends OpenVKPresenter
$manager->save(); $manager->save();
} }
$this->flashFail("succ", "Операция успешна", "Комментарий к администратору удален"); $this->flashFail("succ", tr("success_action"), tr("comment_is_deleted"));
} elseif($comment) { } elseif($comment) {
if(mb_strlen($comment) > 36) { if(mb_strlen($comment) > 36) {
$commentLength = (string) mb_strlen($comment); $commentLength = (string) mb_strlen($comment);
$this->flashFail("err", "Ошибка", "Комментарий слишком длинный ($commentLength символов вместо 36 символов)"); $this->flashFail("err", tr("error"), tr("comment_is_too_long", $commentLength));
} }
if($club->getOwner()->getId() == $user->getId()) { if($club->getOwner()->getId() == $user->getId()) {
@ -180,16 +189,16 @@ final class GroupPresenter extends OpenVKPresenter
$manager->save(); $manager->save();
} }
$this->flashFail("succ", "Операция успешна", "Комментарий к администратору изменён"); $this->flashFail("succ", tr("success_action"), tr("comment_is_changed"));
}else{ }else{
if($club->canBeModifiedBy($user)) { if($club->canBeModifiedBy($user)) {
$club->removeManager($user); $club->removeManager($user);
$this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " более не администратор."); $this->flashFail("succ", tr("success_action"), tr("x_no_more_admin", $user->getCanonicalName()));
} else { } else {
$club->addManager($user); $club->addManager($user);
(new ClubModeratorNotification($user, $club, $this->user->identity))->emit(); (new ClubModeratorNotification($user, $club, $this->user->identity))->emit();
$this->flashFail("succ", "Операция успешна", $user->getCanonicalName() . " назначен(а) администратором."); $this->flashFail("succ", tr("success_action"), tr("x_is_admin", $user->getCanonicalName()));
} }
} }
@ -214,10 +223,16 @@ final class GroupPresenter extends OpenVKPresenter
$club->setName((empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $club->getName() : $this->postParam("name")); $club->setName((empty($this->postParam("name")) || mb_strlen(trim($this->postParam("name"))) === 0) ? $club->getName() : $this->postParam("name"));
$club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about")); $club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about"));
$club->setWall(empty($this->postParam("wall")) ? 0 : 1); try {
$club->setWall(empty($this->postParam("wall")) ? 0 : (int)$this->postParam("wall"));
} catch(\Exception $e) {
$this->flashFail("err", tr("error"), tr("error_invalid_wall_value"));
}
$club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display"));
$club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1);
$club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1); $club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1);
$club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1);
$club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1); $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1);
$website = $this->postParam("website") ?? ""; $website = $this->postParam("website") ?? "";
@ -245,7 +260,7 @@ final class GroupPresenter extends OpenVKPresenter
(new Albums)->getClubAvatarAlbum($club)->addPhoto($photo); (new Albums)->getClubAvatarAlbum($club)->addPhoto($photo);
} catch(ISE $ex) { } catch(ISE $ex) {
$name = $album->getName(); $name = $album->getName();
$this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию."); $this->flashFail("err", tr("error"), tr("error_when_uploading_photo"));
} }
} }
@ -253,12 +268,12 @@ final class GroupPresenter extends OpenVKPresenter
$club->save(); $club->save();
} catch(\PDOException $ex) { } catch(\PDOException $ex) {
if($ex->getCode() == 23000) if($ex->getCode() == 23000)
$this->flashFail("err", "Ошибка", "Произошла ошибка на стороне сервера. Обратитесь к системному администратору."); $this->flashFail("err", tr("error"), tr("error_on_server_side"));
else else
throw $ex; throw $ex;
} }
$this->flash("succ", "Изменения сохранены", "Новые данные появятся в вашей группе."); $this->flash("succ", tr("changes_saved"), tr("new_changes_desc"));
} }
} }
@ -298,7 +313,7 @@ final class GroupPresenter extends OpenVKPresenter
} catch(ISE $ex) { } catch(ISE $ex) {
$name = $album->getName(); $name = $album->getName();
$this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию."); $this->flashFail("err", tr("error"), tr("error_when_uploading_photo"));
} }
} }
$this->returnJson([ $this->returnJson([
@ -350,7 +365,7 @@ final class GroupPresenter extends OpenVKPresenter
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
if(!eventdb()) if(!eventdb())
$this->flashFail("err", "Ошибка подключения", "Не удалось подключится к службе телеметрии."); $this->flashFail("err", tr("connection_error"), tr("connection_error_desc"));
$club = $this->clubs->get($id); $club = $this->clubs->get($id);
if(!$club->canBeModifiedBy($this->user->identity)) if(!$club->canBeModifiedBy($this->user->identity))
@ -411,4 +426,37 @@ final class GroupPresenter extends OpenVKPresenter
$this->flashFail("succ", tr("information_-1"), tr("group_owner_setted", $newOwner->getCanonicalName(), $club->getName())); $this->flashFail("succ", tr("information_-1"), tr("group_owner_setted", $newOwner->getCanonicalName(), $club->getName()));
} }
function renderSuggested(int $id): void
{
$this->assertUserLoggedIn();
$club = $this->clubs->get($id);
if(!$club)
$this->notFound();
else
$this->template->club = $club;
if($club->getWallType() == 0) {
$this->flash("err", tr("error_suggestions"), tr("error_suggestions_closed"));
$this->redirect("/club".$club->getId());
}
if($club->getWallType() == 1) {
$this->flash("err", tr("error_suggestions"), tr("error_suggestions_open"));
$this->redirect("/club".$club->getId());
}
if(!$club->canBeModifiedBy($this->user->identity)) {
$this->template->posts = iterator_to_array((new Posts)->getSuggestedPostsByUser($club->getId(), $this->user->id, (int) ($this->queryParam("p") ?? 1)));
$this->template->count = (new Posts)->getSuggestedPostsCountByUser($club->getId(), $this->user->id);
$this->template->type = "my";
} else {
$this->template->posts = iterator_to_array((new Posts)->getSuggestedPosts($club->getId(), (int) ($this->queryParam("p") ?? 1)));
$this->template->count = (new Posts)->getSuggestedPostsCount($club->getId());
$this->template->type = "everyone";
}
$this->template->page = (int) ($this->queryParam("p") ?? 1);
}
} }

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 openvk\Web\Models\Repositories\{Posts, Comments};
use MessagePack\MessagePack; use MessagePack\MessagePack;
use Chandler\Session\Session; use Chandler\Session\Session;
@ -95,4 +96,41 @@ final class InternalAPIPresenter extends OpenVKPresenter
]); ]);
} }
} }
function renderGetPhotosFromPost(int $owner_id, int $post_id) {
if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("иди нахуй заебал");
}
if($this->postParam("parentType", false) == "post") {
$post = (new Posts)->getPostById($owner_id, $post_id, true);
} else {
$post = (new Comments)->get($post_id);
}
if(is_null($post)) {
$this->returnJson([
"success" => 0
]);
} else {
$response = [];
$attachments = $post->getChildren();
foreach($attachments as $attachment)
{
if($attachment instanceof \openvk\Web\Models\Entities\Photo)
{
$response[] = [
"url" => $attachment->getURLBySizeId('normal'),
"id" => $attachment->getPrettyId()
];
}
}
$this->returnJson([
"success" => 1,
"body" => $response
]);
}
}
} }

View file

@ -128,7 +128,7 @@ final class MessengerPresenter extends OpenVKPresenter
$messages = []; $messages = [];
$correspondence = new Correspondence($this->user->identity, $correspondent); $correspondence = new Correspondence($this->user->identity, $correspondent);
foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg) as $message) foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg, NULL, 0) as $message)
$messages[] = $message->simplify(); $messages[] = $message->simplify();
header("Content-Type: application/json"); header("Content-Type: application/json");

View file

@ -177,26 +177,25 @@ final class NoSpamPresenter extends OpenVKPresenter
if ($conditions) { if ($conditions) {
$logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`"); $logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`");
if (!$where) {
foreach ($logs as $log) {
$log = (new Logs)->get($log->id);
$response[] = $log->getObject()->unwrap();
}
} else {
foreach ($logs as $log) { foreach ($logs as $log) {
$log = (new Logs)->get($log->id); $log = (new Logs)->get($log->id);
$object = $log->getObject()->unwrap(); $object = $log->getObject()->unwrap();
if (!$object) continue; if (!$object) continue;
if ($where) {
if (str_starts_with($where, " AND")) { if (str_starts_with($where, " AND")) {
$where = substr_replace($where, "", 0, strlen(" AND")); $where = substr_replace($where, "", 0, strlen(" AND"));
} }
foreach ($db->query("SELECT * FROM `$table` WHERE $where")->fetchAll() as $o) { $a = $db->query("SELECT * FROM `$table` WHERE $where")->fetchAll();
if ($object->id === $o["id"]) { foreach ($a as $o) {
if ($object->id == $o["id"]) {
$response[] = $object; $response[] = $object;
} }
} }
} else {
$response[] = $object;
} }
} }
} }
@ -210,12 +209,16 @@ final class NoSpamPresenter extends OpenVKPresenter
$processed = 0; $processed = 0;
$where = $this->postParam("where"); $where = $this->postParam("where");
$ip = $this->postParam("ip"); $ip = addslashes($this->postParam("ip"));
$useragent = $this->postParam("useragent"); $useragent = addslashes($this->postParam("useragent"));
$searchTerm = $this->postParam("q"); $searchTerm = addslashes($this->postParam("q"));
$ts = (int)$this->postParam("ts"); $ts = (int)$this->postParam("ts");
$te = (int)$this->postParam("te"); $te = (int)$this->postParam("te");
$user = $this->postParam("user"); $user = addslashes($this->postParam("user"));
if ($where) {
$where = explode(";", $where)[0];
}
if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user) if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user)
$this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]); $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]);
@ -256,12 +259,7 @@ final class NoSpamPresenter extends OpenVKPresenter
} }
$rows = []; $rows = [];
if ($ip || $useragent || $ts || $te || $user) {
$rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user);
}
if (count($rows) === 0) {
if (!$searchTerm) {
if (str_starts_with($where, " AND")) { if (str_starts_with($where, " AND")) {
if ($searchTerm && !$this->postParam("where")) { if ($searchTerm && !$this->postParam("where")) {
$where = substr_replace($where, "", 0, strlen(" AND")); $where = substr_replace($where, "", 0, strlen(" AND"));
@ -270,6 +268,9 @@ final class NoSpamPresenter extends OpenVKPresenter
} }
} }
if ($ip || $useragent || $ts || $te || $user) {
$rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user);
} else {
if (!$where) { if (!$where) {
$rows = []; $rows = [];
} else { } else {
@ -277,7 +278,6 @@ final class NoSpamPresenter extends OpenVKPresenter
$rows = $result->fetchAll(); $rows = $result->fetchAll();
} }
} }
}
if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) { if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) {
foreach ($rows as $key => $object) { foreach ($rows as $key => $object) {
@ -347,11 +347,18 @@ final class NoSpamPresenter extends OpenVKPresenter
} }
if (in_array((int)$this->postParam("ban"), [2, 3])) { if (in_array((int)$this->postParam("ban"), [2, 3])) {
$reason = mb_strlen(trim($this->postParam("ban_reason"))) > 0 ? addslashes($this->postParam("ban_reason")) : ("**content-noSpamTemplate-" . $log->getId() . "**");
$is_forever = (string)$this->postParam("is_forever") === "true";
$unban_time = $is_forever ? 0 : (int)$this->postParam("unban_time") ?? NULL;
if ($owner) { if ($owner) {
$_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId());
if (!in_array($_id, $banned_ids)) { if (!in_array($_id, $banned_ids)) {
if ($owner instanceof User) { if ($owner instanceof User) {
$owner->ban("**content-noSpamTemplate-" . $log->getId() . "**", false, time() + $owner->getNewBanTime(), $this->user->id); if (!$unban_time && !$is_forever)
$unban_time = time() + $owner->getNewBanTime();
$owner->ban($reason, false, $unban_time, $this->user->id);
} else { } else {
$owner->ban("Подозрительная активность"); $owner->ban("Подозрительная активность");
} }

View file

@ -109,7 +109,7 @@ final class NotesPresenter extends OpenVKPresenter
if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted()) if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted())
$this->notFound(); $this->notFound();
if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
$this->template->note = $note; $this->template->note = $note;
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
@ -137,11 +137,11 @@ final class NotesPresenter extends OpenVKPresenter
if(!$note) $this->notFound(); if(!$note) $this->notFound();
if($note->getOwner()->getId() . "_" . $note->getId() !== $owner . "_" . $id || $note->isDeleted()) $this->notFound(); if($note->getOwner()->getId() . "_" . $note->getId() !== $owner . "_" . $id || $note->isDeleted()) $this->notFound();
if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity)) if(is_null($this->user) || !$note->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
$name = $note->getName(); $name = $note->getName();
$note->delete(); $note->delete();
$this->flash("succ", "Заметка удалена", "Заметка \"$name\" была успешно удалена."); $this->flash("succ", tr("note_is_deleted"), tr("note_x_is_now_deleted", $name));
$this->redirect("/notes" . $this->user->id); $this->redirect("/notes" . $this->user->id);
} }
} }

View file

@ -198,6 +198,9 @@ abstract class OpenVKPresenter extends SimplePresenter
{ {
$user = Authenticator::i()->getUser(); $user = Authenticator::i()->getUser();
if(!$this->template)
$this->template = new \stdClass;
$this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false;
$this->template->isTimezoned = Session::i()->get("_timezoneOffset"); $this->template->isTimezoned = Session::i()->get("_timezoneOffset");

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\{Club, Photo, Album}; use openvk\Web\Models\Entities\{Club, Photo, Album, User};
use openvk\Web\Models\Repositories\{Photos, Albums, Users, Clubs}; use openvk\Web\Models\Repositories\{Photos, Albums, Users, Clubs};
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
@ -27,7 +27,7 @@ final class PhotosPresenter extends OpenVKPresenter
if(!$user) $this->notFound(); if(!$user) $this->notFound();
if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL) || !$user->canBeViewedBy($this->user->identity)) if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL) || !$user->canBeViewedBy($this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); $this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$this->template->albums = $this->albums->getUserAlbums($user, $this->queryParam("p") ?? 1); $this->template->albums = $this->albums->getUserAlbums($user, (int)($this->queryParam("p") ?? 1));
$this->template->count = $this->albums->getUserAlbumsCount($user); $this->template->count = $this->albums->getUserAlbumsCount($user);
$this->template->owner = $user; $this->template->owner = $user;
$this->template->canEdit = false; $this->template->canEdit = false;
@ -36,7 +36,7 @@ final class PhotosPresenter extends OpenVKPresenter
} else { } else {
$club = (new Clubs)->get(abs($owner)); $club = (new Clubs)->get(abs($owner));
if(!$club) $this->notFound(); if(!$club) $this->notFound();
$this->template->albums = $this->albums->getClubAlbums($club, $this->queryParam("p") ?? 1); $this->template->albums = $this->albums->getClubAlbums($club, (int)($this->queryParam("p") ?? 1));
$this->template->count = $this->albums->getClubAlbumsCount($club); $this->template->count = $this->albums->getClubAlbumsCount($club);
$this->template->owner = $club; $this->template->owner = $club;
$this->template->canEdit = false; $this->template->canEdit = false;
@ -46,7 +46,7 @@ final class PhotosPresenter extends OpenVKPresenter
$this->template->paginatorConf = (object) [ $this->template->paginatorConf = (object) [
"count" => $this->template->count, "count" => $this->template->count,
"page" => $this->queryParam("p") ?? 1, "page" => (int)($this->queryParam("p") ?? 1),
"amount" => NULL, "amount" => NULL,
"perPage" => OPENVK_DEFAULT_PER_PAGE, "perPage" => OPENVK_DEFAULT_PER_PAGE,
]; ];
@ -94,7 +94,7 @@ final class PhotosPresenter extends OpenVKPresenter
if(!$album) $this->notFound(); if(!$album) $this->notFound();
if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound();
if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity) || $album->isDeleted()) if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity) || $album->isDeleted())
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
$this->template->album = $album; $this->template->album = $album;
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
@ -106,7 +106,7 @@ final class PhotosPresenter extends OpenVKPresenter
$album->setEdited(time()); $album->setEdited(time());
$album->save(); $album->save();
$this->flash("succ", "Изменения сохранены", "Новые данные приняты."); $this->flash("succ", tr("changes_saved"), tr("new_data_accepted"));
} }
} }
@ -120,13 +120,13 @@ final class PhotosPresenter extends OpenVKPresenter
if(!$album) $this->notFound(); if(!$album) $this->notFound();
if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound(); if($album->getPrettyId() !== $owner . "_" . $id || $album->isDeleted()) $this->notFound();
if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
$name = $album->getName(); $name = $album->getName();
$owner = $album->getOwner(); $owner = $album->getOwner();
$album->delete(); $album->delete();
$this->flash("succ", "Альбом удалён", "Альбом $name был успешно удалён."); $this->flash("succ", tr("album_is_deleted"), tr("album_x_is_deleted", $name));
$this->redirect("/albums" . ($owner instanceof Club ? "-" : "") . $owner->getId()); $this->redirect("/albums" . ($owner instanceof Club ? "-" : "") . $owner->getId());
} }
@ -150,7 +150,7 @@ final class PhotosPresenter extends OpenVKPresenter
$this->template->photos = iterator_to_array( $album->getPhotos( (int) ($this->queryParam("p") ?? 1), 20) ); $this->template->photos = iterator_to_array( $album->getPhotos( (int) ($this->queryParam("p") ?? 1), 20) );
$this->template->paginatorConf = (object) [ $this->template->paginatorConf = (object) [
"count" => $album->getPhotosCount(), "count" => $album->getPhotosCount(),
"page" => $this->queryParam("p") ?? 1, "page" => (int)($this->queryParam("p") ?? 1),
"amount" => sizeof($this->template->photos), "amount" => sizeof($this->template->photos),
"perPage" => 20, "perPage" => 20,
"atBottom" => true "atBottom" => true
@ -209,13 +209,13 @@ final class PhotosPresenter extends OpenVKPresenter
$photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
if(!$photo) $this->notFound(); if(!$photo) $this->notFound();
if(is_null($this->user) || $this->user->id != $ownerId) if(is_null($this->user) || $this->user->id != $ownerId)
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
$photo->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $photo->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc"));
$photo->save(); $photo->save();
$this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с фоткой."); $this->flash("succ", tr("changes_saved"), tr("new_description_will_appear"));
$this->redirect("/photo" . $photo->getPrettyId()); $this->redirect("/photo" . $photo->getPrettyId());
} }
@ -225,39 +225,82 @@ final class PhotosPresenter extends OpenVKPresenter
function renderUploadPhoto(): void function renderUploadPhoto(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction(true);
if(is_null($this->queryParam("album")))
$this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>DELETED</b>.");
if(is_null($this->queryParam("album"))) {
$album = $this->albums->getUserWallAlbum($this->user->identity);
} else {
[$owner, $id] = explode("_", $this->queryParam("album")); [$owner, $id] = explode("_", $this->queryParam("album"));
$album = $this->albums->get((int) $id); $album = $this->albums->get((int) $id);
}
if(!$album) if(!$album)
$this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>DELETED</b>."); $this->flashFail("err", tr("error"), tr("error_adding_to_deleted"), 500, true);
if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); # Для быстрой загрузки фоток из пикера фотографий нужен альбом, но юзер не может загружать фото
# в системные альбомы, так что так.
if(is_null($this->user) || !is_null($this->queryParam("album")) && !$album->canBeModifiedBy($this->user->identity))
$this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), 500, true);
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!isset($_FILES["blob"])) if($this->queryParam("act") == "finish") {
$this->flashFail("err", "Нету фотографии", "Выберите файл."); $result = json_decode($this->postParam("photos"), true);
foreach($result as $photoId => $description) {
$phot = $this->photos->get($photoId);
if(!$phot || $phot->isDeleted() || $phot->getOwner()->getId() != $this->user->id)
continue;
if(iconv_strlen($description) > 255)
$this->flashFail("err", tr("error"), tr("description_too_long"), 500, true);
$phot->setDescription($description);
$phot->save();
$album = $phot->getAlbum();
}
$this->returnJson(["success" => true,
"album" => $album->getId(),
"owner" => $album->getOwner() instanceof User ? $album->getOwner()->getId() : $album->getOwner()->getId() * -1]);
}
if(!isset($_FILES))
$this->flashFail("err", tr("no_photo"), tr("select_file"), 500, true);
$photos = [];
if((int)$this->postParam("count") > 10)
$this->flashFail("err", tr("no_photo"), "ты еблан", 500, true);
for($i = 0; $i < $this->postParam("count"); $i++) {
try { try {
$photo = new Photo; $photo = new Photo;
$photo->setOwner($this->user->id); $photo->setOwner($this->user->id);
$photo->setDescription($this->postParam("desc")); $photo->setDescription("");
$photo->setFile($_FILES["blob"]); $photo->setFile($_FILES["photo_".$i]);
$photo->setCreated(time()); $photo->setCreated(time());
$photo->save(); $photo->save();
$photos[] = [
"url" => $photo->getURLBySizeId("tiny"),
"id" => $photo->getId(),
"vid" => $photo->getVirtualId(),
"owner" => $photo->getOwner()->getId(),
"link" => $photo->getURL()
];
} catch(ISE $ex) { } catch(ISE $ex) {
$name = $album->getName(); $name = $album->getName();
$this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>$name</b>."); $this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в $name.", 500, true);
} }
$album->addPhoto($photo); $album->addPhoto($photo);
$album->setEdited(time()); $album->setEdited(time());
$album->save(); $album->save();
}
$this->redirect("/photo" . $photo->getPrettyId() . "?from=album" . $album->getId()); $this->returnJson(["success" => true,
"photos" => $photos]);
} else { } else {
$this->template->album = $album; $this->template->album = $album;
} }
@ -273,7 +316,7 @@ final class PhotosPresenter extends OpenVKPresenter
if(!$album || !$photo) $this->notFound(); if(!$album || !$photo) $this->notFound();
if(!$album->hasPhoto($photo)) $this->notFound(); if(!$album->hasPhoto($photo)) $this->notFound();
if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity)) if(is_null($this->user) || !$album->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
$this->assertNoCSRF(); $this->assertNoCSRF();
@ -281,7 +324,7 @@ final class PhotosPresenter extends OpenVKPresenter
$album->setEdited(time()); $album->setEdited(time());
$album->save(); $album->save();
$this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); $this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc"));
$this->redirect("/album" . $album->getPrettyId()); $this->redirect("/album" . $album->getPrettyId());
} }
} }
@ -289,18 +332,26 @@ final class PhotosPresenter extends OpenVKPresenter
function renderDeletePhoto(int $ownerId, int $photoId): void function renderDeletePhoto(int $ownerId, int $photoId): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction($_SERVER["REQUEST_METHOD"] === "POST");
$this->assertNoCSRF(); $this->assertNoCSRF();
$photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
if(!$photo) $this->notFound(); if(!$photo) $this->notFound();
if(is_null($this->user) || $this->user->id != $ownerId) if(is_null($this->user) || $this->user->id != $ownerId)
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
if(!is_null($album = $photo->getAlbum()))
$redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId;
else
$redirect = "/id0";
$photo->isolate(); $photo->isolate();
$photo->delete(); $photo->delete();
$this->flash("succ", "Фотография удалена", "Эта фотография была успешно удалена."); if($_SERVER["REQUEST_METHOD"] === "POST")
$this->redirect("/id0"); $this->returnJson(["success" => true]);
$this->flash("succ", tr("photo_is_deleted"), tr("photo_is_deleted_desc"));
$this->redirect($redirect);
} }
} }

View file

@ -23,7 +23,7 @@ final class ReportPresenter extends OpenVKPresenter
if ($_SERVER["REQUEST_METHOD"] === "POST") if ($_SERVER["REQUEST_METHOD"] === "POST")
$this->assertNoCSRF(); $this->assertNoCSRF();
$act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL; $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]) ? $this->queryParam("act") : NULL;
if (!$this->queryParam("orig")) { if (!$this->queryParam("orig")) {
$this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST");
@ -88,7 +88,7 @@ final class ReportPresenter extends OpenVKPresenter
if(!$id) if(!$id)
exit(json_encode([ "error" => tr("error_segmentation") ])); exit(json_encode([ "error" => tr("error_segmentation") ]));
if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) { if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) {
if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) {
$report = new Report; $report = new Report;
$report->setUser_id($this->user->id); $report->setUser_id($this->user->id);
@ -118,22 +118,22 @@ final class ReportPresenter extends OpenVKPresenter
$report->deleteContent(); $report->deleteContent();
$report->banUser($this->user->identity->getId()); $report->banUser($this->user->identity->getId());
$this->flash("suc", "Смэрть...", "Пользователь успешно забанен."); $this->flash("suc", tr("death"), tr("user_successfully_banned"));
} else if ($this->postParam("delete")) { } else if ($this->postParam("delete")) {
$report->deleteContent(); $report->deleteContent();
$this->flash("suc", "Нехай живе!", "Контент удалён, а пользователю прилетело предупреждение."); $this->flash("suc", tr("nehay"), tr("content_is_deleted"));
} else if ($this->postParam("ignore")) { } else if ($this->postParam("ignore")) {
$report->delete(); $report->delete();
$this->flash("suc", "Нехай живе!", "Жалоба проигнорирована."); $this->flash("suc", tr("nehay"), tr("report_is_ignored"));
} else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) { } else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) {
if ($report->getContentType() !== "group") if ($report->getContentType() !== "group")
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
$club = $report->getContentObject(); $club = $report->getContentObject();
if (!$club || $club->isBanned()) if (!$club || $club->isBanned())
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
if ($this->postParam("banClubOwner")) { if ($this->postParam("banClubOwner")) {
$club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId()); $club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId());
@ -143,7 +143,7 @@ final class ReportPresenter extends OpenVKPresenter
$report->delete(); $report->delete();
$this->flash("suc", "Смэрть...", ($this->postParam("banClubOwner") ? "Создатель сообщества успешно забанен." : "Сообщество успешно забанено")); $this->flash("suc", tr("death"), ($this->postParam("banClubOwner") ? tr("group_owner_is_banned") : tr("group_is_banned")));
} }
$this->redirect("/scumfeed"); $this->redirect("/scumfeed");

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\{User, Club}; use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes}; use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes, Audios};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
final class SearchPresenter extends OpenVKPresenter final class SearchPresenter extends OpenVKPresenter
@ -13,6 +13,7 @@ final class SearchPresenter extends OpenVKPresenter
private $videos; private $videos;
private $apps; private $apps;
private $notes; private $notes;
private $audios;
function __construct(Users $users, Clubs $clubs) function __construct(Users $users, Clubs $clubs)
{ {
@ -23,22 +24,21 @@ final class SearchPresenter extends OpenVKPresenter
$this->videos = new Videos; $this->videos = new Videos;
$this->apps = new Applications; $this->apps = new Applications;
$this->notes = new Notes; $this->notes = new Notes;
$this->audios = new Audios;
parent::__construct(); parent::__construct();
} }
function renderIndex(): void function renderIndex(): void
{ {
$this->assertUserLoggedIn();
$query = $this->queryParam("query") ?? ""; $query = $this->queryParam("query") ?? "";
$type = $this->queryParam("type") ?? "users"; $type = $this->queryParam("type") ?? "users";
$sorter = $this->queryParam("sort") ?? "id"; $sorter = $this->queryParam("sort") ?? "id";
$invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC"; $invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC";
$page = (int) ($this->queryParam("p") ?? 1); $page = (int) ($this->queryParam("p") ?? 1);
$this->willExecuteWriteAction();
if($query != "")
$this->assertUserLoggedIn();
# https://youtu.be/pSAWM5YuXx8 # https://youtu.be/pSAWM5YuXx8
$repos = [ $repos = [
@ -47,7 +47,7 @@ final class SearchPresenter extends OpenVKPresenter
"posts" => "posts", "posts" => "posts",
"comments" => "comments", "comments" => "comments",
"videos" => "videos", "videos" => "videos",
"audios" => "posts", "audios" => "audios",
"apps" => "apps", "apps" => "apps",
"notes" => "notes" "notes" => "notes"
]; ];
@ -63,6 +63,16 @@ final class SearchPresenter extends OpenVKPresenter
case "rating": case "rating":
$sort = "rating " . $invert; $sort = "rating " . $invert;
break; break;
case "length":
if($type != "audios") break;
$sort = "length " . $invert;
break;
case "listens":
if($type != "audios") break;
$sort = "listens " . $invert;
break;
} }
$parameters = [ $parameters = [
@ -88,17 +98,20 @@ final class SearchPresenter extends OpenVKPresenter
"after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL, "after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL,
"gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL, "gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL,
"doNotSearchPrivate" => true, "doNotSearchPrivate" => true,
"only_performers" => $this->queryParam("only_performers") == "on" ? "1" : NULL,
"with_lyrics" => $this->queryParam("with_lyrics") == "on" ? true : 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, $parameters, $sort); $results = $this->{$repo}->find($query, $parameters, $sort);
$iterator = $results->page($page); $iterator = $results->page($page, 14);
$count = $results->size(); $count = $results->size();
$this->template->iterator = iterator_to_array($iterator); $this->template->iterator = iterator_to_array($iterator);
$this->template->count = $count; $this->template->count = $count;
$this->template->type = $type; $this->template->type = $type;
$this->template->page = $page; $this->template->page = $page;
$this->template->perPage = 14;
} }
} }

View file

@ -67,7 +67,7 @@ final class SupportPresenter extends OpenVKPresenter
$this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id); $this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id);
if($this->template->mode === "list") { if($this->template->mode === "list") {
$this->template->page = (int) ($this->queryParam("p") ?? 1); $this->template->page = (int) ($this->queryParam("p") ?? 1);
$this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page); $this->template->tickets = iterator_to_array($this->tickets->getTicketsByUserId($this->user->id, $this->template->page));
} }
if($this->template->mode === "new") if($this->template->mode === "new")

View file

@ -111,7 +111,7 @@ final class TopicsPresenter extends OpenVKPresenter
$video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $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", tr("error_when_publishing_comment"), tr("error_comment_file_too_big"));
$this->redirect("/topic" . $topic->getPrettyId()); $this->redirect("/topic" . $topic->getPrettyId());
} }
@ -126,7 +126,7 @@ final class TopicsPresenter extends OpenVKPresenter
$comment->setFlags($flags); $comment->setFlags($flags);
$comment->save(); $comment->save();
} catch (\LengthException $ex) { } catch (\LengthException $ex) {
$this->flash("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой."); $this->flash("err", tr("error_when_publishing_comment"), tr("error_comment_too_big"));
$this->redirect("/topic" . $topic->getPrettyId()); $this->redirect("/topic" . $topic->getPrettyId());
} }

View file

@ -5,7 +5,7 @@ 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};
use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications}; use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Audios};
use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator; use openvk\Web\Util\Validator;
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
@ -49,6 +49,9 @@ final class UserPresenter extends OpenVKPresenter
$this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->videosCount = (new Videos)->getUserVideosCount($user);
$this->template->notes = (new Notes)->getUserNotes($user, 1, 4); $this->template->notes = (new Notes)->getUserNotes($user, 1, 4);
$this->template->notesCount = (new Notes)->getUserNotesCount($user); $this->template->notesCount = (new Notes)->getUserNotesCount($user);
$this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($user->getId());
$this->template->audiosCount = (new Audios)->getUserCollectionSize($user);
$this->template->audioStatus = $user->getCurrentAudioStatus();
$this->template->user = $user; $this->template->user = $user;
} }
@ -59,7 +62,7 @@ final class UserPresenter extends OpenVKPresenter
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$user = $this->users->get($id); $user = $this->users->get($id);
$page = abs($this->queryParam("p") ?? 1); $page = abs((int)($this->queryParam("p") ?? 1));
if(!$user) if(!$user)
$this->notFound(); $this->notFound();
elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL) || !$user->canBeViewedBy($this->user->identity)) elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL) || !$user->canBeViewedBy($this->user->identity))
@ -76,7 +79,7 @@ final class UserPresenter extends OpenVKPresenter
if(!is_null($this->user)) { if(!is_null($this->user)) {
if($this->template->mode !== "friends" && $this->user->id !== $id) { if($this->template->mode !== "friends" && $this->user->id !== $id) {
$name = $user->getFullName(); $name = $user->getFullName();
$this->flash("err", "Ошибка доступа", "Вы не можете просматривать полный список подписок $name."); $this->flash("err", tr("error_access_denied_short"), tr("error_viewing_subs", $name));
$this->redirect($user->getURL()); $this->redirect($user->getURL());
} }
@ -111,11 +114,11 @@ final class UserPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
if(!$club->canBeModifiedBy($this->user->identity ?? NULL)) if(!$club->canBeModifiedBy($this->user->identity ?? NULL))
$this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс.", NULL, true); $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"), NULL, true);
$isClubPinned = $this->user->identity->isClubPinned($club); $isClubPinned = $this->user->identity->isClubPinned($club);
if(!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10) if(!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10)
$this->flashFail("err", "Ошибка", "Находится в левом меню могут максимум 10 групп", NULL, true); $this->flashFail("err", tr("error"), tr("error_max_pinned_clubs"), NULL, true);
if($club->getOwner()->getId() === $this->user->identity->getId()) { if($club->getOwner()->getId() === $this->user->identity->getId()) {
$club->setOwner_Club_Pinned(!$isClubPinned); $club->setOwner_Club_Pinned(!$isClubPinned);
@ -168,11 +171,25 @@ final class UserPresenter extends OpenVKPresenter
if ($this->postParam("marialstatus") <= 8 && $this->postParam("marialstatus") >= 0) if ($this->postParam("marialstatus") <= 8 && $this->postParam("marialstatus") >= 0)
$user->setMarital_Status($this->postParam("marialstatus")); $user->setMarital_Status($this->postParam("marialstatus"));
if ($this->postParam("maritalstatus-user")) {
if (in_array((int) $this->postParam("marialstatus"), [0, 1, 8])) {
$user->setMarital_Status_User(NULL);
} else {
$mUser = (new Users)->getByAddress($this->postParam("maritalstatus-user"));
if ($mUser) {
if ($mUser->getId() !== $this->user->id) {
$user->setMarital_Status_User($mUser->getId());
}
}
}
}
if ($this->postParam("politViews") <= 9 && $this->postParam("politViews") >= 0) if ($this->postParam("politViews") <= 9 && $this->postParam("politViews") >= 0)
$user->setPolit_Views($this->postParam("politViews")); $user->setPolit_Views($this->postParam("politViews"));
if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0) if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0)
$user->setSex($this->postParam("gender")); $user->setSex($this->postParam("gender"));
$user->setAudio_broadcast_enabled($this->checkbox("broadcast_music"));
if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) { if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) {
if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"]) if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"])
@ -241,10 +258,11 @@ final class UserPresenter extends OpenVKPresenter
} 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"));
$this->flashFail("err", "Ошибка", "Статус слишком длинный ($statusLength символов вместо 255 символов)", NULL, true); $this->flashFail("err", tr("error"), tr("error_status_too_long", $statusLength), NULL, true);
} }
$user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status"));
$user->setAudio_broadcast_enabled($this->postParam("broadcast") == 1);
$user->save(); $user->save();
$this->returnJson([ $this->returnJson([
@ -285,7 +303,7 @@ final class UserPresenter extends OpenVKPresenter
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!$user->verifyNumber($this->postParam("code") ?? 0)) if(!$user->verifyNumber($this->postParam("code") ?? 0))
$this->flashFail("err", "Ошибка", "Не удалось подтвердить номер телефона: неверный код."); $this->flashFail("err", tr("error"), tr("invalid_code"));
$this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment"));
} }
@ -434,10 +452,11 @@ final class UserPresenter extends OpenVKPresenter
"friends.add", "friends.add",
"wall.write", "wall.write",
"messages.write", "messages.write",
"audios.read",
]; ];
foreach($settings as $setting) { foreach($settings as $setting) {
$input = $this->postParam(str_replace(".", "_", $setting)); $input = $this->postParam(str_replace(".", "_", $setting));
$user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting)))); $user->setPrivacySetting($setting, min(3, (int)abs((int)$input ?? $user->getPrivacySetting($setting))));
} }
$prof = $this->postParam("profile_type") == 1 || $this->postParam("profile_type") == 0 ? (int)$this->postParam("profile_type") : 0; $prof = $this->postParam("profile_type") == 1 || $this->postParam("profile_type") == 0 ? (int)$this->postParam("profile_type") : 0;
@ -482,6 +501,7 @@ final class UserPresenter extends OpenVKPresenter
} else if($_GET['act'] === "lMenu") { } else if($_GET['act'] === "lMenu") {
$settings = [ $settings = [
"menu_bildoj" => "photos", "menu_bildoj" => "photos",
"menu_muziko" => "audios",
"menu_filmetoj" => "videos", "menu_filmetoj" => "videos",
"menu_mesagoj" => "messages", "menu_mesagoj" => "messages",
"menu_notatoj" => "notes", "menu_notatoj" => "notes",
@ -489,6 +509,7 @@ final class UserPresenter extends OpenVKPresenter
"menu_novajoj" => "news", "menu_novajoj" => "news",
"menu_ligiloj" => "links", "menu_ligiloj" => "links",
"menu_standardo" => "poster", "menu_standardo" => "poster",
"menu_aplikoj" => "apps"
]; ];
foreach($settings as $checkbox => $setting) foreach($settings as $checkbox => $setting)
$user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox)); $user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox));

View file

@ -233,8 +233,13 @@ final class VKAPIPresenter extends OpenVKPresenter
$this->badMethodCall($object, $method, $parameter->getName()); $this->badMethodCall($object, $method, $parameter->getName());
} }
try {
settype($val, $parameter->getType()->getName()); settype($val, $parameter->getType()->getName());
$params[] = $val; $params[] = $val;
} catch (\Throwable $e) {
// Just ignore the exception, since
// some args are intended for internal use
}
} }
define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false); define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false);

View file

@ -59,7 +59,7 @@ final class VideosPresenter extends OpenVKPresenter
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading'])
$this->flashFail("err", tr("error"), "Video uploads are disabled by the system administrator."); $this->flashFail("err", tr("error"), tr("video_uploads_disabled"));
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!empty($this->postParam("name"))) { if(!empty($this->postParam("name"))) {
@ -75,18 +75,18 @@ final class VideosPresenter extends OpenVKPresenter
else if(!empty($this->postParam("link"))) else if(!empty($this->postParam("link")))
$video->setLink($this->postParam("link")); $video->setLink($this->postParam("link"));
else else
$this->flashFail("err", "Нету видеозаписи", "Выберите файл или укажите ссылку."); $this->flashFail("err", tr("no_video_error"), tr("no_video_description"));
} catch(\DomainException $ex) { } catch(\DomainException $ex) {
$this->flashFail("err", "Произошла ошибка", "Файл повреждён или не содержит видео." ); $this->flashFail("err", tr("error_video"), tr("file_corrupted"));
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", "Произошла ошибка", "Возможно, ссылка некорректна."); $this->flashFail("err", tr("error_video"), tr("link_incorrect"));
} }
$video->save(); $video->save();
$this->redirect("/video" . $video->getPrettyId()); $this->redirect("/video" . $video->getPrettyId());
} else { } else {
$this->flashFail("err", "Произошла ошибка", "Видео не может быть опубликовано без названия."); $this->flashFail("err", tr("error_video"), tr("no_name_error"));
} }
} }
} }
@ -100,14 +100,14 @@ final class VideosPresenter extends OpenVKPresenter
if(!$video) if(!$video)
$this->notFound(); $this->notFound();
if(is_null($this->user) || $this->user->id !== $owner) if(is_null($this->user) || $this->user->id !== $owner)
$this->flashFail("err", "Ошибка доступа", "Вы не имеете права редактировать этот ресурс."); $this->flashFail("err", tr("access_denied_error"), tr("access_denied_error_description"));
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
$video->setName(empty($this->postParam("name")) ? NULL : $this->postParam("name")); $video->setName(empty($this->postParam("name")) ? NULL : $this->postParam("name"));
$video->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc")); $video->setDescription(empty($this->postParam("desc")) ? NULL : $this->postParam("desc"));
$video->save(); $video->save();
$this->flash("succ", "Изменения сохранены", "Обновлённое описание появится на странице с видосиком."); $this->flash("succ", tr("changes_saved"), tr("changes_saved_video_comment"));
$this->redirect("/video" . $video->getPrettyId()); $this->redirect("/video" . $video->getPrettyId());
} }
@ -129,9 +129,29 @@ final class VideosPresenter extends OpenVKPresenter
$video->deleteVideo($owner, $vid); $video->deleteVideo($owner, $vid);
} }
} else { } else {
$this->flashFail("err", "Не удалось удалить пост", "Вы не вошли в аккаунт."); $this->flashFail("err", tr("cant_delete_video"), tr("cant_delete_video_comment"));
} }
$this->redirect("/videos" . $owner); $this->redirect("/videos" . $owner);
} }
function renderLike(int $owner, int $video_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->assertNoCSRF();
$video = $this->videos->getByOwnerAndVID($owner, $video_id);
if(!$video || $video->isDeleted() || $video->getOwner()->isDeleted()) $this->notFound();
if(method_exists($video, "canBeViewedBy") && !$video->canBeViewedBy($this->user->identity)) {
$this->flashFail("err", tr("error"), tr("forbidden"));
}
if(!is_null($this->user)) {
$video->toggleLike($this->user->identity);
}
$this->returnJson(["success" => true]);
}
} }

View file

@ -2,8 +2,8 @@
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification}; use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification, PostAcceptedNotification, NewSuggestedPostsNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes}; use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
use Bhaktaraz\RSSGenerator\Item; use Bhaktaraz\RSSGenerator\Item;
@ -67,10 +67,31 @@ final class WallPresenter extends OpenVKPresenter
if($user < 0) if($user < 0)
$this->template->club = $owner; $this->template->club = $owner;
$iterator = NULL;
$count = 0;
$type = $this->queryParam("type") ?? "all";
switch($type) {
default:
case "all":
$iterator = $this->posts->getPostsFromUsersWall($user, (int) ($_GET["p"] ?? 1));
$count = $this->posts->getPostCountOnUserWall($user);
break;
case "owners":
$iterator = $this->posts->getOwnersPostsFromWall($user, (int) ($_GET["p"] ?? 1));
$count = $this->posts->getOwnersCountOnUserWall($user);
break;
case "others":
$iterator = $this->posts->getOthersPostsFromWall($user, (int) ($_GET["p"] ?? 1));
$count = $this->posts->getOthersCountOnUserWall($user);
break;
}
$this->template->owner = $user; $this->template->owner = $user;
$this->template->canPost = $canPost; $this->template->canPost = $canPost;
$this->template->count = $this->posts->getPostCountOnUserWall($user); $this->template->count = $count;
$this->template->posts = iterator_to_array($this->posts->getPostsFromUsersWall($user, (int) ($_GET["p"] ?? 1))); $this->template->type = $type;
$this->template->posts = iterator_to_array($iterator);
$this->template->paginatorConf = (object) [ $this->template->paginatorConf = (object) [
"count" => $this->template->count, "count" => $this->template->count,
"page" => (int) ($_GET["p"] ?? 1), "page" => (int) ($_GET["p"] ?? 1),
@ -150,11 +171,12 @@ final class WallPresenter extends OpenVKPresenter
->select("id") ->select("id")
->where("wall IN (?)", $ids) ->where("wall IN (?)", $ids)
->where("deleted", 0) ->where("deleted", 0)
->where("suggested", 0)
->order("created DESC"); ->order("created DESC");
$this->template->paginatorConf = (object) [ $this->template->paginatorConf = (object) [
"count" => sizeof($posts), "count" => sizeof($posts),
"page" => (int) ($_GET["p"] ?? 1), "page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($posts->page((int) ($_GET["p"] ?? 1), $perPage)), "amount" => $posts->page((int) ($_GET["p"] ?? 1), $perPage)->count(),
"perPage" => $perPage, "perPage" => $perPage,
]; ];
$this->template->posts = []; $this->template->posts = [];
@ -170,7 +192,7 @@ final class WallPresenter extends OpenVKPresenter
$pPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50); $pPage = min((int) ($_GET["posts"] ?? OPENVK_DEFAULT_PER_PAGE), 50);
$queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)"; $queryBase = "FROM `posts` LEFT JOIN `groups` ON GREATEST(`posts`.`wall`, 0) = 0 AND `groups`.`id` = ABS(`posts`.`wall`) LEFT JOIN `profiles` ON LEAST(`posts`.`wall`, 0) = 0 AND `profiles`.`id` = ABS(`posts`.`wall`)";
$queryBase .= "WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0"; # AND `posts`.`suggested` = 0 $queryBase .= "WHERE (`groups`.`hide_from_global_feed` = 0 OR `groups`.`name` IS NULL) AND (`profiles`.`profile_type` = 0 OR `profiles`.`first_name` IS NULL) AND `posts`.`deleted` = 0 AND `posts`.`suggested` = 0";
if($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT) if($this->user->identity->getNsfwTolerance() === User::NSFW_INTOLERANT)
$queryBase .= " AND `nsfw` = 0"; $queryBase .= " AND `nsfw` = 0";
@ -183,7 +205,7 @@ final class WallPresenter extends OpenVKPresenter
$this->template->paginatorConf = (object) [ $this->template->paginatorConf = (object) [
"count" => $count, "count" => $count,
"page" => (int) ($_GET["p"] ?? 1), "page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($posts), "amount" => $posts->getRowCount(),
"perPage" => $pPage, "perPage" => $pPage,
]; ];
foreach($posts as $post) foreach($posts as $post)
@ -233,9 +255,6 @@ 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);
@ -253,23 +272,23 @@ final class WallPresenter extends OpenVKPresenter
if($this->postParam("force_sign") === "on") if($this->postParam("force_sign") === "on")
$flags |= 0b01000000; $flags |= 0b01000000;
try { $photos = [];
$photo = NULL;
$video = NULL;
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
$album = NULL;
if(!$anon && $wall > 0 && $wall === $this->user->id)
$album = (new Albums)->getUserWallAlbum($wallOwner);
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon); if(!empty($this->postParam("photos"))) {
$un = rtrim($this->postParam("photos"), ",");
$arr = explode(",", $un);
if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$photo = (new Photos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$photo || $photo->isDeleted())
continue;
$photos[] = $photo;
}
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
$video = Video::fastMake($this->user->id, $_FILES["_vid_attachment"]["name"], $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
} catch(\DomainException $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
} catch(ISE $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
} }
try { try {
@ -297,7 +316,45 @@ final class WallPresenter extends OpenVKPresenter
} }
} }
if(empty($this->postParam("text")) && !$photo && !$video && !$poll && !$note) $videos = [];
if(!empty($this->postParam("videos"))) {
$un = rtrim($this->postParam("videos"), ",");
$arr = explode(",", $un);
if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$video = (new Videos)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$video || $video->isDeleted())
continue;
$videos[] = $video;
}
}
}
$audios = [];
if(!empty($this->postParam("audios"))) {
$un = rtrim($this->postParam("audios"), ",");
$arr = explode(",", $un);
if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$audio || $audio->isDeleted())
continue;
$audios[] = $audio;
}
}
}
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1 && !$poll && !$note)
$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 {
@ -309,16 +366,21 @@ final class WallPresenter extends OpenVKPresenter
$post->setAnonymous($anon); $post->setAnonymous($anon);
$post->setFlags($flags); $post->setFlags($flags);
$post->setNsfw($this->postParam("nsfw") === "on"); $post->setNsfw($this->postParam("nsfw") === "on");
if($wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2)
$post->setSuggested(1);
$post->save(); $post->save();
} catch (\LengthException $ex) { } catch (\LengthException $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_too_big"));
} }
if(!is_null($photo)) foreach($photos as $photo)
$post->attach($photo); $post->attach($photo);
if(!is_null($video)) if(sizeof($videos) > 0)
$post->attach($video); foreach($videos as $vid)
$post->attach($vid);
if(!is_null($poll)) if(!is_null($poll))
$post->attach($poll); $post->attach($poll);
@ -326,6 +388,9 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($note)) if(!is_null($note))
$post->attach($note); $post->attach($note);
foreach($audios as $audio)
$post->attach($audio);
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();
@ -333,13 +398,33 @@ final class WallPresenter extends OpenVKPresenter
if($wall > 0) if($wall > 0)
$excludeMentions[] = $wall; $excludeMentions[] = $wall;
if($wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2) {
# Чтобы не было упоминаний из предложки
} else {
$mentions = iterator_to_array($post->resolveMentions($excludeMentions)); $mentions = iterator_to_array($post->resolveMentions($excludeMentions));
foreach($mentions as $mentionee) foreach($mentions as $mentionee)
if($mentionee instanceof User) if($mentionee instanceof User)
(new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit(); (new MentionNotification($mentionee, $post, $post->getOwner(), strip_tags($post->getText())))->emit();
}
if($wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2) {
$suggsCount = $this->posts->getSuggestedPostsCount($wallOwner->getId());
if($suggsCount % 10 == 0) {
$managers = $wallOwner->getManagers();
$owner = $wallOwner->getOwner();
(new NewSuggestedPostsNotification($owner, $wallOwner))->emit();
foreach($managers as $manager)
(new NewSuggestedPostsNotification($manager->getUser(), $wallOwner))->emit();
}
$this->redirect("/club".$wallOwner->getId()."/suggested");
} else {
$this->redirect($wallOwner->getURL()); $this->redirect($wallOwner->getURL());
} }
}
function renderPost(int $wall, int $post_id): void function renderPost(int $wall, int $post_id): void
{ {
@ -452,7 +537,7 @@ final class WallPresenter extends OpenVKPresenter
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
$post = $this->posts->getPostById($wall, $post_id); $post = $this->posts->getPostById($wall, $post_id, true);
if(!$post) if(!$post)
$this->notFound(); $this->notFound();
$user = $this->user->id; $user = $this->user->id;
@ -467,6 +552,9 @@ final class WallPresenter extends OpenVKPresenter
else $canBeDeletedByOtherUser = false; else $canBeDeletedByOtherUser = false;
if(!is_null($user)) { if(!is_null($user)) {
if($post->getTargetWall() < 0 && !$post->getWallOwner()->canBeModifiedBy($this->user->identity) && $post->getWallOwner()->getWallType() != 1 && $post->getSuggestionType() == 0)
$this->flashFail("err", tr("failed_to_delete_post"), tr("error_deleting_suggested"));
if($post->getOwnerPost() == $user || $post->getTargetWall() == $user || $canBeDeletedByOtherUser) { if($post->getOwnerPost() == $user || $post->getTargetWall() == $user || $canBeDeletedByOtherUser) {
$post->unwire(); $post->unwire();
$post->delete(); $post->delete();
@ -502,4 +590,153 @@ final class WallPresenter extends OpenVKPresenter
# TODO localize message based on language and ?act=(un)pin # TODO localize message based on language and ?act=(un)pin
$this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment")); $this->flashFail("succ", tr("information_-1"), tr("changes_saved_comment"));
} }
function renderEdit()
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if($_SERVER["REQUEST_METHOD"] !== "POST")
$this->redirect("/id0");
if($this->postParam("type") == "post")
$post = $this->posts->get((int)$this->postParam("postid"));
else
$post = (new Comments)->get((int)$this->postParam("postid"));
if(!$post || $post->isDeleted())
$this->returnJson(["error" => "Invalid post"]);
if(!$post->canBeEditedBy($this->user->identity))
$this->returnJson(["error" => "Access denied"]);
$attachmentsCount = sizeof(iterator_to_array($post->getChildren()));
if(empty($this->postParam("newContent")) && $attachmentsCount < 1)
$this->returnJson(["error" => "Empty post"]);
$post->setEdited(time());
try {
$post->setContent($this->postParam("newContent"));
} catch(\LengthException $e) {
$this->returnJson(["error" => $e->getMessage()]);
}
if($this->postParam("type") === "post") {
$post->setNsfw($this->postParam("nsfw") == "true");
$flags = 0;
if($post->getTargetWall() < 0 && $post->getWallOwner()->canBeModifiedBy($this->user->identity)) {
if($this->postParam("fromgroup") == "true") {
$flags |= 0b10000000;
$post->setFlags($flags);
} else
$post->setFlags($flags);
}
}
$post->save(true);
$this->returnJson(["error" => "no",
"new_content" => $post->getText(),
"new_edited" => (string)$post->getEditTime(),
"nsfw" => $this->postParam("type") === "post" ? (int)$post->isExplicit() : 0,
"from_group" => $this->postParam("type") === "post" && $post->getTargetWall() < 0 ?
((int)$post->isPostedOnBehalfOfGroup()) : "false",
"new_text" => $post->getText(false),
"author" => [
"name" => $post->getOwner()->getCanonicalName(),
"avatar" => $post->getOwner()->getAvatarUrl()
]]);
}
function renderAccept() {
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("Ты дебил, это точка апи.");
}
$id = $this->postParam("id");
$sign = $this->postParam("sign") == 1;
$content = $this->postParam("new_content");
$post = (new Posts)->get((int)$id);
if(!$post || $post->isDeleted())
$this->flashFail("err", "Error", tr("error_accepting_invalid_post"), NULL, true);
if($post->getSuggestionType() == 0)
$this->flashFail("err", "Error", tr("error_accepting_not_suggested_post"), NULL, true);
if($post->getSuggestionType() == 2)
$this->flashFail("err", "Error", tr("error_accepting_declined_post"), NULL, true);
if(!$post->canBePinnedBy($this->user->identity))
$this->flashFail("err", "Error", "Can't accept this post.", NULL, true);
$author = $post->getOwner();
$flags = 0;
$flags |= 0b10000000;
if($sign)
$flags |= 0b01000000;
$post->setSuggested(0);
$post->setCreated(time());
$post->setApi_Source_Name(NULL);
$post->setFlags($flags);
if(mb_strlen($content) > 0)
$post->setContent($content);
$post->save();
if($author->getId() != $this->user->id)
(new PostAcceptedNotification($author, $post, $post->getWallOwner()))->emit();
$this->returnJson([
"success" => true,
"id" => $post->getPrettyId(),
"new_count" => (new Posts)->getSuggestedPostsCount($post->getWallOwner()->getId())
]);
}
function renderDecline() {
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
if($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("Ты дебил, это метод апи.");
}
$id = $this->postParam("id");
$post = (new Posts)->get((int)$id);
if(!$post || $post->isDeleted())
$this->flashFail("err", "Error", tr("error_declining_invalid_post"), NULL, true);
if($post->getSuggestionType() == 0)
$this->flashFail("err", "Error", tr("error_declining_not_suggested_post"), NULL, true);
if($post->getSuggestionType() == 2)
$this->flashFail("err", "Error", tr("error_declining_declined_post"), NULL, true);
if(!$post->canBePinnedBy($this->user->identity))
$this->flashFail("err", "Error", "Can't decline this post.", NULL, true);
$post->setSuggested(2);
$post->setDeleted(1);
$post->save();
$this->returnJson([
"success" => true,
"new_count" => (new Posts)->getSuggestedPostsCount($post->getWallOwner()->getId())
]);
}
} }

View file

@ -13,9 +13,12 @@
<script src="/language/{php echo getLanguage()}.js" crossorigin="anonymous"></script> <script src="/language/{php echo getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"} {script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/jquery-ui/dist/jquery-ui.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"} {script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/l10n.js"} {script "js/l10n.js"}
{script "js/openvk.cls.js"} {script "js/openvk.cls.js"}
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
{script "js/al_music.js"}
{css "js/node_modules/tippy.js/dist/backdrop.css"} {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/border.css"}
@ -38,9 +41,8 @@
<body> <body>
<div id="sudo-banner" n:if="isset($thisUser) && $userTainted"> <div id="sudo-banner" n:if="isset($thisUser) && $userTainted">
<p> <p>
Вы вошли как <b>{$thisUser->getCanonicalName()}</b>. Пожалуйста, уважайте {_you_entered_as} <b>{$thisUser->getCanonicalName()}</b>. {_please_rights}
право на тайну переписки других людей и не злоупотребляйте подменой пользователя. {_click_on} <a href="/setSID/unset?hash={rawurlencode($csrfToken)}">{_there}</a>, {_to_leave}.
Нажмите <a href="/setSID/unset?hash={rawurlencode($csrfToken)}">здесь</a>, чтобы выйти.
</p> </p>
</div> </div>
@ -123,6 +125,7 @@
<option value="comments">{_s_by_comments}</option> <option value="comments">{_s_by_comments}</option>
<option value="videos">{_s_by_videos}</option> <option value="videos">{_s_by_videos}</option>
<option value="apps">{_s_by_apps}</option> <option value="apps">{_s_by_apps}</option>
<option value="audios">{_s_by_audios}</option>
</select> </select>
</form> </form>
<div class="searchTips" id="srcht" hidden> <div class="searchTips" id="srcht" hidden>
@ -141,13 +144,13 @@
<option value="comments" {if str_contains($_SERVER['REQUEST_URI'], "type=comments")}selected{/if}>{_s_by_comments}</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="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> <option value="apps" {if str_contains($_SERVER['REQUEST_URI'], "type=apps")}selected{/if}>{_s_by_apps}</option>
<option value="audios" {if str_contains($_SERVER['REQUEST_URI'], "type=audios")}selected{/if}>{_s_by_audios}</option>
</select> </select>
<button class="searchBtn"><span style="color:white;font-weight: 600;font-size:12px;">{_header_search}</span></button> <button class="searchBtn"><span style="color:white;font-weight: 600;font-size:12px;">{_header_search}</span></button>
</form> </form>
<script> <script>
let els = document.querySelectorAll("div.dec") let els = document.querySelectorAll("div.dec")
for(const element of els) for(const element of els) {
{
element.style.display = "none" element.style.display = "none"
} }
</script> </script>
@ -183,6 +186,7 @@
</a> </a>
<a n:if="$thisUser->getLeftMenuItemStatus('photos')" href="/albums{$thisUser->getId()}" class="link">{_my_photos}</a> <a n:if="$thisUser->getLeftMenuItemStatus('photos')" href="/albums{$thisUser->getId()}" class="link">{_my_photos}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('videos')" href="/videos{$thisUser->getId()}" class="link">{_my_videos}</a> <a n:if="$thisUser->getLeftMenuItemStatus('videos')" href="/videos{$thisUser->getId()}" class="link">{_my_videos}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('audios')" href="/audios{$thisUser->getId()}" class="link">{_my_audios}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('messages')" href="/im" class="link">{_my_messages} <a n:if="$thisUser->getLeftMenuItemStatus('messages')" href="/im" class="link">{_my_messages}
<object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0"> <object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0">
(<b>{$thisUser->getUnreadMessagesCount()}</b>) (<b>{$thisUser->getUnreadMessagesCount()}</b>)
@ -196,7 +200,7 @@
(<b>{$thisUser->getNotificationsCount()}</b>) (<b>{$thisUser->getNotificationsCount()}</b>)
{/if} {/if}
</a> </a>
<a href="/apps?act=installed" class="link">{_my_apps}</a> <a n:if="$thisUser->getLeftMenuItemStatus('apps')" href="/apps?act=installed" class="link">{_my_apps}</a>
<a href="/settings" class="link">{_my_settings}</a> <a href="/settings" class="link">{_my_settings}</a>
{var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
@ -229,7 +233,16 @@
<div id="_groupListPinnedGroups"> <div id="_groupListPinnedGroups">
<div n:if="$thisUser->getPinnedClubCount() > 0" class="menu_divider"></div> <div n:if="$thisUser->getPinnedClubCount() > 0" class="menu_divider"></div>
<a n:foreach="$thisUser->getPinnedClubs() as $club" href="{$club->getURL()}" class="link group_link">{$club->getName()}</a>
<a n:foreach="$thisUser->getPinnedClubs() as $club" href="{$club->getURL()}" class="link group_link">
{ovk_proc_strtr($club->getName(), 14)}
<object type="internal/link" style="white-space: normal;" id="sug{$club->getId()}" n:if="$club->getSuggestedPostsCount($thisUser) > 0 && $club->getWallType() == 2">
<a href="/club{$club->getId()}/suggested" class="linkunderline">
(<b>{$club->getSuggestedPostsCount($thisUser)}</b>)
</a>
</object>
</a>
</div> </div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && $thisUser->getCoins() != 0" id="votesBalance"> <div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && $thisUser->getCoins() != 0" id="votesBalance">
@ -301,7 +314,7 @@
</div> </div>
</div> </div>
{ifset $thisUser} {ifset $thisUser}
{if !$thisUser->isBanned()} {if !$thisUser->isBanned() && !$thisUser->isDeleted()}
</div> </div>
{/if} {/if}
{/ifset} {/ifset}
@ -367,6 +380,10 @@
</p> </p>
</div> </div>
<div id="ajloader" class="loader">
<img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
</div>
{include "components/cookies.xml"} {include "components/cookies.xml"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"} {script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
@ -380,6 +397,7 @@
{script "js/al_api.js"} {script "js/al_api.js"}
{script "js/al_mentions.js"} {script "js/al_mentions.js"}
{script "js/al_polls.js"} {script "js/al_polls.js"}
{script "js/al_suggestions.js"}
{ifset $thisUser} {ifset $thisUser}
{script "js/al_notifs.js"} {script "js/al_notifs.js"}
@ -427,6 +445,12 @@
//]]> //]]>
</script> </script>
<script>
window.openvk = {
"audio_genres": {\openvk\Web\Models\Entities\Audio::genres}
}
</script>
{ifset bodyScripts} {ifset bodyScripts}
{include bodyScripts} {include bodyScripts}
{/ifset} {/ifset}

View file

@ -1,12 +1,10 @@
{extends "../@layout.xml"} {extends "../@layout.xml"}
{block title}Ваш браузер устарел{/block} {block title}{_deprecated_browser}{/block}
{block header} {block header}
Устаревший браузер {_deprecated_browser}
{/block} {/block}
{block content} {block content}
Для просмотра этого контента вам понадобится Firefox ESR 52+ или {_deprecated_browser_description}
эквивалентный по функционалу навигатор по всемирной сети интернет.<br/>
Сожалеем об этом.
{/block} {/block}

View file

@ -9,5 +9,5 @@
<div id="faqhead">Для кого этот сайт?</div> <div id="faqhead">Для кого этот сайт?</div>
<div id="faqcontent">Сайт предназначен для поиска друзей и знакомых, а также просмотр данных пользователя. Это как справочник города, с помощью которого люди могут быстро найти актуальную информацию о человеке. Также этот сайт подойдёт для ностальгираторов и тех, кто решил слезть с трубы "ВКонтакте", которого клон и является.<br></div> <div id="faqcontent">Сайт предназначен для поиска друзей и знакомых, а также просмотр данных пользователя. Это как справочник города, с помощью которого люди могут быстро найти актуальную информацию о человеке. Также этот сайт подойдёт для ностальгираторов и тех, кто решил слезть с трубы "ВКонтакте", которого клон и является.<br></div>
Я попозже допишу ок ~~ veselcraft - 12.01.2020 - 22:05 GMT+3 Я попозже допишу ок ~~ veselcraft - 12.01.2020 - 22:05 GMT+3
Давай
{/block} {/block}

View file

@ -15,7 +15,7 @@
.navigation-lang .link_new { .navigation-lang .link_new {
display: inline-block; display: inline-block;
padding: 25px 25px 20px 25px; padding: 20px 10px 5px 10px;
text-decoration: none; text-decoration: none;
border-top: 1px solid #fff; border-top: 1px solid #fff;
color: #000; color: #000;

View file

@ -2,7 +2,7 @@
{block title}Sandbox{/block} {block title}Sandbox{/block}
{block header} {block header}
Sandbox для разработчиков {_sandbox_for_developers}
{/block} {/block}
{block content} {block content}

View file

@ -179,9 +179,27 @@
<ul class="listing"> <ul class="listing">
<li><span>{_tour_section_6_text_1|noescape}</span></li> <li><span>{_tour_section_6_text_1|noescape}</span></li>
<li><span>{_tour_section_6_text_2|noescape}</span></li>
<li><span>{_tour_section_6_text_3|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios.png" width="440">
</ul> </ul>
<ul class="listing">
<li><span>{_tour_section_6_text_4|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios_search.png" width="440">
<li><span>{_tour_section_6_text_5|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios_upload.png" width="440">
</ul>
<p class="big">{_tour_section_6_bottom_text_1|noescape}</p>
<h2>{_tour_section_6_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_6_text_6|noescape}</span></li>
<li><span>{_tour_section_6_text_7|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios_playlists.png" width="440">
</ul>
<br> <br>
</div> </div>

View file

@ -97,6 +97,9 @@
<li> <li>
<a href="/admin/bannedLinks">{_admin_banned_links}</a> <a href="/admin/bannedLinks">{_admin_banned_links}</a>
</li> </li>
<li>
<a href="/admin/music">{_admin_music}</a>
</li>
</ul> </ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>Chandler</strong> <strong>Chandler</strong>

View file

@ -1,7 +1,7 @@
{extends "./@layout.xml"} {extends "./@layout.xml"}
{block title} {block title}
История блокировок {_bans_history}
{/block} {/block}
{block heading} {block heading}
@ -13,13 +13,13 @@
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Забаненный</th> <th>{_bans_history_blocked}</th>
<th>Инициатор</th> <th>{_bans_history_initiator}</th>
<th>Начало</th> <th>{_bans_history_start}</th>
<th>Конец</th> <th>{_bans_history_end}</th>
<th>Время</th> <th>{_bans_history_time}</th>
<th>Причина</th> <th>{_bans_history_reason}</th>
<th>Снята</th> <th>{_bans_history_removed}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -77,7 +77,7 @@
{_admin_banned} {_admin_banned}
</span> </span>
{else} {else}
<b style="color: red;">Активная блокировка</b> <b style="color: red;">{_bans_history_active}</b>
{/if} {/if}
</td> </td>
</tr> </tr>

View file

@ -0,0 +1,81 @@
{extends "@layout.xml"}
{block title}
{_edit} {$audio->getName()}
{/block}
{block heading}
{$audio->getName()}
{/block}
{block content}
<div class="aui-tabs horizontal-tabs">
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text medium-field" type="number" id="id" disabled value="{$audio->getId()}" />
</div>
<div class="field-group">
<label>{_created}</label>
{$audio->getPublicationTime()}
</div>
<div class="field-group">
<label>{_edited}</label>
{$audio->getEditTime() ?? "never"}
</div>
<div class="field-group">
<label for="name">{_name}</label>
<input class="text medium-field" type="text" id="name" name="name" value="{$audio->getTitle()}" />
</div>
<div class="field-group">
<label for="performer">{_performer}</label>
<input class="text medium-field" type="text" id="performer" name="performer" value="{$audio->getPerformer()}" />
</div>
<div class="field-group">
<label for="ext">{_lyrics}</label>
<textarea class="text medium-field" type="text" id="text" name="text" style="resize: vertical;">{$audio->getLyrics()}</textarea>
</div>
<div class="field-group">
<label>{_admin_audio_length}</label>
{$audio->getFormattedLength()}
</div>
<div class="field-group">
<label for="ext">{_genre}</label>
<select class="select medium-field" name="genre">
<option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre'
n:attr="selected: $genre == $audio->getGenre()" value="{$genre}">
{$genre}
</option>
</select>
</div>
<div class="field-group">
<label>{_admin_original_file}</label>
<audio controls src="{$audio->getOriginalURL(true)}">
</div>
<hr />
<div class="field-group">
<label for="owner">{_owner}</label>
<input class="text medium-field" type="number" id="owner_id" name="owner" value="{$owner}" />
</div>
<div class="field-group">
<label for="explicit">Explicit</label>
<input class="toggle-large" type="checkbox" id="explicit" name="explicit" value="1" {if $audio->isExplicit()} checked {/if} />
</div>
<div class="field-group">
<label for="deleted">{_deleted}</label>
<input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $audio->isDeleted()} checked {/if} />
</div>
<div class="field-group">
<label for="withdrawn">{_withdrawn}</label>
<input class="toggle-large" type="checkbox" id="withdrawn" name="withdrawn" value="1" {if $audio->isWithdrawn()} checked {/if} />
</div>
<hr />
<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>
{/block}

View file

@ -0,0 +1,54 @@
{extends "@layout.xml"}
{block title}
{_edit} {$playlist->getName()}
{/block}
{block heading}
{$playlist->getName()}
{/block}
{block content}
<div class="aui-tabs horizontal-tabs">
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text medium-field" type="number" id="id" disabled value="{$playlist->getId()}" />
</div>
<div class="field-group">
<label for="name">{_name}</label>
<input class="text medium-field" type="text" id="name" name="name" value="{$playlist->getName()}" />
</div>
<div class="field-group">
<label for="ext">{_description}</label>
<textarea class="text medium-field" type="text" id="description" name="description" style="resize: vertical;">{$playlist->getDescription()}</textarea>
</div>
<div class="field-group">
<label for="ext">{_admin_cover_id}</label>
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
<span class="aui-avatar-inner">
<img src="{$playlist->getCoverUrl()}" style="object-fit: cover;"></img>
</span>
</span>
<br />
<input class="text medium-field" type="number" id="photo" name="photo" value="{$playlist->getCoverPhotoId()}" />
</div>
<hr />
<div class="field-group">
<label for="owner">{_owner}</label>
<input class="text medium-field" type="number" id="owner_id" name="owner" value="{$playlist->getOwner()->getId()}" />
</div>
<div class="field-group">
<label for="deleted">{_deleted}</label>
<input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $playlist->isDeleted()} checked {/if} />
</div>
<hr />
<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>
{/block}

View file

@ -1,11 +1,11 @@
{extends "@layout.xml"} {extends "@layout.xml"}
{block title} {block title}
Логи {_logs}
{/block} {/block}
{block heading} {block heading}
Логи {_logs}
{/block} {/block}
{block content} {block content}
@ -18,23 +18,23 @@
</style> </style>
<form class="aui"> <form class="aui">
<div> <div>
<select class="select medium-field" type="number" id="type" name="type" placeholder="Тип изменения"> <select class="select medium-field" type="number" id="type" name="type" placeholder="{_logs_change_type}">
<option value="any" n:attr="selected => !$type">Любое</option> <option value="any" n:attr="selected => !$type">{_logs_anything}</option>
<option value="0" n:attr="selected => $type === 0">Создание</option> <option value="0" n:attr="selected => $type === 0">{_logs_adding}</option>
<option value="1" n:attr="selected => $type === 1">Редактирование</option> <option value="1" n:attr="selected => $type === 1">{_logs_editing}</option>
<option value="2" n:attr="selected => $type === 2">Удаление</option> <option value="2" n:attr="selected => $type === 2">{_logs_removing}</option>
<option value="3" n:attr="selected => $type === 3">Восстановление</option> <option value="3" n:attr="selected => $type === 3">{_logs_restoring}</option>
</select> </select>
<input class="text medium-field" type="number" id="id" name="id" placeholder="ID записи" n:attr="value => $id"/> <input class="text medium-field" type="number" id="id" name="id" placeholder="{_logs_id_post}" n:attr="value => $id"/>
<input class="text medium-field" type="text" id="uid" name="uid" placeholder="UUID пользователя" n:attr="value => $user"/> <input class="text medium-field" type="text" id="uid" name="uid" placeholder="{_logs_uuid_user}" n:attr="value => $user"/>
</div> </div>
<div style="margin: 8px 0;" /> <div style="margin: 8px 0;" />
<div> <div>
<select class="select medium-field" id="obj_type" name="obj_type" placeholder="Тип объекта"> <select class="select medium-field" id="obj_type" name="obj_type" placeholder="{_logs_change_object}">
<option value="any" n:attr="selected => !$obj_type">Любой</option> <option value="any" n:attr="selected => !$obj_type">{_logs_anything}</option>
<option n:foreach="$object_types as $type" n:attr="selected => $obj_type === $type">{$type}</option> <option n:foreach="$object_types as $type" n:attr="selected => $obj_type === $type">{$type}</option>
</select> </select>
<input class="text medium-field" type="number" id="obj_id" name="obj_id" placeholder="ID объекта" n:attr="value => $obj_id"/> <input class="text medium-field" type="number" id="obj_id" name="obj_id" placeholder="{_logs_id_object}" n:attr="value => $obj_id"/>
<input type="submit" class="aui-button aui-button-primary medium-field" value="Поиск" style="width: 165px;"/> <input type="submit" class="aui-button aui-button-primary medium-field" value="Поиск" style="width: 165px;"/>
</div> </div>
</form> </form>
@ -42,11 +42,11 @@
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Пользователь</th> <th>{_logs_user}</th>
<th>Объект</th> <th>{_logs_object}</th>
<th>Тип</th> <th>{_logs_type}</th>
<th>Изменения</th> <th>{_logs_changes}</th>
<th>Время</th> <th>{_logs_time}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View file

@ -0,0 +1,135 @@
{extends "@layout.xml"}
{var $search = $mode === "audios"}
{block title}
{_audios}
{/block}
{block heading}
{_audios}
{/block}
{block searchTitle}
{include title}
{/block}
{block content}
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav" resolved="">
<li n:attr="class => $mode === 'audios' ? 'aui-nav-selected' : ''">
<a href="?act=audios">{_audios}</a>
</li>
<li n:attr="class => $mode === 'playlists' ? 'aui-nav-selected' : ''">
<a href="?act=playlists">{_playlists}</a>
</li>
</ul>
</div>
</div>
</nav>
<table class="aui aui-table-list">
{if $mode === "audios"}
{var $audios = iterator_to_array($audios)}
{var $amount = sizeof($audios)}
<thead>
<tr>
<th>ID</th>
<th>{_admin_author}</th>
<th>{_peformer}</th>
<th>{_admin_title}</th>
<th>{_genre}</th>
<th>Explicit</th>
<th>{_withdrawn}</th>
<th>{_deleted}</th>
<th>{_created}</th>
<th>{_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$audios as $audio">
<td>{$audio->getId()}</td>
<td>
{var $owner = $audio->getOwner()}
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
<span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>{$audio->getPerformer()}</td>
<td>{$audio->getTitle()}</td>
<td>{$audio->getGenre()}</td>
<td>{$audio->isExplicit() ? tr("yes") : tr("no")}</td>
<td n:attr="style => $audio->isWithdrawn() ? 'color: red;' : ''">
{$audio->isWithdrawn() ? tr("yes") : tr("no")}
</td>
<td n:attr="style => $audio->isDeleted() ? 'color: red;' : ''">
{$audio->isDeleted() ? tr("yes") : tr("no")}
</td>
<td>{$audio->getPublicationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/music/{$audio->getId()}/edit">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
</a>
</td>
</tr>
</tbody>
{else}
{var $playlists = iterator_to_array($playlists)}
{var $amount = sizeof($playlists)}
<thead>
<tr>
<th>ID</th>
<th>{_admin_author}</th>
<th>{_name}</th>
<th>{_created_playlist}</th>
<th>{_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$playlists as $playlist">
<td>{$playlist->getId()}</td>
<td>
{var $owner = $playlist->getOwner()}
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
<span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$playlist->getCoverURL()}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
{ovk_proc_strtr($playlist->getName(), 30)}
</td>
<td>{$playlist->getCreationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/playlist/{$playlist->getId()}/edit">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
</td>
</tr>
</tbody>
{/if}
</table>
<br/>
<div align="right">
{var $isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

View file

@ -7,7 +7,7 @@
{block header} {block header}
{$name} {$name}
<a style="float: right;" onClick="reportApp()" n:if="$canReport ?? false">Пожаловаться</a> <a style="float: right;" onClick="reportApp()" n:if="$canReport ?? false">{_report}</a>
{/block} {/block}
{block content} {block content}
@ -37,20 +37,20 @@
<script n:if="$canReport ?? false"> <script n:if="$canReport ?? false">
function reportApp() { function reportApp() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данное приложение."; uReportMsgTxt = {_going_to_report_app};
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?"; uReportMsgTxt += "<br/>"+tr("report_question_text");
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />" uReportMsgTxt += "<br/><br/><b>"+tr("report_reason")+"</b>: <input type='text' id='uReportMsgInput' placeholder='" + tr("reason") + "' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [ MessageBox(tr("report_question"), uReportMsgTxt, [tr("confirm_m"), tr("cancel")], [
(function() { (function() {
res = document.querySelector("#uReportMsgInput").value; res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$id} + "?reason=" + res + "&type=app", true); xhr.open("GET", "/report/" + {$id} + "?reason=" + res + "&type=app", true);
xhr.onload = (function() { xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1) if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]); MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
else else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]); MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
}); });
xhr.send(null); xhr.send(null);
}), }),

View file

@ -0,0 +1,7 @@
<input type="hidden" name="count" value="{$count}">
<input type="hidden" name="pagesCount" value="{$pagesCount}">
<input type="hidden" name="page" value="{$page}">
{foreach $audios as $audio}
{include "player.xml", audio => $audio, hideButtons => true}
{/foreach}

View file

@ -0,0 +1,95 @@
{extends "../@layout.xml"}
{block title}{_edit_playlist}{/block}
{block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
<a href="/audios{$ownerId}">{_audios}</a>
»
<a href="/playlist{$playlist->getPrettyId()}">{_playlist}</a>
»
{_edit_playlist}
{/block}
{block content}
<div class="playlistBlock" style="display: flex;margin-top: 0px;">
<div class="playlistCover">
<a>
<img src="{$playlist->getCoverURL('normal')}" alt="{_playlist_cover}">
</a>
<div class="profile_links" style="width: 139px;">
<a class="profile_link" style="width: 98%;" id="_deletePlaylist" data-id="{$playlist->getId()}">{_delete_playlist}</a>
</div>
</div>
<div style="padding-left: 13px;width:75%">
<div class="playlistInfo">
<input value="{$playlist->getName()}" type="text" name="title" maxlength="125">
</div>
<div class="moreInfo">
<textarea name="description" maxlength="2045" style="margin-top: 11px;">{$playlist->getDescription()}</textarea>
</div>
</div>
</div>
<div style="margin-top: 19px;">
<input id="playlist_query" type="text" style="height: 26px;" placeholder="{_header_search}">
<div class="playlistAudiosContainer editContainer">
<div id="newPlaylistAudios" n:foreach="$audios as $audio">
<div class="playerContainer">
{include "player.xml", audio => $audio, hideButtons => true}
</div>
<div class="attachAudio addToPlaylist" data-id="{$audio->getId()}">
<span>{_remove_from_playlist}</span>
</div>
</div>
</div>
<div class="showMoreAudiosPlaylist" data-page="2" data-playlist="{$playlist->getId()}" n:if="$pagesCount > 1">
{_show_more_audios}
</div>
</div>
<form method="post" id="editPlaylistForm" data-id="{$playlist->getId()}" enctype="multipart/form-data">
<input type="hidden" name="title" maxlength="128" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<textarea style="display:none;" name="description" maxlength="2048" />
<input type="hidden" name="audios">
<input type="file" style="display:none;" name="new_cover" accept=".jpg,.png">
<div style="float:right;margin-top: 8px;">
<button class="button" type="submit">{_save}</button>
</div>
</form>
<script>
document.querySelector("input[name='audios']").value = {$audiosIds}
u("#editPlaylistForm").on("submit", (e) => {
document.querySelector("#editPlaylistForm input[name='title']").value = document.querySelector(".playlistInfo input[name='title']").value
document.querySelector("#editPlaylistForm textarea[name='description']").value = document.querySelector(".playlistBlock textarea[name='description']").value
})
u("#editPlaylistForm input[name='new_cover']").on("change", (e) => {
if(!e.currentTarget.files[0].type.startsWith("image/")) {
fastError(tr("not_a_photo"))
return
}
let image = URL.createObjectURL(e.currentTarget.files[0])
document.querySelector(".playlistCover img").src = image
})
u(".playlistCover img").on("click", (e) => {
document.querySelector("input[name='new_cover']").click()
})
document.querySelector("#editPlaylistForm input[name='new_cover']").value = ""
</script>
{script "js/al_playlists.js"}
{/block}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
<link rel="icon">
<title>{$audio->getName()}</title>
{css "css/main.css"}
{css "css/audios.css"}
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
</head>
<body id="audioEmbed">
{include "player.xml", audio => $audio}
{script "js/al_music.js"}
</body>
</html>

View file

@ -0,0 +1,126 @@
{extends "../@layout.xml"}
{block title}
{if $mode == 'list'}
{if $ownerId > 0}
{_audios} {$owner->getMorphedName("genitive", false)}
{else}
{_audios_group}
{/if}
{elseif $mode == 'new'}
{_audio_new}
{elseif $mode == 'popular'}
{_audio_popular}
{else}
{if $ownerId > 0}
{_playlists} {$owner->getMorphedName("genitive", false)}
{else}
{_playlists_group}
{/if}
{/if}
{/block}
{block header}
<div n:if="$mode == 'list'">
<div n:if="$isMy">{_my_audios_small}</div>
<div n:if="!$isMy">
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
{_audios}
</div>
</div>
<div n:if="$mode == 'new'">
{_audios}
»
{_audio_new}
</div>
<div n:if="$mode == 'popular'">
{_audios}
»
{_audio_popular}
</div>
<div n:if="$mode == 'playlists'">
{_audios}
»
{if $isMy}{_my_playlists}{else}{_playlists}{/if}
</div>
{/block}
{block content}
{* ref: https://archive.li/P32em *}
{include "bigplayer.xml"}
<input n:if="$mode == 'list'" type="hidden" name="bigplayer_context" data-type="entity_audios" data-entity="{$ownerId}" data-page="{$page}">
<input n:if="$mode == 'new'" type="hidden" name="bigplayer_context" data-type="new_audios" data-entity="0" data-page="1">
<input n:if="$mode == 'popular'" type="hidden" name="bigplayer_context" data-type="popular_audios" data-entity="0" data-page="1">
<div class="bigPlayerDetector"></div>
<div style="width: 100%;display: flex;margin-bottom: -10px;" class="audiosDiv">
<div style="width: 74%;" class="audiosContainer" n:if="$mode != 'playlists'">
<div style="padding: 8px;">
<div n:if="$audiosCount <= 0">
{include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_audios_thisuser") : tr("no_audios_user")) : tr("no_audios_club")}
</div>
<div n:if="$audiosCount > 0" class="infContainer">
<div class="infObj" n:foreach="$audios as $audio">
{include "player.xml", audio => $audio, club => $club}
</div>
</div>
<div n:if="$mode != 'new' && $mode != 'popular'">
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $audiosCount,
"amount" => sizeof($audios),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
</div>
</div>
</div>
<div style="width: 74%;" n:if="$mode == 'playlists'">
<div style="padding: 8px;">
<div n:if="$playlistsCount <= 0">
{include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_playlists_thisuser") : tr("no_playlists_user")) : tr("no_playlists_club")}
</div>
<div class="infContainer playlistContainer" n:if="$playlistsCount > 0">
<div class="infObj" n:foreach="$playlists as $playlist">
<a href="/playlist{$playlist->getPrettyId()}">
<div class="playlistCover">
<img src="{$playlist->getCoverURL()}" alt="{_playlist_cover}">
</div>
</a>
<div class="playlistInfo">
<a href="/playlist{$playlist->getPrettyId()}">
<span style="font-size: 12px" class="playlistName">
{ovk_proc_strtr($playlist->getName(), 15)}
</span>
</a>
<a href="{$playlist->getOwner()->getURL()}">{ovk_proc_strtr($playlist->getOwner()->getCanonicalName(), 20)}</a>
</div>
</div>
</div>
<div>
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $playlistsCount,
"amount" => sizeof($playlists),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
</div>
</div>
</div>
{include "tabs.xml"}
</div>
{/block}

View file

@ -0,0 +1,109 @@
{extends "../@layout.xml"}
{block title}
{_new_playlist}
{/block}
{block header}
{if !$_GET["gid"]}
<a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
»
<a href="/audios{$thisUser->getId()}">{_audios}</a>
{else}
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a>
»
<a href="/audios-{$club->getId()}">{_audios}</a>
{/if}
»
{_new_playlist}
{/block}
{block content}
<style>
textarea[name='description'] {
padding: 4px;
}
.playlistInfo {
width: 76%;
margin-left: 8px;
}
</style>
<div style="display:flex;">
<div class="playlistCover" onclick="document.querySelector(`#newPlaylistForm input[name='cover']`).click()">
<a>
<img src="/assets/packages/static/openvk/img/song.jpg" alt="{_playlist_cover}">
</a>
</div>
<div style="padding-left: 17px;width: 75%;" class="plinfo">
<div>
<input type="text" name="title" placeholder="{_title}" maxlength="125" />
</div>
<div class="moreInfo" style="margin-top: 11px;">
<textarea placeholder="{_description}" name="description" maxlength="2045" />
</div>
</div>
</div>
<div style="margin-top: 19px;">
<input id="playlist_query" type="text" style="height: 26px;" placeholder="{_header_search}">
<div class="playlistAudiosContainer editContainer">
<div id="newPlaylistAudios" n:foreach="$audios as $audio">
<div style="width: 78%;float: left;">
{include "player.xml", audio => $audio, hideButtons => true}
</div>
<div class="attachAudio addToPlaylist" data-id="{$audio->getId()}">
<span>{_add_to_playlist}</span>
</div>
</div>
</div>
<div class="showMoreAudiosPlaylist" data-page="2" {if !is_null($_GET["gid"])}data-club="{abs($_GET['gid'])}"{/if} n:if="$pagesCount > 1">
{_show_more_audios}
</div>
</div>
<form method="post" id="newPlaylistForm" enctype="multipart/form-data">
<input type="hidden" name="title" maxlength="125" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<textarea style="display:none;" name="description" maxlength="2045" />
<input type="hidden" name="audios">
<input type="file" style="display:none;" name="cover" accept=".jpg,.png">
<div style="float: right;margin-top: 9px;">
<button class="button" type="submit">{_create}</button>
</div>
</form>
<script>
document.querySelector("input[name='audios']").value = ""
u("#newPlaylistForm").on("submit", (e) => {
document.querySelector("#newPlaylistForm input[name='title']").value = document.querySelector(".plinfo input[name='title']").value
document.querySelector("#newPlaylistForm textarea[name='description']").value = document.querySelector(".plinfo textarea[name='description']").value
})
u("#newPlaylistForm input[name='cover']").on("change", (e) => {
if(!e.currentTarget.files[0].type.startsWith("image/")) {
fastError(tr("not_a_photo"))
return
}
let image = URL.createObjectURL(e.currentTarget.files[0])
document.querySelector(".playlistCover img").src = image
document.querySelector(".playlistCover img").style.display = "block"
})
u(".playlistCover img").on("click", (e) => {
document.querySelector("#newPlaylistForm input[name='cover']").value = ""
e.currentTarget.href = ""
})
document.querySelector("#newPlaylistForm input[name='cover']").value = ""
</script>
{script "js/al_playlists.js"}
{/block}

View file

@ -0,0 +1,81 @@
{extends "../@layout.xml"}
{block title}{_playlist}{/block}
{block headIncludes}
<meta property="og:type" content="music.album">
<meta property="og:title" content="{$playlist->getName()}">
<meta property="og:url" content="{$playlist->getURL()}">
<meta property="og:description" content="{$playlist->getDescription()}">
<meta property="og:image" content="{$playlist->getCoverURL()}">
<script type="application/ld+json">
{
"@context": "http://schema.org/",
"type": "MusicAlbum",
"name": {$playlist->getName()},
"url": {$playlist->getURL()}
}
</script>
{/block}
{block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
<a href="/audios{$ownerId}">{_audios}</a>
»
{_playlist}
{/block}
{block content}
{include "bigplayer.xml"}
{php $count = $playlist->size()}
<input type="hidden" name="bigplayer_context" data-type="playlist_context" data-entity="{$playlist->getId()}" data-page="{$page}">
<div class="playlistBlock">
<div class="playlistCover" style="float: left;">
<a href="{$playlist->getCoverURL()}" target="_blank">
<img src="{$playlist->getCoverURL('normal')}" alt="{_playlist_cover}">
</a>
<div class="profile_links" style="width: 139px;" n:if="isset($thisUser)">
<a class="profile_link" style="width: 98%;" href="/playlist{$playlist->getPrettyId()}/edit" n:if="$playlist->canBeModifiedBy($thisUser)">{_edit_playlist}</a>
<a class="profile_link" style="width: 98%;" id="bookmarkPlaylist" data-id="{$playlist->getId()}" n:if="!$isBookmarked">{_bookmark}</a>
<a class="profile_link" style="width: 98%;" id="unbookmarkPlaylist" data-id="{$playlist->getId()}" n:if="$isBookmarked">{_unbookmark}</a>
</div>
</div>
<div style="float: left;padding-left: 13px;width:75%">
<div class="playlistInfo">
<h4 style="border-bottom:unset;">{$playlist->getName()}</h4>
<div class="moreInfo">
{$playlist->getMetaDescription()|noescape}
<div style="margin-top: 11px;">
<span>{nl2br($playlist->getDescriptionHTML())|noescape}</span>
</div>
<hr style="color: #f7f7f7;">
</div>
</div>
<div class="audiosContainer infContainer" style="margin-top: 14px;">
{if $count < 1}
{_empty_playlist}
{else}
<div class="infObj" n:foreach="$audios as $audio">
{include "player.xml", audio => $audio}
</div>
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($audios),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
{/if}
</div>
</div>
</div>
{/block}

View file

@ -0,0 +1,212 @@
{extends "../@layout.xml"}
{block title}
{_upload_audio}
{/block}
{block header}
{if !is_null($group)}
<a href="{$group->getURL()}">{$group->getCanonicalName()}</a>
»
<a href="/audios-{$group->getId()}">{_audios}</a>
{else}
<a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
»
<a href="/audios{$thisUser->getId()}">{_audios}</a>
{/if}
»
{_upload_audio}
{/block}
{block content}
<div class="container_gray" style="border: 0;margin-top: -10px;">
<div id="upload_container">
<div id="firstStep">
<h4>{_select_audio}</h4><br/>
<b><a href="javascript:void(0)">{_limits}</a></b>
<ul>
<li>{tr("audio_requirements", 1, 30, 25)}</li>
<li>{tr("audio_requirements_2")}</li>
</ul>
<div id="audio_upload">
<form enctype="multipart/form-data" method="POST">
<input type="hidden" name="name" />
<input type="hidden" name="performer" />
<input type="hidden" name="lyrics" />
<input type="hidden" name="genre" />
<input type="hidden" name="explicit" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input id="audio_input" type="file" name="blob" accept="audio/*" style="display:none" />
<input value="{_upload_button}" class="button" type="button" onclick="document.querySelector('#audio_input').click()">
</form>
</div><br/>
<span>{_you_can_also_add_audio_using} <b><a href="/search?type=audios">{_search_audio_inst}</a></b>.<span>
</div>
<div id="lastStep" style="display:none;">
<table cellspacing="7" cellpadding="0" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top"><span class="nobold">{_audio_name}:</span></td>
<td><input type="text" name="name" autocomplete="off" maxlength="80" /></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_performer}:</span></td>
<td><input name="performer" type="text" autocomplete="off" maxlength="80" /></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_genre}:</span></td>
<td>
<select name="genre">
<option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre' n:attr="selected: $genre == 'Other'" value="{$genre}">
{$genre}
</option>
</select>
</td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_lyrics}:</span></td>
<td><textarea name="lyrics" style="resize: vertical;max-height: 300px;"></textarea></td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<label><input type="checkbox" name="explicit">{_audios_explicit}</label>
</td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<input class="button" type="button" id="uploadMuziko" value="{_upload_button}">
<input class="button" type="button" id="backToUpload" value="{_select_another_file}">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script type="module">
import * as id3 from "/assets/packages/static/openvk/js/node_modules/id3js/lib/id3.js";
u("#audio_input").on("change", async function(e) {
let files = e.currentTarget.files
if(files.length <= 0)
return;
document.querySelector("#firstStep").style.display = "none"
document.querySelector("#lastStep").style.display = "block"
let tags = await id3.fromFile(files[0]);
if(tags != null) {
console.log("ID" + tags.kind + " detected, setting values...");
if(tags.title != null)
document.querySelector("#lastStep input[name=name]").value = tags.title;
else
document.querySelector("#lastStep input[name=name]").value = files[0].name
if(tags.artist != null)
document.querySelector("#lastStep input[name=performer]").value = tags.artist;
else
document.querySelector("#lastStep input[name=performer]").value = tr("track_unknown");
if(tags.genre != null) {
if(document.querySelector("#lastStep select[name=genre] > option[value='" + tags.genre + "']") != null) {
document.querySelector("#lastStep select[name=genre]").value = tags.genre;
} else {
console.warn("Unknown genre: " + tags.genre);
document.querySelector("#lastStep select[name=genre]").value = "Other"
}
}
} else {
document.querySelector("#lastStep input[name=name]").value = files[0].name
document.querySelector("#lastStep select[name=genre]").value = "Other"
document.querySelector("#lastStep input[name=performer]").value = tr("track_unknown");
}
});
u("#backToUpload").on("click", (e) => {
document.querySelector("#firstStep").style.display = "block"
document.querySelector("#lastStep").style.display = "none"
document.querySelector("#lastStep input[name=name]").value = ""
document.querySelector("#lastStep input[name=performer]").value = ""
document.querySelector("#lastStep select[name=genre]").value = ""
document.querySelector("#lastStep textarea[name=lyrics]").value = ""
document.querySelector("#audio_input").value = ""
})
u("#uploadMuziko").on("click", (e) => {
var name_ = document.querySelector("#audio_upload input[name=name]");
var perf_ = document.querySelector("#audio_upload input[name=performer]");
var genre_ = document.querySelector("#audio_upload input[name=genre]");
var lyrics_ = document.querySelector("#audio_upload input[name=lyrics]");
var explicit_ = document.querySelector("#audio_upload input[name=explicit]");
name_.value = document.querySelector("#lastStep input[name=name]").value
perf_.value = document.querySelector("#lastStep input[name=performer]").value
genre_.value = document.querySelector("#lastStep select[name=genre]").value
lyrics_.value = document.querySelector("#lastStep textarea[name=lyrics]").value
explicit_.value = document.querySelector("#lastStep input[name=explicit]").checked ? "on" : "off"
$("#audio_upload > form").trigger("submit");
})
$(document).on("dragover drop", (e) => {
e.preventDefault()
return false;
})
$(".container_gray").on("drop", (e) => {
e.originalEvent.dataTransfer.dropEffect = 'move';
e.preventDefault()
let file = e.originalEvent.dataTransfer.files[0]
if(!file.type.startsWith('audio/')) {
MessageBox(tr("error"), tr("only_audios_accepted", escapeHtml(file.name)), [tr("ok")], [() => Function.noop])
return;
}
document.getElementById("audio_input").files = e.originalEvent.dataTransfer.files
u("#audio_input").trigger("change")
})
$("#audio_upload").on("submit", "form", (e) => {
e.preventDefault()
let fd = new FormData(e.currentTarget)
fd.append("ajax", 1)
$.ajax({
type: "POST",
url: location.href,
contentType: false,
processData: false,
data: fd,
beforeSend: function() {
document.querySelector("#lastStep").classList.add("lagged")
document.querySelector("#upload_container").classList.add("uploading")
},
success: (response) => {
document.querySelector("#lastStep").classList.remove("lagged")
document.querySelector("#upload_container").classList.remove("uploading")
if(response.success) {
u("#backToUpload").trigger("click")
NewNotification(tr("success"), tr("audio_successfully_uploaded"), null, () => {
window.location.assign(response.redirect_link)
})
} else {
fastError(response.flash.message)
}
}
})
})
</script>
{/block}

View file

@ -0,0 +1,56 @@
<div class="bigPlayer">
<audio class="audio" />
<div class="paddingLayer">
<div class="playButtons">
<div class="playButton musicIcon" title="{_play_tip} [Space]"></div>
<div class="arrowsButtons">
<div>
<div class="nextButton musicIcon"></div>
</div>
<div>
<div class="backButton musicIcon"></div>
</div>
</div>
</div>
<div class="trackPanel">
<div class="trackInfo">
<div class="trackName">
<b>{_track_unknown}</b>
<span>{_track_noname}</span>
</div>
<div class="timer" style="float:right">
<span class="time">00:00</span>
<span>/</span>
<span class="elapsedTime" style="cursor:pointer">-00:00</span>
</div>
</div>
<div class="track" style="margin-top: -2px;">
<div class="bigPlayerTip">00:00</div>
<div class="selectableTrack">
<div style="width: 95%;position: relative;">&nbsp;
<div class="slider"></div>
</div>
</div>
</div>
</div>
<div class="volumePanel">
<div class="selectableTrack">
<div style="position: relative;width:72%">&nbsp;
<div class="slider"></div>
</div>
</div>
</div>
<div class="additionalButtons">
<div class="repeatButton musicIcon" title="{_repeat_tip} [R]" ></div>
<div class="shuffleButton musicIcon" title="{_shuffle_tip}"></div>
<div class="deviceButton musicIcon" title="{_mute_tip} [M]"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,69 @@
{php $id = $audio->getId() . rand(0, 1000)}
{php $isWithdrawn = $audio->isWithdrawn()}
{php $editable = isset($thisUser) && $audio->canBeModifiedBy($thisUser)}
<div id="audioEmbed-{$id}" data-realid="{$audio->getId()}" {if $hideButtons}data-prettyid="{$audio->getPrettyId()}" data-name="{$audio->getName()}"{/if} data-genre="{$audio->getGenre()}" class="audioEmbed {if !$audio->isAvailable()}processed{/if} {if $isWithdrawn}withdrawn{/if}" data-length="{$audio->getLength()}" data-keys="{json_encode($audio->getKeys())}" data-url="{$audio->getURL()}">
<audio class="audio" />
<div id="miniplayer" class="audioEntry" style="min-height: 39px;">
<div style="display: flex;">
<div class="playerButton">
<div class="playIcon"></div>
</div>
<div class="status" style="margin-top: 12px;">
<div class="mediaInfo noOverflow" style="margin-bottom: -8px; cursor: pointer;display:flex;width: 85%;">
<div class="info">
<strong class="performer">
<a href="/search?query=&type=audios&sort=id&only_performers=on&query={$audio->getPerformer()}">{$audio->getPerformer()}</a>
</strong>
<span class="title {if !empty($audio->getLyrics())}withLyrics{/if}">{$audio->getTitle()}</span>
</div>
<div class="explicitMark" n:if="$audio->isExplicit()"></div>
</div>
</div>
<div class="volume" style="display: flex; flex-direction: column;width:14%;">
<span class="nobold {if !$hideButtons}hideOnHover{/if}" data-unformatted="{$audio->getLength()}" style="text-align: center;margin-top: 12px;">{$audio->getFormattedLength()}</span>
<div class="buttons" style="margin-top: 8px;">
{php $hasAudio = isset($thisUser) && $audio->isInLibraryOf($thisUser)}
{if !$hideButtons}
<div class="remove-icon musicIcon" data-id="{$audio->getId()}" n:if="isset($thisUser) && $hasAudio" ></div>
<div class="add-icon musicIcon hovermeicon" data-id="{$audio->getId()}" n:if="isset($thisUser) && !$hasAudio && !$isWithdrawn" ></div>
<div class="remove-icon-group musicIcon" data-id="{$audio->getId()}" data-club="{$club->getId()}" n:if="isset($thisUser) && isset($club) && $club->canBeModifiedBy($thisUser)" ></div>
<div class="add-icon-group musicIcon" data-id="{$audio->getId()}" n:if="isset($thisUser) && !$isWithdrawn" ></div>
<div class="edit-icon musicIcon" data-lyrics="{$audio->getLyrics()}" data-title="{$audio->getTitle()}" data-performer="{$audio->getPerformer()}" data-explicit="{(int)$audio->isExplicit()}" data-searchable="{(int)!$audio->isUnlisted()}" n:if="isset($thisUser) && $editable && !$isWithdrawn" ></div>
<div class="report-icon musicIcon" data-id="{$audio->getId()}" n:if="isset($thisUser) && !$editable && !$isWithdrawn" ></div>
{/if}
</div>
</div>
</div>
<div class="subTracks">
<div style="width: 100%;">
<div class="track lengthTrack" style="margin-top: 3px;display:none">
<div class="selectableTrack" style="width: 100%;" n:attr="style => $isWithdrawn ? 'display: none;' : ''">
<div style="position: relative;width: calc(100% - 18px);">
<div class="slider"></div>
</div>
</div>
</div>
</div>
<div style="width: 81px;margin-left: 16px;">
<div class="track volumeTrack" style="margin-top: 3px;display:none">
<div class="selectableTrack" style="width: 100%;" n:attr="style => $isWithdrawn ? 'display: none;' : ''">
<div style="position: relative;width: calc(100% - 18px);">
<div class="slider"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="lyrics" n:if="!empty($audio->getLyrics())">
{nl2br($audio->getLyrics())|noescape}
</div>
</div>

View file

@ -0,0 +1,40 @@
<div class="searchOptions newer">
<div class="searchList" style="margin-top:10px">
<a n:attr="id => $mode === 'list' && $isMy ? 'used' : 'ki'" href="/audios{$thisUser->getId()}" n:if="isset($thisUser)">{_my_music}</a>
<a href="/player/upload{if $isMyClub}?gid={abs($ownerId)}{/if}" n:if="isset($thisUser)">{_upload_audio}</a>
<a n:attr="id => $mode === 'new' ? 'used' : 'ki'" href="/audios/new">{_audio_new}</a>
<a n:attr="id => $mode === 'popular' ? 'used' : 'ki'" href="/audios/popular">{_audio_popular}</a>
<a href="/search?type=audios" n:if="isset($thisUser)">{_audio_search}</a>
<hr n:if="isset($thisUser)">
<a n:attr="id => $mode === 'playlists' && $ownerId == $thisUser->getId() ? 'used' : 'ki'" href="/playlists{$thisUser->getId()}" n:if="isset($thisUser)">{_my_playlists}</a>
<a n:if="isset($thisUser)" href="/audios/newPlaylist">{_new_playlist}</a>
{if !$isMy && $mode !== 'popular' && $mode !== 'new'}
<hr>
<a n:if="!$isMy" n:attr="id => $mode === 'list' ? 'used' : 'ki'" href="/audios{$ownerId}">{if $ownerId > 0}{_music_user}{else}{_music_club}{/if}</a>
<a href="/player/upload?gid={abs($ownerId)}" n:if="isset($thisUser) && isset($club) && $club->canUploadAudio($thisUser)">{_upload_audio}</a>
<a n:attr="id => $mode === 'playlists' && $ownerId != $thisUser->getId() ? 'used' : 'ki'" href="/playlists{$ownerId}" n:if="isset($thisUser) && isset($ownerId) && !$isMy">{if $ownerId > 0}{_playlists_user}{else}{_playlists_club}{/if}</a>
<a href="/audios/newPlaylist{if $isMyClub}?gid={abs($ownerId)}{/if}" n:if="isset($thisUser) && $isMyClub">{_new_playlist}</a>
{/if}
{if $friendsAudios}
<div class="friendsAudiosList">
<a href="/audios{$fr->getRealId()}" style="width: 94%;padding-left: 10px;" n:foreach="$friendsAudios as $fr">
<div class="elem">
<img src="{$fr->getAvatarURL()}" />
<div class="additionalInfo">
{php $audioStatus = $fr->getCurrentAudioStatus()}
<span class="name">{$fr->getCanonicalName()}</span>
<span class="desc">{$audioStatus ? $audioStatus->getName() : tr("audios_count", $fr->getAudiosCollectionSize())}</span>
</div>
</div>
</a>
</div>
{/if}
</div>
</div>

View file

@ -1,9 +1,9 @@
{extends "../@layout.xml"} {extends "../@layout.xml"}
{block title}Переход по ссылке заблокирован{/block} {block title}{_transition_is_blocked}{/block}
{block header} {block header}
Предупреждение {_caution}
{/block} {/block}
{block content} {block content}

View file

@ -77,7 +77,12 @@
<span class="nobold">{_wall}: </span> <span class="nobold">{_wall}: </span>
</td> </td>
<td> <td>
<input type="checkbox" name="wall" value="1" n:attr="checked => $club->canPost()" /> {_group_allow_post_for_everyone}<br> <select name="wall">
<option value="1" n:attr="selected => $club->getWallType() == 1" /> {_group_allow_post_for_everyone}</option>
<option value="2" n:attr="selected => $club->getWallType() == 2" /> {_group_limited_post}</option>
<option value="0" n:attr="selected => $club->getWallType() == 0" /> {_group_closed_post}</option>
<select>
<input type="checkbox" name="hide_from_global_feed" value="1" n:attr="checked => $club->isHideFromGlobalFeedEnabled()" /> {_group_hide_from_global_feed} <input type="checkbox" name="hide_from_global_feed" value="1" n:attr="checked => $club->isHideFromGlobalFeedEnabled()" /> {_group_hide_from_global_feed}
</td> </td>
</tr> </tr>
@ -102,6 +107,15 @@
</td> </td>
</tr> </tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_audios}: </span>
</td>
<td>
<label><input type="checkbox" name="upload_audios" value="1" n:attr="checked => $club->isEveryoneCanUploadAudios()" /> {_everyone_can_upload_audios}</label>
</td>
</tr>
<tr> <tr>
<td> <td>

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