Add reports (#634)

* Reports: [INDEV] Undone implementation of reports

* Reports: Backend is done

* Reports: Still makin it...

* Reports: Added report window

* Reports: Corrected the content type

* Reports: Make it work

* Reports: Minor fixes and localization

* Reports: Ability to hide Share and Like buttons

Also renamed the .sql file

* Revent some changes from 8f8d7bb

I will move them to the master branch

* Reports: Only for those who can access Helpdesk

* Reports: Modified the route

* Reports: Change the routes

* Reports: Show reports count

* Report: Fix URL

* Обновление репортов (#715)

* Репорты живы

* 2

* Better reports

* Логи

* Update DBEntity.updated.php

* noSpam

* Сбор IP и UserAgent + фикс логирования в IPs

* Новые поля для поиска etc.

* Fixes

* Fixes and enhancements

* Поиск по нескольким разделам

* Reports enhancements

* Совместимость с новыми логами

* Совместимость с новыми логами

* Update Logs.xml

* Update Logs.xml

* Logs i18n

* Update Logs.xml

* Update AdminPresenter.php

---------

Co-authored-by: veselcraft <veselcraft@icloud.com>
Co-authored-by: Ilya Prokopenko <55238545+Xenforce@users.noreply.github.com>
Co-authored-by: n1rwana <aydashkin@vk.com>
This commit is contained in:
Jill 2023-08-11 13:50:19 +00:00 committed by GitHub
parent 8265dc0fc6
commit 6159262026
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 2514 additions and 67 deletions

View file

@ -306,11 +306,14 @@ class Application extends RowModel
function delete(bool $softly = true): void
{
if($softly)
throw new \UnexpectedValueException("Can't delete apps softly.");
throw new \UnexpectedValueException("Can't delete apps softly."); // why
$cx = DatabaseConnection::i()->getContext();
$cx->table("app_users")->where("app", $this->getId())->delete();
parent::delete(false);
}
function getPublicationTime(): string
{ return tr("recently"); }
}

View file

@ -0,0 +1,66 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Util\DateTime;
use openvk\Web\Models\Repositories\{Users};
use Nette\Database\Table\ActiveRow;
class Ban extends RowModel
{
protected $tableName = "bans";
function getId(): int
{
return $this->getRecord()->id;
}
function getReason(): ?string
{
return $this->getRecord()->reason;
}
function getUser(): ?User
{
return (new Users)->get($this->getRecord()->user);
}
function getInitiator(): ?User
{
return (new Users)->get($this->getRecord()->initiator);
}
function getStartTime(): int
{
return $this->getRecord()->iat;
}
function getEndTime(): int
{
return $this->getRecord()->exp;
}
function getTime(): int
{
return $this->getRecord()->time;
}
function isPermanent(): bool
{
return $this->getEndTime() === 0;
}
function isRemovedManually(): bool
{
return (bool) $this->getRecord()->removed_manually;
}
function isOver(): bool
{
return $this->isRemovedManually();
}
function whoRemoved(): ?User
{
return (new Users)->get($this->getRecord()->removed_by);
}
}

View file

@ -355,6 +355,18 @@ class Club extends RowModel
return $this->getRecord()->website;
}
function ban(string $reason): void
{
$this->setBlock_Reason($reason);
$this->save();
}
function unban(): void
{
$this->setBlock_Reason(null);
$this->save();
}
function getAlert(): ?string
{
return $this->getRecord()->alert;

View file

@ -0,0 +1,71 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Util\DateTime;
use openvk\Web\Models\Repositories\{Users};
use Nette\Database\Table\ActiveRow;
class NoSpamLog extends RowModel
{
protected $tableName = "noSpam_templates";
function getId(): int
{
return $this->getRecord()->id;
}
function getUser(): ?User
{
return (new Users)->get($this->getRecord()->user);
}
function getModel(): string
{
return $this->getRecord()->model;
}
function getRegex(): ?string
{
return $this->getRecord()->regex;
}
function getRequest(): ?string
{
return $this->getRecord()->request;
}
function getCount(): int
{
return $this->getRecord()->count;
}
function getTime(): DateTime
{
return new DateTime($this->getRecord()->time);
}
function getItems(): ?array
{
return explode(",", $this->getRecord()->items);
}
function getTypeRaw(): int
{
return $this->getRecord()->ban_type;
}
function getType(): string
{
switch ($this->getTypeRaw()) {
case 1: return "О";
case 2: return "Б";
case 3: return "ОБ";
default: return (string) $this->getTypeRaw();
}
}
function isRollbacked(): bool
{
return !is_null($this->getRecord()->rollback);
}
}

View file

@ -35,6 +35,7 @@ abstract class Postable extends Attachable
if(!$real && $this->isAnonymous())
$oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"];
$oid = abs($oid);
if($oid > 0)
return (new Users)->get($oid);
else

View file

@ -0,0 +1,142 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Util\DateTime;
use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\RowModel;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Users, Posts, Photos, Videos, Clubs};
use Chandler\Database\DatabaseConnection as DB;
use Nette\InvalidStateException as ISE;
use Nette\Database\Table\Selection;
class Report extends RowModel
{
protected $tableName = "reports";
function getId(): int
{
return $this->getRecord()->id;
}
function getStatus(): int
{
return $this->getRecord()->status;
}
function getContentType(): string
{
return $this->getRecord()->type;
}
function getReason(): string
{
return $this->getRecord()->reason;
}
function getTime(): DateTime
{
return new DateTime($this->getRecord()->date);
}
function isDeleted(): bool
{
if ($this->getRecord()->deleted === 0)
{
return false;
} elseif ($this->getRecord()->deleted === 1) {
return true;
}
}
function authorId(): int
{
return $this->getRecord()->user_id;
}
function getUser(): User
{
return (new Users)->get((int) $this->getRecord()->user_id);
}
function getContentId(): int
{
return (int) $this->getRecord()->target_id;
}
function getContentObject()
{
if ($this->getContentType() == "post") return (new Posts)->get($this->getContentId());
else if ($this->getContentType() == "photo") return (new Photos)->get($this->getContentId());
else if ($this->getContentType() == "video") return (new Videos)->get($this->getContentId());
else if ($this->getContentType() == "group") return (new Clubs)->get($this->getContentId());
else if ($this->getContentType() == "comment") return (new Comments)->get($this->getContentId());
else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId());
else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId());
else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId());
else return null;
}
function getAuthor(): RowModel
{
return (new Posts)->get($this->getContentId())->getOwner();
}
function getReportAuthor(): User
{
return (new Users)->get($this->getRecord()->user_id);
}
function banUser($initiator)
{
$reason = $this->getContentType() !== "user" ? ("**content-" . $this->getContentType() . "-" . $this->getContentId() . "**") : ("Подозрительная активность");
$this->getAuthor()->ban($reason, false, time() + $this->getAuthor()->getNewBanTime(), $initiator);
}
function deleteContent()
{
if ($this->getContentType() !== "user") {
$pubTime = $this->getContentObject()->getPublicationTime();
$name = $this->getContentObject()->getName();
$this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $pubTime ($name) был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать.");
$this->getContentObject()->delete($this->getContentType() !== "app");
}
$this->delete();
}
function getDuplicates(): \Traversable
{
return (new Reports)->getDuplicates($this->getContentType(), $this->getContentId(), $this->getId());
}
function getDuplicatesCount(): int
{
return count(iterator_to_array($this->getDuplicates()));
}
function hasDuplicates(): bool
{
return $this->getDuplicatesCount() > 0;
}
function getContentName(): string
{
if (method_exists($this->getContentObject(), "getCanonicalName"))
return $this->getContentObject()->getCanonicalName();
return $this->getContentType() . " #" . $this->getContentId();
}
public function delete(bool $softly = true): void
{
if ($this->hasDuplicates()) {
foreach ($this->getDuplicates() as $duplicate) {
$duplicate->setDeleted(1);
$duplicate->save();
}
}
$this->setDeleted(1);
$this->save();
}
}

View file

@ -5,7 +5,7 @@ use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift};
use openvk\Web\Models\Repositories\{Photos, Users, Clubs, Albums, Gifts, Notifications};
use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
@ -241,11 +241,60 @@ class User extends RowModel
return $this->getRecord()->alert;
}
function getBanReason(): ?string
function getTextForContentBan(string $type): string
{
switch ($type) {
case "post": return "за размещение от Вашего лица таких <b>записей</b>:";
case "photo": return "за размещение от Вашего лица таких <b>фотографий</b>:";
case "video": return "за размещение от Вашего лица таких <b>видеозаписей</b>:";
case "group": return "за подозрительное вступление от Вашего лица <b>в группу:</b>";
case "comment": return "за размещение от Вашего лица таких <b>комментариев</b>:";
case "note": return "за размещение от Вашего лица таких <b>заметок</b>:";
case "app": return "за создание от Вашего имени <b>подозрительных приложений</b>.";
default: return "за размещение от Вашего лица такого <b>контента</b>:";
}
}
function getRawBanReason(): ?string
{
return $this->getRecord()->block_reason;
}
function getBanReason(?string $for = null)
{
$ban = (new Bans)->get((int) $this->getRecord()->block_reason);
if (!$ban || $ban->isOver()) return null;
$reason = $ban->getReason();
preg_match('/\*\*content-(post|photo|video|group|comment|note|app|noSpamTemplate|user)-(\d+)\*\*$/', $reason, $matches);
if (sizeof($matches) === 3) {
$content_type = $matches[1]; $content_id = (int) $matches[2];
if (in_array($content_type, ["noSpamTemplate", "user"])) {
$reason = "Подозрительная активность";
} else {
if ($for !== "banned") {
$reason = "Подозрительная активность";
} else {
$reason = [$this->getTextForContentBan($content_type), $content_type];
switch ($content_type) {
case "post": $reason[] = (new Posts)->get($content_id); break;
case "photo": $reason[] = (new Photos)->get($content_id); break;
case "video": $reason[] = (new Videos)->get($content_id); break;
case "group": $reason[] = (new Clubs)->get($content_id); break;
case "comment": $reason[] = (new Comments)->get($content_id); break;
case "note": $reason[] = (new Notes)->get($content_id); break;
case "app": $reason[] = (new Applications)->get($content_id); break;
case "user": $reason[] = (new Users)->get($content_id); break;
default: $reason[] = null;
}
}
}
}
return $reason;
}
function getBanInSupportReason(): ?string
{
return $this->getRecord()->block_in_support_reason;
@ -833,7 +882,7 @@ class User extends RowModel
]);
}
function ban(string $reason, bool $deleteSubscriptions = true, ?int $unban_time = NULL): void
function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = NULL, ?int $initiator = NULL): void
{
if($deleteSubscriptions) {
$subs = DatabaseConnection::i()->getContext()->table("subscriptions");
@ -846,8 +895,33 @@ class User extends RowModel
$subs->delete();
}
$this->setBlock_Reason($reason);
$this->setUnblock_time($unban_time);
$iat = time();
$ban = new Ban;
$ban->setUser($this->getId());
$ban->setReason($reason);
$ban->setInitiator($initiator);
$ban->setIat($iat);
$ban->setExp($unban_time !== "permanent" ? $unban_time : 0);
$ban->setTime($unban_time === "permanent" ? 0 : ($unban_time ? ($unban_time - $iat) : 0));
$ban->save();
$this->setBlock_Reason($ban->getId());
// $this->setUnblock_time($unban_time);
$this->save();
}
function unban(int $removed_by): void
{
$ban = (new Bans)->get((int) $this->getRawBanReason());
if (!$ban || $ban->isOver())
return;
$ban->setRemoved_Manually(true);
$ban->setRemoved_By($removed_by);
$ban->save();
$this->setBlock_Reason(NULL);
// $user->setUnblock_time(NULL);
$this->save();
}
@ -1099,7 +1173,11 @@ class User extends RowModel
function getUnbanTime(): ?string
{
return !is_null($this->getRecord()->unblock_time) ? date('d.m.Y', $this->getRecord()->unblock_time) : NULL;
$ban = (new Bans)->get((int) $this->getRecord()->block_reason);
if (!$ban || $ban->isOver() || $ban->isPermanent()) return null;
if ($this->canUnbanThemself()) return tr("today");
return date('d.m.Y', $ban->getEndTime());
}
function canUnbanThemself(): bool
@ -1107,10 +1185,40 @@ class User extends RowModel
if (!$this->isBanned())
return false;
if ($this->getRecord()->unblock_time > time() || $this->getRecord()->unblock_time == 0)
return false;
$ban = (new Bans)->get((int) $this->getRecord()->block_reason);
if (!$ban || $ban->isOver() || $ban->isPermanent()) return false;
return true;
return $ban->getEndTime() <= time() && !$ban->isPermanent();
}
function getNewBanTime()
{
$bans = iterator_to_array((new Bans)->getByUser($this->getid()));
if (!$bans || count($bans) === 0)
return 0;
$last_ban = end($bans);
if (!$last_ban) return 0;
if ($last_ban->isPermanent()) return "permanent";
$values = [0, 3600, 7200, 86400, 172800, 604800, 1209600, 3024000, 9072000];
$response = 0;
$i = 0;
foreach ($values as $value) {
$i++;
if ($last_ban->getTime() === 0 && $value === 0) continue;
if ($last_ban->getTime() < $value) {
$response = $value;
break;
} else if ($last_ban->getTime() >= $value) {
if ($i < count($values)) continue;
$response = "permanent";
break;
}
}
return $response;
}
function toVkApiStruct(): object

View file

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection as DB;
use Nette\Database\Table\{ActiveRow, Selection};
use openvk\Web\Models\Entities\Ban;
class Bans
{
private $context;
private $bans;
function __construct()
{
$this->context = DB::i()->getContext();
$this->bans = $this->context->table("bans");
}
function toBan(?ActiveRow $ar): ?Ban
{
return is_null($ar) ? NULL : new Ban($ar);
}
function get(int $id): ?Ban
{
return $this->toBan($this->bans->get($id));
}
function getByUser(int $user_id): \Traversable
{
foreach ($this->bans->where("user", $user_id) as $ban)
yield new Ban($ban);
}
}

View file

@ -28,7 +28,8 @@ class ChandlerUsers
function getById(string $UUID): ?ChandlerUser
{
return new ChandlerUser($this->users->where("id", $UUID)->fetch());
$user = $this->users->where("id", $UUID)->fetch();
return $user ? new ChandlerUser($user) : NULL;
}
function getList(int $page = 1): \Traversable

View file

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\NoSpamLog;
use openvk\Web\Models\Entities\User;
use Nette\Database\Table\ActiveRow;
class NoSpamLogs
{
private $context;
private $noSpamLogs;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->noSpamLogs = $this->context->table("noSpam_templates");
}
private function toNoSpamLog(?ActiveRow $ar): ?NoSpamLog
{
return is_null($ar) ? NULL : new NoSpamLog($ar);
}
function get(int $id): ?NoSpamLog
{
return $this->toNoSpamLog($this->noSpamLogs->get($id));
}
function getList(array $filter = []): \Traversable
{
foreach ($this->noSpamLogs->where($filter)->order("`id` DESC") as $log)
yield new NoSpamLog($log);
}
}

View file

@ -0,0 +1,67 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Report;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection;
class Reports
{
private $context;
private $reports;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->reports = $this->context->table("reports");
}
private function toReport(?ActiveRow $ar): ?Report
{
return is_null($ar) ? NULL : new Report($ar);
}
function getReports(int $state = 0, int $page = 1, ?string $type = NULL, ?bool $pagination = true): \Traversable
{
$filter = ["deleted" => 0];
if ($type) $filter["type"] = $type;
$reports = $this->reports->where($filter)->order("created DESC")->group("target_id, type");
if ($pagination)
$reports = $reports->page($page, 15);
foreach($reports as $t)
yield new Report($t);
}
function getReportsCount(int $state = 0): int
{
return sizeof($this->reports->where(["deleted" => 0, "type" => $state])->group("target_id, type"));
}
function get(int $id): ?Report
{
return $this->toReport($this->reports->get($id));
}
function getByContentId(int $id): ?Report
{
$post = $this->reports->where(["deleted" => 0, "content_id" => $id])->fetch();
if($post)
return new Report($post);
else
return null;
}
function getDuplicates(string $type, int $target_id, ?int $orig = NULL, ?int $user_id = NULL): \Traversable
{
$filter = ["deleted" => 0, "type" => $type, "target_id" => $target_id];
if ($orig) $filter[] = "id != $orig";
if ($user_id) $filter["user_id"] = $user_id;
foreach ($this->reports->where($filter) as $report)
yield new Report($report);
}
use \Nette\SmartObject;
}

View file

@ -44,9 +44,9 @@ class Users
return $alias->getUser();
}
function getByChandlerUser(ChandlerUser $user): ?User
function getByChandlerUser(?ChandlerUser $user): ?User
{
return $this->toUser($this->users->where("user", $user->getId())->fetch());
return $user ? $this->toUser($this->users->where("user", $user->getId())->fetch()) : NULL;
}
function find(string $query, array $pars = [], string $sort = "id DESC"): Util\EntityStream

View file

