From d767d8e2eb101bce36f2ad011c6ecea18c5a8db3 Mon Sep 17 00:00:00 2001 From: celestora Date: Sat, 20 Aug 2022 21:07:54 +0300 Subject: [PATCH 1/8] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20(#674)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ServiceAPI/Apps.php | 87 +++++++ ServiceAPI/Wall.php | 16 ++ VKAPI/Handlers/Pay.php | 42 +++ Web/Models/Entities/Application.php | 316 +++++++++++++++++++++++ Web/Models/Repositories/Applications.php | 69 +++++ Web/Presenters/AppsPresenter.php | 138 ++++++++++ Web/Presenters/templates/Apps/Edit.xml | 112 ++++++++ Web/Presenters/templates/Apps/List.xml | 53 ++++ Web/Presenters/templates/Apps/Play.xml | 37 +++ Web/di.yml | 2 + Web/routes.yml | 8 + Web/static/css/style.css | 7 + Web/static/js/GameAPI.js | 54 ++++ Web/static/js/al_games.js | 225 ++++++++++++++++ install/sqls/00030-apps.sql | 22 ++ locales/ru.strings | 68 +++++ openvk-example.yml | 2 + 17 files changed, 1258 insertions(+) create mode 100644 ServiceAPI/Apps.php create mode 100644 VKAPI/Handlers/Pay.php create mode 100644 Web/Models/Entities/Application.php create mode 100644 Web/Models/Repositories/Applications.php create mode 100644 Web/Presenters/AppsPresenter.php create mode 100644 Web/Presenters/templates/Apps/Edit.xml create mode 100644 Web/Presenters/templates/Apps/List.xml create mode 100644 Web/Presenters/templates/Apps/Play.xml create mode 100644 Web/static/js/GameAPI.js create mode 100644 Web/static/js/al_games.js create mode 100644 install/sqls/00030-apps.sql diff --git a/ServiceAPI/Apps.php b/ServiceAPI/Apps.php new file mode 100644 index 00000000..521b117e --- /dev/null +++ b/ServiceAPI/Apps.php @@ -0,0 +1,87 @@ +user = $user; + $this->apps = new Applications; + } + + function getUserInfo(callable $resolve, callable $reject): void + { + $hexId = dechex($this->user->getId()); + $sign = hash_hmac("sha512/224", $hexId, CHANDLER_ROOT_CONF["security"]["secret"], true); + $marketingId = $hexId . "_" . base64_encode($sign); + + $resolve([ + "id" => $this->user->getId(), + "marketing_id" => $marketingId, + "name" => [ + "first" => $this->user->getFirstName(), + "last" => $this->user->getLastName(), + "full" => $this->user->getFullName(), + ], + "ava" => $this->user->getAvatarUrl(), + ]); + } + + function updatePermission(int $app, string $perm, string $state, callable $resolve, callable $reject): void + { + $app = $this->apps->get($app); + if(!$app || !$app->isEnabled()) { + $reject("No application with this id found"); + return; + } + + if(!$app->setPermission($this->user, $perm, $state == "yes")) + $reject("Invalid permission $perm"); + + $resolve(1); + } + + function pay(int $appId, float $amount, callable $resolve, callable $reject): void + { + $app = $this->apps->get($appId); + if(!$app || !$app->isEnabled()) { + $reject("No application with this id found"); + return; + } + + $coinsLeft = $this->user->getCoins() - $amount; + if($coinsLeft < 0) { + $reject(41, "Not enough money"); + return; + } + + $this->user->setCoins($coinsLeft); + $this->user->save(); + $app->addCoins($amount); + + $t = time(); + $resolve($t . "," . hash_hmac("whirlpool", "$appId:$amount:$t", CHANDLER_ROOT_CONF["security"]["secret"])); + } + + function withdrawFunds(int $appId, callable $resolve, callable $reject): void + { + $app = $this->apps->get($appId); + if(!$app) { + $reject("No application with this id found"); + return; + } else if($app->getOwner()->getId() != $this->user->getId()) { + $reject("You don't have rights to edit this app"); + return; + } + + $coins = $app->getBalance(); + $app->withdrawCoins(); + $resolve($coins); + } +} \ No newline at end of file diff --git a/ServiceAPI/Wall.php b/ServiceAPI/Wall.php index af74464f..628ceb22 100644 --- a/ServiceAPI/Wall.php +++ b/ServiceAPI/Wall.php @@ -1,5 +1,6 @@ setOwner($this->user->getId()); + $post->setWall($this->user->getId()); + $post->setCreated(time()); + $post->setContent($text); + $post->setAnonymous(false); + $post->setFlags(0); + $post->setNsfw(false); + $post->save(); + + $resolve($post->getId()); + } } diff --git a/VKAPI/Handlers/Pay.php b/VKAPI/Handlers/Pay.php new file mode 100644 index 00000000..e5fb93a7 --- /dev/null +++ b/VKAPI/Handlers/Pay.php @@ -0,0 +1,42 @@ +fail(4, "Invalid marketing id"); + } catch (\SodiumException $e) { + $this->fail(4, "Invalid marketing id"); + } + + return hexdec($hexId); + } + + function verifyOrder(int $app_id, float $amount, string $signature): bool + { + $this->requireUser(); + + $app = (new Applications())->get($app_id); + if(!$app) + $this->fail(26, "No app found with this id"); + else if($app->getOwner()->getId() != $this->getUser()->getId()) + $this->fail(15, "Access error"); + + [$time, $signature] = explode(",", $signature); + try { + $key = CHANDLER_ROOT_CONF["security"]["secret"]; + if(sodium_memcmp($signature, hash_hmac("whirlpool", "$app_id:$amount:$time", $key)) == -1) + $this->fail(4, "Invalid order"); + } catch (\SodiumException $e) { + $this->fail(4, "Invalid order"); + } + + return true; + } +} \ No newline at end of file diff --git a/Web/Models/Entities/Application.php b/Web/Models/Entities/Application.php new file mode 100644 index 00000000..e79c8db5 --- /dev/null +++ b/Web/Models/Entities/Application.php @@ -0,0 +1,316 @@ +getRecord()->id; + } + + function getOwner(): User + { + return (new Users)->get($this->getRecord()->owner); + } + + function getName(): string + { + return $this->getRecord()->name; + } + + function getDescription(): string + { + return $this->getRecord()->description; + } + + function getAvatarUrl(): string + { + $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; + if(is_null($this->getRecord()->avatar_hash)) + return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; + + $hash = $this->getRecord()->avatar_hash; + switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { + default: + case "default": + case "basic": + return "$serverUrl/blob_" . substr($hash, 0, 2) . "/$hash" . "_app_avatar.png"; + case "accelerated": + return "$serverUrl/openvk-datastore/$hash.app_avatar.png"; + case "server": + $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; + return ( + $settings->protocol ?? ovk_scheme() . + "://" . $settings->host . + $settings->path . + substr($hash, 0, 2) . "/$hash.app_avatar.png" + ); + } + } + + function getNote(): ?Note + { + if(!$this->getRecord()->news) + return NULL; + + return (new Notes)->get($this->getRecord()->news); + } + + function getNoteLink(): string + { + $note = $this->getNote(); + if(!$note) + return ""; + + return ovk_scheme(true) . $_SERVER["HTTP_HOST"] . "/note" . $note->getPrettyId(); + } + + function getBalance(): float + { + return $this->getRecord()->coins; + } + + function getURL(): string + { + return $this->getRecord()->address; + } + + function getOrigin(): string + { + $parsed = parse_url($this->getURL()); + + return ( + ($parsed["scheme"] ?? "https") . "://" + . ($parsed["host"] ?? "127.0.0.1") . ":" + . ($parsed["port"] ?? "443") + ); + } + + function getUsersCount(): int + { + $cx = DatabaseConnection::i()->getContext(); + return sizeof($cx->table("app_users")->where("app", $this->getId())); + } + + function getInstallationEntry(User $user): ?array + { + $cx = DatabaseConnection::i()->getContext(); + $entry = $cx->table("app_users")->where([ + "app" => $this->getId(), + "user" => $user->getId(), + ])->fetch(); + + if(!$entry) + return NULL; + + return $entry->toArray(); + } + + function getPermissions(User $user): array + { + $permMask = 0; + $installInfo = $this->getInstallationEntry($user); + if(!$installInfo) + $this->install($user); + else + $permMask = $installInfo["access"]; + + $res = []; + for($i = 0; $i < sizeof(self::PERMS); $i++) { + $checkVal = 1 << $i; + if(($permMask & $checkVal) > 0) + $res[] = self::PERMS[$i]; + } + + return $res; + } + + function isInstalledBy(User $user): bool + { + return !is_null($this->getInstallationEntry($user)); + } + + function setNoteLink(?string $link): bool + { + if(!$link) { + $this->stateChanges("news", NULL); + + return true; + } + + preg_match("%note([0-9]+)_([0-9]+)$%", $link, $matches); + if(sizeof($matches) != 3) + return false; + + $owner = is_null($this->getRecord()) ? $this->changes["owner"] : $this->getRecord()->owner; + [, $ownerId, $vid] = $matches; + if($ownerId != $owner) + return false; + + $note = (new Notes)->getNoteById((int) $ownerId, (int) $vid); + if(!$note) + return false; + + $this->stateChanges("news", $note->getId()); + + return true; + } + + function setAvatar(array $file): int + { + if($file["error"] !== UPLOAD_ERR_OK) + return -1; + + try { + $image = Image::fromFile($file["tmp_name"]); + } catch (UnknownImageFileException $e) { + return -2; + } + + $hash = hash_file("murmur3a", $file["tmp_name"]); + if(!is_dir($this->getAvatarsDir() . substr($hash, 0, 2))) + if(!mkdir($this->getAvatarsDir() . substr($hash, 0, 2))) + return -3; + + $image->resize(140, 140, Image::STRETCH); + $image->save($this->getAvatarsDir() . substr($hash, 0, 2) . "/$hash" . "_app_avatar.png"); + + $this->stateChanges("avatar_hash", $hash); + + return 0; + } + + function setPermission(User $user, string $perm, bool $enabled): bool + { + $permMask = 0; + $installInfo = $this->getInstallationEntry($user); + if(!$installInfo) + $this->install($user); + else + $permMask = $installInfo["access"]; + + $index = array_search($perm, self::PERMS); + if($index === false) + return false; + + $permVal = 1 << $index; + $permMask = $enabled ? ($permMask | $permVal) : ($permMask ^ $permVal); + + $cx = DatabaseConnection::i()->getContext(); + $cx->table("app_users")->where([ + "app" => $this->getId(), + "user" => $user->getId(), + ])->update([ + "access" => $permMask, + ]); + + return true; + } + + function isEnabled(): bool + { + return (bool) $this->getRecord()->enabled; + } + + function enable(): void + { + $this->stateChanges("enabled", 1); + $this->save(); + } + + function disable(): void + { + $this->stateChanges("enabled", 0); + $this->save(); + } + + function install(User $user): void + { + if(!$this->getInstallationEntry($user)) { + $cx = DatabaseConnection::i()->getContext(); + $cx->table("app_users")->insert([ + "app" => $this->getId(), + "user" => $user->getId(), + ]); + } + } + + function uninstall(User $user): void + { + $cx = DatabaseConnection::i()->getContext(); + $cx->table("app_users")->where([ + "app" => $this->getId(), + "user" => $user->getId(), + ])->delete(); + } + + function addCoins(float $coins): float + { + $res = $this->getBalance() + $coins; + $this->stateChanges("coins", $res); + $this->save(); + + return $res; + } + + function withdrawCoins(): void + { + $balance = $this->getBalance(); + $tax = ($balance / 100) * OPENVK_ROOT_CONF["openvk"]["preferences"]["apps"]["withdrawTax"]; + + $owner = $this->getOwner(); + $owner->setCoins($owner->getCoins() + ($balance - $tax)); + $this->setCoins(0.0); + $this->save(); + $owner->save(); + } + + function delete(bool $softly = true): void + { + if($softly) + throw new \UnexpectedValueException("Can't delete apps softly."); + + $cx = DatabaseConnection::i()->getContext(); + $cx->table("app_users")->where("app", $this->getId())->delete(); + + parent::delete(false); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/Applications.php b/Web/Models/Repositories/Applications.php new file mode 100644 index 00000000..3aa6e5be --- /dev/null +++ b/Web/Models/Repositories/Applications.php @@ -0,0 +1,69 @@ +context = DatabaseConnection::i()->getContext(); + $this->apps = $this->context->table("apps"); + $this->appRels = $this->context->table("app_users"); + } + + private function toApp(?ActiveRow $ar): ?Application + { + return is_null($ar) ? NULL : new Application($ar); + } + + function get(int $id): ?Application + { + return $this->toApp($this->apps->get($id)); + } + + function getList(int $page = 1, ?int $perPage = NULL): \Traversable + { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $apps = $this->apps->where("enabled", 1)->page($page, $perPage); + foreach($apps as $app) + yield new Application($app); + } + + function getListCount(): int + { + return sizeof($this->apps->where("enabled", 1)); + } + + function getByOwner(User $owner, int $page = 1, ?int $perPage = NULL): \Traversable + { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $apps = $this->apps->where("owner", $owner->getId())->page($page, $perPage); + foreach($apps as $app) + yield new Application($app); + } + + function getOwnCount(User $owner): int + { + return sizeof($this->apps->where("owner", $owner->getId())); + } + + function getInstalled(User $user, int $page = 1, ?int $perPage = NULL): \Traversable + { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $apps = $this->appRels->where("user", $user->getId())->page($page, $perPage); + foreach($apps as $appRel) + yield $this->get($appRel->app); + } + + function getInstalledCount(User $user): int + { + return sizeof($this->appRels->where("user", $user->getId())); + } +} \ No newline at end of file diff --git a/Web/Presenters/AppsPresenter.php b/Web/Presenters/AppsPresenter.php new file mode 100644 index 00000000..02fb8922 --- /dev/null +++ b/Web/Presenters/AppsPresenter.php @@ -0,0 +1,138 @@ +apps = $apps; + + parent::__construct(); + } + + function renderPlay(int $app): void + { + $this->assertUserLoggedIn(); + + $app = $this->apps->get($app); + if(!$app || !$app->isEnabled()) + $this->notFound(); + + $this->template->id = $app->getId(); + $this->template->name = $app->getName(); + $this->template->desc = $app->getDescription(); + $this->template->origin = $app->getOrigin(); + $this->template->url = $app->getURL(); + $this->template->owner = $app->getOwner(); + $this->template->news = $app->getNote(); + $this->template->perms = $app->getPermissions($this->user->identity); + } + + function renderUnInstall(): void + { + $this->assertUserLoggedIn(); + $this->assertNoCSRF(); + + $app = $this->apps->get((int) $this->queryParam("app")); + if(!$app) + $this->flashFail("err", tr("app_err_not_found"), tr("app_err_not_found_desc")); + + $app->uninstall($this->user->identity); + $this->flashFail("succ", tr("app_uninstalled"), tr("app_uninstalled_desc")); + } + + function renderEdit(): void + { + $this->assertUserLoggedIn(); + + $app = NULL; + if($this->queryParam("act") !== "create") { + if(empty($this->queryParam("app"))) + $this->flashFail("err", tr("app_err_not_found"), tr("app_err_not_found_desc")); + + $app = $this->apps->get((int) $this->queryParam("app")); + if(!$app) + $this->flashFail("err", tr("app_err_not_found"), tr("app_err_not_found_desc")); + + if($app->getOwner()->getId() != $this->user->identity->getId()) + $this->flashFail("err", tr("forbidden"), tr("app_err_forbidden_desc")); + } + + if($_SERVER["REQUEST_METHOD"] === "POST") { + if(!$app) { + $app = new Application; + $app->setOwner($this->user->id); + } + + if(!filter_var($this->postParam("url"), FILTER_VALIDATE_URL)) + $this->flashFail("err", tr("app_err_url"), tr("app_err_url_desc")); + + if(isset($_FILES["ava"]) && $_FILES["ava"]["size"] > 0) { + if(($res = $app->setAvatar($_FILES["ava"])) !== 0) + $this->flashFail("err", tr("app_err_ava"), tr("app_err_ava_desc", $res)); + } + + if(empty($this->postParam("note"))) { + $app->setNoteLink(NULL); + } else { + if(!$app->setNoteLink($this->postParam("note"))) + $this->flashFail("err", tr("app_err_note"), tr("app_err_note_desc")); + } + + $app->setName($this->postParam("name")); + $app->setDescription($this->postParam("desc")); + $app->setAddress($this->postParam("url")); + if($this->postParam("enable") === "on") + $app->enable(); + else + $app->disable(); # no need to save since enable/disable will call save() internally + + $this->redirect("/editapp?act=edit&app=" . $app->getId()); # will exit here + } + + if(!is_null($app)) { + $this->template->create = false; + $this->template->id = $app->getId(); + $this->template->name = $app->getName(); + $this->template->desc = $app->getDescription(); + $this->template->coins = $app->getBalance(); + $this->template->origin = $app->getOrigin(); + $this->template->url = $app->getURL(); + $this->template->note = $app->getNoteLink(); + $this->template->users = $app->getUsersCount(); + $this->template->on = $app->isEnabled(); + } else { + $this->template->create = true; + } + } + + function renderList(): void + { + $this->assertUserLoggedIn(); + + $act = $this->queryParam("act"); + if(!in_array($act, ["list", "installed", "dev"])) + $act = "installed"; + + $page = (int) ($this->queryParam("p") ?? 1); + if($act == "list") { + $apps = $this->apps->getList($page); + $count = $this->apps->getListCount(); + } else if($act == "installed") { + $apps = $this->apps->getInstalled($this->user->identity, $page); + $count = $this->apps->getInstalledCount($this->user->identity); + } else if($act == "dev") { + $apps = $this->apps->getByOwner($this->user->identity, $page); + $count = $this->apps->getOwnCount($this->user->identity); + } + + $this->template->act = $act; + $this->template->iterator = $apps; + $this->template->count = $count; + $this->template->page = $page; + } +} \ No newline at end of file diff --git a/Web/Presenters/templates/Apps/Edit.xml b/Web/Presenters/templates/Apps/Edit.xml new file mode 100644 index 00000000..b8d9c8b0 --- /dev/null +++ b/Web/Presenters/templates/Apps/Edit.xml @@ -0,0 +1,112 @@ +{extends "../@layout.xml"} + +{block title} + {if $create} + {_create_app} + {else} + {_edit_app} + {/if} +{/block} + +{block header} + {if $create} + {_new_app} + {else} + {_own_apps_alternate} » + {$name} » + {_edit} + {/if} +{/block} + +{block content} +
+

{_main_information}

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {_name}: + + +
+ {_description}: + + +
+ {_avatar}: + + +
+ {_app_news}: + + +
+ URL: + + +
+ {_app_state}: + + {_app_enabled} +
+ + + + +
+
+
+ +

{_additional_information}

+
+
    + {if $create} +
  • {_app_creation_hint_url}
  • +
  • {_app_creation_hint_iframe}
  • + {else} +
  • {tr("app_balance", $coins)|noescape} ({_app_withdrawal_q})
  • +
  • {tr("app_users", $users)|noescape}
  • + {/if} +
+
+
+ + +{/block} diff --git a/Web/Presenters/templates/Apps/List.xml b/Web/Presenters/templates/Apps/List.xml new file mode 100644 index 00000000..fbf4c6cc --- /dev/null +++ b/Web/Presenters/templates/Apps/List.xml @@ -0,0 +1,53 @@ +{extends "../@listView.xml"} + +{block title} + {_apps} +{/block} + +{block header} + {_apps} + +
+ + {_create} + +
+{/block} + +{block tabs} +
+ {_all_apps} +
+ +
+ {_installed_apps} +
+ +
+ {_own_apps} +
+{/block} + +{* BEGIN ELEMENTS DESCRIPTION *} + +{block link|strip|stripHtml} + /app{$x->getId()} +{/block} + +{block preview} + +{/block} + +{block name} + {$x->getName()} +{/block} + +{block description} + {$x->getDescription()} +{/block} + +{block actions} + {_app_play} + {_app_uninstall} + {_app_edit} +{/block} diff --git a/Web/Presenters/templates/Apps/Play.xml b/Web/Presenters/templates/Apps/Play.xml new file mode 100644 index 00000000..91cfe5d5 --- /dev/null +++ b/Web/Presenters/templates/Apps/Play.xml @@ -0,0 +1,37 @@ +{extends "../@layout.xml"} + +{block title} + {$name} +{/block} + +{block header} + {$name} +{/block} + +{block content} +
+ +
+ +
+

{$news->getName()}

+
+ {$news->getText()|noescape} +
+
+ +
+

+ {_app_dev}: {$owner->getFullName()} +

+
+ + + + {script "js/al_games.js"} +{/block} diff --git a/Web/di.yml b/Web/di.yml index 1e47bbc1..daa2e753 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -20,6 +20,7 @@ services: - openvk\Web\Presenters\GiftsPresenter - openvk\Web\Presenters\MessengerPresenter - openvk\Web\Presenters\TopicsPresenter + - openvk\Web\Presenters\AppsPresenter - openvk\Web\Presenters\ThemepacksPresenter - openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Models\Repositories\Users @@ -39,4 +40,5 @@ services: - openvk\Web\Models\Repositories\Vouchers - openvk\Web\Models\Repositories\Gifts - openvk\Web\Models\Repositories\Topics + - openvk\Web\Models\Repositories\Applications - openvk\Web\Models\Repositories\ContentSearchRepository diff --git a/Web/routes.yml b/Web/routes.yml index 55225df3..2b346610 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -259,6 +259,14 @@ routes: handler: "Gifts->userGifts" - url: "/gifts" handler: "Gifts->stub" + - url: "/app{num}" + handler: "Apps->play" + - url: "/apps" + handler: "Apps->list" + - url: "/editapp" + handler: "Apps->edit" + - url: "/apps/uninstall" + handler: "Apps->unInstall" - url: "/admin" handler: "Admin->index" - url: "/admin/users" diff --git a/Web/static/css/style.css b/Web/static/css/style.css index 633d8a29..d35154c1 100644 --- a/Web/static/css/style.css +++ b/Web/static/css/style.css @@ -2044,6 +2044,13 @@ table td[width="120"] { height: 11px; } +#app_news_container { + margin-bottom: 30px; + overflow-x: hidden; + overflow-y: auto; + max-height: 250px; +} + @keyframes appearing { from { opacity: 0; diff --git a/Web/static/js/GameAPI.js b/Web/static/js/GameAPI.js new file mode 100644 index 00000000..66b53ec1 --- /dev/null +++ b/Web/static/js/GameAPI.js @@ -0,0 +1,54 @@ +const VKAPI = Object.create(null); + +VKAPI._makeRequest = function(type, params) { + return new Promise((succ, fail) => { + let uuid = crypto.randomUUID(); + let request = params; + request["@type"] = type; + request.transaction = uuid; + + let listener = e => { + if (e.source !== window.parent) + return; + + if (e.data.transaction !== uuid) + return; + + let resp = e.data; + let ok = resp.ok; + delete resp.transaction; + delete resp.ok; + + (ok ? succ : fail)(resp); + window.removeEventListener("message", listener); + }; + + let origin = document.referrer.split("/").slice(0, 3).join("/"); + window.addEventListener("message", listener); + window.parent.postMessage(request, origin); + }); +} + +VKAPI.getUser = function() { + return VKAPI._makeRequest("UserInfoRequest", {}); +} + +VKAPI.makePost = function(text) { + return VKAPI._makeRequest("WallPostRequest", { + text: text + }); +} + +VKAPI.execute = function(method, params) { + return VKAPI._makeRequest("VkApiRequest", { + method: method, + params: params + }); +} + +VKAPI.buy = function(price, item) { + return VKAPI._makeRequest("PaymentRequest", { + outSum: price, + description: item + }); +} \ No newline at end of file diff --git a/Web/static/js/al_games.js b/Web/static/js/al_games.js new file mode 100644 index 00000000..78bd738c --- /dev/null +++ b/Web/static/js/al_games.js @@ -0,0 +1,225 @@ +const perms = { + friends: [tr("appjs_sperm_friends"), tr("appjs_sperm_friends_desc")], + wall: [tr("appjs_sperm_wall"), tr("appjs_sperm_wall_desc")], + messages: [tr("appjs_sperm_messages"), tr("appjs_sperm_messages_desc")], + groups: [tr("appjs_sperm_groups"), tr("appjs_sperm_groups_desc")], + likes: [tr("appjs_sperm_likes"), tr("appjs_sperm_likes_desc")] +} + +function escapeHtml(unsafe) +{ + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function toQueryString(obj, prefix) { + var str = [], + p; + for (p in obj) { + if (obj.hasOwnProperty(p)) { + var k = prefix ? prefix + "[" + p + "]" : p, + v = obj[p]; + str.push((v !== null && typeof v === "object") ? + serialize(v, k) : + encodeURIComponent(k) + "=" + encodeURIComponent(v)); + } + } + return str.join("&"); +} + +function handleWallPostRequest(event) { + let mBoxContent = ` + ${tr("app")} ${window.appTitle} ${tr("appjs_wall_post_desc")}:
+

${escapeHtml(event.data.text)}

+ `; + + MessageBox(tr("appjs_wall_post"), mBoxContent, [tr("appjs_sperm_allow"), tr("appjs_sperm_deny")], [ + async () => { + let id = await API.Wall.newStatus(event.data.text); + event.source.postMessage({ + transaction: event.data.transaction, + ok: true, + post: { + id: id, + text: event.data.text + } + }, '*'); + }, + () => { + event.source.postMessage({ + transaction: event.data.transaction, + ok: false, + error: "User cancelled action" + }, '*'); + } + ]); +} + +async function handleVkApiRequest(event) { + let method = event.data.method; + if(!/^[a-z]+\.[a-z0-9]+$/.test(method)) { + event.source.postMessage({ + transaction: event.data.transaction, + ok: false, + error: "API Method name is invalid" + }, '*'); + return; + } + + let domain = method.split(".")[0]; + if(domain === "newsfeed") + domain = "wall"; + + if(!window.appPerms.includes(domain)) { + if(typeof perms[domain] === "undefined") { + event.source.postMessage({ + transaction: event.data.transaction, + ok: false, + error: "This API method is not supported" + }, '*'); + return; + } + + let dInfo = perms[domain]; + let allowed = false; + await (new Promise(r => { + MessageBox( + tr("appjs_sperm_request"), + `

${tr("app")} ${window.appTitle} ${tr("appjs_sperm_requests")} ${dInfo[0]}. ${tr("appjs_sperm_can")} ${dInfo[1]}.`, + [tr("appjs_sperm_allow"), tr("appjs_sperm_deny")], + [ + () => { + API.Apps.updatePermission(window.appId, domain, "yes").then(() => { + window.appPerms.push(domain); + allowed = true; + r(); + }); + }, + () => { + r(); + } + ] + ) + })); + + if(!allowed) { + event.source.postMessage({ + transaction: event.data.transaction, + ok: false, + error: "No permission to use this method" + }, '*'); + return; + } + } + + let params = toQueryString(event.data.params); + let apiResponse = await (await fetch("/method/" + method + "?auth_mechanism=roaming&" + params)).json(); + if(typeof apiResponse.error_code !== "undefined") { + event.source.postMessage({ + transaction: event.data.transaction, + ok: false, + error: apiResponse.error_code + ": " + apiResponse.error_msg + }, '*'); + return; + } + + event.source.postMessage({ + transaction: event.data.transaction, + ok: true, + response: apiResponse.response + }, '*'); +} + +function handlePayment(event) { + let payload = event.data; + if(payload.outSum < 0) { + event.source.postMessage({ + transaction: payload.transaction, + ok: false, + error: "negative sum" + }, '*'); + } + + MessageBox( + tr("appjs_payment"), + ` +

${tr("appjs_payment_intro")} ${window.appTitle}.
${tr("appjs_order_items")}: ${payload.description}

+

${tr("appjs_payment_total")}: ${payload.outSum} ${tr("points_count")}. + `, + [tr("appjs_payment_confirm"), tr("cancel")], + [ + async () => { + let sign; + try { + sign = await API.Apps.pay(window.appId, payload.outSum); + } catch(e) { + MessageBox(tr("error"), tr("appjs_err_funds"), ["OK"], [Function.noop]); + + event.source.postMessage({ + transaction: payload.transaction, + ok: false, + error: "Payment error[" + e.code + "]: " + e.message + }, '*'); + + return; + } + + event.source.postMessage({ + transaction: payload.transaction, + ok: true, + outSum: payload.outSum, + description: payload.description, + signature: sign + }, '*'); + }, + () => { + event.source.postMessage({ + transaction: payload.transaction, + ok: false, + error: "User cancelled payment" + }, '*'); + } + ] + ) +} + +async function onNewMessage(event) { + if(event.source !== appFrame.contentWindow) + return; + + let payload = event.data; + switch(payload["@type"]) { + case "VkApiRequest": + handleVkApiRequest(event); + break; + + case "WallPostRequest": + handleWallPostRequest(event); + break; + + case "PaymentRequest": + handlePayment(event); + break; + + case "UserInfoRequest": + event.source.postMessage({ + transaction: payload.transaction, + ok: true, + user: await API.Apps.getUserInfo() + }, '*'); + break; + + default: + event.source.postMessage({ + transaction: payload.transaction, + ok: false, + error: "Unknown query type" + }, '*'); + } +} + +window.addEventListener("message", onNewMessage); \ No newline at end of file diff --git a/install/sqls/00030-apps.sql b/install/sqls/00030-apps.sql new file mode 100644 index 00000000..c1e2125d --- /dev/null +++ b/install/sqls/00030-apps.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS `apps` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `owner` bigint unsigned NOT NULL, + `name` varchar(255) NOT NULL, + `description` text NOT NULL, + `avatar_hash` char(8) DEFAULT NULL, + `news` bigint DEFAULT NULL, + `address` varchar(1024) NOT NULL, + `coins` decimal(20,6) NOT NULL DEFAULT '0.000000', + `enabled` bit(1) NOT NULL DEFAULT b'0', + PRIMARY KEY (`id`), + KEY `owner` (`owner`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE TABLE IF NOT EXISTS `app_users` ( + `app` bigint unsigned NOT NULL, + `user` bigint unsigned NOT NULL, + `access` smallint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`app`,`user`), + KEY `app` (`app`), + KEY `user` (`user`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; diff --git a/locales/ru.strings b/locales/ru.strings index af93c4db..c7e4dea3 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -72,6 +72,7 @@ "description" = "Описание"; "save" = "Сохранить"; "main_information" = "Основная информация"; +"additional_information" = "Дополнительная информация"; "nickname" = "Никнейм"; "online" = "Онлайн"; "was_online" = "был в сети"; @@ -753,6 +754,73 @@ "users_gifts" = "Подарки"; +/* Apps */ +"app" = "Приложение"; +"apps" = "Приложения"; +"all_apps" = "Все приложения"; +"installed_apps" = "Мои приложения"; +"own_apps" = "Управление"; +"own_apps_alternate" = "Мои приложения"; + +"app_play" = "запустить"; +"app_uninstall" = "отключить"; +"app_edit" = "редактировать"; +"app_dev" = "Разработчик"; + +"create_app" = "Создать приложение"; +"edit_app" = "Редактировать приложение"; +"new_app" = "Новое приложение"; +"app_news" = "Заметка с новостями"; +"app_state" = "Состояние"; +"app_enabled" = "Включено"; +"app_creation_hint_url" = "Укажите в URL точный адрес вместе со схемой (https), портом (80) и нужными параметрами запроса."; +"app_creation_hint_iframe" = "Ваше приложение будет открыто в iframe."; +"app_balance" = "На счету вашего приложения $1 голосов."; +"app_users" = "Вашим приложением пользуются $1 человек."; +"app_withdrawal_q" = "вывести?"; +"app_withdrawal" = "Вывод средств"; +"app_withdrawal_empty" = "Не удалось вывести пустоту, извините."; +"app_withdrawal_created" = "Заявка на вывод $1 голосов была создана. Ожидайте зачисления."; + +"appjs_payment" = "Оплата покупки"; +"appjs_payment_intro" = "Вы собираетесь оплатить заказ в приложении"; +"appjs_order_items" = "Состав заказа"; +"appjs_payment_total" = "Итоговая сумма к оплате"; +"appjs_payment_confirm" = "Оплатить"; +"appjs_err_funds" = "Не удалось оплатить покупку: недостаточно средств."; + +"appjs_wall_post" = "Опубликовать пост"; +"appjs_wall_post_desc" = "хочет опубликовать на вашей стене пост"; + +"appjs_sperm_friends" = "вашим Друзьям"; +"appjs_sperm_friends_desc" = "добавлять пользователей в друзья и читать ваш список друзей"; +"appjs_sperm_wall" = "вашей Стене"; +"appjs_sperm_wall_desc" = "смотреть ваши новости, вашу стену и создавать на ней посты"; +"appjs_sperm_messages" = "вашим Сообщениям"; +"appjs_sperm_messages_desc" = "читать и писать от вашего имени сообщения"; +"appjs_sperm_groups" = "вашим Сообществам"; +"appjs_sperm_groups_desc" = "смотреть список ваших групп и подписывать вас на другие"; +"appjs_sperm_likes" = "функционалу Лайков"; +"appjs_sperm_likes_desc" = "ставить и убирать отметки \"мне нравится\" с записей"; + +"appjs_sperm_request" = "Запрос доступа"; +"appjs_sperm_requests" = "запрашивает доступ к"; +"appjs_sperm_can" = "Приложение сможет"; +"appjs_sperm_allow" = "Разрешить"; +"appjs_sperm_disallow" = "Не разрешать"; + +"app_uninstalled" = "Приложение отключено"; +"app_uninstalled_desc" = "Оно больше не сможет выполнять действия от вашего имени."; +"app_err_not_found" = "Приложение не найдено"; +"app_err_not_found_desc" = "Некорректный идентифиактор или оно было отключено."; +"app_err_forbidden_desc" = "Это приложение не ваше."; +"app_err_url" = "Неправильный адрес"; +"app_err_url_desc" = "Адрес приложения не прошёл проверку, убедитесь что он указан правильно."; +"app_err_ava" = "Не удалось загрузить аватарку"; +"app_err_ava_desc" = "Аватарка слишком большая или кривая: ошибка общего характера №$res."; +"app_err_note" = "Не удалось прикрепить новостную заметку"; +"app_err_note_desc" = "Убедитесь что ссылка правильная и заметка принадлежит вам."; + /* Support */ "support_opened" = "Открытые"; diff --git a/openvk-example.yml b/openvk-example.yml index cceb2be3..32f03edd 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -19,6 +19,8 @@ openvk: - "index.php" photos: upgradeStructure: true + apps: + withdrawTax: 8 security: requireEmail: false requirePhone: false From 59cb20628ff5a961910c756f7c6b731e924d69ad Mon Sep 17 00:00:00 2001 From: celestora Date: Sat, 20 Aug 2022 21:32:17 +0300 Subject: [PATCH 2/8] Add apps to menu --- Web/Presenters/templates/@layout.xml | 1 + locales/ru.strings | 1 + 2 files changed, 2 insertions(+) diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index ebd5fb8a..e717acfe 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -173,6 +173,7 @@ ({$thisUser->getNotificationsCount()}) {/if} + {_my_apps} {_my_settings} {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} diff --git a/locales/ru.strings b/locales/ru.strings index c7e4dea3..ff3a6f8c 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -757,6 +757,7 @@ /* Apps */ "app" = "Приложение"; "apps" = "Приложения"; +"my_apps" = "Мои Приложения"; "all_apps" = "Все приложения"; "installed_apps" = "Мои приложения"; "own_apps" = "Управление"; From b328254285dbe426b5af649c010c4ecc7eae72dd Mon Sep 17 00:00:00 2001 From: celestora Date: Sat, 20 Aug 2022 21:35:30 +0300 Subject: [PATCH 3/8] Use adler32 hash for app avatars (php74 compat) --- Web/Models/Entities/Application.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Web/Models/Entities/Application.php b/Web/Models/Entities/Application.php index e79c8db5..74569485 100644 --- a/Web/Models/Entities/Application.php +++ b/Web/Models/Entities/Application.php @@ -74,14 +74,14 @@ class Application extends RowModel case "basic": return "$serverUrl/blob_" . substr($hash, 0, 2) . "/$hash" . "_app_avatar.png"; case "accelerated": - return "$serverUrl/openvk-datastore/$hash.app_avatar.png"; + return "$serverUrl/openvk-datastore/$hash" . "_app_avatar.png"; case "server": $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; return ( $settings->protocol ?? ovk_scheme() . "://" . $settings->host . $settings->path . - substr($hash, 0, 2) . "/$hash.app_avatar.png" + substr($hash, 0, 2) . "/$hash" . "_app_avatar.png" ); } } @@ -205,7 +205,7 @@ class Application extends RowModel return -2; } - $hash = hash_file("murmur3a", $file["tmp_name"]); + $hash = hash_file("adler32", $file["tmp_name"]); if(!is_dir($this->getAvatarsDir() . substr($hash, 0, 2))) if(!mkdir($this->getAvatarsDir() . substr($hash, 0, 2))) return -3; From d35b48c9b37fbd6ae4e93e4a64d7b55924ee129d Mon Sep 17 00:00:00 2001 From: Jaroslaw <92401420+AlesAlte@users.noreply.github.com> Date: Sat, 20 Aug 2022 23:59:53 +0300 Subject: [PATCH 4/8] =?UTF-8?q?=D0=A3=D0=BA=D1=80=D0=B0=D1=97=D0=BD=D1=81?= =?UTF-8?q?=D1=8C=D0=BA=D0=B0=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C:=20?= =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BA=D0=BB=D0=B0=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B2=D0=B2=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8F=20-=20"=D0=94=D0=BE=D0=B4=D0=B0=D1=82=D0=BA?= =?UTF-8?q?=D0=B8"=20(#677)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ua.strings | 78 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/locales/ua.strings b/locales/ua.strings index d4f31e8a..c0dcf8a2 100644 --- a/locales/ua.strings +++ b/locales/ua.strings @@ -74,6 +74,7 @@ "description" = "Опис"; "save" = "Зберегти"; "main_information" = "Основна інформація"; +"additional_information" = "Додаткова інформація"; "nickname" = "Нікнейм"; "online" = "Онлайн"; "was_online" = "був у мережі"; @@ -384,6 +385,11 @@ "edit_note" = "Редагувати нотатку"; "actions" = "Дії"; "notes_start_screen" = "За допомогою нотаток Ви можете ділитися подіями з життя з друзями, а також бути в курсі того, що відбувається у них."; +"note_preview" = "Попередній перегляд"; +"note_preview_warn" = "Увага: Це всього лише попередній перегляд"; +"note_preview_warn_details" = "Після збереження, нотатки можуть мати інший вигляд. Також не викликайте перегляд занадто часто."; +"note_preview_empty_err" = "Попередній перегляд неможливий: Немає імені чи змісту."; + "edited" = "Відредаговано"; "notes_zero" = "Жодної нотатки"; @@ -557,9 +563,9 @@ "two_factor_authentication_login" = "У вас увімкнена двофакторна автентифікація. Для входу введіть код отриманий в додатку."; "two_factor_authentication_settings_1" = "Двофакторну автентифікацію через TOTP можна використовувати навіть без інтернету. Для цього вам знадобиться застосунок для генерації кодів, наприклад Google Authenticator для Android та iOS або відкриті Aegis Authenticator чи andOTP для Android. Важливо: на пристрої має бути точна дата та час."; -"two_factor_authentication_settings_2" = "Використовуючи додаток для двофакторної автентифікації, відскануйте наведений нижче QR-код:"; +"two_factor_authentication_settings_2" = "Використовуючи застосунок для двофакторної автентифікації, відскануйте наведений нижче QR-код:"; "two_factor_authentication_settings_3" = "або вручну введіть секретний ключ: $1."; -"two_factor_authentication_settings_4" = "Тепер введіть код, який вам надав додаток, і пароль від вашої сторінки, щоб ми могли підтвердити, що ви дійсно ви."; +"two_factor_authentication_settings_4" = "Тепер введіть код, який вам надав застосунок, і пароль від вашої сторінки, щоб ми могли підтвердити, що ви дійсно ви."; "connect" = "Підключити"; "enable" = "Включити"; @@ -749,6 +755,74 @@ "users_gifts" = "Подарунки"; +/* Apps */ +"app" = "Застосунок"; +"apps" = "Застосунки"; +"my_apps" = "Мої Застосунки"; +"all_apps" = "Всі застосунки"; +"installed_apps" = "Мої застосунки"; +"own_apps" = "Керування"; +"own_apps_alternate" = "Мої застосунки"; + +"app_play" = "запустити"; +"app_uninstall" = "вимкнути"; +"app_edit" = "редагувати"; +"app_dev" = "Розробник"; + +"create_app" = "Створити застосунок"; +"edit_app" = "Редагувати застосунок"; +"new_app" = "Новий застосунок"; +"app_news" = "Нотатка з новинами"; +"app_state" = "Стан"; +"app_enabled" = "Увімкнено"; +"app_creation_hint_url" = "Вкажіть в URL точну адресу разом зі схемою (https), портом (80) та потрібними параметрами запиту."; +"app_creation_hint_iframe" = "Ваш застосунок буде відкритий в iframe."; +"app_balance" = "На рахунку вашого застосунку $1 голосів."; +"app_users" = "Вашим застосунком користуються $1 користувачів."; +"app_withdrawal" = "Виведення коштів"; +"app_withdrawal_q" = "вивести?"; +"app_withdrawal_empty" = "Не вдалося вивести: Значення не може бути пустим"; +"app_withdrawal_created" = "Заявку на виведення $1 голосів було створено. Чекайте на зарахування."; + +"appjs_payment" = "Оплата покупки"; +"appjs_payment_intro" = "Ви збираєтеся сплатити замовлення в застосунку"; +"appjs_order_items" = "Деталі замовлення"; +"appjs_payment_total" = "Підсумкова сума до оплати"; +"appjs_payment_confirm" = "Сплатити"; +"appjs_err_funds" = "Не вдалося сплатити покупку: недостатньо коштів."; + +"appjs_wall_post" = "Опублікувати пост"; +"appjs_wall_post_desc" = "хоче опублікувати на вашій стіні пост"; + +"appjs_sperm_friends" = "вашим Друзям"; +"appjs_sperm_friends_desc" = "додавати користувачів у друзі та читати ваш список друзів"; +"appjs_sperm_wall" = "вашій Стіні"; +"appjs_sperm_wall_desc" = "дивитися ваші новини, вашу стіну та створювати на ній пости"; +"appjs_sperm_messages" = "вашим повідомленням"; +"appjs_sperm_messages_desc" = "читати та писати від вашого імені повідомлення"; +"appjs_sperm_groups" = "вашим Спільнотам"; +"appjs_sperm_groups_desc" = "дивитися список ваших груп та підписувати вас на інші"; +"appjs_sperm_likes" = "функціоналу Лайків"; +"appjs_sperm_likes_desc" = "ставити та прибирати позначки \"мені подобається\" із записів"; + +"appjs_sperm_request" = "Запит доступу"; +"appjs_sperm_requests" = "запитує доступ до"; +"appjs_sperm_can" = "Застосунок зможе"; +"appjs_sperm_allow" = "Дозволити"; +"appjs_sperm_disallow" = "Відхилити"; + +"app_uninstalled" = "Застосунок вимкнено"; +"app_uninstalled_desc" = "Він не зможе виконувати дії від вашого імені."; +"app_err_not_found" = "Застосунок не знайдено"; +"app_err_not_found_desc" = "Неправильний ідентифікатор або застосунок вимкнено."; +"app_err_forbidden_desc" = "Цей застосунок не ваш."; +"app_err_url" = "Некоректна адреса"; +"app_err_url_desc" = "Адреса застосунку не пройшла перевірку, переконайтеся, що вона вказана правильно."; +"app_err_ava" = "Не вдалося завантажити аватарку"; +"app_err_ava_desc" = "Аватарка занадто велика чи крива: помилка загального характеру №$res."; +"app_err_note" = "Не вдалося прикріпити замітку новин"; +"app_err_note_desc" = "Переконайтеся, що посилання правильне і нотатка належить вам."; + /* Support */ "support_opened" = "Відкриті"; From 33dbc984f28c0b435777bae242a291efcb0837dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= <68551925+Artem13327@users.noreply.github.com> Date: Sun, 21 Aug 2022 00:01:36 +0300 Subject: [PATCH 5/8] Update robots.txt (#661) --- Web/Presenters/AboutPresenter.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Web/Presenters/AboutPresenter.php b/Web/Presenters/AboutPresenter.php index 8e493bc5..6a281d07 100644 --- a/Web/Presenters/AboutPresenter.php +++ b/Web/Presenters/AboutPresenter.php @@ -102,6 +102,15 @@ final class AboutPresenter extends OpenVKPresenter . "# covered from unauthorized persons (for example, due to\n" . "# lack of rights to access the admin panel)\n\n" . "User-Agent: *\n" + . "Disallow: /albums/create\n" + . "Disallow: /videos/upload\n" + . "Disallow: /invite\n" + . "Disallow: /groups_create\n" + . "Disallow: /notifications\n" + . "Disallow: /settings\n" + . "Disallow: /edit\n" + . "Disallow: /gifts\n" + . "Disallow: /support\n" . "Disallow: /rpc\n" . "Disallow: /language\n" . "Disallow: /badbrowser.php\n" From 9ae71641050f35d3695c842096cb1023452ab3ce Mon Sep 17 00:00:00 2001 From: celestora Date: Sun, 21 Aug 2022 00:30:56 +0300 Subject: [PATCH 6/8] VKAPI(Wall.get): fix count param --- VKAPI/Handlers/Wall.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index cb4720dc..c669cd02 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -19,7 +19,7 @@ final class Wall extends VKAPIRequestHandler $items = []; $profiles = []; $groups = []; - $count = $posts->getPostCountOnUserWall($owner_id); + $cnt = $posts->getPostCountOnUserWall($owner_id); $wallOnwer = (new UsersRepo)->get($owner_id); @@ -143,15 +143,15 @@ final class Wall extends VKAPIRequestHandler } return (object) [ - "count" => $count, - "items" => (array)$items, - "profiles" => (array)$profilesFormatted, - "groups" => (array)$groupsFormatted + "count" => $cnt, + "items" => $items, + "profiles" => $profilesFormatted, + "groups" => $groupsFormatted ]; } else return (object) [ - "count" => $count, - "items" => (array)$items + "count" => $cnt, + "items" => $items ]; } From dc6d0e7374d936bec11251407599aa1bc73a6129 Mon Sep 17 00:00:00 2001 From: Ilya Prokopenko Date: Sun, 21 Aug 2022 20:29:41 +0700 Subject: [PATCH 7/8] New strings for en_US + some fixes for ru_RU --- locales/en.strings | 69 ++++++++++++++++++++++++++++++++++++++++++++++ locales/ru.strings | 20 +++++++------- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/locales/en.strings b/locales/en.strings index daa4e27b..a0825392 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -72,6 +72,7 @@ "description" = "Description"; "save" = "Save"; "main_information" = "Main information"; +"additional_information" = "Additional information"; "nickname" = "Nickname"; "online" = "Online"; "was_online" = "was online"; @@ -708,6 +709,74 @@ "users_gifts" = "Gifts"; +/* Apps */ +"app" = "Application"; +"apps" = "Applications"; +"my_apps" = "My Apps"; +"all_apps" = "All apps"; +"installed_apps" = "Installed apps"; +"own_apps" = "Own apps"; +"own_apps_alternate" = "My apps"; + +"app_play" = "start"; +"app_uninstall" = "uninstall"; +"app_edit" = "edit"; +"app_dev" = "Developer"; + +"create_app" = "Create an application"; +"edit_app" = "Edit an application"; +"new_app" = "New application"; +"app_news" = "A news note"; +"app_state" = "Status"; +"app_enabled" = "Enabled"; +"app_creation_hint_url" = "Specify in the URL the exact address together with the scheme (https), the port (80) and the required request parameters."; +"app_creation_hint_iframe" = "Your application will be opened in an iframe."; +"app_balance" = "Your application has $1 votes to its credit."; +"app_users" = "Your application is used by $1 people."; +"app_withdrawal_q" = "withdraw?"; +"app_withdrawal" = "Withdrawal"; +"app_withdrawal_empty" = "Couldn't withdraw emptiness, sorry."; +"app_withdrawal_created" = "A request to withdraw $1 votes has been created. Awaiting crediting."; + +"appjs_payment" = "Purchase payment"; +"appjs_payment_intro" = "You are about to pay for an order in the application"; +"appjs_order_items" = "Order items"; +"appjs_payment_total" = "Total amount payable"; +"appjs_payment_confirm" = "Pay"; +"appjs_err_funds" = "Failed to pay: insufficient funds."; + +"appjs_wall_post" = "Publish a post"; +"appjs_wall_post_desc" = "wants to publish a post on your wall"; + +"appjs_sperm_friends" = "your Friends"; +"appjs_sperm_friends_desc" = "add users as friends and read your friends list"; +"appjs_sperm_wall" = "your Wall"; +"appjs_sperm_wall_desc" = "see your news, your wall and create posts on it"; +"appjs_sperm_messages" = "your Messages"; +"appjs_sperm_messages_desc" = "read and write messages on your behalf"; +"appjs_sperm_groups" = "your Groups"; +"appjs_sperm_groups_desc" = "see a list of your groups and subscribe you to other"; +"appjs_sperm_likes" = "Likes feature"; +"appjs_sperm_likes_desc" = "give and take away likes to posts"; + +"appjs_sperm_request" = "Access request"; +"appjs_sperm_requests" = "requests access to"; +"appjs_sperm_can" = "The app will be able to"; +"appjs_sperm_allow" = "Allow"; +"appjs_sperm_disallow" = "Disallow"; + +"app_uninstalled" = "Application is disabled"; +"app_uninstalled_desc" = "It will no longer be able to perform actions on your behalf."; +"app_err_not_found" = "Application not found"; +"app_err_not_found_desc" = "Incorrect identifier or it has been disabled."; +"app_err_forbidden_desc" = "This application is not yours."; +"app_err_url" = "Incorrect address"; +"app_err_url_desc" = "The address of the application did not pass the check, make sure it is correct."; +"app_err_ava" = "Unable to upload an avatar"; +"app_err_ava_desc" = "Avatar too big or wrong: general error #$res."; +"app_err_note" = "Failed to attach a news note"; +"app_err_note_desc" = "Make sure the link is correct and the note belongs to you."; + /* Support */ "support_opened" = "Opened"; diff --git a/locales/ru.strings b/locales/ru.strings index ff3a6f8c..0e7b7806 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -790,19 +790,19 @@ "appjs_payment_confirm" = "Оплатить"; "appjs_err_funds" = "Не удалось оплатить покупку: недостаточно средств."; -"appjs_wall_post" = "Опубликовать пост"; -"appjs_wall_post_desc" = "хочет опубликовать на вашей стене пост"; +"appjs_wall_post" = "Опубликовать запись"; +"appjs_wall_post_desc" = "хочет опубликовать на Вашей стене запись"; "appjs_sperm_friends" = "вашим Друзьям"; -"appjs_sperm_friends_desc" = "добавлять пользователей в друзья и читать ваш список друзей"; +"appjs_sperm_friends_desc" = "добавлять пользователей в друзья и читать Ваш список друзей"; "appjs_sperm_wall" = "вашей Стене"; -"appjs_sperm_wall_desc" = "смотреть ваши новости, вашу стену и создавать на ней посты"; +"appjs_sperm_wall_desc" = "смотреть Ваши новости, Вашу стену и создавать на ней записи"; "appjs_sperm_messages" = "вашим Сообщениям"; -"appjs_sperm_messages_desc" = "читать и писать от вашего имени сообщения"; +"appjs_sperm_messages_desc" = "читать и писать от Вашего имени сообщения"; "appjs_sperm_groups" = "вашим Сообществам"; -"appjs_sperm_groups_desc" = "смотреть список ваших групп и подписывать вас на другие"; +"appjs_sperm_groups_desc" = "смотреть список Ваших групп и подписывать вас на другие"; "appjs_sperm_likes" = "функционалу Лайков"; -"appjs_sperm_likes_desc" = "ставить и убирать отметки \"мне нравится\" с записей"; +"appjs_sperm_likes_desc" = "ставить и убирать отметки \"Мне нравится\" с записей"; "appjs_sperm_request" = "Запрос доступа"; "appjs_sperm_requests" = "запрашивает доступ к"; @@ -811,16 +811,16 @@ "appjs_sperm_disallow" = "Не разрешать"; "app_uninstalled" = "Приложение отключено"; -"app_uninstalled_desc" = "Оно больше не сможет выполнять действия от вашего имени."; +"app_uninstalled_desc" = "Оно больше не сможет выполнять действия от Вашего имени."; "app_err_not_found" = "Приложение не найдено"; "app_err_not_found_desc" = "Некорректный идентифиактор или оно было отключено."; -"app_err_forbidden_desc" = "Это приложение не ваше."; +"app_err_forbidden_desc" = "Это приложение не Ваше."; "app_err_url" = "Неправильный адрес"; "app_err_url_desc" = "Адрес приложения не прошёл проверку, убедитесь что он указан правильно."; "app_err_ava" = "Не удалось загрузить аватарку"; "app_err_ava_desc" = "Аватарка слишком большая или кривая: ошибка общего характера №$res."; "app_err_note" = "Не удалось прикрепить новостную заметку"; -"app_err_note_desc" = "Убедитесь что ссылка правильная и заметка принадлежит вам."; +"app_err_note_desc" = "Убедитесь, что ссылка правильная и заметка принадлежит Вам."; /* Support */ From e87cb2deacd79725c2eaa727e429f2af3df18215 Mon Sep 17 00:00:00 2001 From: ayaao <58212796+ayaaop@users.noreply.github.com> Date: Sun, 21 Aug 2022 22:33:13 +0600 Subject: [PATCH 8/8] OpenVK Modern: fix fastmenu (#651) --- themepacks/openvk_modern/stylesheet.css | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/themepacks/openvk_modern/stylesheet.css b/themepacks/openvk_modern/stylesheet.css index d19da3b5..590710e4 100644 --- a/themepacks/openvk_modern/stylesheet.css +++ b/themepacks/openvk_modern/stylesheet.css @@ -230,3 +230,21 @@ input[type=checkbox] { #votesBalance { border-bottom: none; } + +.floating_sidebar { + position: fixed; + top: 50px; + right: 0; + align-items: start; + width: 21%; +} +.minilink .counter { + background-color: #2B587A; + margin: 0; + padding: 0.5px 2px; + left: 10px; + font-size: 7px; + color: #fff; + position: absolute; + margin-top: -6px; +}