diff --git a/VKAPI/Handlers/Friends.php b/VKAPI/Handlers/Friends.php
index 77de7d9d..4c109e13 100644
--- a/VKAPI/Handlers/Friends.php
+++ b/VKAPI/Handlers/Friends.php
@@ -98,6 +98,10 @@ final class Friends extends VKAPIRequestHandler
switch ($user->getSubscriptionStatus($this->getUser())) {
case 0:
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "friends.outgoing_sub")) {
+ $this->failTooOften();
+ }
+
$user->toggleSubscription($this->getUser());
return 1;
diff --git a/VKAPI/Handlers/Gifts.php b/VKAPI/Handlers/Gifts.php
index 9ee5d222..460f11df 100644
--- a/VKAPI/Handlers/Gifts.php
+++ b/VKAPI/Handlers/Gifts.php
@@ -61,6 +61,10 @@ final class Gifts extends VKAPIRequestHandler
$this->fail(-105, "Commerce is disabled on this instance");
}
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "gifts.send", false)) {
+ $this->failTooOften();
+ }
+
$user = (new UsersRepo())->get((int) $user_ids); # FAKE прогноз погоды (в данном случае user_ids)
if (!$user || $user->isDeleted()) {
diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php
index 8933ca5a..707dc0f4 100644
--- a/VKAPI/Handlers/Groups.php
+++ b/VKAPI/Handlers/Groups.php
@@ -312,6 +312,10 @@ final class Groups extends VKAPIRequestHandler
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
if ($isMember == 0) {
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "groups.sub")) {
+ $this->failTooOften();
+ }
+
$club->toggleSubscription($this->getUser());
}
diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php
index bafce3d8..73ddcee4 100644
--- a/VKAPI/Handlers/Users.php
+++ b/VKAPI/Handlers/Users.php
@@ -317,6 +317,32 @@ final class Users extends VKAPIRequestHandler
$response[$i]->custom_fields = $append_array;
break;
+ case "bdate":
+ if (!$canView) {
+ $response[$i]->bdate = "01.01.1970";
+ break;
+ }
+ $visibility = $usr->getBirthdayPrivacy();
+ $response[$i]->bdate_visibility = $visibility;
+
+ $birthday = $usr->getBirthday();
+ if ($birthday) {
+ switch ($visibility) {
+ case 1:
+ $response[$i]->bdate = $birthday->format('%d.%m');
+ break;
+ case 2:
+ $response[$i]->bdate = $birthday->format('%d.%m.%Y');
+ break;
+ case 0:
+ default:
+ $response[$i]->bdate = null;
+ break;
+ }
+ } else {
+ $response[$i]->bdate = null;
+ }
+ break;
}
}
diff --git a/VKAPI/Handlers/VKAPIRequestHandler.php b/VKAPI/Handlers/VKAPIRequestHandler.php
index 4804a8dd..1f40fceb 100644
--- a/VKAPI/Handlers/VKAPIRequestHandler.php
+++ b/VKAPI/Handlers/VKAPIRequestHandler.php
@@ -25,6 +25,11 @@ abstract class VKAPIRequestHandler
throw new APIErrorException($message, $code);
}
+ protected function failTooOften(): never
+ {
+ $this->fail(9, "Rate limited");
+ }
+
protected function getUser(): ?User
{
return $this->user;
diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php
index 9113eb6e..c1a8619d 100644
--- a/VKAPI/Handlers/Wall.php
+++ b/VKAPI/Handlers/Wall.php
@@ -713,6 +713,10 @@ final class Wall extends VKAPIRequestHandler
$post->setSuggested(1);
}
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "wall.post")) {
+ $this->failTooOften();
+ }
+
$post->save();
} catch (\LogicException $ex) {
$this->fail(100, "One of the parameters specified was missing or invalid");
@@ -723,7 +727,7 @@ final class Wall extends VKAPIRequestHandler
}
if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) {
- (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
+ (new WallPostNotification($wallOwner, $post, $this->getUser()))->emit();
}
return (object) ["post_id" => $post->getVirtualId()];
@@ -873,6 +877,8 @@ final class Wall extends VKAPIRequestHandler
"id" => $comment->getId(),
"from_id" => $oid,
"date" => $comment->getPublicationTime()->timestamp(),
+ "can_edit" => $post->canBeEditedBy($this->getUser()),
+ "can_delete" => $post->canBeDeletedBy($this->getUser()),
"text" => $comment->getText(false),
"post_id" => $post->getVirtualId(),
"owner_id" => method_exists($post, 'isPostedOnBehalfOfGroup') && $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(),
diff --git a/Web/Models/Entities/Post.php b/Web/Models/Entities/Post.php
index 4079f566..bb833c04 100644
--- a/Web/Models/Entities/Post.php
+++ b/Web/Models/Entities/Post.php
@@ -176,11 +176,13 @@ class Post extends Postable
$platform = $this->getRecord()->api_source_name;
if ($forAPI) {
switch ($platform) {
+ case 'openvk_native':
case 'openvk_refresh_android':
case 'openvk_legacy_android':
return 'android';
break;
+ case 'openvk_native_ios':
case 'openvk_ios':
case 'openvk_legacy_ios':
return 'iphone';
diff --git a/Web/Models/Entities/Traits/TSubscribable.php b/Web/Models/Entities/Traits/TSubscribable.php
index 898f33a8..2e3f3bcd 100644
--- a/Web/Models/Entities/Traits/TSubscribable.php
+++ b/Web/Models/Entities/Traits/TSubscribable.php
@@ -34,9 +34,9 @@ trait TSubscribable
"target" => $this->getId(),
];
$sub = $ctx->table("subscriptions")->where($data);
-
if (!($sub->fetch())) {
$ctx->table("subscriptions")->insert($data);
+
return true;
}
diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php
index df4da4d0..e0fa0a76 100644
--- a/Web/Models/Entities/User.php
+++ b/Web/Models/Entities/User.php
@@ -971,11 +971,13 @@ class User extends RowModel
$platform = $this->getRecord()->client_name;
if ($forAPI) {
switch ($platform) {
+ case 'openvk_native':
case 'openvk_refresh_android':
case 'openvk_legacy_android':
return 'android';
break;
+ case 'openvk_native_ios':
case 'openvk_ios':
case 'openvk_legacy_ios':
return 'iphone';
@@ -1738,4 +1740,52 @@ class User extends RowModel
{
return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count();
}
+
+ public function getEventCounters(array $list): array
+ {
+ $count_of_keys = sizeof(array_keys($list));
+ $ev_str = $this->getRecord()->events_counters;
+ $counters = [];
+
+ if (!$ev_str) {
+ for ($i = 0; $i < sizeof(array_keys($list)); $i++) {
+ $counters[] = 0;
+ }
+ } else {
+ $counters = unpack("S" . $count_of_keys, base64_decode($ev_str, true));
+ }
+
+ return [
+ 'counters' => array_combine(array_keys($list), $counters),
+ 'refresh_time' => $this->getRecord()->events_refresh_time,
+ ];
+ }
+
+ public function stateEvents(array $state_list): void
+ {
+ $pack_str = "";
+
+ foreach ($state_list as $item => $id) {
+ $pack_str .= "S";
+ }
+
+ $this->stateChanges("events_counters", base64_encode(pack($pack_str, ...array_values($state_list))));
+
+ if (!$this->getRecord()->events_refresh_time) {
+ $this->stateChanges("events_refresh_time", time());
+ }
+ }
+
+ public function resetEvents(array $list): void
+ {
+ $values = [];
+
+ foreach ($list as $key => $val) {
+ $values[$key] = 0;
+ }
+
+ $this->stateEvents($values);
+ $this->stateChanges("events_refresh_time", time());
+ $this->save();
+ }
}
diff --git a/Web/Presenters/AboutPresenter.php b/Web/Presenters/AboutPresenter.php
index 9e8e975e..5d52db90 100644
--- a/Web/Presenters/AboutPresenter.php
+++ b/Web/Presenters/AboutPresenter.php
@@ -147,6 +147,29 @@ final class AboutPresenter extends OpenVKPresenter
$this->redirect("https://github.com/openvk/openvk#readme");
}
+ public function renderAssetLinksJSON(): void
+ {
+ # Необходимо любому андроид приложению для автоматического разрешения принимать ссылки с этого сайта.
+ # Не шарю как писать норм на php поэтому тут чутка на вайбкодил - искренне ваш, ZAZiOs.
+ header("Content-Type: application/json");
+
+ $data = [
+ [
+ "relation" => ["delegate_permission/common.handle_all_urls"],
+ "target" => [
+ "namespace" => "android_app",
+ "package_name" => "oss.OpenVK.Native",
+ "sha256_cert_fingerprints" => [
+ "79:67:14:23:DC:6E:FA:49:64:1F:F1:81:0E:B0:A3:AE:6E:88:AB:0D:CF:BC:02:96:F3:6D:76:6B:82:94:D6:9C",
+ ],
+ ],
+ ],
+ ];
+
+ echo json_encode($data, JSON_UNESCAPED_SLASHES);
+ exit;
+ }
+
public function renderDev(): void
{
$this->redirect("https://docs.ovk.to/");
diff --git a/Web/Presenters/GiftsPresenter.php b/Web/Presenters/GiftsPresenter.php
index 007be976..ec2993a4 100644
--- a/Web/Presenters/GiftsPresenter.php
+++ b/Web/Presenters/GiftsPresenter.php
@@ -106,6 +106,10 @@ final class GiftsPresenter extends OpenVKPresenter
return;
}
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "gifts.send")) {
+ $this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
+ }
+
$comment = empty($c = $this->postParam("comment")) ? null : $c;
$notification = new GiftNotification($user, $this->user->identity, $gift, $comment);
$notification->emit();
diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php
index a38dacaa..99d5fa33 100644
--- a/Web/Presenters/GroupPresenter.php
+++ b/Web/Presenters/GroupPresenter.php
@@ -68,6 +68,10 @@ final class GroupPresenter extends OpenVKPresenter
$club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about"));
$club->setOwner($this->user->id);
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.create")) {
+ $this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
+ }
+
try {
$club->save();
} catch (\PDOException $ex) {
@@ -79,6 +83,7 @@ final class GroupPresenter extends OpenVKPresenter
}
$club->toggleSubscription($this->user->identity);
+
$this->redirect("/club" . $club->getId());
} else {
$this->flashFail("err", tr("error"), tr("error_no_group_name"));
@@ -103,6 +108,12 @@ final class GroupPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error"), tr("forbidden"));
}
+ if (!$club->getSubscriptionStatus($this->user->identity)) {
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.sub")) {
+ $this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
+ }
+ }
+
$club->toggleSubscription($this->user->identity);
$this->redirect($club->getURL());
diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php
index f58eaa5c..19254ab0 100644
--- a/Web/Presenters/ReportPresenter.php
+++ b/Web/Presenters/ReportPresenter.php
@@ -103,6 +103,10 @@ final class ReportPresenter extends OpenVKPresenter
exit(json_encode([ "error" => "You can't report yourself" ]));
}
+ if ($this->user->identity->isBannedInSupport()) {
+ exit(json_encode([ "reason" => $this->queryParam("reason") ]));
+ }
+
if (in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"])) {
if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, null, $this->user->id))) <= 0) {
$report = new Report();
diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php
index 7d152df4..869d601e 100644
--- a/Web/Presenters/UserPresenter.php
+++ b/Web/Presenters/UserPresenter.php
@@ -418,6 +418,12 @@ final class UserPresenter extends OpenVKPresenter
if ($this->postParam("act") == "rej") {
$user->changeFlags($this->user->identity, 0b10000000, true);
} else {
+ if ($user->getSubscriptionStatus($this->user->identity) == \openvk\Web\Models\Entities\User::SUBSCRIPTION_ABSENT) {
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "friends.outgoing_sub")) {
+ $this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
+ }
+ }
+
$user->toggleSubscription($this->user->identity);
}
diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php
index dbdfdde0..53e662f6 100644
--- a/Web/Presenters/WallPresenter.php
+++ b/Web/Presenters/WallPresenter.php
@@ -356,6 +356,10 @@ final class WallPresenter extends OpenVKPresenter
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
}
+ if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "wall.post")) {
+ $this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
+ }
+
$should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2;
try {
$post = new Post();
diff --git a/Web/Presenters/templates/Report/Tabs.xml b/Web/Presenters/templates/Report/Tabs.xml
index 1107b44c..8cf548dd 100644
--- a/Web/Presenters/templates/Report/Tabs.xml
+++ b/Web/Presenters/templates/Report/Tabs.xml
@@ -67,7 +67,7 @@
},
success: (response) => {
if (response?.reports?.length != _content) {
- NewNotification("Обратите внимание", "В списке появились новые жалобы. Работа ждёт :)");
+ // NewNotification("Обратите внимание", "В списке появились новые жалобы. Работа ждёт :)");
}
if (response.reports.length > 0) {
diff --git a/Web/Util/EventRateLimiter.php b/Web/Util/EventRateLimiter.php
new file mode 100644
index 00000000..b7d31657
--- /dev/null
+++ b/Web/Util/EventRateLimiter.php
@@ -0,0 +1,95 @@
+config = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"];
+ }
+
+ public function tryToLimit(?User $user, string $event_type, bool $is_update = true): bool
+ {
+ /*
+ Checks count of actions for last x seconds
+
+ Uses OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]
+
+ This check should be peformed only after checking other conditions cuz by default it increments counter
+
+ Returns:
+
+ true — limit has exceed and the action must be restricted
+
+ false — the action can be performed
+
+ Also returns "true" if this option is disabled
+ */
+
+ $isEnabled = $this->config['enable'];
+ $isIgnoreForAdmins = $this->config['ignoreForAdmins'];
+ $restrictionTime = $this->config['restrictionTime'];
+ $eventsList = $this->config['list'];
+
+ if (!$isEnabled) {
+ return false;
+ }
+
+ if ($isIgnoreForAdmins && $user->isAdmin()) {
+ return false;
+ }
+
+ $eventsStats = $user->getEventCounters($eventsList);
+ $limitForThatEvent = $eventsList[$event_type];
+
+ $counters = $eventsStats["counters"];
+ $refresh_time = $eventsStats["refresh_time"];
+ $is_restrict_over = $refresh_time < (time() - $restrictionTime);
+ $event_counter = $counters[$event_type];
+
+ if ($refresh_time && $is_restrict_over) {
+ $user->resetEvents($eventsList);
+
+ return false;
+ }
+
+ $is_limit_exceed = $event_counter >= $limitForThatEvent;
+
+ if (!$is_limit_exceed && $is_update) {
+ $this->incrementEvent($counters, $event_type, $user);
+ }
+
+ return $is_limit_exceed;
+ }
+
+ public function incrementEvent(array $old_values, string $event_type, User $initiator): bool
+ {
+ /*
+ Updates counter for user
+ */
+ $isEnabled = $this->config['enable'];
+ $eventsList = $this->config['list'];
+
+ if (!$isEnabled) {
+ return false;
+ }
+
+ $old_values[$event_type] += 1;
+
+ $initiator->stateEvents($old_values);
+ $initiator->save();
+
+ return true;
+ }
+}
diff --git a/Web/routes.yml b/Web/routes.yml
index f1cf2af6..f4323dca 100644
--- a/Web/routes.yml
+++ b/Web/routes.yml
@@ -407,6 +407,8 @@ routes:
handler: "About->robotsTxt"
- url: "/humans.txt"
handler: "About->humansTxt"
+ - url: "/.well-known/assetlinks.json"
+ handler: "About->AssetLinksJSON"
- url: "/dev"
handler: "About->dev"
- url: "/iapi/getPhotosFromPost/{num}_{num}"
diff --git a/Web/static/img/app_icons/openvk_native.png b/Web/static/img/app_icons/openvk_native.png
new file mode 100644
index 00000000..56d89133
Binary files /dev/null and b/Web/static/img/app_icons/openvk_native.png differ
diff --git a/data/clients.xml b/data/clients.xml
index a387a634..4e0db0ef 100644
--- a/data/clients.xml
+++ b/data/clients.xml
@@ -3,4 +3,6 @@
-
\ No newline at end of file
+
+
+
diff --git a/install/sqls/00057-event-limiting.sql b/install/sqls/00057-event-limiting.sql
new file mode 100644
index 00000000..bf34f5be
--- /dev/null
+++ b/install/sqls/00057-event-limiting.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `profiles`
+ADD `events_counters` VARCHAR(299) NULL DEFAULT NULL AFTER `audio_broadcast_enabled`,
+ADD `events_refresh_time` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `events_counters`;
diff --git a/locales/en.strings b/locales/en.strings
index 1632b165..48576550 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -1657,6 +1657,8 @@
"error_geolocation" = "Error while trying to pin geolocation";
"error_no_geotag" = "There is no geo-tag pinned in this post";
+"limit_exceed_exception" = "You're doing this action too often. Try again later.";
+
/* Admin actions */
"login_as" = "Login as $1";
diff --git a/locales/ru.strings b/locales/ru.strings
index 40ba188e..a13173b7 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -1561,6 +1561,8 @@
"error_geolocation" = "Ошибка при прикреплении геометки";
"error_no_geotag" = "У поста не указана гео-метка";
+"limit_exceed_exception" = "Вы совершаете это действие слишком часто. Повторите позже.";
+
/* Admin actions */
"login_as" = "Войти как $1";
diff --git a/openvk-example.yml b/openvk-example.yml
index 525d7cf8..c1b483ac 100644
--- a/openvk-example.yml
+++ b/openvk-example.yml
@@ -41,6 +41,16 @@ openvk:
maxViolations: 50
maxViolationsAge: 120
autoban: true
+ eventsLimit:
+ enable: true
+ ignoreForAdmins: true
+ restrictionTime: 86400
+ list:
+ groups.create: 5
+ groups.sub: 50
+ friends.outgoing_sub: 25
+ wall.post: 5000
+ gifts.send: 30
blacklists:
limit: 100
applyToAdmins: true