Приложения (#674)

This commit is contained in:
celestora 2022-08-20 21:07:54 +03:00 committed by GitHub
parent d1b878a5a4
commit d767d8e2eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1258 additions and 0 deletions

87
ServiceAPI/Apps.php Normal file
View 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);
}
}

View file

@ -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
View 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;
}
}

View 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);
}
}

View 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()));
}
}

View 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;
}
}

View 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}

View 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}

View 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}

View file

@ -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

View file

@ -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"

View file

@ -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
View 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
View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
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);

View 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;

View file

@ -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" = "Открытые";

View file

@ -19,6 +19,8 @@ openvk:
- "index.php"
photos:
upgradeStructure: true
apps:
withdrawTax: 8
security:
requireEmail: false
requirePhone: false