This commit is contained in:
Richard Dern
2022-01-12 00:35:37 +01:00
commit 400e3d01f1
1363 changed files with 57778 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
<?php
namespace App\Models\Traits\Document;
use App\Models\Bookmark;
use App\Models\Feed;
use Illuminate\Support\Facades\Http;
use SimplePie;
use Storage;
trait AnalysesDocument
{
// -------------------------------------------------------------------------
// ----| Properties |-------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Provides temporary access to response to analyzers.
*
* @var \Illuminate\Http\Client\Response
*/
protected $response;
/**
* Provides temporary access to document's body to analyzers.
*
* @var string
*/
private $body;
// -------------------------------------------------------------------------
// ----| Methods |----------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Begin document analysis.
*/
public function analyze()
{
// Don't bother if document isn't bookmarked anymore
if ($this->isOrphan() && $this->wasOrphanFor(config('cyca.maxOrphanAge.document'))) {
$this->delete();
return;
}
$this->fetchContent();
if (empty($this->response)) {
$this->checked_at = now();
$this->save();
return;
}
if ($homepage = $this->isFeed()) {
$this->convertToFeed($homepage);
return;
}
if ($this->existingDocumentsMerged()) {
return;
}
$this->runAnalyzers();
$this->checked_at = now();
$this->save();
}
/**
* Copy content of resource at document's URL in "local" storage.
*/
protected function fetchContent()
{
$storageRoot = $this->getStoragePath();
$bodyFilename = $storageRoot.'/body';
$responseFilename = $storageRoot.'/response.json';
$debugFilename = $storageRoot.'/debug';
Storage::put($debugFilename, null);
# $debugStream = fopen(storage_path('app/'.$debugFilename), 'w');
#
try {
$this->response = Http::withOptions(array_merge([
# 'debug' => env('APP_DEBUG') ? $debugStream : false,
], config('http_client')))->timeout(30)->get($this->url);
$this->body = $this->response->body();
} catch (\Exception $ex) {
report($ex);
# } finally {
# fclose($debugStream);
}
if (!$this->response) {
return;
}
$psrResponse = $this->response->toPsrResponse();
$responseData = [
'headers' => $this->response->headers(),
'protocol_version' => $psrResponse->getProtocolVersion(),
'response' => $this->response,
];
Storage::put($responseFilename, json_encode($responseData));
if ($this->response->ok()) {
Storage::put($bodyFilename, $this->body);
$this->mimetype = Storage::mimetype($bodyFilename);
}
$this->http_status_code = $this->response->status();
$this->http_status_text = $this->response->getReasonPhrase();
}
/**
* Quickly determine if document is, in fact, a feed, and return
* corresponding home page URL.
*
* @return bool|string Return feed's home page if really a feed, false otherwise
*/
protected function isFeed()
{
$client = new SimplePie();
$client->enable_cache(false);
$client->set_raw_data($this->body);
if ($client->init()) {
return urldecode($client->get_permalink());
}
return false;
}
/**
* Transform this document into a feed, by creating or using an existing
* document with provided homepage URL, creating or using an existing feed
* with current document's URL, linking both, updating any references to
* this document to point to the new document, and finally deleting this
* document.
*
* @param string $homepage
*/
protected function convertToFeed($homepage)
{
$document = self::firstOrCreate(['url' => $homepage]);
$feed = Feed::firstOrCreate(['url' => $this->url]);
if (!$document->feeds()->find($feed->id)) {
$document->feeds()->attach($feed);
}
Bookmark::where('document_id', $this->id)->update(['document_id' => $document->id, 'initial_url' => $homepage]);
$this->delete();
}
/**
* Find another document having the same real URL. If one is found, we will
* update all bookmarks to use the oldest document and delete this one.
*
* Returns true if documents have been merged.
*
* @return bool
*/
protected function existingDocumentsMerged()
{
$realUrl = urldecode((string) $this->response->effectiveUri());
if ($realUrl !== $this->url) {
$document = self::where('url', $realUrl)->first();
if ($document) {
Bookmark::where('document_id', $this->id)->update(['document_id' => $document->id]);
$allBookmarks = Bookmark::where('document_id', $this->id)->get()->groupBy('folder_id');
foreach ($allBookmarks as $folderId => $bookmarks) {
if ($bookmarks->count() > 1) {
array_shift($bookmarks);
foreach ($bookmarks as $bookmark) {
$bookmark->delete();
}
}
}
return true;
}
$this->url = $realUrl;
}
return false;
}
/**
* Select analyzers for this particular document then run them.
*/
protected function runAnalyzers()
{
if (array_key_exists($this->mimetype, config('analyzers'))) {
$this->launchAnalyzerFor($this->mimetype);
} else {
// In doubt, launch HtmlAnalyzer
$this->launchAnalyzerFor('text/html');
}
}
protected function launchAnalyzerFor($mimetype)
{
$className = config(sprintf('analyzers.%s', $mimetype));
$instance = new $className();
$instance->setDocument($this)->setBody($this->body)->setResponse($this->response)->analyze();
}
}