@ -360,13 +360,19 @@ final class AdminPresenter extends OpenVKPresenter
{
$this->assertNoCSRF();
if (str_contains($this->queryParam("reason"), "*"))
exit(json_encode([ "error" => "Incorrect reason" ]));
$unban_time = strtotime($this->queryParam("date")) ?: NULL;
$user = $this->users->get($id);
if(!$user)
exit(json_encode([ "error" => "User does not exist" ]));
$user->ban($this->queryParam("reason"), true, $unban_time);
if ($this->queryParam("incr"))
$unban_time = time() + $user->getNewBanTime();
$user->ban($this->queryParam("reason"), true, $unban_time, $this->user->identity->getId());
exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ]));
}
@ -378,8 +384,16 @@ final class AdminPresenter extends OpenVKPresenter
if(!$user)
exit(json_encode([ "error" => "User does not exist" ]));
$ban = (new Bans)->get((int)$user->getRawBanReason());
if (!$ban || $ban->isOver())
exit(json_encode([ "error" => "User is not banned" ]));
$ban->setRemoved_Manually(true);
$ban->setRemoved_By($this->user->identity->getId());
$ban->save();
$user->setBlock_Reason(NULL);
$user->setUnblock_time(NULL);
// $user->setUnblock_time(NULL);
$user->save();
exit(json_encode([ "success" => true ]));
}
@ -465,6 +479,14 @@ final class AdminPresenter extends OpenVKPresenter
$this->redirect("/admin/bannedLinks");
}
function renderBansHistory(int $user_id) :void
{
$user = (new Users)->get($user_id);
if (!$user) $this->notFound();
$this->template->bans = (new Bans)->getByUser($user_id);
}
function renderChandlerGroups(): void
{
$this->template->groups = (new ChandlerGroups)->getList();

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{IP, User, PasswordReset, EmailVerification};
use openvk\Web\Models\Repositories\{IPs, Logs, Users, Restores, Verifications};
use openvk\Web\Models\Repositories\{Bans, IPs, Users, Restores, Verifications, Logs};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator;
use Chandler\Session\Session;
@ -347,9 +347,16 @@ final class AuthPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error"), tr("forbidden"));
$user = $this->users->get($this->user->id);
$ban = (new Bans)->get((int)$user->getRawBanReason());
if (!$ban || $ban->isOver() || $ban->isPermanent())
$this->flashFail("err", tr("error"), tr("forbidden"));
$ban->setRemoved_Manually(2);
$ban->setRemoved_By($this->user->identity->getId());
$ban->save();
$user->setBlock_Reason(NULL);
$user->setUnblock_Time(NULL);
// $user->setUnblock_Time(NULL);
$user->save();
$this->flashFail("succ", tr("banned_unban_title"), tr("banned_unban_description"));

View file

@ -3,6 +3,8 @@ namespace openvk\Web\Presenters;
final class BlobPresenter extends OpenVKPresenter
{
protected $banTolerant = true;
private function getDirName($dir): string
{
if(gettype($dir) === "integer") {

View file

@ -0,0 +1,377 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use Nette\Database\DriverException;
use Nette\Utils\Finder;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Entities\Comment;
use Chandler\Database\Log;
use openvk\Web\Models\Entities\NoSpamLog;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\ChandlerUsers;
use Chandler\Database\Logs;
use openvk\Web\Models\Repositories\NoSpamLogs;
use openvk\Web\Models\Repositories\Users;
final class NoSpamPresenter extends OpenVKPresenter
{
protected $banTolerant = true;
protected $deactivationTolerant = true;
protected $presenterName = "nospam";
const ENTITIES_NAMESPACE = "openvk\\Web\\Models\\Entities";
function __construct()
{
parent::__construct();
}
function renderIndex(): void
{
$this->assertUserLoggedIn();
$this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0);
$targetDir = __DIR__ . '/../Models/Entities/';
$mode = in_array($this->queryParam("act"), ["form", "templates", "rollback", "reports"]) ? $this->queryParam("act") : "form";
if ($mode === "form") {
$this->template->_template = "NoSpam/Index";
$foundClasses = [];
foreach (Finder::findFiles('*.php')->from($targetDir) as $file) {
$content = file_get_contents($file->getPathname());
$namespacePattern = '/namespace\s+([^\s;]+)/';
$classPattern = '/class\s+([^\s{]+)/';
preg_match($namespacePattern, $content, $namespaceMatches);
preg_match($classPattern, $content, $classMatches);
if (isset($namespaceMatches[1]) && isset($classMatches[1])) {
$classNamespace = trim($namespaceMatches[1]);
$className = trim($classMatches[1]);
$fullClassName = $classNamespace . '\\' . $className;
if ($classNamespace === NoSpamPresenter::ENTITIES_NAMESPACE && class_exists($fullClassName)) {
$foundClasses[] = $className;
}
}
}
$models = [];
foreach ($foundClasses as $class) {
$r = new \ReflectionClass(NoSpamPresenter::ENTITIES_NAMESPACE . "\\$class");
if (!$r->isAbstract() && $r->getName() !== NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence")
$models[] = $class;
}
$this->template->models = $models;
} else if ($mode === "templates") {
$this->template->_template = "NoSpam/Templates.xml";
$filter = [];
if ($this->queryParam("id")) {
$filter["id"] = (int)$this->queryParam("id");
}
$this->template->templates = iterator_to_array((new NoSpamLogs)->getList($filter));
} else if ($mode === "reports") {
$this->redirect("/scumfeed");
} else {
$template = (new NoSpamLogs)->get((int)$this->postParam("id"));
if (!$template || $template->isRollbacked())
$this->returnJson(["success" => false, "error" => "Шаблон не найден"]);
$model = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $template->getModel();
$items = $template->getItems();
if (count($items) > 0) {
$db = DatabaseConnection::i()->getContext();
$unbanned_ids = [];
foreach ($items as $_item) {
try {
$item = new $model;
$table_name = $item->getTableName();
$item = $db->table($table_name)->get((int)$_item);
if (!$item) continue;
$item = new $model($item);
if (key_exists("deleted", $item->unwrap()) && $item->isDeleted()) {
$item->setDeleted(0);
$item->save();
}
if (in_array($template->getTypeRaw(), [2, 3])) {
$owner = NULL;
$methods = ["getOwner", "getUser", "getRecipient", "getInitiator"];
if (method_exists($item, "ban")) {
$owner = $item;
} else {
foreach ($methods as $method) {
if (method_exists($item, $method)) {
$owner = $item->$method();
break;
}
}
}
$_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId());
if (!in_array($_id, $unbanned_ids)) {
$owner->unban($this->user->id);
$unbanned_ids[] = $_id;
}
}
} catch (\Throwable $e) {
$this->returnJson(["success" => false, "error" => $e->getMessage()]);
}
}
} else {
$this->returnJson(["success" => false, "error" => "Объекты не найдены"]);
}
$template->setRollback(true);
$template->save();
$this->returnJson(["success" => true]);
}
}
function renderSearch(): void
{
$this->assertUserLoggedIn();
$this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0);
$this->assertNoCSRF();
$this->willExecuteWriteAction();
function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, ?string $ip = NULL, ?string $useragent = NULL, ?int $ts = NULL, ?int $te = NULL, $user = NULL)
{
$db = DatabaseConnection::i()->getContext();
if ($table && ($ip || $useragent || $ts || $te || $user)) {
$conditions = [];
if ($ip) $conditions[] = "`ip` REGEXP '$ip'";
if ($useragent) $conditions[] = "`useragent` REGEXP '$useragent'";
if ($ts) $conditions[] = "`ts` < $ts";
if ($te) $conditions[] = "`ts` > $te";
if ($user) {
$users = new Users;
$_user = $users->getByChandlerUser((new ChandlerUsers)->getById($user))
?? $users->get((int)$user)
?? $users->getByAddress($user)
?? NULL;
if ($_user) {
$conditions[] = "`user` = '" . $_user->getChandlerGUID() . "'";
}
}
$whereStart = "WHERE `object_table` = '$table'";
if ($table === "profiles") {
$whereStart .= "AND `type` = 0";
}
$conditions = count($conditions) > 0 ? "AND (" . implode(" AND ", $conditions) . ")" : "";
$response = [];
if ($conditions) {
$logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`");
if (!$where) {
foreach ($logs as $log) {
$log = (new Logs)->get($log->id);
$response[] = $log->getObject()->unwrap();
}
} else {
foreach ($logs as $log) {
$log = (new Logs)->get($log->id);
$object = $log->getObject()->unwrap();
if (!$object) continue;
if (str_starts_with($where, " AND")) {
$where = substr_replace($where, "", 0, strlen(" AND"));
}
foreach ($db->query("SELECT * FROM `$table` WHERE $where")->fetchAll() as $o) {
if ($object->id === $o["id"]) {
$response[] = $object;
}
}
}
}
}
return $response;
}
}
try {
$response = [];
$processed = 0;
$where = $this->postParam("where");
$ip = $this->postParam("ip");
$useragent = $this->postParam("useragent");
$searchTerm = $this->postParam("q");
$ts = (int)$this->postParam("ts");
$te = (int)$this->postParam("te");
$user = $this->postParam("user");
if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user)
$this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]);
$models = explode(",", $this->postParam("models"));
foreach ($models as $_model) {
$model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $_model;
if (!class_exists($model_name)) {
continue;
}
$model = new $model_name;
$c = new \ReflectionClass($model_name);
if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") {
continue;
}
$db = DatabaseConnection::i()->getContext();
$table = $model->getTableName();
$columns = $db->getStructure()->getColumns($table);
if ($searchTerm) {
$conditions = [];
$need_deleted = false;
foreach ($columns as $column) {
if ($column["name"] == "deleted") {
$need_deleted = true;
} else {
$conditions[] = "`$column[name]` REGEXP '$searchTerm'";
}
}
$conditions = implode(" OR ", $conditions);
$where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)");
if ($need_deleted) $where .= " AND (`deleted` = 0)";
}
$rows = [];
if ($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 ($searchTerm && !$this->postParam("where")) {
$where = substr_replace($where, "", 0, strlen(" AND"));
} else {
$where = "(" . $this->postParam("where") . ")" . $where;
}
}
if (!$where) {
$rows = [];
} else {
$result = $db->query("SELECT * FROM `$table` WHERE $where");
$rows = $result->fetchAll();
}
}
}
if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) {
foreach ($rows as $key => $object) {
$object = (array)$object;
$_obj = [];
foreach ($object as $key => $value) {
foreach ($columns as $column) {
if ($column["name"] === $key && in_array(strtoupper($column["nativetype"]), ["BLOB", "BINARY", "VARBINARY", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB"])) {
$value = "[BINARY]";
break;
}
}
$_obj[$key] = $value;
$_obj["__model_name"] = $_model;
}
$response[] = $_obj;
}
} else {
$ids = [];
foreach ($rows as $object) {
$object = new $model_name($db->table($table)->get($object->id));
if (!$object) continue;
$ids[] = $object->getId();
}
$log = new NoSpamLog;
$log->setUser($this->user->id);
$log->setModel($_model);
if ($searchTerm) {
$log->setRegex($searchTerm);
} else {
$log->setRequest($where);
}
$log->setBan_Type((int)$this->postParam("ban"));
$log->setCount(count($rows));
$log->setTime(time());
$log->setItems(implode(",", $ids));
$log->save();
$banned_ids = [];
foreach ($rows as $object) {
$object = new $model_name($db->table($table)->get($object->id));
if (!$object) continue;
$owner = NULL;
$methods = ["getOwner", "getUser", "getRecipient", "getInitiator"];
if (method_exists($object, "ban")) {
$owner = $object;
} else {
foreach ($methods as $method) {
if (method_exists($object, $method)) {
$owner = $object->$method();
break;
}
}
}
if ($owner instanceof User && $owner->getId() === $this->user->id) {
if (count($rows) === 1) {
$this->returnJson(["success" => false, "error" => "\"Производственная травма\" — Вы не можете блокировать или удалять свой же контент"]);
} else {
continue;
}
}
if (in_array((int)$this->postParam("ban"), [2, 3])) {
if ($owner) {
$_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId());
if (!in_array($_id, $banned_ids)) {
if ($owner instanceof User) {
$owner->ban("**content-noSpamTemplate-" . $log->getId() . "**", false, time() + $owner->getNewBanTime(), $this->user->id);
} else {
$owner->ban("Подозрительная активность");
}
$banned_ids[] = $_id;
}
}
}
if (in_array((int)$this->postParam("ban"), [1, 3]))
$object->delete();
}
$processed++;
}
}
$this->returnJson(["success" => true, "processed" => $processed, "count" => count($response), "list" => $response]);
} catch (\Throwable $e) {
$this->returnJson(["success" => false, "error" => $e->getMessage()]);
}
}
}

6
Web/Presenters/OpenVKPresenter.php Executable file → Normal file
View file

@ -7,7 +7,7 @@ use Chandler\Security\Authenticator;
use Latte\Engine as TemplatingEngine;
use openvk\Web\Models\Entities\IP;
use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Repositories\{CurrentUser, IPs, Users, APITokens, Tickets};
use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Reports, CurrentUser};
use WhichBrowser;
abstract class OpenVKPresenter extends SimplePresenter
@ -260,8 +260,10 @@ abstract class OpenVKPresenter extends SimplePresenter
}
$this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1);
if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0))
if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) {
$this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0);
$this->template->reportNotAnsweredCount = (new Reports)->getReportsCount(0);
}
}
header("X-OpenVK-User-Validated: $userValidated");

View file

@ -0,0 +1,151 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Reports;
use openvk\Web\Models\Repositories\Posts;
use openvk\Web\Models\Entities\Report;
final class ReportPresenter extends OpenVKPresenter
{
private $reports;
function __construct(Reports $reports)
{
$this->reports = $reports;
parent::__construct();
}
function renderList(): void
{
$this->assertUserLoggedIn();
$this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0);
if ($_SERVER["REQUEST_METHOD"] === "POST")
$this->assertNoCSRF();
$act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL;
if (!$this->queryParam("orig")) {
$this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST");
$this->template->count = $this->reports->getReportsCount();
} else {
$orig = $this->reports->get((int) $this->queryParam("orig"));
if (!$orig) $this->redirect("/scumfeed");
$this->template->reports = $orig->getDuplicates();
$this->template->count = $orig->getDuplicatesCount();
$this->template->orig = $orig->getId();
}
$this->template->paginatorConf = (object) [
"count" => $this->template->count,
"page" => $this->queryParam("p") ?? 1,
"amount" => NULL,
"perPage" => 15,
];
$this->template->mode = $act ?? "all";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$reports = [];
foreach ($this->reports->getReports(0, 0, $act, false) as $report) {
$reports[] = [
"id" => $report->getId(),
"author" => [
"id" => $report->getReportAuthor()->getId(),
"url" => $report->getReportAuthor()->getURL(),
"name" => $report->getReportAuthor()->getCanonicalName(),
"is_female" => $report->getReportAuthor()->isFemale()
],
"content" => [
"name" => $report->getContentName(),
"type" => $report->getContentType(),
"id" => $report->getContentId(),
"url" => $report->getContentType() === "user" ? (new Users)->get((int) $report->getContentId())->getURL() : NULL
],
"duplicates" => $report->getDuplicatesCount(),
];
}
$this->returnJson(["reports" => $reports]);
}
}
function renderView(int $id): void
{
$this->assertUserLoggedIn();
$this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0);
$report = $this->reports->get($id);
if(!$report || $report->isDeleted())
$this->notFound();
$this->template->report = $report;
}
function renderCreate(int $id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if(!$id)
exit(json_encode([ "error" => tr("error_segmentation") ]));
if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) {
if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) {
$report = new Report;
$report->setUser_id($this->user->id);
$report->setTarget_id($id);
$report->setType($this->queryParam("type"));
$report->setReason($this->queryParam("reason"));
$report->setCreated(time());
$report->save();
}
exit(json_encode([ "reason" => $this->queryParam("reason") ]));
} else {
exit(json_encode([ "error" => "Unable to submit a report on this content type" ]));
}
}
function renderAction(int $id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0);
$report = $this->reports->get($id);
if(!$report || $report->isDeleted()) $this->notFound();
if ($this->postParam("ban")) {
$report->deleteContent();
$report->banUser($this->user->identity->getId());
$this->flash("suc", "Смэрть...", "Пользователь успешно забанен.");
} else if ($this->postParam("delete")) {
$report->deleteContent();
$this->flash("suc", "Нехай живе!", "Контент удалён, а пользователю прилетело предупреждение.");
} else if ($this->postParam("ignore")) {
$report->delete();
$this->flash("suc", "Нехай живе!", "Жалоба проигнорирована.");
} else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) {
if ($report->getContentType() !== "group")
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
$club = $report->getContentObject();
if (!$club || $club->isBanned())
$this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса.");
if ($this->postParam("banClubOwner")) {
$club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId());
} else {
$club->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**");
}
$report->delete();
$this->flash("suc", "Смэрть...", ($this->postParam("banClubOwner") ? "Создатель сообщества успешно забанен." : "Сообщество успешно забанено"));
}
$this->redirect("/scumfeed");
}
}

View file

@ -10,8 +10,19 @@
<img src="/assets/packages/static/openvk/img/oof.apng" alt="{_banned_alt}" style="width: 20%;" />
</center>
<p>
{var $ban = $thisUser->getBanReason("banned")}
{if is_string($ban)}
{tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}<br/>
{tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape}
{else}
{tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}
<div>
Эта страница была заморожена {$ban[0]|noescape}
{if $ban[1] !== "app"}
{include "Report/ViewContent.xml", type => $ban[1], object => $ban[2]}
{/if}
</div>
{/if}
{if !$thisUser->getUnbanTime()}
{_banned_perm}

View file

@ -209,6 +209,25 @@
(<b>{$helpdeskTicketNotAnsweredCount}</b>)
{/if}
</a>
<a n:if="$canAccessHelpdesk" href="/scumfeed" class="link">{tr("reports")}
{if $reportNotAnsweredCount > 0}
(<b>{$reportNotAnsweredCount}</b>)
{/if}
</a>
<a n:if="$canAccessHelpdesk" href="/noSpam" class="link">
noSpam
</a>
<a
n:foreach="OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links'] as $menuItem"
href="{$menuItem['url']}"
target="_blank"
class="link">{$menuItem["name"]}</a>
<div id="_groupListPinnedGroups">
<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:if="$thisUser->getLeftMenuItemStatus('links')" n:foreach="OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links'] as $menuItem" href="{$menuItem['url']}" target="_blank" class="link">{strpos($menuItem["name"], "@") === 0 ? tr(substr($menuItem["name"], 1)) : $menuItem["name"]}</a>
@ -285,6 +304,11 @@
{/ifset}
</div>
</div>
{ifset $thisUser}
{if !$thisUser->isBanned()}
</div>
{/if}
{/ifset}
<div class="page_body">
<div id="wrapH">

View file

@ -12,16 +12,24 @@
{include size, x => $dat}
{/ifset}
{ifset before_content}
{include before_content, x => $dat}
{/ifset}
{ifset specpage}
{include specpage, x => $dat}
{else}
<div class="container_gray">
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{ifset top}
{include top, x => $dat}
{/ifset}
{if sizeof($data) > 0}
<div class="content" n:foreach="$data as $dat">
<table>
<tbody>
<tbody n:attr="id => is_null($table_body_id) ? NULL : $table_body_id">
<tr>
<td valign="top">
<a href="{include link, x => $dat}">

View file

@ -0,0 +1,86 @@
{extends "./@layout.xml"}
{block title}
История блокировок
{/block}
{block heading}
{include title}
{/block}
{block content}
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>Забаненный</th>
<th>Инициатор</th>
<th>Начало</th>
<th>Конец</th>
<th>Время</th>
<th>Причина</th>
<th>Снята</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$bans as $ban">
<td>{$ban->getId()}</td>
<td>
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$ban->getUser()->getAvatarUrl('miniscule')}"
alt="{$ban->getUser()->getCanonicalName()}" style="object-fit: cover;"
role="presentation"/>
</span>
</span>
<a href="{$ban->getUser()->getURL()}">{$ban->getUser()->getCanonicalName()}</a>
<span n:if="$ban->getUser()->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="{$ban->getInitiator()->getAvatarUrl('miniscule')}"
alt="{$ban->getInitiator()->getCanonicalName()}" style="object-fit: cover;"
role="presentation"/>
</span>
</span>
<a href="{$ban->getInitiator()->getURL()}">{$ban->getInitiator()->getCanonicalName()}</a>
<span n:if="$ban->getInitiator()->isBanned()"
class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}
</span>
</td>
<td>{date('d.m.Y в H:i:s', $ban->getStartTime())}</td>
<td>{date('d.m.Y в H:i:s', $ban->getEndTime())}</td>
<td>{$ban->getTime()}</td>
<td>
{$ban->getReason()}
</td>
<td>
{if $ban->isRemovedManually()}
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$ban->whoRemoved()->getAvatarUrl('miniscule')}"
alt="{$ban->whoRemoved()->getCanonicalName()}" style="object-fit: cover;"
role="presentation"/>
</span>
</span>
<a href="{$ban->whoRemoved()->getURL()}">{$ban->whoRemoved()->getCanonicalName()}</a>
<span n:if="$ban->whoRemoved()->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">
{_admin_banned}
</span>
{else}
<b style="color: red;">Активная блокировка</b>
{/if}
</td>
</tr>
</tbody>
</table>
{/block}

View file

@ -1,4 +1,5 @@
{extends "../@layout.xml"}
{var $canReport = $owner->getId() !== $thisUser->getId()}
{block title}
{$name}
@ -6,6 +7,7 @@
{block header}
{$name}
<a style="float: right;" onClick="reportApp()" n:if="$canReport ?? false">Пожаловаться</a>
{/block}
{block content}
@ -33,5 +35,29 @@
window.appOrigin = {$origin};
</script>
<script n:if="$canReport ?? false">
function reportApp() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данное приложение.";
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$id} + "?reason=" + res + "&type=app", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>
{script "js/al_games.js"}
{/block}

View file

@ -141,6 +141,34 @@
<input type="submit" id="profile_link" value="{_leave_community}" />
</form>
{/if}
{var $canReport = $thisUser->getId() != $club->getOwner()->getId()}
{if $canReport}
<a class="profile_link" style="display:block;width:96%;" href="javascript:reportVideo()">{_report}</a>
<script>
function reportVideo() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данное сообщество.";
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$club->getId()} + "?reason=" + res + "&type=group", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>
{/if}
</div>
<div>
<div class="content_title_expanded" onclick="hidePanel(this);">

View file

@ -0,0 +1,315 @@
{extends "../@layout.xml"}
{block title}noSpam{/block}
{block header}{include title}{/block}
{block content}
<style>
.noSpamIcon {
width: 20px;
height: 20px;
background: url("/assets/packages/static/openvk/img/supp_icons.png");
}
.noSpamIcon-Add {
background-position: 0 0;
}
.noSpamIcon-Delete {
background-position: 0 -21px;
}
</style>
<div class="tabs">{include "Tabs.xml", mode => "form"}</div>
<br/>
<div style="display: flex; border: 1px solid #ECECEC; padding: 8px;">
<div id="noSpam-form" style="width: 50%; border-right: 1px solid #ECECEC;">
<table cellspacing="7" cellpadding="0" width="100%" border="0">
<tbody id="models-list">
<tr id="0-model">
<td width="83px">
<span class="nobold">Раздел:</span>
</td>
<td>
<div style="display: flex; gap: 8px; justify-content: space-between;">
<div id="add-model" class="noSpamIcon noSpamIcon-Add" style="display: none;" />
<select name="model" id="model" class="model initialModel" style="margin-left: -2px;">
<option selected value="none">Не выбрано</option>
<option n:foreach="$models as $model" value="{$model}">{$model}</option>
</select>
</div>
</td>
</tr>
</tbody>
</table>
<div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/>
<div id="noSpam-fields" style="display: none;">
<table cellspacing="7" cellpadding="0" width="100%" border="0">
<tbody>
<tr style="width: 129px; border-top: 1px solid #ECECEC;">
<td>
<span class="nobold">Подстрока:</span>
</td>
<td>
<input type="text" name="regex" placeholder="Regex" id="regex">
</td>
</tr>
<tr style="width: 129px; border-top: 1px solid #ECECEC;">
<td>
<span class="nobold">Пользователь:</span>
</td>
<td>
<input type="text" name="user" placeholder="Ссылка на страницу" id="user">
</td>
</tr>
<tr style="width: 129px">
<td>
<span class="nobold">IP:</span>
</td>
<td>
<input type="text" name="ip" id="ip" placeholder="или подсеть">
</td>
</tr>
<tr style="width: 129px">
<td>
<span class="nobold">Юзер-агент:</span>
</td>
<td>
<input type="text" name="useragent" id="useragent" placeholder="Mozila 1.0 Blablabla/test">
</td>
</tr>
<tr style="width: 129px">
<td>
<span class="nobold">Время раньше, чем:</span>
</td>
<td>
<input type="datetime-local" name="ts" id="ts">
</td>
</tr>
<tr style="width: 129px">
<td>
<span class="nobold">Время позже, чем:</span>
</td>
<td>
<input type="datetime-local" name="te" id="te">
</td>
</tr>
</tbody>
</table>
<textarea style="resize: vertical; width: calc(100% - 6px)" placeholder='city = "Воскресенск" && id = 1'
name="where" id="where"/>
<span style="color: grey; font-size: 8px;">WHERE для поиска по разделу</span>
<div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/>
<table cellspacing="7" cellpadding="0" width="100%" border="0">
<tbody>
<tr style="width: 129px; border-top: 1px solid #ECECEC;">
<td>
<span class="nobold">Параметры блокировки:</span>
</td>
<td>
<select name="ban_type" id="noSpam-ban-type">
<option value="1">Только откат</option>
<option value="2">Только блокировка</option>
<option value="3">Откат и блокировка</option>
</select>
</td>
</tr>
</tbody>
</table>
<div style="border-top: 1px solid #ECECEC; margin: 8px 0;"/>
<center>
<div id="noSpam-buttons">
<input id="search" type="submit" value="Поиск" class="button"/>
<input id="apply" type="submit" value="Применить" class="button" style="display: none;"/>
</div>
<div id="noSpam-loader" style="display: none;">
<img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
</div>
</center>
</div>
<div id="noSpam-model-not-selected">
<center id="noSpam-model-not-selected-text" style="padding: 71px 25px;">Выберите раздел для начала работы</center>
<center id="noSpam-model-not-selected-loader" style="display: none;">
<img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px; margin: 125px 0;">
</center>
</div>
</div>
<div style="width: 50%;">
<center id="noSpam-results-loader" style="display: none;">
<img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px; margin: 125px 0;">
</center>
<center id="noSpam-results-text" style="margin: 125px 25px;">Здесь будут отображаться результаты поиска</center>
<div id="noSpam-results-block" style="display: none;">
<h4 style="padding: 8px;">Результаты поиска
<span style="color: #a2a2a2; font-weight: inherit">
(<span id="noSpam-results-count" style="color: #a2a2a2; font-weight: inherit;"></span> шт.)
</span>
</h4>
<ul style="padding-inline-start:18px;" id="noSpam-results-list"></ul>
</div>
</div>
</div>
<script>
async function search(ban = false) {
$("#noSpam-results-text").hide();
$("#noSpam-results-block").hide();
$("#apply").hide();
$("#noSpam-buttons").hide();
$("#noSpam-results-loader").show();
$("#noSpam-loader").show();
let models = [];
$(".model").each(function (i) {
let name = $(this).val();
if (!models.includes(name)) {
if (name.length > 0 && name !== "none") {
models.push(name);
}
}
});
models = models.join(",");
let model = $("#model").val();
let regex = $("#regex").val();
let where = $("#where").val();
let ip = $("#ip").val();
let useragent = $("#useragent").val();
let ts = $("#ts").val() ? Math.floor(new Date($("#ts").val()).getTime() / 1000) : null;
let te = $("#te").val() ? Math.floor(new Date($("#te").val()).getTime() / 1000) : null;
let user = $("#user").val();
await $.ajax({
type: "POST",
url: "/al_abuse/search",
data: {
models: models,
model: model,
q: regex,
where: where,
ban: ban,
ip: ip,
useragent: useragent,
ts: ts,
te: te,
user: user,
hash: {=$csrfToken}
},
success: (response) => {
if (response.success) {
console.log(response);
if (response.count > 0) {
$("#noSpam-results-list").empty();
$("#noSpam-results-count").text(response.count);
response.list.forEach((item) => {
const HTML_TAGS_REGEX = /<\/?([^>]+)(>|$)/g;
let fields = "";
Object.entries(item).map(([key, value]) => {
fields += `<b>${ key}</b>: ${ value?.toString()?.replace(HTML_TAGS_REGEX, "[$1]")}<br />`;
});
$("#noSpam-results-list").append(`<li>
<a style="display: block;" onClick="$('#noSpam-result-fields-${ item.__model_name}-${ item.id}').toggle()">
<h4 style="display: inherit; padding: 8px;">${ item.__model_name} #${ item.id}</h4>
</a>
<div style="display: none;" id="noSpam-result-fields-${ item.__model_name}-${ item.id}">${ fields}</div>
</li>`);
});
$("#noSpam-results-block").show();
$("#apply").show();
} else {
$("#noSpam-results-text").text(ban ? "Операция завершена успешно" : "Ничего не найдено :(");
$("#noSpam-results-text").show();
}
} else {
$("#noSpam-results-text").text(response?.error ?? "Неизвестная ошибка");
$("#noSpam-results-text").show();
}
},
error: (error) => {
console.error("Error while searching noSpam:", error);
$("#noSpam-results-text").text("Ошибка при выполнении запроса");
$("#noSpam-results-text").show();
}
});
$("#noSpam-buttons").show();
$("#noSpam-loader").hide();
$("#noSpam-results-loader").hide();
}
$("#search").on("click", () => { search(); });
$("input, textarea").keypress((e) => {
if (e.which === 13 && !e.shiftKey) {
e.preventDefault();
search();
}
});
$("#apply").on("click", () => { search(Number($("#noSpam-ban-type").val())); })
async function selectChange(value) {
console.log(value);
if (value !== "none") {
$("#noSpam-fields").hide();
$("#noSpam-model-not-selected").show();
$("#noSpam-model-not-selected-text").hide();
$("#noSpam-model-not-selected-loader").show();
setTimeout(() => {
$("#noSpam-model-not-selected").hide();
$("#noSpam-fields").show();
$("#add-model").show();
$("#noSpam-model-not-selected-loader").hide();
}, 100)
} else {
if ($(".model").not(".initialModel").length === 0) {
$("#noSpam-fields").hide();
$("#noSpam-model-not-selected").show();
$("#noSpam-model-not-selected-loader").show();
setTimeout(() => {
$("#noSpam-model-not-selected-text").show();
$("#noSpam-model-not-selected-loader").hide();
}, 100)
}
}
}
$(".model").change(async (e) => {
selectChange(e.target.value);
})
$("#add-model").on("click", () => {
console.log($(".model").length);
$("#models-list").append(`
<tr id="${ $('.model').length}-model">
<td width="83px">
</td>
<td>
<div style="display: flex; gap: 8px; justify-content: space-between;">
<div class="noSpamIcon noSpamIcon-Delete" onClick="deleteModelSelect(${ $('.model').length});"></div>
<select name="model" class="model" style="margin-left: -2px;" onChange="selectChange($(this).val())">
<option selected value="none">Не выбрано</option>
{foreach $models as $model}
<option value={$model}>{$model|noescape}</option>
{/foreach}
</select>
</div>
</td>
</tr>`);
});
function deleteModelSelect(id) {
$(`#${ id}-model`).remove();
if ($(".model").length === 0) {
console.log("BLYAT", $(".model"));
$("#noSpam-fields").hide();
$("#noSpam-model-not-selected").show();
$("#noSpam-model-not-selected-loader").show();
setTimeout(() => {
$("#noSpam-model-not-selected-text").show();
$("#noSpam-model-not-selected-loader").hide();
}, 100)
}
}
</script>
{/block}

View file

@ -0,0 +1,9 @@
<div n:attr="id => ($mode === 'form' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($mode === 'form' ? 'act_tab_a' : 'ki')" href="/noSpam">Бан по шаблону</a>
</div>
<div n:attr="id => ($mode === 'templates' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($mode === 'templates' ? 'act_tab_a' : 'ki')" href="/noSpam?act=templates">Действующие шаблоны</a>
</div>
<div n:attr="id => ($mode === 'reports' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($mode === 'reports' ? 'act_tab_a' : 'ki')" href="/scumfeed">Жалобы пользователей</a>
</div>

View file

@ -0,0 +1,131 @@
{extends "../@layout.xml"}
{block title}Шаблоны{/block}
{block header}{include title}{/block}
{block content}
<div class="tabs">{include "Tabs.xml", mode => "templates"}</div>
<style>
table, th, td {
border: 1px solid #ECECEC;
border-collapse: collapse;
border-spacing: 0;
font-family: -apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
}
table {
width: 100%;
}
td, th {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
}
tr:nth-child(odd) {
background-color: #f0f2f5;
}
tr:hover, th:hover {
background-color: #E8EBEF;
}
th {
text-transform: uppercase;
font-size: 0.846em;
color: #626d7a;
}
</style>
<br />
<div>
<table n:if="count($templates) > 0" cellspacing="0" cellpadding="7" width="100%">
<tr>
<th style="text-align: center;">ID</th>
<th>Пользователь</th>
<th style="text-align: center;">Раздел</th>
<th>Подстрока</th>
<th>Where</th>
<th style="text-align: center;">Тип</th>
<th style="text-align: center;">Количество</th>
<th>Время</th>
<th style="text-align: center;">Действия</th>
</tr>
<tr n:foreach="$templates as $template">
<td id="id-{$template->getId()}" onClick="openTableField('id', {$template->getId()})" style="text-align: center;"><b>{$template->getId()}</b></td>
<td id="user-{$template->getId()}" onClick="openTableField('user', {$template->getId()})">
<a href="{$template->getUser()->getURL()}" target="_blank">{$template->getUser()->getCanonicalName()}</a>
</td>
<td id="model-{$template->getId()}" onClick="openTableField('model', {$template->getId()})" style="text-align: center;">{$template->getModel()}</td>
<td id="regex-{$template->getId()}" onClick="openTableField('regex', {$template->getId()})">
<a>{$template->getRegex() ?? "-"}</a>
</td>
<td id="where-{$template->getId()}" onClick="openTableField('where', {$template->getId()})">
<a>{$template->getRequest() ?? "-"}</a>
</td>
<td id="type-{$template->getId()}" onClick="openTableField('type', {$template->getId()})" style="text-align: center;">{$template->getType()}</td>
<td id="count-{$template->getId()}" onClick="openTableField('count', {$template->getId()})" style="text-align: center;">
{$template->getCount()}
</td>
<td id="time-{$template->getId()}" onClick="openTableField('time', {$template->getId()})">{$template->getTime()}</td>
<td style="text-align: center;">
<div id="noSpam-rollback-{$template->getId()}">
<div id="noSpam-rollback-loader-{$template->getId()}" style="display: none;">
<img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
</div>
<a n:if="!$template->isRollbacked()" id="noSpam-rollback-template-link-{$template->getId()}" onClick="rollbackTemplate({$template->getId()})">откатить</a>
<span n:attr="style => $template->isRollbacked() ? '' : 'display: none;'" id="noSpam-rollback-template-rollbacked-{$template->getId()}">откачен</span>
</div>
</td>
</tr>
</table>
<div n:if="count($templates) <= 0">
{include "../components/nothing.xml"}
</div>
</div>
<script>
// Full width block
$(".navigation").hide();
$(".page_content").width("100%");
$(".page_body").width("100%").css("margin-right", 0).css("margin-top", "-2px");
$(".tabs").width("100%");
$(".sidebar").css("margin", 0);
$(".page_header").css("position", "initial");
function openTableField(name, id) {
MessageBox(name, $(`#${ name}-${ id}`).text(), ["OK"], [Function.noop]);
}
async function rollbackTemplate(id) {
$(`#noSpam-rollback-template-link-${ id}`).hide();
$(`#noSpam-rollback-template-rollbacked-${ id}`).hide();
$(`#noSpam-rollback-loader-${ id}`).show();
await $.ajax({
type: "POST",
url: "/noSpam?act=rollback",
data: {
id: id,
hash: {=$csrfToken}
},
success: (response) => {
$(`#noSpam-rollback-loader-${ id}`).hide();
if (response.success) {
$(`#noSpam-rollback-template-rollbacked-${ id}`).show();
} else {
NewNotification("Ошибка", (response?.error ?? "Неизвестная ошибка"), "/assets/packages/static/openvk/img/error.png");
$(`#noSpam-rollback-template-link-${ id}`).show();
}
},
error: (error) => {
console.error(error);
NewNotification("Ошибка", "Ошибка при отправке запроса", "/assets/packages/static/openvk/img/error.png");
$(`#noSpam-rollback-loader-${ id}`).hide();
$(`#noSpam-rollback-template-link-${ id}`).show();
}
});
}
</script>
{/block}

View file

@ -42,10 +42,38 @@
</div>
<br/>
<h4>{_actions}</h4>
{if $thisUser->getId() != $photo->getOwner()->getId()}
{var canReport = true}
{/if}
<div n:if="isset($thisUser) && $thisUser->getId() === $photo->getOwner()->getId()">
<a href="/photo{$photo->getPrettyId()}/edit" class="profile_link" style="display:block;width:96%;">{_edit}</a>
<a id="_photoDelete" href="/photo{$photo->getPrettyId()}/delete" class="profile_link" style="display:block;width:96%;">{_delete}</a>
</div>
<a href="{$photo->getURL()}" class="profile_link" target="_blank" style="display:block;width:96%;">{_"open_original"}</a>
<a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportPhoto()">{_report}</a>
<script n:if="$canReport ?? false">
function reportPhoto() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данную фотографию.";
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$photo->getId()} + "?reason=" + res + "&type=photo", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>
<a href="{$photo->getURL()}" class="profile_link" target="_blank" style="display:block;width:96%;">{_open_original}</a>
</div>
</div>

View file

@ -0,0 +1,60 @@
{extends "../@listView.xml"}
{var iterator = iterator_to_array($reports)}
{var page = $paginatorConf->page}
{var table_body_id = "reports"}
{block tabs}{include "../NoSpam/Tabs.xml", mode => "reports"}{/block}
{block before_content}
{include "./Tabs.xml", mode => $mode}
{/block}
{block title}{_list_of_reports}{/block}
{block header}
{_list_of_reports}
{/block}
{block actions}
{/block}
{block top}
{if !is_null($orig)}
<h4>Дубликаты жалобы №{$orig}</h4>
{/if}
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
/admin/report{$x->getId()}
{/block}
{block preview}
<center><img src="/assets/packages/static/openvk/img/note_icon.png" style="margin-top: 17px;" /></center>
{/block}
{block name}
Жалоба №{$x->getId()}
{/block}
{block description}
<a href="{$x->getReportAuthor()->getURL()}">
{$x->getReportAuthor()->getCanonicalName()}
</a>
пожаловал{!$x->getReportAuthor()->isFemale() ? 'ся' : 'ась'} на
{if $x->getContentType() === "user"}<a href="{$x->getContentObject()->getURL()}">{/if}
{$x->getContentName()}
{if $x->getContentType() === "user"}</a>{/if}
{if $x->hasDuplicates() && !$orig}
<br />
<b>Другие жалобы на этот контент: <a href="/scumfeed?orig={$x->getId()}">{$x->getDuplicatesCount()} шт.</a></b>
{/if}
{/block}
{block bottom}
<center id="reports-loader" style="display: none; padding: 64px;">
<img src="/assets/packages/static/openvk/img/loading_mini.gif" style="width: 40px;">
</center>
{/block}

View file

@ -0,0 +1,145 @@
<style>
.reportsTabs {
display: flex;
flex-wrap: wrap;
justify-content: center;
row-gap: 4px;
gap: 4px;
padding: 8px;
}
.reportsTabs .tab {
display: flex;
flex: 0 0 calc(16.66% - 20px);
justify-content: center;
border-radius: 3px;
padding: 4px;
margin: 0;
cursor: pointer;
}
</style>
<center class="tabs reportsTabs stupid-fix">
<div n:attr="id => ($mode === 'all' ? 'activetabs' : 'ki')" class="tab" mode="all">
<a n:attr="id => ($mode === 'all' ? 'act_tab_a' : 'ki')" mode="all">Все</a>
</div>
<div n:attr="id => ($mode === 'post' ? 'activetabs' : 'ki')" class="tab" mode="post">
<a n:attr="id => ($mode === 'post' ? 'act_tab_a' : 'ki')">Записи</a>
</div>
<div n:attr="id => ($mode === 'photo' ? 'activetabs' : 'ki')" class="tab" mode="photo">
<a n:attr="id => ($mode === 'photo' ? 'act_tab_a' : 'ki')">Фотографии</a>
</div>
<div n:attr="id => ($mode === 'video' ? 'activetabs' : 'ki')" class="tab" mode="video">
<a n:attr="id => ($mode === 'video' ? 'act_tab_a' : 'ki')">Видеозаписи</a>
</div>
<div n:attr="id => ($mode === 'group' ? 'activetabs' : 'ki')" class="tab" mode="group">
<a n:attr="id => ($mode === 'group' ? 'act_tab_a' : 'ki')">Сообщества</a>
</div>
<div n:attr="id => ($mode === 'comment' ? 'activetabs' : 'ki')" class="tab" mode="comment">
<a n:attr="id => ($mode === 'comment' ? 'act_tab_a' : 'ki')">Комментарии</a>
</div>
<div n:attr="id => ($mode === 'note' ? 'activetabs' : 'ki')" class="tab" mode="note">
<a n:attr="id => ($mode === 'note' ? 'act_tab_a' : 'ki')">Заметки</a>
</div>
<div n:attr="id => ($mode === 'app' ? 'activetabs' : 'ki')" class="tab" mode="app">
<a n:attr="id => ($mode === 'app' ? 'act_tab_a' : 'ki')">Приложения</a>
</div>
<div n:attr="id => ($mode === 'user' ? 'activetabs' : 'ki')" class="tab" mode="user">
<a n:attr="id => ($mode === 'user' ? 'act_tab_a' : 'ki')">Пользователи</a>
</div>
</center>
<script>
async function getReports(mode) {
let _content = $(".content").length;
$(".container_gray").empty();
await $.ajax({
type: "POST",
url: `/scumfeed?act=${ mode}`,
data: {
hash: {=$csrfToken}
},
success: (response) => {
if (response?.reports?.length != _content) {
NewNotification("Обратите внимание", "В списке появились новые жалобы. Работа ждёт :)");
}
if (response.reports.length > 0) {
response.reports.forEach((report) => {
$(".container_gray").append(`
<div class="content">
<table>
<tbody>
<tr>
<td valign="top">
<a href="/admin/report${ report.id}">
<center>
<img src="/assets/packages/static/openvk/img/note_icon.png" style="margin-top: 17px;">
</center>
</a>
</td>
<td valign="top" style="width: 100%">
<a href="/admin/report${ report.id}">
<b>
Жалоба №${ report.id}
</b>
</a>
<br>
<a href="${ report.author.url}">
${ report.author.name}
</a>
пожаловал${ report.author.is_female ? "ась" : "ся"} на
${ report.content.type === "user" ? `<a href="${ report.content.url}">` : ''}
${ report.content.name}
${ report.content.type === "user" ? '</a>' : ''}
${ report.duplicates > 0 ? `
<br />
<b>Другие жалобы на этот контент: <a href="/scumfeed?orig=${ report.id}">${ report.duplicates} шт.</a></b>
` : ''}
</td>
<td valign="top" class="action_links" style="width: 150px;">
</td>
</tr>
</tbody>
</table>
</div>
`);
});
} else {
$(".content table").width("100%")
$(".container_gray").html(`
<center style="background: white;border: #DEDEDE solid 1px;">
<span style="color: #707070;margin: 60px 0;display: block;">
{_no_data_description|noescape}
</span>
</center>
`);
}
}
});
}
$(".reportsTabs .tab").on("click", async function () {
let mode = $(this).attr("mode");
$(".reportsTabs #activetabs").attr("id", "ki");
$(".reportsTabs #act_tab_a").attr("id", "ki");
$(`.reportsTabs .tab[mode='${ mode}']`).attr("id", "activetabs");
$(`.reportsTabs .tab[mode='${ mode}'] a`).attr("id", "act_tab_a");
$(".container_gray").hide();
$("#reports-loader").show();
history.pushState(null, null, `/scumfeed?act=${ mode}`);
await getReports(mode);
$(".container_gray").show();
$("#reports-loader").hide();
});
setInterval(async () => {
await getReports($(".reportsTabs #activetabs").attr("mode"));
}, 10000);
</script>

View file

@ -0,0 +1,37 @@
{extends "../@layout.xml"}
{block title}{$report->getReason()}{/block}
{block header}
<a href="/admin/support/reports">{_list_of_reports}</a>
»
{_report_number}{$report->getId()}
{/block}
{block content}
<div class="tabs">{include "../NoSpam/Tabs.xml", mode => "reports"}</div>
<br />
<p>
<b>{$report->getReportAuthor()->getCanonicalName()}</b> пожаловался на <b>{$report->getContentName()}</b>
<br />
<b>{_comment}:</b> {$report->getReason()}
</p>
{include "ViewContent.xml", type => $report->getContentType(), object => $report->getContentObject()}
<center>
<form action="/admin/reportAction{$report->getId()}" method="post">
<center>
<form n:if="$report->getContentType() != 'group'" action="/admin/reportAction{$report->getId()}" method="post">
<input type="hidden" name="hash" value="{$csrfToken}"/>
<input type="submit" name="ban" value="{_ban_user_action}" class="button">
<input n:if="$report->getContentType() !== 'user'" type="submit" name="delete" value="{_delete}" class="button">
<input type="submit" name="ignore" value="{_ignore_report}" class="button">
</form>
<form n:if="$report->getContentType() == 'group'" action="/admin/reportAction{$report->getId()}" method="post">
<input type="hidden" name="hash" value="{$csrfToken}"/>
<input type="submit" name="banClubOwner" value="Заблокировать создателя" class="button">
<input type="submit" name="banClub" value="Заблокировать группу" class="button">
<input type="submit" name="ignore" value="{_ignore_report}" class="button">
</form>
</center>
</form>
{/block}

View file

@ -0,0 +1,30 @@
{block ViewContent}
<div class="container_gray" style="margin-top: 16px; margin-bottom: 16px; max-width: 100%;">
{if $type == "post"}
{include "../components/post/oldpost.xml",
post => $object,
forceNoDeleteLink => true,
forceNoPinLink => true,
forceNoCommentsLink => true,
forceNoShareLink => true,
forceNoLike => true
}
{elseif $type == "photo"}
{include "./content/photo.xml", photo => $object}
{elseif $type == "video"}
{include "./content/video.xml", video => $object}
{elseif $type == "group" || $type == "user"}
{include "../components/group.xml", group => $object, isUser => $type == "user"}
{elseif $type == "comment"}
{include "../components/comment.xml", comment => $object, timeOnly => true}
{elseif $type == "note"}
{include "./content/note.xml", note => $object}
{elseif $type == "app"}
{if $appsSoftDeleting}
{include "./content/app.xml", app => $object}
{/if}
{else}
{include "../components/error.xml", description => tr("version_incompatibility")}
{/if}
</div>
{/block}

View file

@ -0,0 +1,22 @@
{block content}
<div class="content">
<table>
<tbody>
<tr>
<td valign="top">
<a href="/app{$app->getId()}">
<img style="max-width: 75px;" src="{$app->getAvatarUrl()}" />
</a>
</td>
<td valign="top" style="width: 100%">
<a href="/app{$app->getId()}">
<b>{$app->getName()}</b>
</a>
<br/>
{$app->getDescription()}
</td>
</tr>
</tbody>
</table>
</div>
{/block}

View file

@ -0,0 +1,18 @@
{block content}
<article id="userContent" style="margin: 10px 10px 0;">
<div class="note_header">
<div class="note_title">
<div class="note_title">
<a>{$note->getName()}</a>
</div>
</div>
<div class="byline">
<span><a href="{$note->getOwner()->getURL()}">{$note->getOwner()->getCanonicalName()}</a></span> {$note->getPublicationTime()}
<span n:if="$note->getEditTime() > $note->getPublicationTime()">({_edited} {$note->getEditTime()})</span>
</div>
</div>
<div style="margin-left: 6px; width: 535px;">
{$note->getText()|noescape}
</div>
</article>
{/block}

View file

@ -0,0 +1,26 @@
{block content}
<div class="content">
<center style="margin-bottom: 8pt;">
<img src="{$photo->getURLBySizeId('large')}" style="max-width: 80%; max-height: 60vh;" />
</center>
<table>
<tbody>
<tr>
<td valign="top">
</td>
<td valign="top" style="width: 100%">
<div>
<h4>{_information}</h4>
<span style="color: grey;">{_info_description}:</span>
{$photo->getDescription() ?? "(" . tr("none") . ")"}<br/>
<span style="color: grey;">{_info_uploaded_by}:</span>
<a href="{$photo->getOwner()->getURL()}">{$photo->getOwner()->getFullName()}</a><br/>
<span style="color: grey;">{_info_upload_date}:</span>
{$photo->getPublicationTime()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
{/block}

View file

@ -0,0 +1,32 @@
{block content}
<div class="content">
<table>
<tbody>
<tr>
<td valign="top">
<a href="/video{$video->getPrettyId()}">
<div class="video-preview">
<img src="{$video->getThumbnailURL()}"
alt="{$video->getName()}"
style="max-width: 170px; max-height: 127px; margin: auto;" />
</div>
</a>
</td>
<td valign="top" style="width: 100%">
<a href="/video{$video->getPrettyId()}">
<b>
{$video->getName()}
</b>
</a>
<br>
<p>
<span>{$video->getDescription() ?? ""}</span>
</p>
<span style="color: grey;">{_video_uploaded} {$video->getPublicationTime()}</span><br/>
<span style="color: grey;">{_video_updated} {$video->getEditTime() ?? $video->getPublicationTime()}</span>
</td>
</tr>
</tbody>
</table>
</div>
{/block}

View file

@ -8,7 +8,42 @@
{block content}
<div class="post-author">
<a href="#" style="font-size: 13px;"><b>{$ticket->getName()}</b></a><br />
{_author}: <a href="/id{$ticket->getUser()->getId()}">{$ticket->getUser()->getFullName()}</a> | {$ticket->getUser()->getRegistrationIP()} | {_status}: {$ticket->getStatus()}.
{_author}:
<a href="/id{$ticket->getUser()->getId()}">
{$ticket->getUser()->getFullName()}</a>
| {$ticket->getUser()->getRegistrationIP()}
| {_status}: {$ticket->getStatus()}.
| <b n:if="$ticket->getUser()->isBanned()" style="color: red; cursor: pointer;" onclick="$('#ban-reason').toggle();">Блокировка</b>
<div id="ban-reason" style="display: none; padding: 8px;">
<h4 style="padding: 8px;">Причина блокировки</h4>
<div style="padding: 8px;">Так пользователь видит экран с информацией о блокировке:</div>
<div style="padding: 16px; border: 1px solid #C4C4C4; margin: 8px;">
{var $ban = $ticket->getUser()->getBanReason("banned")}
<center>
<img src="/assets/packages/static/openvk/img/oof.apng" alt="{_banned_alt}" style="width: 20%;" />
</center>
<p>
{if is_string($ban)}
{tr("banned_1", htmlentities($ticket->getUser()->getCanonicalName()))|noescape}<br/>
{tr("banned_2", htmlentities($ban))|noescape}
{else}
{tr("banned_1", htmlentities($ticket->getUser()->getCanonicalName()))|noescape}
<div>
Эта страница была заморожена {$ban[0]|noescape}
{if $ban[1] !== "app"}
{include "../Report/ViewContent.xml", type => $ban[1], object => $ban[2]}
{/if}
</div>
{/if}
{if !$ticket->getUser()->getUnbanTime()}
{_banned_perm}
{else}
{tr("banned_until_time", $ticket->getUser()->getUnbanTime())|noescape}
{/if}
</p>
</div>
</div>
</div>
<div class="text" style="padding-top: 10px; border-bottom: #ECECEC solid 1px;">
{$ticket->getText()|noescape}

View file

@ -118,6 +118,9 @@
<a href="javascript:warnUser()" class="profile_link" style="width: 194px;">
{_warn_user_action}
</a>
<a href="/admin/user{$user->getId()}/bans" class="profile_link">
Блокировки
</a>
<a href="/admin/logs?uid={$user->getId()}" class="profile_link" style="width: 194px;">
Последние действия
</a>
@ -166,6 +169,31 @@
<input type="submit" class="profile_link" value="{_friends_delete}" style="width: 194px;" />
</form>
{/if}
<a class="profile_link" style="display:block;width:96%;" href="javascript:reportUser()">{_report}</a>
<script>
function reportUser() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данного пользователя.";
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$user->getId()} + "?reason=" + res + "&type=user", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>
{/if}
<a style="width: 194px;" n:if="$user->getFollowersCount() > 0" href="/friends{$user->getId()}?act=incoming" class="profile_link">{tr("followers", $user->getFollowersCount())}</a>
</div>
@ -604,13 +632,15 @@
uBanMsgTxt += "<br/><b>Предупреждение</b>: Это действие удалит все подписки пользователя и отпишет всех от него.";
uBanMsgTxt += "<br/><br/><b>Причина бана</b>: <input type='text' id='uBanMsgInput' placeholder='придумайте что-нибудь крутое' />"
uBanMsgTxt += "<br/><br/><b>Заблокировать до</b>: <input type='date' id='uBanMsgDate' />";
uBanMsgTxt += "<br/><br/><input id='uBanMsgIncr' type='checkbox' checked='1'/>Автоматически <b>(до " + {date('d.m.Y H\h', time() + $user->getNewBanTime())} + ")</b>";
MessageBox("Забанить " + {$user->getFirstName()}, uBanMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uBanMsgInput").value;
date = document.querySelector("#uBanMsgDate").value;
incr = document.querySelector("#uBanMsgIncr").checked ? '1' : '0';
xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/ban/" + {$user->getId()} + "?reason=" + res + "&date=" + date + "&hash=" + {rawurlencode($csrfToken)}, true);
xhr.open("GET", "/admin/ban/" + {$user->getId()} + "?reason=" + res + "&incr=" + incr + "&date=" + date + "&hash=" + {rawurlencode($csrfToken)}, true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("success") === -1)
MessageBox("Ошибка", "Не удалось забанить пользователя...", ["OK"], [Function.noop]);

View file

@ -3,7 +3,9 @@
<p>
{tr("user_banned", htmlentities($user->getFirstName()))|noescape}<br/>
{_user_banned_comment} <b>{$user->getBanReason()}</b>.<br/>
Пользователь заблокирован до: <b>{$user->getUnbanTime()}</b>
Пользователь заблокирован
<span n:if="$user->getUnbanTime() !== NULL">до: <b>{$user->getUnbanTime()}</b></span>
<span n:if="$user->getUnbanTime() === NULL"><b>навсегда</b></span>
</p>
{if isset($thisUser)}
<p n:if="$thisUser->getChandlerUser()->can('access')->model('admin')->whichBelongsTo(NULL) || $thisUser->getChandlerUser()->can('write')->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)">

View file

@ -59,6 +59,38 @@
{_delete}
</a>
</div>
{if isset($thisUser)}
{if $thisUser->getId() != $video->getOwner()->getId()}
{var canReport = true}
{/if}
{/if}
<a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportVideo()">{_report}</a>
<script n:if="$canReport ?? false">
function reportVideo() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данную видеозапись.";
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$video->getId()} + "?reason=" + res + "&type=video", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>
</div>
</div>
{/block}

View file

@ -28,8 +28,35 @@
<h4>{_actions}</h4>
{if isset($thisUser)}
{var $canDelete = $post->canBeDeletedBy($thisUser)}
{if $thisUser->getId() != $post->getOwner()->getId()}
{var $canReport = true}
{/if}
{/if}
<a n:if="$canDelete ?? false" class="profile_link" style="display:block;width:96%;" href="/wall{$post->getPrettyId()}/delete">{_delete}</a>
<a n:if="$canReport ?? false" class="profile_link" style="display:block;width:96%;" href="javascript:reportPost()">{_report}</a>
</div>
<script n:if="$canReport ?? false">
function reportPost() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данную запись.";
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$post->getId()} + "?reason=" + res + "&type=post", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>
{/block}

View file

@ -29,6 +29,24 @@
</div>
</div>
<div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu">
<a href="#_comment{$comment->getId()}" class="date">{$comment->getPublicationTime()}</a>
{if !$timeOnly}
&nbsp;|
{if $comment->canBeDeletedBy($thisUser)}
<a href="/comment{$comment->getId()}/delete">{_delete}</a>&nbsp;|
{/if}
<a class="comment-reply">{_reply}</a>
{if $thisUser->getId() != $comment->getOwner()->getId()}
{var $canReport = true}
| <a href="javascript:reportComment()">Пожаловаться</a>
{/if}
<div style="float: right; font-size: .7rem;">
<a class="post-like-button" href="/comment{$comment->getId()}/like?hash={rawurlencode($csrfToken)}">
<div class="heart" style="{if $comment->hasLikeFrom($thisUser)}opacity: 1;{else}opacity: 0.4;{/if}"></div>
<span class="likeCnt">{if $comment->getLikesCount() > 0}{$comment->getLikesCount()}{/if}</span>
</a>
</div>
{/if}
{var $target = "wall"}
{if get_class($comment->getTarget()) == "openvk\Web\Models\Entities\Note"}
@ -61,3 +79,26 @@
</tr>
</tbody>
</table>
<script n:if="$canReport ?? false">
function reportComment() {
uReportMsgTxt = "Вы собираетесь пожаловаться на данный комментарий.";
uReportMsgTxt += "<br/>Что именно вам кажется недопустимым в этом материале?";
uReportMsgTxt += "<br/><br/><b>Причина жалобы</b>: <input type='text' id='uReportMsgInput' placeholder='Причина' />"
MessageBox("Пожаловаться?", uReportMsgTxt, ["Подтвердить", "Отмена"], [
(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + {$comment->getId()} + "?reason=" + res + "&type=comment", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox("Ошибка", "Не удалось подать жалобу...", ["OK"], [Function.noop]);
else
MessageBox("Операция успешна", "Скоро её рассмотрят модераторы", ["OK"], [Function.noop]);
});
xhr.send(null);
}),
Function.noop
]);
}
</script>

View file

@ -0,0 +1,43 @@
{block content}
<div class="content">
<table>
<tbody>
<tr>
<td valign="top">
<a href="{$group->getURL()}">
<img src="{$group->getAvatarURL('normal')}" width="75" alt="Фотография">
</a>
</td>
<td valign="top" style="width: 100%">
<table id="basicInfo" class="ugc-table group_info" cellspacing="0" cellpadding="0" border="0">
<tbody>
<tr>
<td class="label">
<span class="nobold">{_name}:</span>
</td>
<td class="data">
<a href="{$group->getURL()}">{!$isUser ? $group->getName() : $group->getCanonicalName()}</a>
<img n:if="$group->isVerified()"
class="name-checkmark"
src="/assets/packages/static/openvk/img/checkmark.png"
/>
</td>
</tr>
<tr n:if="!$isUser">
<td class="label">
<span class="nobold">{_size}:</span>
</td>
<td class="data">
<a href="/club{$group->getId()}/followers">{tr("participants",
$group->getFollowersCount())}
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
{/block}

View file

@ -0,0 +1,5 @@
{block content}
<center style="margin-bottom: 8pt;">
<img src="{$photo->getURL()}" style="max-width: 80%; max-height: 60vh;" />
</center>
{/block}

View file

@ -1,5 +1,6 @@
{block content}
<table>
<tbody>
<tbody>
<tr>
<td valign="top">
<div class="video-preview">
@ -31,5 +32,6 @@
{/ifset}
</td>
</tr>
</tbody>
</table>
</tbody>
</table
{/block}

