<?php
namespace App\Service\Client;
use Frivol\Common\Dict\ActivityFeedType;
use Frivol\Common\JSON;
use Knp\Component\Pager\Pagination\PaginationInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Twig\Error\Error;
use Websocket\InfostreamHandler;
use Websocket\TemplateRendererTwig;
class ActivityFeedService extends AbstractClient
{
public function getTimelineFeedForMember(int $member, int $page, int $perPage): PaginationInterface
{
$key = (new \ReflectionClass($this))->getShortName() . '_' . __FUNCTION__ . '_' . $member . '_' . $page . '_' . $perPage;
return $this->getCacheService()->get($key, function (ItemInterface $item) use ($member, $page, $perPage) {
try {
$response = $this->requestJSON('GET', 'api/public/feeds/activity/timeline/' . $member . '/' . $page, [
'query' => [
'limit' => $perPage,
]
]);
} catch (TransportExceptionInterface $exception) {
return $this->getDefaultPagination([], $page, $perPage);
}
if (is_array($response)) {
$item->expiresAfter(600);
$item->tag([
'timelinefeed',
'timelinefeed-' . $member
]);
}
return $this->getDefaultPagination($response, $page, $perPage);
});
}
/**
* @param int $page
* @param int $limit
*/
public function getGlobalTimelineFeed(int $page, int $limit): PaginationInterface {
$key = (new \ReflectionClass($this))->getShortName() . '_' . __FUNCTION__ . '_' . $page . '_' . $limit;
return $this->getCacheService()->get($key, function (ItemInterface $item) use ($page, $limit) {
try {
$response = $this->requestJSON('GET', 'api/public/feeds/activity/infostream/global/' . $page, [
'query' => [
'limit' => $limit,
]
]);
} catch (TransportExceptionInterface $exception) {
return $this->getDefaultPagination([], $page, $limit);
}
if (is_array($response)) {
$item->expiresAfter(30);
$item->tag([
'timelinefeed-global'
]);
}
return $this->getDefaultPagination($response, $page, $limit);
});
}
/**
* @param int $member
* @return PaginationInterface
* @throws \Psr\Cache\InvalidArgumentException
* @throws \ReflectionException
*/
public function getProfileVisitsForMember(int $member): PaginationInterface
{
$key = (new \ReflectionClass($this))->getShortName() . '_' . __FUNCTION__ . '_' . $member;
return $this->getCacheService()->get($key, function (ItemInterface $item) use ($member) {
$item->expiresAfter(60 * 5);
$response = $this->requestJSON('GET', 'api/user/profilevisits/' . $member);
// old code does not keep more than 30 items in the redis key (applied in api aswell).
return $this->getDefaultPagination($response, 1, 30);
});
}
/**
* @param int $hostMemberId
* @param int $visitorMemberId
* @return bool
*/
public function createProfileVisit(int $hostMemberId, int $visitorMemberId): bool
{
try {
$response = $this->issuePost('api/user/profilevisits/visit/' . $hostMemberId . '/' . $visitorMemberId, []);
} catch (TransportExceptionInterface $e) {
return false;
}
return $response->getStatusCode() === Response::HTTP_CREATED;
}
public function createProfileVisitFromPath(string $profileUri, int $visitorMemberId): bool
{
if (preg_match('#^/profil/([a-z0-9\-]+)$#iu', $profileUri, $matches) !== 1) {
return false;
}
try {
$response = $this->issuePost('api/user/profilevisits/visitusername/' . $matches[1] . '/' . $visitorMemberId, []);
} catch (TransportExceptionInterface $e) {
return false;
}
return $response->getStatusCode() === Response::HTTP_CREATED;
}
public function updatePublicInfostream(TemplateRendererTwig $renderer, \Redis $redis): array
{
$publicActivities = $this->requestJSON('GET', 'api/public/feeds/activity/infostream/public');
$storedStream = [];
foreach ($publicActivities['data'] as $activity) {
try {
$html = $renderer->renderInfostreamItem($activity);
} catch (Error $e) {
$this->logger->error($e->getMessage() . PHP_EOL . $e->getTraceAsString());
continue;
}
if (empty($html)) {
continue;
}
$storedStream[$activity['id']] = $html;
}
krsort($storedStream);
$redis->set(InfostreamHandler::PUBLIC_INFOSTREAM_CACHE_KEY, gzcompress(JSON::encode($storedStream)));
return $storedStream;
}
/**
* Returns list with new activities for ['all'] or [$memberId] to be published by websocket server.
* Need to use activityId as key, to be capable of loading only new events for each member.
* @param string $memberId2ActivityId
* @param TemplateRendererTwig $renderer
* @param \Redis $redis
* @return array[]
* @throws TransportExceptionInterface
* @throws \JsonException
* @throws \RedisException
* @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
*/
public function updateMemberInfostream(string $memberId2ActivityId, TemplateRendererTwig $renderer, \Redis $redis): array
{
$memberActivities = $this->requestJSON('POST', 'api/public/feeds/activity/infostream/members', [
'body' => [
'member_activity_ids' => $memberId2ActivityId,
]
]);
$renderer->setLogger($this->logger);
$activityUpdates = $visitorUpdates = $infostreamUpdates = [];
foreach ($memberActivities['data'] as $memberId => $activities) {
if (empty($activities)) {
continue;
}
[$infostreamItems, $activityItems, $visitItems] = self::getMemberLists($memberId, $redis);
if (!is_array($infostreamItems)) {
$infostreamItems = [];
}
if (!is_array($activityItems)) {
$activityItems = [];
}
if (!is_array($visitItems)) {
$visitItems = [];
}
foreach ($activities as $activity) {
//#564 member infostream is now clone of sidebar activity items handled in app.js & HeaderApp!
//$html = $renderer->renderInfostreamItem($activity, true);
//if ('' !== trim($html)) {
// $infostreamItems[$activity['id']] = $html;
// $infostreamUpdates[$memberId][$activity['id']] = $html;
//}
$html = $renderer->renderSidebarActivityItem($activity);
if ('' !== trim($html)) {
$activityItems[$activity['id']] = $html;
$activityUpdates[$memberId][$activity['id']] = $html;
}
if ($activity['type'] === ActivityFeedType::PROFILE_VISIT && ('' !== $html = $renderer->renderSidebarProfileVisitItem($activity))) {
$visitorUpdates[$memberId][$activity['id']] = $visitItems[$activity['id']] = $html;
}
}
krsort($infostreamItems);
krsort($activityItems);
krsort($visitItems);
if (!$this->setMemberLists($memberId, array_slice($infostreamItems, 0, 20, true), array_slice($activityItems, 0, 100, true), array_slice($visitItems, 0, 100, true), $redis)) {
$this->logger->critical('Failed to store infostream/activitySidebar/visitsSidebar items in redis.');
}
}
krsort($infostreamUpdates);
krsort($activityUpdates);
krsort($visitorUpdates);
return [
'infostreamUpdates' => $infostreamUpdates,
'activityUpdates' => $activityUpdates,
'visitorUpdates' => $visitorUpdates,
];
}
/**
* @param int $memberId
* @param \Redis $redis
* @return array[]
* @throws \JsonException
* @throws \RedisException
*/
public static function getMemberLists(int $memberId, \Redis $redis): array
{
[$infostreamJson, $activitiesJson, $visitsJson] = $redis->mGet([
InfostreamHandler::MEMBER_INFOSTREAM_CACHE_KEY . $memberId,
InfostreamHandler::ACTIVITIES_SIDEBAR_CACHE_KEY . $memberId,
InfostreamHandler::PROFILE_VISIT_SIDEBAR_CACHE_KEY . $memberId,
]);
return [
0 => is_string($infostreamJson) ? JSON::decode(gzuncompress($infostreamJson)) : [],
1 => is_string($activitiesJson) ? JSON::decode(gzuncompress($activitiesJson)) : [],
2 => is_string($visitsJson) ? JSON::decode(gzuncompress($visitsJson)) : [],
];
}
public function getPublicInfostream(\Redis $redis, int $initialEntries = 8): array
{
$returnStruct = ['initial' => [], 'delayed' => []];
$feedJson = $redis->get(InfostreamHandler::PUBLIC_INFOSTREAM_CACHE_KEY);
if (empty($feedJson)) {
return $returnStruct;
}
$feedItems = JSON::decode(gzuncompress($feedJson));
ksort($feedItems);
$processed = $sendDelay = 0;
foreach ($feedItems as $itemHtml) {
$processed++;
if ($processed <= $initialEntries) {
$returnStruct['initial'][] = $itemHtml;
continue;
}
$sendDelay += random_int(1, 3);//Fake constant stream of activity, so users can keep up with reading.
$returnStruct['delayed'][$sendDelay] = $itemHtml;
}
return $returnStruct;
}
private function setMemberLists(int $memberId, array $infostreamItems, array $activityItems, array $visitItems, \Redis $redis): bool
{
return $redis->mSet([
InfostreamHandler::MEMBER_INFOSTREAM_CACHE_KEY . $memberId => gzcompress(JSON::encode($infostreamItems), 6),
InfostreamHandler::ACTIVITIES_SIDEBAR_CACHE_KEY . $memberId => gzcompress(JSON::encode($activityItems), 6),
InfostreamHandler::PROFILE_VISIT_SIDEBAR_CACHE_KEY . $memberId => gzcompress(JSON::encode($visitItems), 6),
]);
}
}