Compare commits

...

6 commits

Author SHA1 Message Date
Vladimir Barinov
8db4bc788b
Merge branch 'master' into n1rwana-geo 2024-12-13 17:53:25 +03:00
mrilyew
bec9079e36
feat(privacy): blacklist v2 (#1183)
* Перенос ветки blacklist (#900)

* Blacklist

* Config

* upd

* Added restrictions in the users.get method

* ok

* Update en.strings

* ok 2.0

---------

Co-authored-by: Vladimir Barinov <veselcraft@icloud.com>

* Create 00038-blacklist.sql

* typo xd

* Blacklists: Make it barely work (xd)

* БЛЯЯЯЯЯЯЯЯТЬ

* remove all

* account.ban, account.unban, account.getBanned

* rewrite ui

* add link

* add ignore button to blacklisted users

* fields blacklisted_by_me, blacklisted

* ad ability to blacklist when you ar blacklisted

---------

Co-authored-by: n1rwana <me@n1rwana.xyz>
Co-authored-by: Vladimir Barinov <veselcraft@icloud.com>
Co-authored-by: n1rwana <aydashkin@vk.com>
2024-12-13 17:51:10 +03:00
n1rwana
2e70a26283
feat(api): reports (#959)
* API для отправки жалобы

* make compatible with vk api

---------

Co-authored-by: mrilyew <99399973+mrilyew@users.noreply.github.com>
2024-12-13 17:11:26 +03:00
veselcraft
29f4de2dab
fix(video): tweak ffmpeg args 2024-12-13 17:06:56 +03:00
mrilyew
0a1f717b45 fix(xss): fix #1181 2024-12-13 16:43:34 +03:00
veselcraft
198bf7472d
fix(datetime): adjust to timezone 2024-12-13 16:24:51 +03:00
26 changed files with 512 additions and 24 deletions

View file

@ -228,4 +228,72 @@ final class Account extends VKAPIRequestHandler
return (object) ['votes' => $this->getUser()->getCoins()]; return (object) ['votes' => $this->getUser()->getCoins()];
} }
function ban(int $owner_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
if($owner_id < 0)
return 1;
if($owner_id == $this->getUser()->getId())
$this->fail(15, "Access denied: cannot blacklist yourself");
$config_limit = OPENVK_ROOT_CONF['openvk']['preferences']['blacklists']['limit'] ?? 100;
$user_blocks = $this->getUser()->getBlacklistSize();
if(($user_blocks + 1) > $config_limit)
$this->fail(-7856, "Blacklist limit exceeded");
$entity = get_entity_by_id($owner_id);
if(!$entity || $entity->isDeleted())
return 0;
if($entity->isBlacklistedBy($this->getUser()))
return 1;
$this->getUser()->addToBlacklist($entity);
return 1;
}
function unban(int $owner_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
if($owner_id < 0)
return 1;
if($owner_id == $this->getUser()->getId())
return 1;
$entity = get_entity_by_id($owner_id);
if(!$entity || $entity->isDeleted())
return 0;
if(!$entity->isBlacklistedBy($this->getUser()))
return 1;
$this->getUser()->removeFromBlacklist($entity);
return 1;
}
function getBanned(int $offset = 0, int $count = 100, string $fields = ""): object
{
$this->requireUser();
$result = (object)[
'count' => $this->getUser()->getBlacklistSize(),
'items' => [],
];
$banned = $this->getUser()->getBlacklist($offset, $count);
foreach($banned as $ban) {
if(!$ban) continue;
$result->items[] = $ban->toVkApiStruct($this->getUser(), $fields);
}
return $result;
}
} }

View file

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\Report;
use openvk\Web\Models\Repositories\Reports as ReportsRepo;
final class Reports extends VKAPIRequestHandler
{
function add(int $owner_id = 0, string $comment = "", int $reason = 0, string $type = "", string $report_source = ""): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$allowed_types = ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"];
if($type == "" || !in_array($type, $allowed_types)) {
$this->fail(100, "One of the parameters specified was missing or invalid: type should be ".implode(", ", $allowed_types));
}
if($owner_id <= 0) {
$this->fail(100, "One of the parameters specified was missing or invalid: Bad input");
}
if(mb_strlen($comment) === 0) {
$this->fail(100, "One of the parameters specified was missing or invalid: Comment can't be empty");
}
if($type == "user" && $owner_id == $this->getUser()->getId()) {
return 1;
}
if($this->getUser()->isBannedInSupport()) {
return 0;
}
if(sizeof(iterator_to_array((new ReportsRepo)->getDuplicates($type, $owner_id, NULL, $this->getUser()->getId()))) > 0) {
return 1;
}
try {
$report = new Report;
$report->setUser_id($this->getUser()->getId());
$report->setTarget_id($owner_id);
$report->setType($type);
$report->setReason($comment);
$report->setCreated(time());
$report->save();
} catch(\Throwable $e) {
$this->fail(-1, "Unknown error failed");
}
return 1;
}
}

View file