View file

@ -16,6 +16,7 @@ services:
- openvk\Web\Presenters\UnknownTextRouteStrategyPresenter
- openvk\Web\Presenters\NotificationPresenter
- openvk\Web\Presenters\SupportPresenter
- openvk\Web\Presenters\ReportPresenter
- openvk\Web\Presenters\AdminPresenter
- openvk\Web\Presenters\GiftsPresenter
- openvk\Web\Presenters\MessengerPresenter
@ -36,6 +37,7 @@ services:
- openvk\Web\Models\Repositories\Tickets
- openvk\Web\Models\Repositories\Messages
- openvk\Web\Models\Repositories\Restores
- openvk\Web\Models\Repositories\Reports
- openvk\Web\Models\Repositories\Verifications
- openvk\Web\Models\Repositories\Notifications
- openvk\Web\Models\Repositories\TicketComments
@ -49,3 +51,4 @@ services:
- openvk\Web\Models\Repositories\BannedLinks
- openvk\Web\Models\Repositories\ChandlerGroups
- openvk\Web\Presenters\MaintenancePresenter
- openvk\Web\Presenters\NoSpamPresenter

View file

@ -321,12 +321,26 @@ routes:
handler: "Support->quickBanInSupport"
- url: "/admin/support/unban/{num}"
handler: "Support->quickUnbanInSupport"
- url: "/admin/support/reports"
handler: "Report->list"
- url: "/scumfeed"
handler: "Report->list"
- url: "/admin/report{num}"
handler: "Report->view"
- url: "/admin/report{num}"
handler: "Report->view"
- url: "/admin/reportAction{num}"
handler: "Report->action"
- url: "/report/{num}"
handler: "Report->create"
- url: "/admin/bannedLinks"
handler: "Admin->bannedLinks"
- url: "/admin/bannedLink/id{num}"
handler: "Admin->bannedLink"
- url: "/admin/bannedLink/id{num}/unban"
handler: "Admin->unbanLink"
- url: "/admin/user{num}/bans"
handler: "Admin->bansHistory"
- url: "/upload/photo/{text}"
handler: "VKAPI->photoUpload"
- url: "/method/{text}.{text}"
@ -341,6 +355,10 @@ routes:
handler: "Admin->chandlerGroup"
- url: "/admin/chandler/users/{slug}"
handler: "Admin->chandlerUser"
- url: "/noSpam"
handler: "NoSpam->index"
- url: "/al_abuse/search"
handler: "NoSpam->search"
- url: "/admin/logs"
handler: "Admin->logs"
- url: "/internal/wall{num}"

