Merge branch 'master' of github.com:openvk/openvk

This commit is contained in:
veselcraft 2021-10-22 23:49:37 +03:00
commit 16d3280336
21 changed files with 315 additions and 40 deletions

View file

@ -0,0 +1,83 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use Latte\Engine as TemplatingEngine;
use RdKafka\{Conf as RDKConf, KafkaConsumer};
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Notifications as N};
class Notifications implements Handler
{
protected $user;
protected $notifs;
function __construct(?User $user)
{
$this->user = $user;
$this->notifs = new N;
}
function ack(callable $resolve, callable $reject): void
{
$this->user->updateNotificationOffset();
$this->user->save();
$resolve("OK");
}
function fetch(callable $resolve, callable $reject): void
{
$kafkaConf = OPENVK_ROOT_CONF["openvk"]["credentials"]["notificationsBroker"];
if(!$kafkaConf["enable"]) {
$reject(1999, "Disabled");
return;
}
$kafkaConf = $kafkaConf["kafka"];
$conf = new RDKConf();
$conf->set("metadata.broker.list", $kafkaConf["addr"] . ":" . $kafkaConf["port"]);
$conf->set("group.id", "UserFetch-" . $this->user->getId()); # Чтобы уведы приходили только на разные устройства одного чебупелика
$conf->set("auto.offset.reset", "latest");
set_time_limit(30);
$consumer = new KafkaConsumer($conf);
$consumer->subscribe([ $kafkaConf["topic"] ]);
while(true) {
$message = $consumer->consume(30*1000);
switch ($message->err) {
case RD_KAFKA_RESP_ERR_NO_ERROR:
$descriptor = $message->payload;
[,$user,] = explode(",", $descriptor);
if(((int) $user) === $this->user->getId()) {
$data = (object) [];
$notification = $this->notifs->fromDescriptor($descriptor, $data);
if(!$notification) {
$reject(1982, "Server Error");
return;
}
$tplDir = __DIR__ . "/../Web/Presenters/templates/components/notifications/";
$tplId = "$tplDir$data->actionCode/_$data->originModelType" . "_" . $data->targetModelType . "_.xml";
$latte = new TemplatingEngine;
$latte->setTempDirectory(CHANDLER_ROOT . "/tmp/cache/templates");
$latte->addFilter("translate", fn($trId) => tr($trId));
$resolve([
"title" => tr("notif_" . $data->actionCode . "_" . $data->originModelType . "_" . $data->targetModelType),
"body" => trim(preg_replace('%(\s){2,}%', "$1", $latte->renderToString($tplId, ["notification" => $notification]))),
"ava" => $notification->getModel(1)->getAvatarUrl(),
"priority" => 1,
]);
return;
}
break;
case RD_KAFKA_RESP_ERR__TIMED_OUT:
case RD_KAFKA_RESP_ERR__PARTITION_EOF:
$reject(1983, "Nothing to report");
break 2;
default:
$reject(1981, "Kafka Error: " . $message->errstr());
break 2;
}
}
}
}

View file

@ -3,6 +3,7 @@ namespace openvk\Web\Models\Entities\Notifications;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{User};
use openvk\Web\Util\DateTime;
use RdKafka\{Conf, Producer};
class Notification
{
@ -80,14 +81,14 @@ class Notification
return false;
$data = [
$this->recipient->getId(),
$this->encodeType($this->originModel),
$this->originModel->getId(),
$this->encodeType($this->targetModel),
$this->targetModel->getId(),
$this->actionCode,
$this->data,
$this->time,
"recipient" => $this->recipient->getId(),
"originModelType" => $this->encodeType($this->originModel),
"originModelId" => $this->originModel->getId(),
"targetModelType" => $this->encodeType($this->targetModel),
"targetModelId" => $this->targetModel->getId(),
"actionCode" => $this->actionCode,
"additionalPayload" => $this->data,
"timestamp" => $this->time,
];
$edb = $e->getConnection();
@ -96,12 +97,33 @@ class Notification
$query = <<<'QUERY'
SELECT * FROM `notifications` WHERE `recipientType`=0 AND `recipientId`=? AND `originModelType`=? AND `originModelId`=? AND `targetModelType`=? AND `targetModelId`=? AND `modelAction`=? AND `additionalData`=? AND `timestamp` > (? - ?)
QUERY;
$result = $edb->query($query, ...array_merge($data, [ $this->threshold ]));
$result = $edb->query($query, ...array_merge(array_values($data), [ $this->threshold ]));
if($result->getRowCount() > 0)
return false;
}
$edb->query("INSERT INTO notifications VALUES (0, ?, ?, ?, ?, ?, ?, ?, ?)", ...array_values($data));
$edb->query("INSERT INTO notifications VALUES (0, ?, ?, ?, ?, ?, ?, ?, ?)", ...$data);
$kafkaConf = OPENVK_ROOT_CONF["openvk"]["credentials"]["notificationsBroker"];
if($kafkaConf["enable"]) {
$kafkaConf = $kafkaConf["kafka"];
$brokerConf = new Conf();
$brokerConf->set("log_level", (string) LOG_DEBUG);
$brokerConf->set("debug", "all");
$producer = new Producer($brokerConf);
$producer->addBrokers($kafkaConf["addr"] . ":" . $kafkaConf["port"]);
$descriptor = implode(",", [
str_replace("\\", ".", get_class($this)),
$this->recipient->getId(),
base64_encode(serialize((object) $data)),
]);
$notifTopic = $producer->newTopic($kafkaConf["topic"]);
$notifTopic->produce(RD_KAFKA_PARTITION_UA, RD_KAFKA_MSG_F_BLOCK, $descriptor);
$producer->flush(100);
}
return true;
}

View file

@ -1,8 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Util\Shell\Shell;
use openvk\Web\Util\Shell\Shell\Exceptions\ShellUnavailableException;
use openvk\Web\Util\Shell\Shell\Exceptions\UnknownCommandException;
use openvk\Web\Util\Shell\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException};
use openvk\Web\Models\VideoDrivers\VideoDriver;
use Nette\InvalidStateException as ISE;
@ -18,7 +17,24 @@ class Video extends Media
protected function saveFile(string $filename, string $hash): bool
{
if(!Shell::commandAvailable("ffmpeg")) exit(VIDEOS_FRIENDLY_ERROR);
if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
exit(VIDEOS_FRIENDLY_ERROR);
$error = NULL;
$streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error);
if($error !== 0)
throw new \DomainException("$filename is not a valid video file");
else if(empty($streams) || ctype_space($streams))
throw new \DomainException("$filename does not contain any video streams");
$durations = [];
preg_match('%duration=([0-9\.]++)%', $streams, $durations);
if(sizeof($durations[1]) === 0)
throw new \DomainException("$filename does not contain any meaningful video streams");
foreach($durations[1] as $duration)
if(floatval($duration) < 1.0)
throw new \DomainException("$filename does not contain any meaningful video streams");
try {
if(!is_dir($dirId = $this->pathFromHash($hash)))

View file

@ -43,6 +43,19 @@ class Notifications
return $query;
}
private function assemble(int $act, int $originModelType, int $originModelId, int $targetModelType, int $targetModelId, int $recipientId, int $timestamp, $data, ?string $class = NULL): Notification
{
$class ??= 'openvk\Web\Models\Entities\Notifications\Notification';
$originModel = $this->getModel($originModelType, $originModelId);
$targetModel = $this->getModel($targetModelType, $targetModelId);
$recipient = (new Users)->get($recipientId);
$notification = new $class($recipient, $originModel, $targetModel, $timestamp, $data);
$notification->setActionCode($act);
return $notification;
}
function getNotificationCountByUser(User $user, int $offset, bool $archived = false): int
{
$db = $this->getEDB(false);
@ -64,13 +77,38 @@ class Notifications
$results = $this->getEDB()->query($this->getQuery($user, false, $offset, $archived, $page, $perPage));
foreach($results->fetchAll() as $notif) {
$originModel = $this->getModel($notif->originModelType, $notif->originModelId);
$targetModel = $this->getModel($notif->targetModelType, $notif->targetModelId);
$recipient = (new Users)->get($notif->recipientId);
$notification = new Notification($recipient, $originModel, $targetModel, $notif->timestamp, $notif->additionalData);
$notification->setActionCode($notif->modelAction);
yield $notification;
yield $this->assemble(
$notif->modelAction,
$notif->originModelType,
$notif->originModelId,
$notif->targetModelType,
$notif->targetModelId,
$notif->recipientId,
$notif->timestamp,
$notif->additionalData
);
}
}
function fromDescriptor(string $descriptor, ?object &$parsedData = nullptr)
{
[$class, $recv, $data] = explode(",", $descriptor);
$class = str_replace(".", "\\", $class);
$parsedData = unserialize(base64_decode($data));
return $this->assemble(
$parsedData->actionCode,
$parsedData->originModelType,
$parsedData->originModelId,
$parsedData->targetModelType,
$parsedData->targetModelId,
$parsedData->recipient,
$parsedData->timestamp,
$parsedData->additionalPayload,
);
}
}