@ -266,6 +266,20 @@ final class Users extends VKAPIRequestHandler
case 'nickname': case 'nickname':
$response[$i]->nickname = $usr->getPseudo(); $response[$i]->nickname = $usr->getPseudo();
break; break;
case 'blacklisted_by_me':
if(!$authuser) {
continue;
}
$response[$i]->blacklisted_by_me = (int)$usr->isBlacklistedBy($this->getUser());
break;
case 'blacklisted':
if(!$authuser) {
continue;
}
$response[$i]->blacklisted = (int)$this->getUser()->isBlacklistedBy($usr);
break;
} }
} }

View file

@ -5,7 +5,7 @@ use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime; use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift, Audio}; 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;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -511,7 +511,7 @@ class User extends RowModel
else if($user->getId() === $this->getId()) else if($user->getId() === $this->getId())
return true; return true;
if($permission != "messages.write" && !$this->canBeViewedBy($user)) if(/*$permission != "messages.write" && */!$this->canBeViewedBy($user, true))
return false; return false;
switch($permStatus) { switch($permStatus) {
@ -1228,6 +1228,11 @@ class User extends RowModel
return (bool) $this->getRecord()->activated; return (bool) $this->getRecord()->activated;
} }
function isAdmin(): bool
{
return $this->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL);
}
function isDead(): bool function isDead(): bool
{ {
return $this->onlineStatus() == 2; return $this->onlineStatus() == 2;
@ -1289,17 +1294,21 @@ class User extends RowModel
return $this->getRecord()->profile_type; return $this->getRecord()->profile_type;
} }
function canBeViewedBy(?User $user = NULL): bool function canBeViewedBy(?User $user = NULL, bool $blacklist_check = true): bool
{ {
if(!is_null($user)) { if(!is_null($user)) {
if($this->getId() == $user->getId()) { if($this->getId() == $user->getId()) {
return true; return true;
} }
if($user->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)) { if($user->isAdmin() && !(OPENVK_ROOT_CONF['openvk']['preferences']['blacklists']['applyToAdmins'] ?? true)) {
return true; return true;
} }
if($blacklist_check && ($this->isBlacklistedBy($user) || $user->isBlacklistedBy($this))) {
return false;
}
if($this->getProfileType() == 0) { if($this->getProfileType() == 0) {
return true; return true;
} else { } else {
@ -1409,6 +1418,20 @@ class User extends RowModel
case 'real_id': case 'real_id':
$res->real_id = $this->getRealId(); $res->real_id = $this->getRealId();
break; break;
case "blacklisted_by_me":
if(!$user) {
continue;
}
$res->blacklisted_by_me = (int)$this->isBlacklistedBy($user);
break;
case "blacklisted":
if(!$user) {
continue;
}
$res->blacklisted = (int)$user->isBlacklistedBy($this);
break;
} }
} }
@ -1486,6 +1509,76 @@ class User extends RowModel
return DatabaseConnection::i()->getContext()->table("ignored_sources")->where("owner", $this->getId())->count(); return DatabaseConnection::i()->getContext()->table("ignored_sources")->where("owner", $this->getId())->count();
} }
function isBlacklistedBy(?User $user = NULL): bool
{
if(!$user)
return false;
$ctx = DatabaseConnection::i()->getContext();
$data = [
"author" => $user->getId(),
"target" => $this->getRealId(),
];
$sub = $ctx->table("blacklist_relations")->where($data);
return $sub->count() > 0;
}
function addToBlacklist(?User $user)
{
DatabaseConnection::i()->getContext()->table("blacklist_relations")->insert([
"author" => $this->getRealId(),
"target" => $user->getRealId(),
"created" => time(),
]);
DatabaseConnection::i()->getContext()->table("subscriptions")->where([
"follower" => $user->getId(),
"model" => static::class,
"target" => $this->getId(),
])->delete();
DatabaseConnection::i()->getContext()->table("subscriptions")->where([
"follower" => $this->getId(),
"model" => static::class,
"target" => $user->getId(),
])->delete();
return true;
}
function removeFromBlacklist(?User $user): bool
{
DatabaseConnection::i()->getContext()->table("blacklist_relations")->where([
"author" => $this->getRealId(),
"target" => $user->getRealId(),
])->delete();
return true;
}
function getBlacklist(int $offset = 0, int $limit = 10)
{
$sources = DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->limit($limit, $offset)->order('created ASC');
$output_array = [];
foreach($sources as $source) {
$entity_id = (int)$source->target ;
$entity = (new Users)->get($entity_id);
if(!$entity)
continue;
$output_array[] = $entity;
}
return $output_array;
}
function getBlacklistSize()
{
return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count();
}
use Traits\TBackDrops; use Traits\TBackDrops;
use Traits\TSubscribable; use Traits\TSubscribable;
use Traits\TAudioStatuses; use Traits\TAudioStatuses;

View file

@ -13,7 +13,7 @@ Move-Item $file $temp
# video stub logic was implicitly deprecated, so we start processing at once # video stub logic was implicitly deprecated, so we start processing at once
ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif" ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif"
ffmpeg -i $temp -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=480:-1,setsar=1" -y $temp2 ffmpeg -i $temp -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=iw*min(1\,if(gt(iw\,ih)\,640/iw\,(640*sar)/ih)):(floor((ow/dar)/2))*2" -y $temp2
Move-Item $temp2 "$dir$hashT/$hash.mp4" Move-Item $temp2 "$dir$hashT/$hash.mp4"
Remove-Item $temp Remove-Item $temp

