Notifications: WIP: Add ajax notifications :D

This commit is contained in:
Celestora 2021-10-15 23:05:27 +03:00
parent 931f2a1149
commit 0b7b67e2e7
10 changed files with 244 additions and 27 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\RowModel;
use openvk\Web\Models\Entities\{User}; use openvk\Web\Models\Entities\{User};
use openvk\Web\Util\DateTime; use openvk\Web\Util\DateTime;
use RdKafka\{Conf, Producer};
class Notification class Notification
{ {
@ -80,14 +81,14 @@ class Notification
return false; return false;
$data = [ $data = [
$this->recipient->getId(), "recipient" => $this->recipient->getId(),
$this->encodeType($this->originModel), "originModelType" => $this->encodeType($this->originModel),
$this->originModel->getId(), "originModelId" => $this->originModel->getId(),
$this->encodeType($this->targetModel), "targetModelType" => $this->encodeType($this->targetModel),
$this->targetModel->getId(), "targetModelId" => $this->targetModel->getId(),
$this->actionCode, "actionCode" => $this->actionCode,
$this->data, "additionalPayload" => $this->data,
$this->time, "timestamp" => $this->time,
]; ];
$edb = $e->getConnection(); $edb = $e->getConnection();
@ -96,12 +97,33 @@ class Notification
$query = <<<'QUERY' $query = <<<'QUERY'
SELECT * FROM `notifications` WHERE `recipientType`=0 AND `recipientId`=? AND `originModelType`=? AND `originModelId`=? AND `targetModelType`=? AND `targetModelId`=? AND `modelAction`=? AND `additionalData`=? AND `timestamp` > (? - ?) SELECT * FROM `notifications` WHERE `recipientType`=0 AND `recipientId`=? AND `originModelType`=? AND `originModelId`=? AND `targetModelType`=? AND `targetModelId`=? AND `modelAction`=? AND `additionalData`=? AND `timestamp` > (? - ?)
QUERY; 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) if($result->getRowCount() > 0)
return false; 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; return true;
} }

View file

@ -43,6 +43,19 @@ class Notifications
return $query; 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 function getNotificationCountByUser(User $user, int $offset, bool $archived = false): int
{ {
$db = $this->getEDB(false); $db = $this->getEDB(false);
@ -64,13 +77,38 @@ class Notifications
$results = $this->getEDB()->query($this->getQuery($user, false, $offset, $archived, $page, $perPage)); $results = $this->getEDB()->query($this->getQuery($user, false, $offset, $archived, $page, $perPage));
foreach($results->fetchAll() as $notif) { foreach($results->fetchAll() as $notif) {
$originModel = $this->getModel($notif->originModelType, $notif->originModelId); yield $this->assemble(
$targetModel = $this->getModel($notif->targetModelType, $notif->targetModelId); $notif->modelAction,
$recipient = (new Users)->get($notif->recipientId); $notif->originModelType,
$notif->originModelId,
$notification = new Notification($recipient, $originModel, $targetModel, $notif->timestamp, $notif->additionalData);
$notification->setActionCode($notif->modelAction); $notif->targetModelType,
yield $notification; $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

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

View file

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

View file

@ -4,8 +4,3 @@
<a href="{$user->getURL()}"><b>{$user->getCanonicalName()}</b></a> <a href="{$user->getURL()}"><b>{$user->getCanonicalName()}</b></a>
{$notification->getDateTime()} {_nt_liked_yours} {$notification->getDateTime()} {_nt_liked_yours}
<a href="/wall{$post->getPrettyId()}"><b>{_nt_post_nominative}</b></a> {_nt_from} {$post->getPublicationTime()}. <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())}
?>

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

Binary file not shown.

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", "ky": "^0.19.0",
"literallycanvas": "^0.5.2", "literallycanvas": "^0.5.2",
"monaco-editor": "^0.20.0", "monaco-editor": "^0.20.0",
"msgpack-lite": "^0.1.26",
"plotly.js-dist": "^1.52.3", "plotly.js-dist": "^1.52.3",
"react": "15.1", "react": "15.1",
"react-dom": "15.1", "react-dom": "15.1",
"react-dom-factories": "^1.0.2", "react-dom-factories": "^1.0.2",
"requirejs": "^2.3.6", "requirejs": "^2.3.6",
"soundjs": "^1.0.1",
"umbrellajs": "^3.1.0" "umbrellajs": "^3.1.0"
} }
} }

View file

@ -66,6 +66,11 @@ encoding@^0.1.11:
dependencies: dependencies:
iconv-lite "^0.6.2" 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: fancy-file-input@~2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/fancy-file-input/-/fancy-file-input-2.0.4.tgz#698c216482e07649a827681c4db3054fddc9a32b" 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: dependencies:
safer-buffer ">= 2.1.2 < 3.0.0" 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: is-stream@^1.0.1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= 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: isomorphic-fetch@^2.1.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" 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" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ== 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: node-fetch@^1.0.1:
version "1.7.3" version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 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" resolved "https://registry.yarnpkg.com/skatejs/-/skatejs-0.13.17.tgz#7a21fbb3434da45e52b47b61647168ee9e778071"
integrity sha1-eiH7s0NNpF5StHthZHFo7p53gHE= 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: trim-extra-html-whitespace@1.3.0:
version "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" resolved "https://registry.yarnpkg.com/trim-extra-html-whitespace/-/trim-extra-html-whitespace-1.3.0.tgz#b47efb0d1a5f2a56a85cc45cea525651e93404cf"