View file

@ -43,6 +43,6 @@ class Videos
function getUserVideosCount(User $user): int
{
return sizeof($this->videos->where("owner", $user->getId())->where("deleted", 0));
return sizeof($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0]));
}
}

View file

@ -58,8 +58,8 @@ final class InternalAPIPresenter extends OpenVKPresenter
try {
$params = array_merge($input->params ?? [], [function($data) {
$this->succ($data);
}, function($data) {
$this->fail($data);
}, function(int $errno, string $errstr) {
$this->fail($errno, $errstr);
}]);
$handler->{$method}(...$params);
} catch(\TypeError $te) {
@ -68,4 +68,4 @@ final class InternalAPIPresenter extends OpenVKPresenter
$this->fail(-32603, "Uncaught " . get_class($ex));
}
}
}
}

View file

@ -68,6 +68,8 @@ final class VideosPresenter extends OpenVKPresenter
$video->setLink($this->postParam("link"));
else
$this->flashFail("err", "Нету видеозаписи", "Выберите файл или укажите ссылку.");
} catch(\DomainException $ex) {
$this->flashFail("err", "Произошла ошибка", "Файл повреждён или не содержит видео." );
} catch(ISE $ex) {
$this->flashFail("err", "Произошла ошибка", "Возможно, ссылка некорректна.");
}

View file

@ -200,6 +200,8 @@ final class WallPresenter extends OpenVKPresenter
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"]);
}
} catch(\DomainException $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён.");
} catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён или слишком велик.");
}

View file

@ -252,12 +252,13 @@
<a href="/privacy" class="link">{_footer_privacy}</a>
</div>
<p>OpenVK <a href="/about:openvk2">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {$dbVersion}</p>
<p n:ifcontent>
<p n:ifcontent="ifcontent">
{php echo OPENVK_ROOT_CONF["openvk"]["appearance"]["motd"]}
</p>
</div>
<script src="https://rawgit.com/kawanet/msgpack-lite/master/dist/msgpack.min.js"></script>
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/node_modules/soundjs/lib/soundjs.min.js"}
{script "js/node_modules/ky/umd.js"}
{script "js/messagebox.js"}
{script "js/notifications.js"}
@ -265,6 +266,10 @@
{script "js/al_wall.js"}
{script "js/al_api.js"}
{ifset $thisUser}
{script "js/al_notifs.js"}
{/ifset}
<script src="https://unpkg.com/fartscroll@1.0.0/fartscroll.js"></script>
<script n:if="OPENVK_ROOT_CONF['openvk']['preferences']['bellsAndWhistles']['fartscroll']">
fartscroll(400);

View file

