mirror of
https://github.com/openvk/openvk
synced 2024-12-25 10:01:05 +03:00
Merge branch 'openvk:master' into bug-tracker
This commit is contained in:
commit
a40caa6fa2
23 changed files with 1439 additions and 9 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);
|
<?php declare(strict_types=1);
|
||||||
namespace openvk\ServiceAPI;
|
namespace openvk\ServiceAPI;
|
||||||
|
use openvk\Web\Models\Entities\Post;
|
||||||
use openvk\Web\Models\Entities\User;
|
use openvk\Web\Models\Entities\User;
|
||||||
use openvk\Web\Models\Repositories\Posts;
|
use openvk\Web\Models\Repositories\Posts;
|
||||||
|
|
||||||
|
@ -55,4 +56,19 @@ class Wall implements Handler
|
||||||
|
|
||||||
$resolve((array) $res);
|
$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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ final class Wall extends VKAPIRequestHandler
|
||||||
$items = [];
|
$items = [];
|
||||||
$profiles = [];
|
$profiles = [];
|
||||||
$groups = [];
|
$groups = [];
|
||||||
$count = $posts->getPostCountOnUserWall($owner_id);
|
$cnt = $posts->getPostCountOnUserWall($owner_id);
|
||||||
|
|
||||||
$wallOnwer = (new UsersRepo)->get($owner_id);
|
$wallOnwer = (new UsersRepo)->get($owner_id);
|
||||||
|
|
||||||
|
@ -143,15 +143,15 @@ final class Wall extends VKAPIRequestHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
return (object) [
|
return (object) [
|
||||||
"count" => $count,
|
"count" => $cnt,
|
||||||
"items" => (array)$items,
|
"items" => $items,
|
||||||
"profiles" => (array)$profilesFormatted,
|
"profiles" => $profilesFormatted,
|
||||||
"groups" => (array)$groupsFormatted
|
"groups" => $groupsFormatted
|
||||||
];
|
];
|
||||||
} else
|
} else
|
||||||
return (object) [
|
return (object) [
|
||||||
"count" => $count,
|
"count" => $cnt,
|
||||||
"items" => (array)$items
|
"items" => $items
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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("adler32", $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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -102,6 +102,15 @@ final class AboutPresenter extends OpenVKPresenter
|
||||||
. "# covered from unauthorized persons (for example, due to\n"
|
. "# covered from unauthorized persons (for example, due to\n"
|
||||||
. "# lack of rights to access the admin panel)\n\n"
|
. "# lack of rights to access the admin panel)\n\n"
|
||||||
. "User-Agent: *\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: /rpc\n"
|
||||||
. "Disallow: /language\n"
|
. "Disallow: /language\n"
|
||||||
. "Disallow: /badbrowser.php\n"
|
. "Disallow: /badbrowser.php\n"
|
||||||
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -173,6 +173,7 @@
|
||||||
(<b>{$thisUser->getNotificationsCount()}</b>)
|
(<b>{$thisUser->getNotificationsCount()}</b>)
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/apps?act=installed" class="link">{_my_apps}</a>
|
||||||
<a href="/settings" class="link">{_my_settings}</a>
|
<a href="/settings" class="link">{_my_settings}</a>
|
||||||
|
|
||||||
{var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
|
{var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
|
||||||
|
|
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}
|
|
@ -21,6 +21,7 @@ services:
|
||||||
- openvk\Web\Presenters\GiftsPresenter
|
- openvk\Web\Presenters\GiftsPresenter
|
||||||
- openvk\Web\Presenters\MessengerPresenter
|
- openvk\Web\Presenters\MessengerPresenter
|
||||||
- openvk\Web\Presenters\TopicsPresenter
|
- openvk\Web\Presenters\TopicsPresenter
|
||||||
|
- openvk\Web\Presenters\AppsPresenter
|
||||||
- openvk\Web\Presenters\ThemepacksPresenter
|
- openvk\Web\Presenters\ThemepacksPresenter
|
||||||
- openvk\Web\Presenters\VKAPIPresenter
|
- openvk\Web\Presenters\VKAPIPresenter
|
||||||
- openvk\Web\Models\Repositories\Users
|
- openvk\Web\Models\Repositories\Users
|
||||||
|
@ -44,4 +45,5 @@ services:
|
||||||
- openvk\Web\Models\Repositories\Vouchers
|
- openvk\Web\Models\Repositories\Vouchers
|
||||||
- openvk\Web\Models\Repositories\Gifts
|
- openvk\Web\Models\Repositories\Gifts
|
||||||
- openvk\Web\Models\Repositories\Topics
|
- openvk\Web\Models\Repositories\Topics
|
||||||
|
- openvk\Web\Models\Repositories\Applications
|
||||||
- openvk\Web\Models\Repositories\ContentSearchRepository
|
- openvk\Web\Models\Repositories\ContentSearchRepository
|
||||||
|
|
|
@ -283,6 +283,14 @@ routes:
|
||||||
handler: "Gifts->userGifts"
|
handler: "Gifts->userGifts"
|
||||||
- url: "/gifts"
|
- url: "/gifts"
|
||||||
handler: "Gifts->stub"
|
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"
|
- url: "/admin"
|
||||||
handler: "Admin->index"
|
handler: "Admin->index"
|
||||||
- url: "/admin/users"
|
- url: "/admin/users"
|
||||||
|
|
|
@ -2044,6 +2044,13 @@ table td[width="120"] {
|
||||||
height: 11px;
|
height: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#app_news_container {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes appearing {
|
@keyframes appearing {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
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" = "Description";
|
"description" = "Description";
|
||||||
"save" = "Save";
|
"save" = "Save";
|
||||||
"main_information" = "Main information";
|
"main_information" = "Main information";
|
||||||
|
"additional_information" = "Additional information";
|
||||||
"nickname" = "Nickname";
|
"nickname" = "Nickname";
|
||||||
"online" = "Online";
|
"online" = "Online";
|
||||||
"was_online" = "was online";
|
"was_online" = "was online";
|
||||||
|
@ -708,6 +709,74 @@
|
||||||
|
|
||||||
"users_gifts" = "Gifts";
|
"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 <b>$1</b> votes to its credit.";
|
||||||
|
"app_users" = "Your application is used by <b>$1</b> 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 */
|
||||||
|
|
||||||
"support_opened" = "Opened";
|
"support_opened" = "Opened";
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
"description" = "Описание";
|
"description" = "Описание";
|
||||||
"save" = "Сохранить";
|
"save" = "Сохранить";
|
||||||
"main_information" = "Основная информация";
|
"main_information" = "Основная информация";
|
||||||
|
"additional_information" = "Дополнительная информация";
|
||||||
"nickname" = "Никнейм";
|
"nickname" = "Никнейм";
|
||||||
"online" = "Онлайн";
|
"online" = "Онлайн";
|
||||||
"was_online" = "был в сети";
|
"was_online" = "был в сети";
|
||||||
|
@ -753,6 +754,74 @@
|
||||||
|
|
||||||
"users_gifts" = "Подарки";
|
"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" = "На счету вашего приложения <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 */
|
||||||
|
|
||||||
"support_opened" = "Открытые";
|
"support_opened" = "Открытые";
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"description" = "Опис";
|
"description" = "Опис";
|
||||||
"save" = "Зберегти";
|
"save" = "Зберегти";
|
||||||
"main_information" = "Основна інформація";
|
"main_information" = "Основна інформація";
|
||||||
|
"additional_information" = "Додаткова інформація";
|
||||||
"nickname" = "Нікнейм";
|
"nickname" = "Нікнейм";
|
||||||
"online" = "Онлайн";
|
"online" = "Онлайн";
|
||||||
"was_online" = "був у мережі";
|
"was_online" = "був у мережі";
|
||||||
|
@ -384,6 +385,11 @@
|
||||||
"edit_note" = "Редагувати нотатку";
|
"edit_note" = "Редагувати нотатку";
|
||||||
"actions" = "Дії";
|
"actions" = "Дії";
|
||||||
"notes_start_screen" = "За допомогою нотаток Ви можете ділитися подіями з життя з друзями, а також бути в курсі того, що відбувається у них.";
|
"notes_start_screen" = "За допомогою нотаток Ви можете ділитися подіями з життя з друзями, а також бути в курсі того, що відбувається у них.";
|
||||||
|
"note_preview" = "Попередній перегляд";
|
||||||
|
"note_preview_warn" = "Увага: Це всього лише попередній перегляд";
|
||||||
|
"note_preview_warn_details" = "Після збереження, нотатки можуть мати інший вигляд. Також не викликайте перегляд занадто часто.";
|
||||||
|
"note_preview_empty_err" = "Попередній перегляд неможливий: Немає імені чи змісту.";
|
||||||
|
|
||||||
"edited" = "Відредаговано";
|
"edited" = "Відредаговано";
|
||||||
|
|
||||||
"notes_zero" = "Жодної нотатки";
|
"notes_zero" = "Жодної нотатки";
|
||||||
|
@ -557,9 +563,9 @@
|
||||||
"two_factor_authentication_login" = "У вас увімкнена двофакторна автентифікація. Для входу введіть код отриманий в додатку.";
|
"two_factor_authentication_login" = "У вас увімкнена двофакторна автентифікація. Для входу введіть код отриманий в додатку.";
|
||||||
|
|
||||||
"two_factor_authentication_settings_1" = "Двофакторну автентифікацію через TOTP можна використовувати навіть без інтернету. Для цього вам знадобиться застосунок для генерації кодів, наприклад Google Authenticator для Android та iOS або відкриті <b>Aegis Authenticator чи andOTP</b> для Android. Важливо: на пристрої має бути точна дата та час.";
|
"two_factor_authentication_settings_1" = "Двофакторну автентифікацію через TOTP можна використовувати навіть без інтернету. Для цього вам знадобиться застосунок для генерації кодів, наприклад Google Authenticator для Android та iOS або відкриті <b>Aegis Authenticator чи andOTP</b> для Android. Важливо: на пристрої має бути точна дата та час.";
|
||||||
"two_factor_authentication_settings_2" = "Використовуючи додаток для двофакторної автентифікації, відскануйте наведений нижче QR-код:";
|
"two_factor_authentication_settings_2" = "Використовуючи застосунок для двофакторної автентифікації, відскануйте наведений нижче QR-код:";
|
||||||
"two_factor_authentication_settings_3" = "або вручну введіть секретний ключ: <b>$1</b>.";
|
"two_factor_authentication_settings_3" = "або вручну введіть секретний ключ: <b>$1</b>.";
|
||||||
"two_factor_authentication_settings_4" = "Тепер введіть код, який вам надав додаток, і пароль від вашої сторінки, щоб ми могли підтвердити, що ви дійсно ви.";
|
"two_factor_authentication_settings_4" = "Тепер введіть код, який вам надав застосунок, і пароль від вашої сторінки, щоб ми могли підтвердити, що ви дійсно ви.";
|
||||||
|
|
||||||
"connect" = "Підключити";
|
"connect" = "Підключити";
|
||||||
"enable" = "Включити";
|
"enable" = "Включити";
|
||||||
|
@ -749,6 +755,74 @@
|
||||||
|
|
||||||
"users_gifts" = "Подарунки";
|
"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" = "На рахунку вашого застосунку <b>$1</b> голосів.";
|
||||||
|
"app_users" = "Вашим застосунком користуються <b>$1</b> користувачів.";
|
||||||
|
"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 */
|
||||||
|
|
||||||
"support_opened" = "Відкриті";
|
"support_opened" = "Відкриті";
|
||||||
|
|
|
@ -19,6 +19,8 @@ openvk:
|
||||||
- "index.php"
|
- "index.php"
|
||||||
photos:
|
photos:
|
||||||
upgradeStructure: true
|
upgradeStructure: true
|
||||||
|
apps:
|
||||||
|
withdrawTax: 8
|
||||||
security:
|
security:
|
||||||
requireEmail: false
|
requireEmail: false
|
||||||
requirePhone: false
|
requirePhone: false
|
||||||
|
|
|
@ -230,3 +230,21 @@ input[type=checkbox] {
|
||||||
#votesBalance {
|
#votesBalance {
|
||||||
border-bottom: none;
|
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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue