openvk/VKAPI/Handlers/Audio.php
Alexander Minkin 6ec54a379d
feat: add linting of code (#1220)
* feat(lint): add php-cs-fixer for linting

Removing previous CODE_STYLE as it was not enforced anyway and using PER-CS 2.0.

This is not the reformatting commit.

* style: format code according to PER-CS 2.0 with php-cs-fixer

* ci(actions): add lint action

Resolves #1132.
2025-01-31 18:20:13 +03:00

865 lines
27 KiB
PHP

<?php
declare(strict_types=1);
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
{
private function toSafeAudioStruct(?AEntity $audio, ?string $hash = null, bool $need_user = false): object
{
if (!$audio) {
$this->fail(0o404, "Audio not found");
} elseif (!$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");
}
} elseif (!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);
}
} elseif (sizeof($descriptor) === 2) {
$audio = (new Audios())->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]);
} else {
$this->fail(8, "Invalid audio $id");
}
return $audio;
}
public function getById(string $audios, ?string $hash = null, int $need_user = 0): object
{
$this->requireUser();
$audioIds = array_unique(explode(",", $audios));
if (sizeof($audioIds) === 1) {
$audio = $this->audioFromAnyId($audioIds[0]);
return (object) [
"count" => 1,
"items" => [
$this->toSafeAudioStruct($audio, $hash, (bool) $need_user),
],
];
} elseif (sizeof($audioIds) > 6000) {
$this->fail(1980, "Can't get more than 6000 audios at once");
}
$audios = [];
foreach ($audioIds as $id) {
$audios[] = $this->getById($id, $hash)->items[0];
}
return (object) [
"count" => sizeof($audios),
"items" => $audios,
];
}
public function isLagtrain(string $audio_id): int
{
$this->requireUser();
$audio = $this->audioFromAnyId($audio_id);
if (!$audio) {
$this->fail(0o404, "Audio not found");
}
# Possible information disclosure risks are acceptable :D
return (int) (strpos($audio->getName(), "Lagtrain") !== false);
}
// TODO stub
public function getRecommendations(): object
{
return (object) [
"count" => 0,
"items" => [],
];
}
public 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);
}
public 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);
}
public 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");
} elseif ($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);
}
public 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(0o404, "Group not found");
}
return (new Audios())->getClubCollectionSize($group);
}
$user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id);
if (!$user) {
$this->fail(0o404, "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);
}
public 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(0o404, "album_id invalid");
} elseif (!$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");
} elseif (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(0o602, "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,
];
}
public function getLyrics(int $lyrics_id): object
{
$this->requireUser();
$audio = (new Audios())->get($lyrics_id);
if (!$audio || !$audio->getLyrics()) {
$this->fail(0o404, "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()),
];
}
public function beacon(int $aid, ?int $gid = null): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$audio = (new Audios())->get($aid);
if (!$audio) {
$this->fail(0o404, "Not Found");
} elseif (!$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(0o404, "Not Found");
} elseif (!$group->canBeModifiedBy($this->getUser())) {
$this->fail(203, "Insufficient rights to this group");
}
}
return (int) $audio->listen($group ?? $this->getUser());
}
public 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(0o404, "Not Found");
} elseif (!$group->canBeModifiedBy($this->getUser())) {
$this->fail(203, "Insufficient rights to this group");
}
$ids[] = $id;
$this->beacon($song ? $song->getId() : 0, $id * -1);
}
return $ids;
}
public 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,
];
}
public 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(0o404, "Not Found");
} elseif (!$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);
} elseif (!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;
}
public 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(0o404, "Invalid group_id");
} elseif (!$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(0o404, "Not found");
} elseif (!$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();
}
public 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(0o404, "Invalid group_id");
} elseif (!$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(0o404, "Not found");
}
$audio->remove($from);
return 1;
}
public 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];
}
public 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,
];
}
public function searchAlbums(string $query = '', int $offset = 0, int $limit = 25, int $drop_private = 0, int $order = 0, int $from_me = 0): object
{
$this->requireUser();
$playlists = [];
$params = [];
$order_str = (['id', 'length', 'listens'][$order] ?? 'id');
if ($from_me === 1) {
$params['from_me'] = $this->getUser()->getId();
}
$search = (new Audios())->findPlaylists($query, $params, ['type' => $order_str, 'invert' => false]);
foreach ($search->offsetLimit($offset, $limit) as $playlist) {
if (!$playlist->canBeViewedBy($this->getUser())) {
if ($drop_private == 0) {
$playlists[] = null;
}
continue;
}
$playlists[] = $playlist->toVkApiStruct($this->getUser());
}
return (object) [
"count" => $search->size(),
"items" => $playlists,
];
}
public 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(0o404, "Invalid group_id");
} elseif (!$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();
}
public 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(0o404, "Album not found");
} elseif (!$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);
}
public function deleteAlbum(int $album_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios())->getPlaylist($album_id);
if (!$album) {
$this->fail(0o404, "Album not found");
} elseif (!$album->canBeModifiedBy($this->getUser())) {
$this->fail(600, "Insufficient rights to this album");
}
$album->delete();
return 1;
}
public function moveToAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios())->getPlaylist($album_id);
if (!$album) {
$this->fail(0o404, "Album not found");
} elseif (!$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;
} elseif (!$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;
}
public function removeFromAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios())->getPlaylist($album_id);
if (!$album) {
$this->fail(0o404, "Album not found");
} elseif (!$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;
} elseif ($audio->canBeViewedBy($this->getUser())) {
continue;
}
$audios[] = $audio;
}
if (sizeof($audios) < 1) {
return 0;
}
foreach ($audios as $audio) {
$album->remove($audio);
}
return 1;
}
public function copyToAlbum(int $album_id, string $audio_ids): int
{
return $this->moveToAlbum($album_id, $audio_ids);
}
public function bookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios())->getPlaylist($id);
if (!$album) {
$this->fail(0o404, "Not found");
}
if (!$album->canBeViewedBy($this->getUser())) {
$this->fail(600, "Access error");
}
return (int) $album->bookmark($this->getUser());
}
public function unBookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios())->getPlaylist($id);
if (!$album) {
$this->fail(0o404, "Not found");
}
if (!$album->canBeViewedBy($this->getUser())) {
$this->fail(600, "Access error");
}
return (int) $album->unbookmark($this->getUser());
}
}