@ -397,8 +397,8 @@
<tr>
<td class="e">
Vladimir Barinov (veselcraft), Alexandra Katunina (rem-pai), Konstantin Kichulkin (kosfurler),
Nikita Volkov (sup_ban), Daniil Myslivets (myslivets), Alexander Kotov (l-lacker),
Alexey Assemblerov (BiosNod), Ilya Prokopenko (dsrev) and Vladimir Lapskiy (0x7d5)
Nikita Volkov (sup_ban), Daniel Myslivets (myslivets), Alexander Kotov (l-lacker),
Alexey Assemblerov (BiosNod) and Ilya Prokopenko (dsrev)
</td>
</tr>
</tbody>

View file

@ -4,8 +4,3 @@
<a href="{$user->getURL()}"><b>{$user->getCanonicalName()}</b></a>
{$notification->getDateTime()} {_nt_liked_yours}
<a href="/wall{$post->getPrettyId()}"><b>{_nt_post_nominative}</b></a> {_nt_from} {$post->getPublicationTime()}.
<?php
// костыльно скрыл лол, сами исправите проблему - гфх
//{tr('notifications_like', '<a href="'.$user->getURL().'"><b>'.$user->getCanonicalName().'</b></a>', '<a href="/wall'.$post->getPrettyId().'"><b>', '</b></a>', $post->getPublicationTime())}
?>

View file

@ -1,7 +1,12 @@
{var microblogEnabled = isset($thisUser) ? $thisUser->hasMicroblogEnabled() : false}
{if !$post->isPostedOnBehalfOfGroup()}
{var then = date_create("@" . $post->getOwner()->getOnline()->timestamp())}
{var now = date_create()}
{var diff = date_diff($now, $then)}
{/if}
{if $microblogEnabled}
{include "post/microblogpost.xml", post => $post}
{include "post/microblogpost.xml", post => $post, diff => $diff}
{else}
{include "post/oldpost.xml", post => $post}
{include "post/oldpost.xml", post => $post, diff => $diff}
{/if}

View file

@ -7,6 +7,13 @@
<img
src="{$author->getAvatarURL()}"
width="{ifset $compact}25{else}50{/ifset}" />
{if !$post->isPostedOnBehalfOfGroup() && !$compact}
<span n:if="$author->getOnline()->timestamp() + 2505600 > time()" class="post-online">
{if $diff->i <= 5}
{_online}
{/if}
</span>
{/if}
</td>
<td width="100%" valign="top">
<div class="post-author">

View file

@ -7,6 +7,13 @@
<img
src="{$author->getAvatarURL()}"
width="50" />
{if !$post->isPostedOnBehalfOfGroup() && !$compact}
<span n:if="$author->getOnline()->timestamp() + 2505600 > time()" class="post-online">
{if $diff->i <= 5}
{_online}
{/if}
</span>
{/if}
</td>
<td width="100%" valign="top">
<div class="post-author">

View file

@ -16,14 +16,16 @@ class Shell
static function commandAvailable(string $name): bool
{
if(!Shell::shellAvailable()) throw new Exceptions\ShellUnavailableException;
if(!Shell::shellAvailable())
throw new Exceptions\ShellUnavailableException;
return !is_null(`command -v $name`);
}
static function __callStatic(string $name, array $arguments): object
{
if(!Shell::commandAvailable($name)) throw new Exceptions\UnknownCommandException($name);
if(!Shell::commandAvailable($name))
throw new Exceptions\UnknownCommandException($name);
$command = implode(" ", array_merge([$name], $arguments));
@ -36,14 +38,17 @@ class Shell
$this->command = $cmd;
}
function execute(): string
function execute(?int &$result = nullptr): string
{
return shell_exec($this->command);
$stdout = [];
exec($this->command, $stdout, $result);
return implode(PHP_EOL, $stdout);
}
function start(): string
{
shell_exec("nohup " . $this->command . " > /dev/null 2>/dev/null &");
system("nohup " . $this->command . " > /dev/null 2>/dev/null &");
return $this->command;
}

BIN
Web/static/audio/Bruh.mp3 Normal file

Binary file not shown.

View file

