mirror of
https://github.com/openvk/openvk
synced 2024-12-22 16:42:32 +03:00
Приложения (#674)
This commit is contained in:
parent
d1b878a5a4
commit
d767d8e2eb
17 changed files with 1258 additions and 0 deletions
87
ServiceAPI/Apps.php
Normal file
87
ServiceAPI/Apps.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\ServiceAPI;
|
||||
|
||||
use openvk\Web\Models\Entities\User;
|
||||
use openvk\Web\Models\Repositories\Applications;
|
||||
|
||||
class Apps implements Handler
|
||||
{
|
||||
private $user;
|
||||
private $apps;
|
||||
|
||||
public function __construct(?User $user)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\ServiceAPI;
|
||||
use openvk\Web\Models\Entities\Post;
|
||||
use openvk\Web\Models\Entities\User;
|
||||
use openvk\Web\Models\Repositories\Posts;
|
||||
|
||||
|
@ -55,4 +56,19 @@ class Wall implements Handler
|
|||
|
||||
$resolve((array) $res);
|
||||
}
|
||||
|
||||
function newStatus(string $text, callable $resolve, callable $reject): void
|
||||
{
|
||||
$post = new Post;
|
||||
$post->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());
|
||||
}
|
||||
}
|
||||
|
|
42
VKAPI/Handlers/Pay.php
Normal file
42
VKAPI/Handlers/Pay.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\VKAPI\Handlers;
|
||||
use openvk\Web\Models\Repositories\Applications;
|
||||
|
||||
final class Pay extends VKAPIRequestHandler
|
||||
{
|
||||
function getIdByMarketingId(string $marketing_id): int
|
||||
{
|
||||
[$hexId, $signature] = explode("_", $marketing_id);
|
||||
try {
|
||||
$key = CHANDLER_ROOT_CONF["security"]["secret"];
|
||||
if(sodium_memcmp(base64_decode($signature), hash_hmac("sha512/224", $hexId, $key, true)) == -1)
|
||||
$this->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;
|
||||
}
|
||||
}
|
316
Web/Models/Entities/Application.php
Normal file
316
Web/Models/Entities/Application.php
Normal file
|
@ -0,0 +1,316 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Models\Entities;
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
use Nette\Utils\Image;
|
||||
use Nette\Utils\UnknownImageFileException;
|
||||
use openvk\Web\Models\Repositories\Notes;
|
||||
use openvk\Web\Models\Repositories\Users;
|
||||
use openvk\Web\Models\RowModel;
|
||||
|
||||
class Application extends RowModel
|
||||
{
|
||||
protected $tableName = "apps";
|
||||
|
||||
const PERMS = [
|
||||
"notify",
|
||||
"friends",
|
||||
"photos",
|
||||
"audio",
|
||||
"video",
|
||||
"stories",
|
||||
"pages",
|
||||
"status",
|
||||
"notes",
|
||||
"messages",
|
||||
"wall",
|
||||
"ads",
|
||||
"docs",
|
||||
"groups",
|
||||
"notifications",
|
||||
"stats",
|
||||
"email",
|
||||
"market",
|
||||
];
|
||||
|
||||
private function getAvatarsDir(): string
|
||||
{
|
||||
$uploadSettings = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"];
|
||||
if($uploadSettings["mode"] === "server" && $uploadSettings["server"]["kind"] === "cdn")
|
||||
return $uploadSettings["server"]["directory"];
|
||||
else
|
||||
return OPENVK_ROOT . "/storage/";
|
||||
}
|
||||
|
||||
function getId(): int
|
||||
{
|
||||
return $this->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);
|
||||
}
|
||||
}
|
69
Web/Models/Repositories/Applications.php
Normal file
69
Web/Models/Repositories/Applications.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Models\Repositories;
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
use Nette\Database\Table\ActiveRow;
|
||||
use openvk\Web\Models\Entities\Application;
|
||||
use openvk\Web\Models\Entities\User;
|
||||
|
||||
class Applications
|
||||
{
|
||||
private $context;
|
||||
private $apps;
|
||||
private $appRels;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->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()));
|
||||
}
|
||||
}
|
138
Web/Presenters/AppsPresenter.php
Normal file
138
Web/Presenters/AppsPresenter.php
Normal file
|
@ -0,0 +1,138 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Presenters;
|
||||
use openvk\Web\Models\Entities\Application;
|
||||
use openvk\Web\Models\Repositories\Applications;
|
||||
|
||||
final class AppsPresenter extends OpenVKPresenter
|
||||
{
|
||||
private $apps;
|
||||
|
||||
function __construct(Applications $apps)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
112
Web/Presenters/templates/Apps/Edit.xml
Normal file
112
Web/Presenters/templates/Apps/Edit.xml
Normal file
|
@ -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}
|
||||
<a href="/apps?act=dev">{_own_apps_alternate}</a> »
|
||||
<a href="/app{$id}">{$name}</a> »
|
||||
{_edit}
|
||||
{/if}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
<div class="container_gray">
|
||||
<h4>{_main_information}</h4>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">{_name}: </span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="name" value="{$name ?? ''}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">{_description}: </span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="desc" value="{$desc ?? ''}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">{_avatar}: </span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="file" name="ava" accept="image/*" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">{_app_news}: </span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="note" placeholder="{ovk_scheme(true) . $_SERVER['HTTP_HOST']}/note{$thisUser->getId()}_10" value="{$note ?? ''}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">URL: </span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="url" value="{$url ?? ''}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top">
|
||||
<span class="nobold">{_app_state}: </span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="enable" n:attr="checked => ($on ?? false)" /> {_app_enabled}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||
<input type="submit" value="{_save}" class="button" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<br/>
|
||||
|
||||
<h4>{_additional_information}</h4>
|
||||
<div>
|
||||
<ul style="color: unset;">
|
||||
{if $create}
|
||||
<li>{_app_creation_hint_url}</li>
|
||||
<li>{_app_creation_hint_iframe}</li>
|
||||
{else}
|
||||
<li>{tr("app_balance", $coins)|noescape} (<a href="javascript:withdraw({$id})">{_app_withdrawal_q}</a>)</li>
|
||||
<li>{tr("app_users", $users)|noescape}</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function withdraw(id) {
|
||||
let coins = await API.Apps.withdrawFunds(id);
|
||||
if(coins == 0)
|
||||
MessageBox({_app_withdrawal}, {_app_withdrawal_empty}, ["OK"], [Function.noop]);
|
||||
else
|
||||
MessageBox({_app_withdrawal}, {tr("app_withdrawal_created", $coins)}, ["OK"], [Function.noop]);
|
||||
}
|
||||
</script>
|
||||
{/block}
|
53
Web/Presenters/templates/Apps/List.xml
Normal file
53
Web/Presenters/templates/Apps/List.xml
Normal file
|
@ -0,0 +1,53 @@
|
|||
{extends "../@listView.xml"}
|
||||
|
||||
{block title}
|
||||
{_apps}
|
||||
{/block}
|
||||
|
||||
{block header}
|
||||
{_apps}
|
||||
|
||||
<div n:if="$act === 'dev'" style="float: right;">
|
||||
<span><b>
|
||||
<a href="/editapp?act=create">{_create}</a>
|
||||
</b></span>
|
||||
</div>
|
||||
{/block}
|
||||
|
||||
{block tabs}
|
||||
<div n:attr="id => ($act === 'list' ? 'activetabs' : 'ki')" class="tab">
|
||||
<a n:attr="id => ($act === 'list' ? 'act_tab_a' : 'ki')" href="?act=list">{_all_apps}</a>
|
||||
</div>
|
||||
|
||||
<div n:attr="id => ($act === 'installed' ? 'activetabs' : 'ki')" class="tab">
|
||||
<a n:attr="id => ($act === 'installed' ? 'act_tab_a' : 'ki')" href="?act=installed">{_installed_apps}</a>
|
||||
</div>
|
||||
|
||||
<div n:attr="id => ($act === 'dev' ? 'activetabs' : 'ki')" class="tab">
|
||||
<a n:attr="id => ($act === 'dev' ? 'act_tab_a' : 'ki')" href="?act=dev">{_own_apps}</a>
|
||||
</div>
|
||||
{/block}
|
||||
|
||||
{* BEGIN ELEMENTS DESCRIPTION *}
|
||||
|
||||
{block link|strip|stripHtml}
|
||||
/app{$x->getId()}
|
||||
{/block}
|
||||
|
||||
{block preview}
|
||||
<img style="max-width: 75px;" src="{$x->getAvatarUrl()}" />
|
||||
{/block}
|
||||
|
||||
{block name}
|
||||
{$x->getName()}
|
||||
{/block}
|
||||
|
||||
{block description}
|
||||
{$x->getDescription()}
|
||||
{/block}
|
||||
|
||||
{block actions}
|
||||
<a href="/app{$x->getId()}" class="profile_link">{_app_play}</a>
|
||||
<a n:if="$x->isInstalledBy($thisUser)" href="/apps/uninstall?hash={rawurlencode($csrfToken)}&app={$x->getId()}" class="profile_link">{_app_uninstall}</a>
|
||||
<a n:if="$thisUser->getId() == $x->getOwner()->getId()" href="/editapp?app={$x->getId()}" class="profile_link">{_app_edit}</a>
|
||||
{/block}
|
37
Web/Presenters/templates/Apps/Play.xml
Normal file
37
Web/Presenters/templates/Apps/Play.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
{extends "../@layout.xml"}
|
||||
|
||||
{block title}
|
||||
{$name}
|
||||
{/block}
|
||||
|
||||
{block header}
|
||||
{$name}
|
||||
{/block}
|
||||
|
||||
{block content}
|
||||
<center>
|
||||
<iframe id="appFrame" referrerpolicy="unsafe-url" sandbox="allow-scripts" frameBorder="0" src="{$url}" height="600" width="600"></iframe>
|
||||
</center>
|
||||
|
||||
<div n:if="!is_null($news)" id="news">
|
||||
<h4>{$news->getName()}</h4>
|
||||
<div id="app_news_container">
|
||||
{$news->getText()|noescape}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<center>
|
||||
<p>
|
||||
{_app_dev}: <a href="{$owner->getURL()}">{$owner->getFullName()}</a>
|
||||
</p>
|
||||
</center>
|
||||
|
||||
<script>
|
||||
window.appId = {$id};
|
||||
window.appTitle = {$name};
|
||||
window.appPerms = {$perms};
|
||||
window.appOrigin = {$origin};
|
||||
</script>
|
||||
|
||||
{script "js/al_games.js"}
|
||||
{/block}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
54
Web/static/js/GameAPI.js
Normal file
54
Web/static/js/GameAPI.js
Normal file
|
@ -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
|
||||
});
|
||||
}
|
225
Web/static/js/al_games.js
Normal file
225
Web/static/js/al_games.js
Normal file
|
@ -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, """)
|
||||
.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 = `
|
||||
<b>${tr("app")} <i>${window.appTitle}</i> ${tr("appjs_wall_post_desc")}:</b><br/>
|
||||
<p style="padding: 8px; border: 1px solid gray;">${escapeHtml(event.data.text)}</p>
|
||||
`;
|
||||
|
||||
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"),
|
||||
`<p>${tr("app")} <b>${window.appTitle}</b> ${tr("appjs_sperm_requests")} <b>${dInfo[0]}</b>. ${tr("appjs_sperm_can")} <b>${dInfo[1]}</b>.`,
|
||||
[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"),
|
||||
`
|
||||
<p>${tr("appjs_payment_intro")} <b>${window.appTitle}</b>.<br/>${tr("appjs_order_items")}: <b>${payload.description}</b></p>
|
||||
<p>${tr("appjs_payment_total")}: <big><b>${payload.outSum}</b></big> ${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);
|
22
install/sqls/00030-apps.sql
Normal file
22
install/sqls/00030-apps.sql
Normal file
|
@ -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;
|
|
@ -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" = "На счету вашего приложения <b>$1</b> голосов.";
|
||||
"app_users" = "Вашим приложением пользуются <b>$1</b> человек.";
|
||||
"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" = "Открытые";
|
||||
|
|
|
@ -19,6 +19,8 @@ openvk:
|
|||
- "index.php"
|
||||
photos:
|
||||
upgradeStructure: true
|
||||
apps:
|
||||
withdrawTax: 8
|
||||
security:
|
||||
requireEmail: false
|
||||
requirePhone: false
|
||||
|
|
Loading…
Reference in a new issue