Added live communication backend

This commit is contained in:
Lucas Bartholemy 2017-01-20 13:57:45 +01:00
parent 8694225c73
commit 98b975da13
14 changed files with 663 additions and 2 deletions

View File

@ -128,6 +128,12 @@ $config = [
'class' => 'humhub\components\queue\driver\Sync',
],
],
'live' => [
'class' => humhub\modules\live\components\Sender::class,
'driver' => [
'class' => 'humhub\modules\live\driver\Database',
],
],
],
'params' => [
'installed' => false,

View File

@ -48,6 +48,7 @@ class Content extends \humhub\components\ActiveRecord
// Visibility Modes
const VISIBILITY_PRIVATE = 0;
const VISIBILITY_PUBLIC = 1;
const VISIBILITY_NONE = 2;
/**
* @var ContentContainerActiveRecord the Container (e.g. Space or User) where this content belongs to.
@ -184,7 +185,7 @@ class Content extends \humhub\components\ActiveRecord
if ($insert && !$contentSource instanceof \humhub\modules\activity\models\Activity) {
$notifyUsers = array_merge($this->notifyUsersOfNewContent, Yii::$app->notification->getFollowers($this));
\humhub\modules\content\notifications\ContentCreated::instance()
->from($this->user)
->about($contentSource)
@ -193,7 +194,7 @@ class Content extends \humhub\components\ActiveRecord
\humhub\modules\content\activities\ContentCreated::instance()
->about($contentSource)->save();
}
return parent::afterSave($insert, $changedAttributes);
}

View File

@ -0,0 +1,65 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live;
use Yii;
use humhub\modules\live\Module;
use humhub\modules\friendship\FriendshipEvent;
use humhub\modules\space\MemberEvent;
use humhub\modules\user\events\FollowEvent;
use humhub\modules\content\components\ContentContainerActiveRecord;
/**
* Events provides callbacks to handle events.
*
* @since 1.2
* @author luke
*/
class Events extends \yii\base\Object
{
/**
* On hourly cron job, add database cleanup task
*/
public static function onHourlyCronRun()
{
Yii::$app->queue->push(new jobs\DatabaseCleanup());
}
/**
* MemberEvent is called when a user left or joined a space
* Used to clear the cache legitimate cache.
*/
public static function onMemberEvent(MemberEvent $event)
{
Yii::$app->cache->delete(Module::$legitimateCachePrefix . $event->user->id);
}
/**
* FriendshipEvent is called when a friendship was created or removed
* Used to clear the cache legitimate cache.
*/
public static function onFriendshipEvent(FriendshipEvent $event)
{
Yii::$app->cache->delete(Module::$legitimateCachePrefix . $event->user1->id);
Yii::$app->cache->delete(Module::$legitimateCachePrefix . $event->user2->id);
}
/**
* FollowEvent is called when a following was created or removed
* Used to clear the cache legitimate cache.
*/
public static function onFollowEvent(FollowEvent $event)
{
if ($event->target instanceof ContentContainerActiveRecord) {
Yii::$app->cache->delete(Module::$legitimateCachePrefix . $event->user->id);
}
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live;
use Yii;
use humhub\modules\content\models\Content;
use humhub\modules\user\models\User;
use humhub\modules\user\models\Follow;
use humhub\modules\friendship\models\Friendship;
/**
* Live module provides a live channel to the users browser.
*
* @since 1.2
*/
class Module extends \humhub\components\Module
{
/**
* @inheritdoc
*/
public $isCoreModule = true;
/**
* @var int seconds to delete old live events
*/
public $maxLiveEventAge = 600;
/**
* @var string cache prefix for legitimate content container ids by user
*/
public static $legitimateCachePrefix = 'live.contentcontainerId.legitmation.';
/**
* Returns an array of content container ids which belongs to the given user.
*
* There are three separeted lists by visibility level:
* - Content::VISIBILITY_PUBLIC [1,2,3,4] (Public visibility only)
* - Content::VISIBILITY_PRIVATE [5,6,7] (Public and private visibility)
* - Content::VISIBILITY_NONE (10) (No visibility, direct to the user)
*
* @todo Add user to user following
* @param User $user the User
* @param boolean $cached use caching
* @return array multi dimensional array of user content container ids
*/
public function getLegitimateContentContainerIds(User $user, $cached = true)
{
$legitimation = Yii::$app->cache->get(self::$legitimateCachePrefix . $user->id);
if ($legitimation === false) {
$legitimation = [
Content::VISIBILITY_PUBLIC => [],
Content::VISIBILITY_PRIVATE => [],
Content::VISIBILITY_NONE => [],
];
// Add users own content container (user == contentcontainer)
$legitimation[Content::VISIBILITY_NONE][] = $user->contentContainerRecord->id;
// Collect user space membership with private content visibility
$spaces = \humhub\modules\space\models\Membership::GetUserSpaces($user->id);
foreach ($spaces as $space) {
$legitimation[Content::VISIBILITY_PRIVATE][] = $space->contentContainerRecord->id;
}
// Include friends
if (Yii::$app->getModule('friendship')->isEnabled) {
foreach (Friendship::getFriendsQuery($user)->all() as $user) {
$legitimation[Content::VISIBILITY_PRIVATE] = $user->contentContainerRecord->id;
}
}
// Collect spaces which the users follows
foreach (Follow::getFollowedSpacesQuery($user)->all() as $space) {
$legitimation[Content::VISIBILITY_PUBLIC][] = $space->contentContainerRecord->id;
}
Yii::$app->cache->set(self::$legitimateCachePrefix . $user->id, $legitimation);
};
return $legitimation;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live\components;
/**
* LiveEvent implements a message which can be send via live communication
*
* @since 1.2
* @author Luke
*/
abstract class LiveEvent extends \yii\base\Object
{
/**
* @see \humhub\modules\content\components\ContentContainerActiveRecord
* @var int
*/
public $contentContainerId;
/**
* @see \humhub\modules\content\models\Content::VISIBILITY_*
* @var int
*/
public $visibility;
/**
* Returns the data of this event as array
*
* @return array the live event data
*/
public function getData()
{
$data = get_object_vars($this);
unset($data['visibility']);
unset($data['contentContainerId']);
return [
'type' => str_replace('\\', '.', $this->className()),
'contentContainerId' => $this->contentContainerId,
'visibility' => $this->visibility,
'data' => $data
];
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live\components;
use Yii;
use yii\base\Component;
/**
* Live Data Sender
*
* @since 1.2
* @author Luke
*/
class Sender extends Component
{
/**
* @var \humhub\modules\live\driver\BaseDriver|array|string
*/
public $driver = [];
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->driver = Yii::createObject($this->driver);
}
/**
* Sends a live event
*
* @param LiveEvent $event the live event
*/
public function send($event)
{
return $this->driver->send($event);
}
}

View File

@ -0,0 +1,23 @@
<?php
use humhub\modules\live\Events;
use humhub\modules\space\models\Membership;
use humhub\modules\friendship\models\Friendship;
use humhub\modules\user\models\Follow;
use humhub\commands\CronController;
return [
'id' => 'live',
'class' => humhub\modules\live\Module::class,
'isCoreModule' => true,
'events' => [
[Membership::class, Membership::EVENT_MEMBER_ADDED, [Events::class, 'onMemberEvent']],
[Membership::class, Membership::EVENT_MEMBER_REMOVED, [Events::class, 'onMemberEvent']],
[Friendship::class, Friendship::EVENT_FRIENDSHIP_CREATED, [Events::class, 'onFriendshipEvent']],
[Friendship::class, Friendship::EVENT_FRIENDSHIP_REMOVED, [Events::class, 'onFriendshipEvent']],
[Follow::class, Follow::EVENT_FOLLOWING_CREATED, [Events::class, 'onFollowEvent']],
[Follow::class, Follow::EVENT_FOLLOWING_REMOVED, [Events::class, 'onFollowEvent']],
[CronController::class, CronController::EVENT_ON_HOURLY_RUN, [Events::class, 'onHourlyCronRun']]
],
];
?>

View File

@ -0,0 +1,191 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live\controllers;
use Yii;
use yii\db\Expression;
use yii\base\Exception;
use humhub\modules\content\models\Content;
use humhub\components\Controller;
use humhub\modules\live\models\Live;
use humhub\modules\live\components\LiveEvent;
/**
* PollController is used by the live database driver to deliever events
*
* @see \humhub\modules\live\driver\Database
* @since 1.2
* @author Luke
*/
class PollController extends Controller
{
/**
* @var int maximum events by query
*/
public $maxEventsByQuery = 500;
/**
* @var int maximum decay for last query time
*/
public $maxTimeDecay = 500;
/**
* An array of legitimate content container ids
*
* @see \humhub\modules\live\Module::getLegitimateContentContainerIds()
* @var array
*/
protected $containerIds = [];
/**
* @inheritdoc
*/
public function beforeAction($action)
{
if (Yii::$app->user->isGuest) {
return false;
}
if (parent::beforeAction($action)) {
if (!Yii::$app->live->driver instanceof \humhub\modules\live\driver\Database) {
throw new Exception('Polling is only available when using the live database driver!');
}
$this->containerIds = $this->module->getLegitimateContentContainerIds(Yii::$app->user->getIdentity());
return true;
}
return false;
}
/**
* Returns a list of new live events for the current user
* The GET parameter is a unix timestamp of the last update.
*
* @return string the json response
*/
public function actionIndex()
{
$lastQueryTime = $this->getLastQueryTime();
$results = [];
$results['queryTime'] = time();
$results['lastQueryTime'] = $lastQueryTime;
$results['events'] = [];
foreach ($this->buildLookupQuery($lastQueryTime)->all() as $live) {
$liveEvent = $this->unserializeEvent($live->serialized_data);
if ($liveEvent !== null && $this->checkVisibility($liveEvent)) {
$results['events'][$live->id] = $liveEvent->getData();
}
}
Yii::$app->response->format = 'json';
return $results;
}
/**
* Unserializes an event from database
*
* @param string serialized event
* @return LiveEvent the live event
*/
protected function unserializeEvent($serializedEvent)
{
try {
/* @var $liveEvent LiveEvent */
$liveEvent = unserialize($serializedEvent);
if (!$liveEvent instanceof LiveEvent) {
throw new Exception('Invalid live event class after unserialize!');
}
} catch (\Exception $ex) {
Yii::error('Could not unserialize live event! ' . $ex->getMessage(), 'live');
return null;
}
return $liveEvent;
}
/**
* Checks if the live event is visible for the current user.
*
* @param LiveEvent $liveEvent
* @return boolean is visible
*/
protected function checkVisibility(LiveEvent $liveEvent)
{
return true;
}
/**
* Creates a query to lookup live events.
*
* @param int $lastQueryTime the last lookup
* @return \yii\db\ActiveQuery the query
*/
protected function buildLookupQuery($lastQueryTime)
{
$query = Live::find();
// Public content e.g. following
$query->andWhere([
'and',
['IN', 'contentcontainer_id', $this->containerIds[Content::VISIBILITY_PUBLIC]],
['visibility' => Content::VISIBILITY_PUBLIC],
]);
// Private content e.g. space membership, friends
$query->orWhere([
'and',
['IN', 'contentcontainer_id', $this->containerIds[Content::VISIBILITY_PRIVATE]],
['IN', 'visibility', [Content::VISIBILITY_PRIVATE, Content::VISIBILITY_PUBLIC]],
]);
// Own content e.g. direct chat message, own profile
$query->orWhere([
'and',
['IN', 'contentcontainer_id', $this->containerIds[Content::VISIBILITY_NONE]],
['IN', 'visibility', [Content::VISIBILITY_PRIVATE, Content::VISIBILITY_PUBLIC, Content::VISIBILITY_NONE]],
]);
// Global messages
$query->orWhere(['IS', 'contentcontainer_id', new Expression('NULL')]);
$query->andWhere(['>=', 'created_at', $lastQueryTime]);
$query->limit($this->maxEventsByQuery);
return $query;
}
/**
* Returns the last query timestamp by the last GET parameter
* The parameter is validated, if invalid or empty the current time
* will be returned.
*
* @return int the validated last query time
*/
protected function getLastQueryTime()
{
$currentTime = time();
$last = (int) Yii::$app->request->get('last', $currentTime);
if (empty($last)) {
$last = time();
}
if ($last + $this->maxTimeDecay < $currentTime) {
Yii::warning('User requested too old live data! Requested: ' . $last . ' Now: ' . $currentTime, 'live');
$last = $currentTime;
}
return $last;
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live\driver;
use yii\base\Object;
use humhub\modules\live\components\LiveEvent;
/**
* Base driver for live event storage and distribution
*
* @since 1.2
* @author Luke
*/
abstract class BaseDriver extends Object
{
/**
* Sends a live event
*
* @param LiveEvent $liveEvent The live event to send
* @return boolean indicates the sent was successful
*/
abstract public function send(LiveEvent $liveEvent);
}

View File

@ -0,0 +1,38 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live\driver;
use humhub\modules\live\driver\BaseDriver;
use humhub\modules\live\components\LiveEvent;
use humhub\modules\live\models\Live;
/**
* Database driver for live events
*
* @since 1.2
* @author Luke
*/
class Database extends BaseDriver
{
/**
* @inheritdoc
*/
public function send(LiveEvent $liveEvent)
{
$model = new Live();
$model->serialized_data = serialize($liveEvent);
$model->created_at = time();
$model->visibility = $liveEvent->visibility;
$model->contentcontainer_id = $liveEvent->contentContainerId;
$model->created_at = time();
return $model->save();
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\live\jobs;
use Yii;
use humhub\modules\live\models\Live;
use humhub\components\queue\ActiveJob;
/**
* DatabaseCleanup removes old live events
*
* @since 1.2
* @author Luke
*/
class DatabaseCleanup extends ActiveJob
{
/**
* @inheritdoc
*/
public function run()
{
Live::deleteAll('created_at +' . Yii::$app->getModule('live')->maxLiveEventAge . ' < ' . time());
}
}

View File

@ -0,0 +1,27 @@
<?php
use yii\db\Migration;
class m170119_160740_initial extends Migration
{
public function up()
{
$this->createTable('live', [
'id' => $this->primaryKey(),
'contentcontainer_id' => $this->integer()->null(),
'visibility' => $this->integer(1)->null(),
'serialized_data' => $this->text()->notNull(),
'created_at' => $this->integer()->notNull()
]);
$this->addForeignKey('contentcontainer', 'live', 'contentcontainer_id', 'contentcontainer', 'id', 'CASCADE', 'CASCADE');
}
public function down()
{
echo "m170119_160740_initial cannot be reverted.\n";
return false;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace humhub\modules\live\models;
use humhub\modules\content\models\ContentContainer;
/**
* This is the model class for table "live".
*
* @property integer $id
* @property integer $contentcontainer_id
* @property integer $visibility
* @property string $serialized_data
* @property integer $created_at
*
* @property Contentcontainer $contentcontainer
*/
class Live extends \humhub\components\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return 'live';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['contentcontainer_id', 'visibility', 'created_at'], 'integer'],
[['serialized_data', 'created_at'], 'required'],
[['serialized_data'], 'string'],
[['contentcontainer_id'], 'exist', 'skipOnError' => true, 'targetClass' => ContentContainer::className(), 'targetAttribute' => ['contentcontainer_id' => 'id']],
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getContentcontainer()
{
return $this->hasOne(Contentcontainer::className(), ['id' => 'contentcontainer_id']);
}
}

View File

@ -0,0 +1,9 @@
{
"id": "live",
"name": "Live",
"description": "Live Core",
"keywords": [
"core"
],
"version": "1.0"
}