View File

@@ -0,0 +1,222 @@
<?php
namespace App\Models\Traits\Feed;
use App\Models\FeedItem;
use App\Models\FeedItemState;
use App\Notifications\UnreadItemsChanged;
use DomDocument;
use DOMXPath;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;
use SimplePie;
trait AnalysesFeed
{
// --------------------------------------------------------------------------
// ----| Properties |--------------------------------------------------------
// --------------------------------------------------------------------------
/**
* SimplePie client.
*
* @var SimplePie
*/
private $client;
// --------------------------------------------------------------------------
// ----| Methods |-----------------------------------------------------------
// --------------------------------------------------------------------------
/**
* Begin feed analysis.
*/
public function analyze()
{
// Don't bother if feed isn't attached to any document anymore
if ($this->isOrphan() && $this->wasOrphanFor(config('cyca.maxOrphanAge.feed'))) {
$this->delete();
return;
}
$this->prepareClient();
if (!$this->client->init()) {
$this->error = $this->client->error();
$this->checked_at = now();
$this->save();
return;
}
if ($this->client->subscribe_url() !== $this->url) {
$this->url = $this->client->subscribe_url();
}
$this->title = \App\Helpers\Cleaner::cleanupString($this->client->get_title(), true, true);
$this->description = \App\Helpers\Cleaner::cleanupString($this->client->get_description());
$this->checked_at = now();
$this->save();
$this->createItems($this->client->get_items());
}
/**
* Prepare the client.
*/
protected function prepareClient()
{
$this->client = new SimplePie();
Storage::disk('local')->makeDirectory($this->getStoragePath() . '/cache');
$this->client->force_feed(true);
$this->client->set_cache_location(storage_path('app/' . $this->getStoragePath() . '/cache'));
$this->client->set_feed_url($this->url);
}
/**
* Store feed items in database.
*
* @param array $items
*/
protected function createItems($items)
{
$toSync = $this->feedItems()->pluck('feed_items.id')->all();
$newItems = [];
foreach ($items as $item) {
$feedItem = FeedItem::where('hash', $item->get_id(true))->first();
if (!$feedItem) {
$feedItem = new FeedItem();
$feedItem->hash = $item->get_id(true);
$feedItem->title = \App\Helpers\Cleaner::cleanupString($item->get_title(), true, true);
$feedItem->url = $item->get_permalink();
$feedItem->description = $this->formatText($item->get_description(true));
$feedItem->content = $this->formatText($item->get_content(true));
$feedItem->published_at = $item->get_gmdate();
if (empty($feedItem->published_at)) {
$feedItem->published_at = now();
}
if ($feedItem->published_at->addDays(config('cyca.maxOrphanAge.feeditems'))->lt(now())) {
continue;
}
$feedItem->save();
$data = collect($item->data)->except([
'data',
'child',
]);
Storage::put($feedItem->getStoragePath() . '/data.json', $data->toJson());
}
if (!in_array($feedItem->id, $toSync)) {
$toSync[] = $feedItem->id;
}
$newItems[] = $feedItem;
}
$this->feedItems()->sync($toSync);
$this->createUnreadItems($newItems);
}
/**
* Apply various transformations to specified text.
*
* @param string $text
*
* @return string
*/
protected function formatText($text)
{
if (empty($text)) {
return;
}
$text = mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8');
if (empty($text)) {
return;
}
libxml_use_internal_errors(true);
$domDocument = new DomDocument('1.0', 'UTF-8');
$domDocument->loadHtml($text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
$xpath = new DOMXPath($domDocument);
$anchors = $xpath->query('//a');
foreach ($anchors as $anchor) {
$anchor->setAttribute('rel', 'noopener noreferrer');
$anchor->setAttribute('href', urldecode($anchor->getAttribute('href')));
}
$text = $domDocument->saveHTML();
return \App\Helpers\Cleaner::cleanupString($text);
}
protected function createUnreadItems($feedItems)
{
$ignoredByUsers = $this->ignored()->pluck('user_id')->all();
$documentsChanged = [];
$foldersChanged = [];
$usersToNotify = [];
foreach ($this->documents()->get() as $document) {
$folders = $document->folders()->get();
foreach ($folders as $folder) {
if (!array_key_exists($folder->id, $foldersChanged)) {
$foldersChanged[$folder->id] = $folder;
}
$users = $folder->group->activeUsers()->whereNotIn('users.id', $ignoredByUsers)->get();
foreach ($users as $user) {
if (!array_key_exists($user->id, $usersToNotify)) {
$usersToNotify[$user->id] = $user;
}
foreach ($feedItems as $feedItem) {
$feedItemStateData = [
'document_id' => $document->id,
'feed_id' => $this->id,
'user_id' => $user->id,
'feed_item_id' => $feedItem->id,
];
$feedItemState = FeedItemState::where('user_id', $user->id)
->where('feed_item_id', $feedItem->id)
->first();
if (!$feedItemState) {
FeedItemState::create($feedItemStateData);
if (!in_array($document->id, $documentsChanged)) {
$documentsChanged[] = $document->id;
}
}
}
}
}
}
Notification::send($usersToNotify, new UnreadItemsChanged(['folders' => $foldersChanged, 'documents' => $documentsChanged]));
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Models\Traits\Folder;
use App\Models\Group;
use App\Models\User;
use Arr;
/**
* Constructs tree representation.
*/
trait BuildsTree
{
/**
* Return user's folders as a flat tree.
*
* @return \Illuminate\Support\Collection
*/
public static function getFlatTreeFor(User $user, Group $group)
{
$tree = [];
$query = $group->folders()
->select([
'id',
'parent_id',
'type',
'title',
'position',
'group_id',
])
->withCount(['children', 'feedItemStates' => function ($query) use ($user) {
$query->where('is_read', false)->where('user_id', $user->id);
}])
->orderBy('parent_id', 'asc')->orderBy('position', 'asc')
->orderBy('title', 'asc');
$folders = $query->get();
$roots = $folders->collect()->filter(function ($folder) {
return $folder->parent_id === null;
});
foreach ($roots as $root) {
if ($root->type === 'unread_items') {
$root->feed_item_states_count = $group->feedItemStatesCount;
}
$branch = self::buildBranch($root, $folders, 0);
$tree[] = $branch;
}
return collect(Arr::flatten($tree));
}
/**
* Construct a flat array of sub-folders for specified parent folder.
*
* @param \App\Models\Folder $folder Parent folder for the branch
* @param \Illuminate\Support\Collection $allFolders All folders associated to the same user as the parent folder
* @param int $depth Current depth
*
* @return array
*/
public static function buildBranch(self $folder, $allFolders, $depth)
{
$folder->depth = $depth;
$branch = [];
$branch[] = $folder;
$subFolders = $allFolders->collect()->where('parent_id', $folder->id);
foreach ($subFolders as $subFolder) {
$branch[] = self::buildBranch($subFolder, $allFolders, $depth + 1);
}
return $branch;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models\Traits\Folder;
use App\Models\Group;
use App\Models\User;
trait CreatesDefaultFolders
{
// --------------------------------------------------------------------------
// ----| Constants |---------------------------------------------------------
// --------------------------------------------------------------------------
/**
* Position of the unread items folder in the folders hierarchy.
*/
private static $POSITION_UNREAD_ITEMS = 0;
/**
* Position of the root folder in the folders hierarchy.
*/
private static $POSITION_ROOT = 1;
// --------------------------------------------------------------------------
// ----| Methods |-----------------------------------------------------------
// --------------------------------------------------------------------------
/**
* Create default folders for specified group. This method should be called
* only once when group is created.
*
* @param \App\Models\User $user User creating the folders
*
* @throws \App\Exceptions\UserDoesNotExistsException
*/
public static function createDefaultFoldersFor(User $user, Group $group)
{
$group->folders()->saveMany([
new self([
'type' => 'unread_items',
'title' => 'Unread items',
'position' => self::$POSITION_UNREAD_ITEMS,
'user_id' => $user->id,
]),
new self([
'type' => 'root',
'title' => 'Root',
'position' => self::$POSITION_ROOT,
'user_id' => $user->id,
]),
]);
session([
sprintf('selectedFolder.%d', $group->id) => $group->folders()->ofType('root')->first()->id,
]);
}
}

50
app/Models/Traits/HasUrl.php Executable file
View File

@@ -0,0 +1,50 @@
<?php
namespace App\Models\Traits;
trait HasUrl
{
/**
* Return url in its idn form. Adds HTML markup to "syntax highlight" url
* elements.
*
* @return string
*/
public function getAsciiUrlAttribute()
{
if (empty($this->attributes['url'])) {
return null;
}
mb_internal_encoding('UTF-8');
$url = \urldecode($this->attributes['url']);
$host = \parse_url($url, PHP_URL_HOST);
$ascii = \idn_to_ascii($host);
$idnUrl = str_replace($host, $ascii, $url);
$finalUrl = '';
foreach (preg_split('//u', $idnUrl, null, PREG_SPLIT_NO_EMPTY) as $char) {
if (mb_strlen($char) != strlen($char)) {
$class = 'suspicious';
} elseif (preg_match('#[A-Z]#', $char)) {
$class = 'capital';
} elseif (preg_match('#[a-z]#', $char)) {
$class = 'letter';
} elseif (preg_match('#[0-9]#', $char)) {
$class = 'number';
} elseif (preg_match('#([:/.?$\#_=])#', $char)) {
$class = 'operator';
} elseif (empty($char)) {
$class = 'empty';
} else {
$class = 'other';
}
$finalUrl .= sprintf('<span class="%s">%s</span>', $class, $char);
}
return $finalUrl;
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Models\Traits\User;
use App\Models\Document;
use App\Models\FeedItemState;
use App\Models\Folder;
use App\Models\Group;
trait HasFeeds
{
// -------------------------------------------------------------------------
// ----| Relations |--------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Associated feed item state.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function feedItemStates()
{
return $this->hasMany(FeedItemState::class);
}
// -------------------------------------------------------------------------
// ----| Methods |----------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Mark feed items as read in specified folders, as an array of folder ids.
*
* @param array $folders
*/
public function markFeedItemsReadInFolders($folders, Group $group = null)
{
if (empty($group)) {
$group = $this->selectedGroup();
}
$unreadItemsFolder = $group->folders()->ofType('unread_items')->first();
$query = $group->folders()->with('documents:documents.id');
if (!in_array($unreadItemsFolder->id, $folders)) {
$query = $query->whereIn('folders.id', $folders);
}
$folders = $query->get();
$documentIds = $query->get()->pluck('documents')->flatten()->pluck('id')->unique();
$query = $this->feedItemStates()->unread()->whereIn('document_id', $documentIds);
$feedItemIds = $query->pluck('feed_item_id')->unique();
$query->update(['is_read' => true]);
return $this->countUnreadItems([
'folders' => $folders,
'documents' => $documentIds,
'updated_feed_items' => $feedItemIds,
]);
}
/**
* Mark feed items as read in specified documents, as an array of document
* ids.
*
* @param array $documents
*/
public function markFeedItemsReadInDocuments($documents)
{
$query = $this->feedItemStates()->unread()->whereIn('document_id', $documents);
$feedItemIds = $query->pluck('feed_item_id')->unique();
$query->update(['is_read' => true]);
return $this->countUnreadItems([
'documents' => $documents,
'updated_feed_items' => $feedItemIds,
]);
}
/**
* Mark feed items as read in specified feeds, as an array of feed ids.
*
* @param array $feeds
*/
public function markFeedItemsReadInFeeds($feeds)
{
abort(404);
}
/**
* Mark specified feed items as read, as an array of feed item ids.
*
* @param array $feedItems
*/
public function markFeedItemsRead($feedItems)
{
$query = $this->feedItemStates()->unread()->whereIn('feed_item_id', $feedItems);
$feedItemIds = $query->pluck('feed_item_id')->unique();
$documentIds = $query->pluck('document_id')->unique();
$query->update(['is_read' => true]);
return $this->countUnreadItems([
'documents' => $documentIds,
'updated_feed_items' => $feedItemIds,
]);
}
/**
* Calculate unread items counts for current user. The $for array allows to
* be more specific by specifying ids for feed_items, documents or folder
* to re-count for in particular.
*
* This method returns an array containing the id of each document and
* folder along with corresponding unread items count, as well as a total
* of unread items count for each group and folders of type "unread_items".
*
* @param array $for
*
* @return array
*/
public function countUnreadItems($for)
{
$data = [];
if (empty($for['documents'])) {
if (!empty($for['folders'])) {
$for['documents'] = Folder::listDocumentIds(collect($for['folders'])->pluck('id')->all(), $this->selectedGroup());
}
}
$countPerDocument = $this->feedItemStates()->unread()->whereIn('document_id', $for['documents'])->get()->countBy('document_id')->all();
$countPerGroup = [];
if (!empty($for['documents'])) {
foreach ($for['documents'] as $id) {
if (!array_key_exists($id, $countPerDocument)) {
$countPerDocument[$id] = 0;
}
}
if (empty($for['folders'])) {
$folderIds = Document::with('folders')->find($for['documents'])->pluck('folders')->flatten()->pluck('id');
$for['folders'] = Folder::find($folderIds);
}
}
foreach ($for['folders'] as $folder) {
$countPerFolder[$folder->id] = $this->feedItemStates()->unread()->whereIn('document_id', $folder->getDocumentIds())->count();
}
foreach ($this->groups as $group) {
$totalUnreadItems = $group->getUnreadFeedItemsCountFor($this);
$unreadItemsFolderId = $group->folders()->ofType('unread_items')->first()->id;
$countPerFolder[$unreadItemsFolderId] = $totalUnreadItems;
$countPerGroup[$group->id] = $totalUnreadItems;
}
return [
'documents' => $countPerDocument,
'folders' => $countPerFolder,
'groups' => $countPerGroup,
'updated_feed_items' => !empty($for['updated_feed_items']) ? $for['updated_feed_items'] : null,
];
}
}

View File

@@ -0,0 +1,244 @@
<?php
namespace App\Models\Traits\User;
use App\Models\Folder;
use App\Models\Group;
use App\Models\Permission;
trait HasFolders
{
// -------------------------------------------------------------------------
// ----| Properties |-------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Currently selected folder in each group.
*
* @var array
*/
protected $selectedFolders = [];
// -------------------------------------------------------------------------
// ----| Relations |--------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Folders owned (created) by this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function createdFolders()
{
return $this->hasMany(Folder::class);
}
// -------------------------------------------------------------------------
// ----| Methods |----------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Return user's folders as a flat tree.
*
* @return \Illuminate\Support\Collection
*/
public function getFlatTree(Group $group = null)
{
if (empty($group)) {
$group = $this->selectedGroup();
}
return Folder::getFlatTreeFor($this, $group);
}
/**
* Return current user's selected folder in specified group.
*
* @return null|\App\Models\Group
*/
public function selectedFolder(Group $group = null)
{
if (empty($group)) {
$group = $this->selectedGroup();
}
if (empty($this->selectedFolders[$group->id])) {
$this->selectedFolders[$group->id] = $this->fetchSelectedFolder($group);
}
return $this->selectedFolders[$group->id];
}
/**
* Remember user's selected folder in specified (or current) group.
*
* @param \App\Models\Folder $folder
*
* @return array
*/
public function setSelectedFolder(Folder $folder = null, Group $group = null)
{
if (empty($group)) {
$group = $this->selectedGroup();
}
$this->selectedFolders[$group->id] = $folder;
$this->storeSelectedFolder($group);
}
/**
* Return specified folder's expanded/collapsed state in specified group.
*
* @return bool
*/
public function getFolderExpandedState(Folder $folder = null, Group $group = null)
{
if (empty($folder)) {
$folder = $this->selectedFolder($group);
}
$key = $this->folderExpandedStoreKey($folder, $group);
return (bool) cache($key, false);
}
/**
* Set specified folder's expanded/collapsed state in specified group.
*
* @param bool $expanded
* @param bool $recursive Apply new state recursively
*/
public function setFolderExpandedState($expanded, Folder $folder = null, Group $group = null, $recursive = false)
{
$key = $this->folderExpandedStoreKey($folder, $group);
cache()->forever($key, (bool) $expanded);
if ($recursive) {
foreach ($folder->children as $subFolder) {
$this->setFolderExpandedState($expanded, $subFolder, $group, $recursive);
}
}
}
/**
* Make sure specified folder's ancestor are all expanded, so the folder is
* visible.
*
* @param \App\Models\Folder $folder
*/
public function ensureAncestorsAreExpanded(Folder $folder = null)
{
if (empty($folder)) {
$folder = $this->selectedFolder();
}
while ($folder = $folder->parent) {
$this->setFolderExpandedState(true, $folder);
}
}
/**
* Define this user permission for specified folder and ability.
*
* @param null|mixed $ability
* @param mixed $grant
*/
public function setFolderPermissions(Folder $folder, $ability = null, $grant = false)
{
$permissions = $this->permissions()->where('folder_id', $folder->id)->first();
if (!$permissions) {
$permissions = new Permission();
$permissions->user()->associate($this);
$permissions->folder()->associate($folder);
}
$defaultPermissions = $folder->getDefaultPermissions();
if ($ability !== null) {
$permissions->{$ability} = $grant;
} else {
foreach ($defaultPermissions as $defaultAbility => $defaultGrant) {
$permissions->{$defaultAbility} = $defaultGrant;
}
}
$permissions->save();
}
/**
* Return the key to access reminded selected folder for this user and
* specified group.
*
* @return string
*/
protected function selectedFolderStoreKey(Group $group)
{
if (empty($group)) {
$group = $this->selectedGroup();
}
return sprintf('selectedFolder.%d.%d', $this->id, $group->id);
}
/**
* Return stored user's selected folder in specified group.
*
* @return \App\Models\Folder
*/
protected function fetchSelectedFolder(Group $group)
{
$key = $this->selectedFolderStoreKey($group);
if (cache()->has($key)) {
$folder = $group->folders()->find(cache($key));
if (!empty($folder)) {
return $folder;
}
}
return $group->folders()->ofType('root')->first();
}
/**
* Save user's selected folder in specified group.
*/
protected function storeSelectedFolder(Group $group)
{
if (empty($group)) {
$group = $this->selectedGroup();
}
$key = $this->selectedFolderStoreKey($group);
$folder = $this->selectedFolders[$group->id];
if (!empty($folder)) {
cache()->forever($key, $folder->id);
} else {
cache()->forget($key);
}
}
/**
* Return the key to get specified folder's expanded/collased state in
* specified group.
*
* @return string
*/
protected function folderExpandedStoreKey(Folder $folder = null, Group $group = null)
{
if (empty($group)) {
$group = $this->selectedGroup();
}
if (empty($folder)) {
$folder = $this->selectedFolder($group);
}
return sprintf('folderExpandedState.%d.%d.%d', $this->id, $group->id, $folder->id);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Models\Traits\User;
use App\Models\Group;
trait HasGroups
{
// -------------------------------------------------------------------------
// ----| Properties |-------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Currently selected group.
*
* @var \App\Models\Group
*/
protected $selectedGroup;
// -------------------------------------------------------------------------
// ----| Relations |--------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Groups created by this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function createdGroups()
{
return $this->hasMany(Group::class);
}
/**
* Associated groups.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function groups()
{
return $this->belongsToMany(Group::class, 'user_groups')->withPivot(['status', 'position']);
}
// -------------------------------------------------------------------------
// ----| Methods |----------------------------------------------------------
// -------------------------------------------------------------------------
/**
* Create and return user's primary group.
*
* @return \App\Models\Group
*/
public function createOwnGroup()
{
$group = Group::create([
'name' => $this->name,
'invite_only' => true,
'user_id' => $this->id,
]);
$this->groups()->attach($group, [
'status' => Group::$STATUS_OWN,
]);
return $group;
}
/**
* Return current user's selected group.
*
* @return \App\Models\Group
*/
public function selectedGroup()
{
if ($this->selectedGroup === null) {
$this->selectedGroup = $this->fetchSelectedGroup();
}
return $this->selectedGroup;
}
/**
* Remember user's selected group.
*/
public function setSelectedGroup(Group $group)
{
$this->selectedGroup = $group;
$this->storeSelectedGroup();
return $this->getFlatTree();
}
/**
* Return a list of groups user is active in.
*
* @return \Illuminate\Support\Collection
*/
public function listActiveGroups()
{
$userId = $this->id;
return $this->groups()
->select([
'groups.id',
'groups.name',
])->active()
->orderBy('position')->get();
}
/**
* Update group status for current user. If group is not associated with
* user, association will be made. It won't change user group if it is
* marked as being owned or created by current user, unless $force is true.
*
* @param string $newStatus
* @param mixed $force
*/
public function updateGroupStatus(Group $group, $newStatus, $force = false)
{
$userGroup = $this->groups()->find($group->id);
if ($userGroup) {
if (in_array($userGroup->pivot->status, [Group::$STATUS_OWN, Group::$STATUS_CREATED]) && !$force) {
return;
}
$this->groups()->updateExistingPivot($group->id, [
'status' => $newStatus,
]);
} else {
$this->groups()->save($group, [
'status' => $newStatus,
]);
}
}
/**
* Return the key to access reminded selected group for this user.
*
* @return string
*/
protected function selectedGroupStoreKey()
{
return sprintf('selectedGroup.%d', $this->id);
}
/**
* Return stored user's selected group.
*
* @return \App\Models\Group
*/
protected function fetchSelectedGroup()
{
$key = $this->selectedGroupStoreKey();
if (cache()->has($key)) {
$group = $this->groups()->active()->find(cache($key));
if (!empty($group)) {
return $group;
}
}
return $this->groups()->own()->first();
}
/**
* Save user's selected group.
*/
protected function storeSelectedGroup()
{
$key = $this->selectedGroupStoreKey();
cache()->forever($key, $this->selectedGroup->id);
}
}