View file

@ -3,7 +3,7 @@ tmpfile="$RANDOM-$(date +%s%N)"
cp $2 "/tmp/vid_$tmpfile.bin" cp $2 "/tmp/vid_$tmpfile.bin"
nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif
nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=480:-1,setsar=1" -y "/tmp/ffmOi$tmpfile.mp4" nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=iw*min(1\,if(gt(iw\,ih)\,640/iw\,(640*sar)/ih)):(floor((ow/dar)/2))*2" -y "/tmp/ffmOi$tmpfile.mp4"
rm -rf $3${4:0:2}/$4.mp4 rm -rf $3${4:0:2}/$4.mp4
mv "/tmp/ffmOi$tmpfile.mp4" $3${4:0:2}/$4.mp4 mv "/tmp/ffmOi$tmpfile.mp4" $3${4:0:2}/$4.mp4

View file

@ -119,7 +119,7 @@ final class GroupPresenter extends OpenVKPresenter
$this->template->paginatorConf = (object) [ $this->template->paginatorConf = (object) [
"count" => $this->template->count, "count" => $this->template->count,
"page" => $this->queryParam("p") ?? 1, "page" => $this->queryParam("p") ?? 1,
"amount" => NULL, "amount" => 10,
"perPage" => OPENVK_DEFAULT_PER_PAGE, "perPage" => OPENVK_DEFAULT_PER_PAGE,
]; ];
} }

View file