@ -481,6 +481,14 @@ h4 {
display: block;
}
.post-online {
color: #AAA;
text-align: center;
width: 50px;
padding-top: 5px;
display: block;
}
.post-author {
background-color: #F6F6F6;
border-top: #8B8B8B solid 1px;

View file

@ -0,0 +1,37 @@
createjs.Sound.registerSound("/assets/packages/static/openvk/audio/Bruh.mp3", "notification");
function __actualPlayNotifSound() {
createjs.Sound.play("notification");
}
window.playNotifSound = Function.noop;
async function setupNotificationListener() {
console.warn("Setting up notifications listener...");
while(true) {
let notif;
try {
notif = await API.Notifications.fetch();
} catch(rejection) {
if(rejection.message !== "Nothing to report") {
console.error(rejection);
break;
}
console.info("No new notifications discovered... Redialing event broker");
continue;
}
playNotifSound();
NewNotification(notif.title, notif.body, notif.ava, Function.noop, notif.priority * 6000);
console.info("New notification", notif);
API.Notifications.ack();
}
};
setupNotificationListener();
u(document.body).on("click", () => window.playNotifSound = window.__actualPlayNotifSound);

View file

@ -7,11 +7,13 @@
"ky": "^0.19.0",
"literallycanvas": "^0.5.2",
"monaco-editor": "^0.20.0",
"msgpack-lite": "^0.1.26",
"plotly.js-dist": "^1.52.3",
"react": "15.1",
"react-dom": "15.1",
"react-dom-factories": "^1.0.2",
"requirejs": "^2.3.6",
"soundjs": "^1.0.1",
"umbrellajs": "^3.1.0"
}
}

View file

@ -66,6 +66,11 @@ encoding@^0.1.11:
dependencies:
iconv-lite "^0.6.2"
event-lite@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.2.tgz#838a3e0fdddef8cc90f128006c8e55a4e4e4c11b"
integrity sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==
fancy-file-input@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fancy-file-input/-/fancy-file-input-2.0.4.tgz#698c216482e07649a827681c4db3054fddc9a32b"
@ -91,11 +96,26 @@ iconv-lite@^0.6.2:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
ieee754@^1.1.8:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
int64-buffer@^0.1.9:
version "0.1.10"
resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423"
integrity sha1-J3siiofZWtd30HwTgyAiQGpHNCM=
is-stream@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
isarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
@ -148,6 +168,16 @@ monaco-editor@^0.20.0:
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==
msgpack-lite@^0.1.26:
version "0.1.26"
resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89"
integrity sha1-3TxQsm8FnyXn7e42REGDWOKprYk=
dependencies:
event-lite "^0.1.1"
ieee754 "^1.1.8"
int64-buffer "^0.1.9"
isarray "^1.0.0"
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -229,6 +259,11 @@ skatejs@0.13.17:
resolved "https://registry.yarnpkg.com/skatejs/-/skatejs-0.13.17.tgz#7a21fbb3434da45e52b47b61647168ee9e778071"
integrity sha1-eiH7s0NNpF5StHthZHFo7p53gHE=
soundjs@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/soundjs/-/soundjs-1.0.1.tgz#99970542d28d0df2a1ebd061ae75c961a98c8180"
integrity sha512-MgFPvmKYfpcNiE3X5XybNvScie3DMQlZgmNzUn4puBcpw64f4LqjH/fhM8Sb/eTJ8hK57Crr7mWy0bfJOqPj6Q==
trim-extra-html-whitespace@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/trim-extra-html-whitespace/-/trim-extra-html-whitespace-1.3.0.tgz#b47efb0d1a5f2a56a85cc45cea525651e93404cf"

View file

@ -68,3 +68,9 @@ openvk:
dsn: "mysql:unix_socket=/tmp/mysql.sock;dbname=openvk-eventdb"
user: "root"
password: "DATABASE_PASSWORD"
notificationsBroker:
enable: false
kafka:
addr: "127.0.0.1"
port: 9092
topic: "OvkEvents"