View file

@ -671,6 +671,7 @@ input[type~="phone"],
input[type="search"],
input[type~="search"],
input[type~="date"],
input[type~="datetime-local"],
select {
border: 1px solid #C0CAD5;
padding: 3px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS `reports` (
`id` bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` bigint(20) NOT NULL,
`target_id` bigint(20) NOT NULL,
`type` varchar(64) NOT NULL,
`reason` text NOT NULL,
`deleted` tinyint(1) NOT NULL DEFAULT '0',
`created` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `reports` ADD INDEX (`id`);

View file

@ -0,0 +1,19 @@
CREATE TABLE `bans`
(
`id` bigint(20) UNSIGNED NOT NULL,
`user` bigint(20) UNSIGNED NOT NULL,
`initiator` bigint(20) UNSIGNED NOT NULL,
`iat` bigint(20) UNSIGNED NOT NULL,
`exp` bigint(20) NOT NULL,
`time` bigint(20) NOT NULL,
`reason` text COLLATE utf8mb4_unicode_ci NOT NULL,
`removed_manually` tinyint(1) DEFAULT 0,
`removed_by` bigint(20) UNSIGNED NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE `bans`
ADD PRIMARY KEY (`id`);
ALTER TABLE `bans`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;

View file

@ -0,0 +1,21 @@
CREATE TABLE `noSpam_templates`
(
`id` bigint(20) UNSIGNED NOT NULL,
`user` bigint(20) UNSIGNED NOT NULL,
`model` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`regex` longtext COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`request` longtext COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`ban_type` tinyint(4) NOT NULL,
`count` bigint(20) NOT NULL,
`time` bigint(20) UNSIGNED NOT NULL,
`items` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`rollback` tinyint(1) DEFAULT NULL
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
ALTER TABLE `noSpam_templates`
ADD PRIMARY KEY (`id`);
ALTER TABLE `noSpam_templates`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;

View file

@ -889,6 +889,8 @@
"support_new_title" = "Enter the topic of your ticket";
"support_new_content" = "Describe the issue or suggestion";
"reports" = "Reports";
"support_rate_good_answer" = "This is good answer";
"support_rate_bad_answer" = "This is bad answer";
"support_good_answer_user" = "You left a positive feedback.";
@ -901,6 +903,12 @@
"fast_answers" = "Fast answers";
"ignore_report" = "Ignore report";
"report_number" = "Report #";
"list_of_reports" = "List of reports";
"text_of_the_post" = "Text of the post";
"today" = "today";
"comment" = "Comment";
"sender" = "Sender";
@ -1285,6 +1293,8 @@
"url_is_banned_title" = "Link to a suspicious site";
"url_is_banned_proceed" = "Follow the link";
"recently" = "Recently";
/* Helpdesk */
"helpdesk" = "Support";
"helpdesk_agent" = "Support Agent";

View file

@ -823,6 +823,8 @@
"support_new" = "Новое обращение";
"support_new_title" = "Введите тему вашего обращения";
"support_new_content" = "Опишите проблему или предложение";
"support_rate_good_answer" = "Это хороший ответ";
"support_rate_bad_answer" = "Это плохой ответ";
"support_good_answer_user" = "Вы оставили положительный отзыв.";
@ -833,6 +835,14 @@
"support_rated_bad" = "Вы оставили негативный отзыв об ответе.";
"wrong_parameters" = "Неверные параметры запроса.";
"fast_answers" = "Быстрые ответы";
"reports" = "Жалобы";
"ignore_report" = "Игнорировать жалобу";
"report_number" = "Жалоба №";
"list_of_reports" = "Список жалоб";
"text_of_the_post" = "Текст записи";
"today" = "сегодня";
"comment" = "Комментарий";
"sender" = "Отправитель";
"author" = "Автор";
@ -1173,6 +1183,8 @@
"url_is_banned_title" = "Ссылка на подозрительный сайт";
"url_is_banned_proceed" = "Перейти по ссылке";
"recently" = "Недавно";
/* Helpdesk */
"helpdesk" = "Поддержка";
"helpdesk_agent" = "Агент Поддержки";