@ -27,6 +27,7 @@ final class PhotosPresenter extends OpenVKPresenter
if(!$user) $this->notFound(); if(!$user) $this->notFound();
if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL)) if (!$user->getPrivacyPermission('photos.read', $this->user->identity ?? NULL))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); $this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$this->template->albums = $this->albums->getUserAlbums($user, (int)($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;
@ -161,8 +162,10 @@ final class PhotosPresenter extends OpenVKPresenter
{ {
$photo = $this->photos->getByOwnerAndVID($ownerId, $photoId); $photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
if(!$photo || $photo->isDeleted()) $this->notFound(); if(!$photo || $photo->isDeleted()) $this->notFound();
if(!$photo->canBeViewedBy($this->user->identity)) if(!$photo->canBeViewedBy($this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); $this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
if(!is_null($this->queryParam("from"))) { if(!is_null($this->queryParam("from"))) {
if(preg_match("%^album([0-9]++)$%", $this->queryParam("from"), $matches) === 1) { if(preg_match("%^album([0-9]++)$%", $this->queryParam("from"), $matches) === 1) {
$album = $this->albums->get((int) $matches[1]); $album = $this->albums->get((int) $matches[1]);

View file

@ -90,6 +90,9 @@ final class ReportPresenter extends OpenVKPresenter
if(!$id) if(!$id)
exit(json_encode([ "error" => tr("error_segmentation") ])); exit(json_encode([ "error" => tr("error_segmentation") ]));
if ($this->queryParam("type") === "user" && $id === $this->user->id)
exit(json_encode([ "error" => "You can't report yourself" ]));
if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) { 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;

View file

@ -29,10 +29,22 @@ final class UserPresenter extends OpenVKPresenter
function renderView(int $id): void function renderView(int $id): void
{ {
$user = $this->users->get($id); $user = $this->users->get($id);
if(!$user || $user->isDeleted() || !$user->canBeViewedBy($this->user->identity)) { if(!$user || $user->isDeleted() || !$user->canBeViewedBy($this->user->identity)) {
if(!is_null($user) && $user->isDeactivated()) { if(!is_null($user) && $user->isDeactivated()) {
$this->template->_template = "User/deactivated.xml"; $this->template->_template = "User/deactivated.xml";
$this->template->user = $user;
} else if($this->user->identity->isBlacklistedBy($user)) {
$this->template->_template = "User/blacklisted.xml";
$this->template->blacklist_status = $user->isBlacklistedBy($this->user->identity);
$this->template->ignore_status = $user->isIgnoredBy($this->user->identity);
$this->template->user = $user;
} else if($user->isBlacklistedBy($this->user->identity)) {
$this->template->_template = "User/blacklisted_pov.xml";
$this->template->ignore_status = $user->isIgnoredBy($this->user->identity);
$this->template->user = $user; $this->template->user = $user;
} else if(!is_null($user) && !$user->canBeViewedBy($this->user->identity)) { } else if(!is_null($user) && !$user->canBeViewedBy($this->user->identity)) {
$this->template->_template = "User/private.xml"; $this->template->_template = "User/private.xml";
@ -57,6 +69,7 @@ final class UserPresenter extends OpenVKPresenter
if($id !== $this->user->id) { if($id !== $this->user->id) {
$this->template->ignore_status = $user->isIgnoredBy($this->user->identity); $this->template->ignore_status = $user->isIgnoredBy($this->user->identity);
$this->template->blacklist_status = $user->isBlacklistedBy($this->user->identity);
} }
} }
} }
@ -578,7 +591,7 @@ final class UserPresenter extends OpenVKPresenter
$this->flash("succ", tr("changes_saved"), tr("changes_saved_comment")); $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment"));
} }
$this->template->mode = in_array($this->queryParam("act"), [ $this->template->mode = in_array($this->queryParam("act"), [
"main", "security", "privacy", "finance", "finance.top-up", "interface" "main", "security", "privacy", "finance", "finance.top-up", "interface", "blacklist"
]) ? $this->queryParam("act") ]) ? $this->queryParam("act")
: "main"; : "main";
@ -591,6 +604,19 @@ final class UserPresenter extends OpenVKPresenter
$this->template->qrCodeType = substr($qrCode[0], 5); $this->template->qrCodeType = substr($qrCode[0], 5);
$this->template->qrCodeData = $qrCode[1]; $this->template->qrCodeData = $qrCode[1];
} else if($this->template->mode === "blacklist") {
$page = (int)($this->queryParam('p') ?? 1);
$count = 10;
$offset = ($page - 1) * $count;
$this->template->blSize = $this->user->identity->getBlacklistSize();
$this->template->blItems = $this->user->identity->getBlacklist($offset, $count);
$this->template->paginatorConf = (object) [
"count" => $this->template->blSize,
"page" => $page,
"amount" => sizeof($this->template->blItems),
"perPage" => OPENVK_DEFAULT_PER_PAGE,
];
} }
$this->template->user = $user; $this->template->user = $user;

View file

@ -164,11 +164,11 @@
<tbody> <tbody>
<tr> <tr>
<td width="120" valign="top"><span class="nobold">${tr('performer')}:</span></td> <td width="120" valign="top"><span class="nobold">${tr('performer')}:</span></td>
<td><input value='${audio_element.info.performer}' name="performer" type="text" autocomplete="off" maxlength="80" /></td> <td><input value='${escapeHtml(audio_element.info.performer)}' name="performer" type="text" autocomplete="off" maxlength="80" /></td>
</tr> </tr>
<tr> <tr>
<td width="120" valign="top"><span class="nobold">${tr('audio_name')}:</span></td> <td width="120" valign="top"><span class="nobold">${tr('audio_name')}:</span></td>
<td><input type="text" value='${audio_element.info.name}' name="name" autocomplete="off" maxlength="80" /></td> <td><input type="text" value='${escapeHtml(audio_element.info.name)}' name="name" autocomplete="off" maxlength="80" /></td>
</tr> </tr>
<tr> <tr>
<td width="120" valign="top"><span class="nobold">${tr('genre')}:</span></td> <td width="120" valign="top"><span class="nobold">${tr('genre')}:</span></td>
@ -178,7 +178,7 @@
</tr> </tr>
<tr> <tr>
<td width="120" valign="top"><span class="nobold">${tr('lyrics')}:</span></td> <td width="120" valign="top"><span class="nobold">${tr('lyrics')}:</span></td>
<td><textarea name="lyrics" style="resize: vertical;max-height: 300px;">${audio_element.info.lyrics}</textarea></td> <td><textarea name="lyrics" style="resize: vertical;max-height: 300px;">${escapeHtml(audio_element.info.lyrics)}</textarea></td>
</tr> </tr>
<tr> <tr>
<td width="120" valign="top"></td> <td width="120" valign="top"></td>

View file

@ -16,7 +16,7 @@
{elseif $type == "group" || $type == "user"} {elseif $type == "group" || $type == "user"}
{include "../components/group.xml", group => $object, isUser => $type == "user"} {include "../components/group.xml", group => $object, isUser => $type == "user"}
{elseif $type == "comment"} {elseif $type == "comment"}
{include "../components/comment.xml", comment => $object, timeOnly => true, linkWithPost => true} {include "../components/comment.xml", comment => $object, timeOnly => true, correctLink => true}
{elseif $type == "note"} {elseif $type == "note"}
{include "./content/note.xml", note => $object} {include "./content/note.xml", note => $object}
{elseif $type == "app"} {elseif $type == "app"}

View file

@ -13,6 +13,7 @@
{var $isFinance = $mode === 'finance'} {var $isFinance = $mode === 'finance'}
{var $isFinanceTU = $mode === 'finance.top-up'} {var $isFinanceTU = $mode === 'finance.top-up'}
{var $isInterface = $mode === 'interface'} {var $isInterface = $mode === 'interface'}
{var $isBl = $mode === 'blacklist'}
<div class="tabs"> <div class="tabs">
<div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab">
@ -24,6 +25,9 @@
<div n:attr="id => ($isPrivacy ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isPrivacy ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isPrivacy ? 'act_tab_a' : 'ki')" href="/settings?act=privacy">{_privacy}</a> <a n:attr="id => ($isPrivacy ? 'act_tab_a' : 'ki')" href="/settings?act=privacy">{_privacy}</a>
</div> </div>
<div n:attr="id => ($isBl ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isBl ? 'act_tab_a' : 'ki')" href="/settings?act=blacklist">{_blacklist}</a>
</div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce']" n:attr="id => (($isFinance || $isFinanceTU) ? 'activetabs' : 'ki')" class="tab"> <div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce']" n:attr="id => (($isFinance || $isFinanceTU) ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => (($isFinance || $isFinanceTU) ? 'act_tab_a' : 'ki')" href="/settings?act=finance">{_points}</a> <a n:attr="id => (($isFinance || $isFinanceTU) ? 'act_tab_a' : 'ki')" href="/settings?act=finance">{_points}</a>
</div> </div>
@ -713,7 +717,29 @@
</tbody> </tbody>
</table> </table>
</form> </form>
{elseif $isBl}
{if $blSize < 1}
{include "../components/error.xml", description => tr("bl_count_zero_desc")}
{else}
<h4 style="margin-bottom: 10px;">{tr("bl_count", $blSize)}.</h4>
<div class='entity_vertical_list mini m_mini scroll_container'>
<div n:foreach="$blItems as $item" class="entity_vertical_list_item scroll_node">
<div class="first_column">
<a href="{$item->getURL()}" class="avatar">
<img src='{$item->getAvatarURL()}'>
</a>
<div class="info">
<b class="noOverflow">
<a href="{$item->getURL()}">
{$item->getCanonicalName()}
</a>
</b>
</div>
</div>
</div>
</div>
{include "../components/paginator.xml", conf => $paginatorConf}
{/if}
{/if} {/if}
</div> </div>

View file

@ -165,6 +165,9 @@
</form> </form>
{/if} {/if}
<a n:if="!$blacklist_status" id="_bl_toggler" data-name="{$user->getMorphedName('genitive', false)}" data-val="1" data-id="{$user->getRealId()}" class="profile_link" style="display:block;width:96%;">{_bl_add}</a>
{* 4 admins *}
<a n:if="$blacklist_status" id="_bl_toggler" data-val="0" data-id="{$user->getRealId()}" class="profile_link" style="display:block;width:96%;">{_bl_remove}</a>
<a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser({$user->getId()})">{_report}</a> <a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser({$user->getId()})">{_report}</a>
<a n:if="!$user->isHideFromGlobalFeedEnabled()" class="profile_link" style="display:block;width:96%;" id="__ignoreSomeone" data-val='{!$ignore_status ? 1 : 0}' data-id="{$user->getId()}"> <a n:if="!$user->isHideFromGlobalFeedEnabled()" class="profile_link" style="display:block;width:96%;" id="__ignoreSomeone" data-val='{!$ignore_status ? 1 : 0}' data-id="{$user->getId()}">
{if !$ignore_status}{_ignore_user}{else}{_unignore_user}{/if} {if !$ignore_status}{_ignore_user}{else}{_unignore_user}{/if}

View file

@ -0,0 +1,42 @@
{extends "../@layout.xml"}
{block title}{$user->getCanonicalName()}{/block}
{block header}
{$user->getCanonicalName()}
<img n:if="$user->isVerified()"
class="name-checkmark"
src="/assets/packages/static/openvk/img/checkmark.png"
/>
{/block}
{block content}
<div class="left_small_block">
<div>
<img src="{$user->getAvatarUrl('normal')}"
alt="{$user->getCanonicalName()}"
style="width: 100%; image-rendering: -webkit-optimize-contrast;" />
</div>
<div id="profile_links" n:if="isset($thisUser)">
<a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser({$user->getId()})">{_report}</a>
<a n:if="!$blacklist_status" id="_bl_toggler" data-name="{$user->getMorphedName('genitive', false)}" data-val="1" data-id="{$user->getRealId()}" class="profile_link" style="display:block;width:96%;">{_bl_add}</a>
<a n:if="$blacklist_status" id="_bl_toggler" data-val="0" data-id="{$user->getRealId()}" class="profile_link" style="display:block;width:96%;">{_bl_remove}</a>
<a n:if="!$user->isHideFromGlobalFeedEnabled()" class="profile_link" style="display:block;width:96%;" id="__ignoreSomeone" data-val='{!$ignore_status ? 1 : 0}' data-id="{$user->getId()}">
{if !$ignore_status}{_ignore_user}{else}{_unignore_user}{/if}
</a>
</div>
</div>
<div class="right_big_block">
<div class="page_info">
<div class="accountInfo clearFix">
<div class="profileName">
<h2>{$user->getFullName()}</h2>
</div>
</div>
<div class="msg msg_yellow" style="width: 93%;margin-top: 10px;">
{var $m = $user->isFemale() ? "f" : "m"}
{tr("limited_access_to_page_$m", $user->getFirstName())}
</div>
</div>
</div>
{/block}

View file

@ -0,0 +1,40 @@
{extends "../@layout.xml"}
{block title}{$user->getCanonicalName()}{/block}
{block header}
{$user->getCanonicalName()}
<img n:if="$user->isVerified()"
class="name-checkmark"
src="/assets/packages/static/openvk/img/checkmark.png"
/>
{/block}
{block content}
<div class="left_small_block">
<div>
<img src="{$user->getAvatarUrl('normal')}"
alt="{$user->getCanonicalName()}"
style="width: 100%; image-rendering: -webkit-optimize-contrast;" />
</div>
<div id="profile_links" n:if="isset($thisUser)">
<a n:if="!$blacklist_status" id="_bl_toggler" data-val="0" data-id="{$user->getRealId()}" class="profile_link" style="display:block;width:96%;">{_bl_remove}</a>
<a n:if="!$user->isHideFromGlobalFeedEnabled()" class="profile_link" style="display:block;width:96%;" id="__ignoreSomeone" data-val='{!$ignore_status ? 1 : 0}' data-id="{$user->getId()}">
{if !$ignore_status}{_ignore_user}{else}{_unignore_user}{/if}
</a>
<a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser({$user->getId()})">{_report}</a>
</div>
</div>
<div class="right_big_block">
<div class="page_info">
<div class="accountInfo clearFix">
<div class="profileName">
<h2>{$user->getFullName()}</h2>
</div>
</div>
<div class="msg msg_yellow" style="width: 93%;margin-top: 10px;">
{tr("you_blacklisted", $user->getMorphedName("genitive", false))}.
</div>
</div>
</div>
{/block}

View file

@ -41,6 +41,7 @@
<input type="submit" class="profile_link" value="{_friends_reject}" style="width: 194px;" /> <input type="submit" class="profile_link" value="{_friends_reject}" style="width: 194px;" />
</form> </form>
{/if} {/if}
<a id="_bl_toggler" data-name="{$user->getMorphedName('genitive', false)}" data-val="1" data-id="{$user->getRealId()}" class="profile_link" style="display:block;width:96%;">{_bl_add}</a>
<a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser({$user->getId()})">{_report}</a> <a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser({$user->getId()})">{_report}</a>
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Util; namespace openvk\Web\Util;
use Chandler\Session\Session;
class DateTime class DateTime
{ {
@ -21,17 +22,19 @@ class DateTime
$then = date_create("@" . $this->timestamp); $then = date_create("@" . $this->timestamp);
$now = date_create(); $now = date_create();
$diff = date_diff($now, $then); $diff = date_diff($now, $then);
$sessionOffset = intval(Session::i()->get("_timezoneOffset"));
if($diff->invert === 0) if($diff->invert === 0)
return ovk_strftime_safe("%e %B %Y ", $this->timestamp) . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp); return ovk_strftime_safe("%e %B %Y ", $this->timestamp) . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp);
if($this->timestamp >= strtotime("midnight")) { # Today if(($this->timestamp + $sessionOffset) >= (strtotime("midnight") + $sessionOffset)) { # Today
if($diff->h >= 1) if($diff->h >= 1)
return tr("time_today") . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp); return tr("time_today") . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp);
else if($diff->i < 2) else if($diff->i < 2)
return tr("time_just_now"); return tr("time_just_now");
else else
return $diff->i === 5 ? tr("time_exactly_five_minutes_ago") : tr("time_minutes_ago", $diff->i); return $diff->i === 5 ? tr("time_exactly_five_minutes_ago") : tr("time_minutes_ago", $diff->i);
} else if($this->timestamp >= strtotime("-1day midnight")) { # Yesterday } else if(($this->timestamp + $sessionOffset) >= (strtotime("-1day midnight") + $sessionOffset)) { # Yesterday
return tr("time_yesterday") . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp); return tr("time_yesterday") . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp);
} else if(ovk_strftime_safe("%Y", $this->timestamp) === ovk_strftime_safe("%Y", time())) { # In this year } else if(ovk_strftime_safe("%Y", $this->timestamp) === ovk_strftime_safe("%Y", time())) { # In this year
return ovk_strftime_safe("%e %h ", $this->timestamp) . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp); return ovk_strftime_safe("%e %h ", $this->timestamp) . tr("time_at_sp") . ovk_strftime_safe(" %R", $this->timestamp);

View file

@ -3602,6 +3602,11 @@ hr {
overflow-y: auto; overflow-y: auto;
} }
.entity_vertical_list.scroll_container {
height: unset;
overflow-y: unset;
}
.entity_vertical_list .entity_vertical_list_item { .entity_vertical_list .entity_vertical_list_item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -3616,6 +3621,15 @@ hr {
gap: 4px; gap: 4px;
} }
.entity_vertical_list.m_mini .entity_vertical_list_item .first_column {
gap: 10px;
}
.entity_vertical_list.m_mini .entity_vertical_list_item .first_column .avatar img {
width: 30px;
height: 30px;
}
.entity_vertical_list .entity_vertical_list_item .avatar { .entity_vertical_list .entity_vertical_list_item .avatar {
display: block; display: block;
} }

View file

@ -1306,12 +1306,12 @@ u(document).on("click", ".musicIcon.edit-icon", (e) => {
MessageBox(tr("edit_audio"), ` MessageBox(tr("edit_audio"), `
<div> <div>
${tr("performer")} ${tr("performer")}
<input name="performer" maxlength="256" type="text" value="${performer}"> <input name="performer" maxlength="256" type="text" value="${escapeHtml(performer)}">
</div> </div>
<div style="margin-top: 11px"> <div style="margin-top: 11px">
${tr("audio_name")} ${tr("audio_name")}
<input name="name" maxlength="256" type="text" value="${name}"> <input name="name" maxlength="256" type="text" value="${escapeHtml(name)}">
</div> </div>
<div style="margin-top: 11px"> <div style="margin-top: 11px">
@ -1359,7 +1359,7 @@ u(document).on("click", ".musicIcon.edit-icon", (e) => {
e.target.setAttribute("data-performer", escapeHtml(response.new_info.performer)) e.target.setAttribute("data-performer", escapeHtml(response.new_info.performer))
e.target.setAttribute("data-title", escapeHtml(response.new_info.name)) e.target.setAttribute("data-title", escapeHtml(response.new_info.name))
e.target.setAttribute("data-lyrics", response.new_info.lyrics_unformatted) e.target.setAttribute("data-lyrics", escapeHtml(response.new_info.lyrics_unformatted))
e.target.setAttribute("data-explicit", Number(response.new_info.explicit)) e.target.setAttribute("data-explicit", Number(response.new_info.explicit))
e.target.setAttribute("data-searchable", Number(!response.new_info.unlisted)) e.target.setAttribute("data-searchable", Number(!response.new_info.unlisted))
player.setAttribute("data-genre", response.new_info.genre) player.setAttribute("data-genre", response.new_info.genre)
@ -1374,7 +1374,7 @@ u(document).on("click", ".musicIcon.edit-icon", (e) => {
} else { } else {
player.insertAdjacentHTML("beforeend", ` player.insertAdjacentHTML("beforeend", `
<div class="lyrics"> <div class="lyrics">
${response.new_info.lyrics} ${escapeHtml(response.new_info.lyrics)}
</div> </div>
`) `)

View file

@ -2579,7 +2579,7 @@ async function changeStatus() {
document.querySelector("#page_status_text").innerHTML = `[ ${tr("change_status")} ]`; document.querySelector("#page_status_text").innerHTML = `[ ${tr("change_status")} ]`;
document.querySelector("#page_status_text").className = "edit_link page_status_edit_button"; document.querySelector("#page_status_text").className = "edit_link page_status_edit_button";
} else { } else {
document.querySelector("#page_status_text").innerHTML = status; document.querySelector("#page_status_text").innerHTML = escapeHtml(status);
document.querySelector("#page_status_text").className = "page_status page_status_edit_button"; document.querySelector("#page_status_text").className = "page_status page_status_edit_button";
} }
@ -2828,3 +2828,42 @@ function openNearPosts(posts) {
MessageBox(tr('nearest_posts'), `<center style='color: grey;'>${tr('no_nearest_posts')}</center>`, ["OK"], [Function.noop]); MessageBox(tr('nearest_posts'), `<center style='color: grey;'>${tr('no_nearest_posts')}</center>`, ["OK"], [Function.noop]);
} }
} }
u(document).on('click', '#_bl_toggler', async (e) => {
e.preventDefault()
const target = u(e.target)
const val = Number(target.attr('data-val'))
const id = Number(target.attr('data-id'))
const name = target.attr('data-name')
const fallback = (e) => {
fastError(e.message)
target.removeClass('lagged')
}
if(val == 1) {
const msg = new CMessageBox({
title: tr('addition_to_bl'),
body: `<span>${escapeHtml(tr('adding_to_bl_sure', name))}</span>`,
buttons: [tr('yes'), tr('no')],
callbacks: [async () => {
try {
target.addClass('lagged')
await window.OVKAPI.call('account.ban', {'owner_id': id})
window.router.route(location.href)
} catch(e) {
fallback(e)
}
}, () => Function.noop]
})
} else {
try {
target.addClass('lagged')
await window.OVKAPI.call('account.unban', {'owner_id': id})
window.router.route(location.href)
} catch(e) {
fallback(e)
}
}
})

View file

@ -0,0 +1,8 @@
CREATE TABLE `blacklist_relations` (
`index` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`author` BIGINT UNSIGNED NOT NULL,
`target` BIGINT UNSIGNED NOT NULL,
`created` BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (`index`)
) ENGINE = InnoDB;
ALTER TABLE `blacklist_relations` ADD INDEX(`author`, `target`);

View file

@ -1711,6 +1711,7 @@
"admin_commerce_disabled" = "Commerce has been disabled by the system administrator"; "admin_commerce_disabled" = "Commerce has been disabled by the system administrator";
"admin_commerce_disabled_desc" = "The voucher and gift settings will be saved, but will have no effect."; "admin_commerce_disabled_desc" = "The voucher and gift settings will be saved, but will have no effect.";
"admin_privacy_warning" = "Be careful with this information";
"admin_longpool_broken" = "Longpool is broken and will not work!"; "admin_longpool_broken" = "Longpool is broken and will not work!";
"admin_longpool_broken_desc" = "Make sure file at the path <code>$1</code> exist and have correct rights and ownership."; "admin_longpool_broken_desc" = "Make sure file at the path <code>$1</code> exist and have correct rights and ownership.";
@ -1827,6 +1828,28 @@
"cookies_popup_content" = "Just like how kids love cookies, this website uses Cookies to identify your session and nothing more. Check <a href='/privacy'>our privacy policy</a> for more information."; "cookies_popup_content" = "Just like how kids love cookies, this website uses Cookies to identify your session and nothing more. Check <a href='/privacy'>our privacy policy</a> for more information.";
"cookies_popup_agree" = "Accept"; "cookies_popup_agree" = "Accept";
/* Blacklist */
"blacklist" = "Blacklist";
"user_blacklisted_you" = "This user has blacklisted you.";
"user_blacklisted" = "$1 has been blacklisted";
"user_removed_from_the_blacklist" = "$1 has been removed from the blacklist.";
"adding_to_bl_sure" = "You sure you want to blacklist $1?";
"bl_count_zero_desc" = "There are no users on your blacklist yet.";
"bl_count_zero" = "There are no users on your blacklist";
"bl_count_one" = "You have one user on your blacklist";
"bl_count_few" = "You have $1 users on your blacklist";
"bl_count_many" = "You have $1 users on your blacklist";
"bl_count_other" = "You have $1 users on your blacklist";
"you_blacklisted" = "You blacklisted $1";
"bl_add" = "Add to blacklist";
"bl_remove" = "Remove from blacklist";
"addition_to_bl" = "Addition to blacklist";
/* Away */ /* Away */
"transition_is_blocked" = "Transition is blocked"; "transition_is_blocked" = "Transition is blocked";

View file

@ -1603,8 +1603,11 @@
"admin_about_instance" = "Инстанция"; "admin_about_instance" = "Инстанция";
"admin_commerce_disabled" = "Коммерция отключена системным администратором"; "admin_commerce_disabled" = "Коммерция отключена системным администратором";
"admin_commerce_disabled_desc" = "Настройки ваучеров и подарков будут сохранены, но не будут оказывать никакого влияния."; "admin_commerce_disabled_desc" = "Настройки ваучеров и подарков будут сохранены, но не будут оказывать никакого влияния.";
"admin_privacy_warning" = "Будьте осторожны с этой информацией";
"admin_longpool_broken" = "Longpool сломан!"; "admin_longpool_broken" = "Longpool сломан!";
"admin_longpool_broken_desc" = "Проверьте, существует ли файл по пути <code>$1</code> и выданы ли у него правильные права на запись."; "admin_longpool_broken_desc" = "Проверьте, существует ли файл по пути <code>$1</code> и выданы ли у него правильные права на запись.";
"admin_banned_links" = "Заблокированные ссылки"; "admin_banned_links" = "Заблокированные ссылки";
"admin_banned_link" = "Ссылка"; "admin_banned_link" = "Ссылка";
"admin_banned_domain" = "Домен"; "admin_banned_domain" = "Домен";
@ -1702,6 +1705,7 @@
"edit_action" = "Изменить"; "edit_action" = "Изменить";
"transfer" = "Передать"; "transfer" = "Передать";
"close" = "Закрыть"; "close" = "Закрыть";
"success" = "Успех";
"warning" = "Внимание"; "warning" = "Внимание";
"question_confirm" = "Это действие нельзя отменить. Вы действительно уверены в том что хотите сделать?"; "question_confirm" = "Это действие нельзя отменить. Вы действительно уверены в том что хотите сделать?";
"confirm_m" = "Подтвердить"; "confirm_m" = "Подтвердить";
@ -1720,6 +1724,28 @@
"cookies_popup_content" = "Все дети любят печенье, поэтому этот веб-сайт использует Cookies для того, чтобы идентифицировать вашу сессию и ничего более. Ознакомьтесь с нашей <a href='/privacy'>политикой конфиденциальности</a> для получения дополнительной информации."; "cookies_popup_content" = "Все дети любят печенье, поэтому этот веб-сайт использует Cookies для того, чтобы идентифицировать вашу сессию и ничего более. Ознакомьтесь с нашей <a href='/privacy'>политикой конфиденциальности</a> для получения дополнительной информации.";
"cookies_popup_agree" = "Согласен"; "cookies_popup_agree" = "Согласен";
/* Blacklist */
"blacklist" = "Чёрный список";
"user_blacklisted_you" = "Пользователь внёс Вас в чёрный список.";
"user_blacklisted" = "$1 занесён в чёрный список.";
"user_removed_from_the_blacklist" = "$1 удалён из чёрного списка.";
"adding_to_bl_sure" = "Вы уверены, что хотите внести $1 в чёрный список?";
"bl_count_zero_desc" = "В вашем чёрном списке ещё нет пользователей.";
"bl_count_zero" = "В вашем чёрном списке нет пользователей";
"bl_count_one" = "В вашем чёрном списке один пользователь";
"bl_count_few" = "В вашем чёрном списке $1 пользователя";
"bl_count_many" = "В вашем чёрном списке $1 пользователей";
"bl_count_other" = "В вашем чёрном списке $1 пользователей";
"you_blacklisted" = "Вы внесли $1 в чёрный список";
"bl_add" = "Добавить в чёрный список";
"bl_remove" = "Удалить из чёрного списка";
"addition_to_bl" = "Добавление в чёрный список";
/* Away */ /* Away */
"transition_is_blocked" = "Переход по ссылке заблокирован"; "transition_is_blocked" = "Переход по ссылке заблокирован";

View file

@ -38,6 +38,9 @@ openvk:
maxViolations: 50 maxViolations: 50
maxViolationsAge: 120 maxViolationsAge: 120
autoban: true autoban: true
blacklists:
limit: 100
applyToAdmins: true
registration: registration:
enable: true enable: true
disablingReason: "" disablingReason: ""