«Лента друзей»: разрабатываем компонент «1С-Битрикс»
Сергей Лещенко (sle_e@mail.ru)
Прошло время, когда социальные сети считались бесполезной для бизнеса игрушкой. Все больше компаний осознают ценность данного инструмента. Появился даже специальный термин – Enterprise 2.0, обозначающий комплекс методов и подходов, позволяющих применить технологии Web 2.0 для решения типичных корпоративных задач. Рынок Web-разработок не мог не отреагировать на такие тенденции, и сегодня сложно найти систему управления сайтом (CMS), которая не предоставляла бы возможность создавать социальные сети.
Компания «1С-Битрикс» в конце 2008 г. тоже выпустила модуль «Социальные сети» для своего пакета «1С-Битрикс: Управление сайтом» (БУС), который позволяет организовывать сообщества (группы), устанавливать «дружеские отношения», вести черные списки, распределять права доступа. Пользователь или группа получают развитый набор служб: блоги, фотогалереи с массовой загрузкой фотографий, рейтингами и обсуждениями, форумы, онлайновая переписка и др. Это весьма мощная разработка, более подробный обзор которой можно найти в PC Magazine/RE, 1/2009 (ознакомиться с продуктом «вживую» можно на www.pcmag.ru/club).

Модуль «Лента друзей» на сайте www.pcmag.ru/club
В этом же обзоре речь пойдет о том, чего недостает модулю «Социальная сеть» – о так называемой «ленте друзей». С точки зрения архитектуры и принципов организации данных социальный модуль БУС похож на популярную сеть FaceBook (в России более известен ее клон «ВКонтакте»). Однако в русскоязычном сегменте Сети не меньшей популярностью пользуется служба «Живой Журнал», интерфейс которой отличается от интерфейсов FaceBook и его клонов (порой радикально). Среди прочих различий в «Живом Журнале» имеется модуль «Лента друзей» (чаще «френдлента»).
Организация материала в виде «Ленты друзей» имеет ряд преимуществ. Она удобна для быстрого просмотра за чашкой утреннего кофе (или вечернего пива) новых поступлений в дружественные дневники. В БУС есть подсистема «лог обновлений», но это не совсем удачная замена. Слишком много несущественной информации и ссылок, по которым приходится лишний раз кликать. Вроде бы мелочи, но именно мелочи определяют впечатление пользователя от сайта. В общем недоработка налицо, и мы постараемся ее исправить.
Итак, постановка задачи: необходим компонент, имитирующий в БУС «Ленту друзей». «Лента друзей» конкретного пользователя как минимум должна аккумулировать новые записи из блогов друзей (полностью или в виде анонсов) и групп, в которых состоит владелец ленты. В «Ленте друзей» групп должны собираться новые записи из блогов ее участников. И, кстати, следует отметить существенное отличие блогов «Битрикс» от блогов «Живого журнала»: в «Битрикс» пользователь может публиковать сообщения не только в своем блоге и блогах групп, в которых он состоит, но и в других блогах, если владелец блога разрешил это делать.
Сначала разберемся с внутренними объектами (или сущностями) социальной сети «пользователь» и «группа»: какие свойства они имеют и как эти свойства влияют на результат. Заметим, что в обзоре будут затронуты только вопросы, касающиеся функциональности «Ленты друзей» для блогов; форумы, фотогалереи и т. д. мы во внимание не принимаем (принципы останутся теми же, а объем статьи увеличится существенно). Материал излагается в предположении, что читатель знаком с основами разработки на PHP и с API системы «1С-Битрикс: Управление сайтом».
«Пользователь»
Из свойств объекта пользователя нас интересуют настройки, которые можно задать на персональной странице социальной сети в формах настроек приватности и прав доступа к блогам. Среди настроек приватности для нашей задачи важны два поля: «кто может смотреть друзей» и «кто может смотреть мои группы». Эти поля влияют на включение в ленту записей из блогов друзей и из блогов групп, в которых состоит владелец. Причем записи из блогов друзей или групп должны включаться только, если право на просмотр имеют все пользователи. Иными словами, если Иван зайдет на страницу «Ленты друзей» Петра, а Петр разрешает просмотр своих групп только друзьям, то Иван не должен видеть записи из блогов групп Петра, даже если между ними установлены дружеские связи (в соответствующем интерфейсе модуля «Социальная сеть»).

Рис. 1. Формы настроек прав доступа объекта «пользователь»
В настройках блогов нас интересует параметр «кто может просматривать сообщения». От него зависит, войдут ли записи блога в ленту друзей пользователя и групп, в которых он состоит, причем они будут включаться только в случае, если права на просмотр разрешены всем пользователям. Например, если Петр разрешает читать записи своего блога только друзьям и друзьям друзей, то они не попадут ни в одну из лент.
Если блоги вообще отключены, следует автоматически исключить все записи, оставленные ранее в блоге пользователя, из всех лент всех друзей.
«Группа»
Аналогично из свойств групп нас интересуют настройки, которые будут заданы в формах редактирования группы и настроек блогов группы.
Параметр «группа видима всем посетителям» должен учитываться при включении записей блога группы в ленты ее участников. В настройках блогов нас интересует параметр «кто может просматривать сообщения». От него зависит, войдут ли записи блога в ленты участников группы, причем они будут включаться только, если просмотр будет разрешен всем пользователям.
Идеи и принципы
Теперь подберем функции, которые будут использоваться для выборки записей блогов с учетом прав доступа и связей пользователей внутри социальной сети. Сначала выясним, не заложены ли в системе уже готовые решения, на базе которых можно сформировать «Ленту друзей» или хотя бы нечто похожее. Первое, что приходит на ум – если есть компонент ведения журнала событий, то нет ли в нем подходящих функций? Изучаем список свойств и методов (или код компонента) и видим, что для получения списка событий в нем используется функция CSocNetLogEvents::GetUserLogEvents():
CSocNetLogEvents:: GetUserLogEvents(int userID, array arFilter = Array());
Функция возвращает структуру, содержащую список событий социальной сети по фильтру arFilter. Параметр userID – идентификатор пользователя, в массиве arFilter сохраняется набор параметров для выборки данных из БД. Он имеет структуру вида:
array(«фильтруемое поле»=>"значение фильтра" [, ...])
где фильтруемое поле может принимать специфичные значения: ENTITY_TYPE (тип сущности социальной сети, U – пользователь или G – группа), ENTITY_ID – идентификатор сущности социальной сети, EVENT_ID – идентификатор инициатора события (сигнатуры blog, photo, forum или system), LOG_DATE_DAYS – количество дней для выборки журнала. Фактически количество дней ограничено временем жизни журнала (неделя), очистка выполняется агентом CSocNetLog::ClearOldAgent().
Функция возвращает структуру, содержащую идентификатор события, его тип, дату и время, текстовое описание, заголовок, ссылку на страницу, имеющую отношение к событию, идентификатор модуля, где событие было инициировано, набор данных, определяющих права доступа, данные о пользователе и др. К сожалению, ознакомившись с результатами работы этой функции, напрашивается вывод, что она не совсем подходит.
Во-первых, функция CSocNetLogEvents::GetUserLogEvents() работает только в контексте пользователя, а нам нужны еще и данные групп. Во-вторых, не передаются типы записей, т. е. записи добавления комментариев и сообщений ничем не отличаются друг от друга, идентифицировать их «по-человечески» не получится. Не возвращаются ID блогов и сообщений, а они нам нужны, чтобы сформировать ссылки. Время жизни записи «Ленты друзей» принудительно ограничивается агентом модуля (это поправимо, но требует вмешательства в обработку соответствующего события, чего делать не хотелось бы). Возможно, в будущих версиях «Социальной сети» разработчики «Битрикс» предпримут какие-то меры, но сегодня ситуация такова.

Рис. 2. Формы настроек прав доступа объекта «группа»
Поиск других подходящих функций в модуле тоже не дал результата. Остается два пути. Первый – самостоятельно реализовать нужный нам метод. С точки зрения системного подхода, это более правильное решение, но... Здесь могут возникнуть проблемы обратной совместимости. Модуль довольно новый, мало ли что может измениться в недалеком будущем. Второй – получить необходимые данные, скомбинировав результаты нескольких стандартных методов, благо изучение исходных текстов модуля дает четкое представление, как это сделать. Вот и попробуем.
Общая идея выглядит так. Для объектов типа «пользователь» мы получаем списки идентификаторов пользователей-друзей и идентификаторов групп, для сущности «группа» – список идентификаторов пользователей-участников. Далее из списков идентификаторов пользователей исключаем тех, чьи записи не должны войти в «Ленту друзей». Аналогично обрабатываем список идентификаторов групп. Какие именно записи из блогов не должны включаться в ленты, мы выяснили при разборе свойств объектов «пользователь» и «группа». На основе созданных списков пользователей и групп составляем списки идентификаторов блогов, извлекаем необходимые записи и передаем их в шаблон для вывода на Web-странице, откуда был вызван компонент.
Для получения списка записей в блогах будем использовать функцию CBlogPost::GetList(). На первый взгляд, больше подходит функция CBlogUser::GetUserFriendsList(), которая специально предназначена для формирования списка сообщений друзей пользователя, но, к сожалению, она опирается на связи пользователей внутри модуля блогов, а не социальной сети, а это значит, что она нам не подходит. Функция же CBlogPost::GetList() возвращает список записей, соответствующих заданному фильтру, с возможностью сортировки и разбивки на страницы.
Включение в «Ленты» записей собственных блогов (для группы или пользователя) будем рассматривать как необязательное. Для ускорения обработки запросов и уменьшения нагрузки на сервер информацию, независящую от конкретного обратившегося посетителя, будем кэшировать. Итак, основные входящие параметры:
• $arParams['USER_ID'] – ID пользователя для построения ленты;
• $arParams['GROUP_ID'] – ID группы социальной сети для построения ленты;
• $arParams['BLOG_GROUP_ID'] – ID группы блогов, к которой принадлежат все блоги социальной сети;
• $arParams['INC_SELF_MESSAGES'] – включать ли в ленту сообщения из блога сущности.
Определение базовых прав пользователя, которые заодно будут использоваться как дополнительный идентификатор кэша (см. листинг 1). Обратим внимание на структуру $arResult['CURRENT_ACCESS']. В ней задаются права, доступные текущему пользователю по умолчанию. Далее получаем его идентификатор и определяем реальное состояние прав (листинг 2).
Листинг 1
// Определим права текущего пользователя (который в данный
// момент смотрит "Ленту друзей")
$arResult = array();
// $arResult['ENTITY_TYPE'] – тип ленты,
// U – "лента пользователя", G – "лента группы"
$arResult['ENTITY_TYPE'] = $arParams['USER_ID'] >
0 ? 'U' : 'G';
// $arResult['ENTITY_ID'] – ID пользователя или группы
// (в зависимости от типа ленты)
$arResult['ENTITY_ID'] = $arResult['ENTITY_TYPE'] ==
'U' ? $arParams['USER_ID'] : $arParams['GROUP_ID'];
$arResult['CURRENT_ACCESS'] = array(
'canViewUserFriends' => false, //можно ли смотреть
// друзей пользователя
'canViewUserGroups' => false, // можно ли смотреть
// группы пользователя
'canViewUserSelfMessages' => false, // можно ли смотреть
// собственные записи
// блога пользователя
'canViewGroup' => false, // видима ли группа
'canViewGroupSelfMessages' => false // можно ли смотреть
// собственные записи
// блога группы
);
Листинг 2
$isModuleAdmin = CSocNetUser::IsCurrentUserModuleAdmin();
$currentUserID = $GLOBALS['USER']->GetID();
if($arResult['ENTITY_TYPE'] == 'G') {
// для ленты групп проверим право на доступ к ней
$arResult['GROUP_INFO'] = CSocNetGroup::GetByID($arResult['ENTITY_ID']);
$arCurrentUserPerms = CSocNetUserToGroup::InitUserPerms($currentUserID, $arResult['GROUP_INFO'], $isModuleAdmin);
$arResult['CURRENT_ACCESS']['canViewGroup'] = $arCurrentUserPerms['UserCanViewGroup'];
unset($arCurrentUserPerms);
if($arParams['INC_SELF_MESSAGES'] && $arResult['CURRENT_ACCESS']['canViewGroup']) {
$arResult['CURRENT_ACCESS']['canViewGroupSelfMessages'] = CSocNetFeaturesPerms::CanPerformOperation($currentUserID,
SONET_ENTITY_GROUP, $arResult['ENTITY_ID'], 'blog', 'view_post', $isModuleAdmin);
}
} else {
//для ленты пользователя проверим доступ к ней текущего пользователя
$arCurrentUserPerms = CSocNetUserPerms::InitUserPerms($currentUserID, $arResult['ENTITY_ID'], $isModuleAdmin);
$arResult['CURRENT_ACCESS']['canViewUserFriends'] = $arCurrentUserPerms['Operations']['viewfriends'];
$arResult['CURRENT_ACCESS']['canViewUserGroups'] = $arCurrentUserPerms['Operations']['viewgroups'];
if($arParams['INC_SELF_MESSAGES']) {
// если не нужно включать в ленту сообщения из своего блога, то и проверять ID пользователя не будем
// (экономим на количестве кэш-файлов)
$arResult['CURRENT_ACCESS']['canViewUserSelfMessages'] = $currentUserID == $arResult['ENTITY_ID'];
}
unset($arCurrentUserPerms);
}
unset($currentUserID, $isModuleAdmin);
Значение $arResult['CURRENT_ACCESS'] и будет дополнительным идентификатором кэша:
if($this->StartResultCache(false, array($arNavigation,
$arResult['CURRENT_ACCESS']), $cachePath)) {
//код компонента
//подключение шаблона сохранения результатов в кэш.
$this->IncludeComponentTemplate();
}
где $arNavigation – массив управляющих параметров для постраничной навигации, $cachePath – путь для хранения кэш-файла. Определение идентификаторов друзей и групп для получения по ним ID блогов (исходный текст несколько сокращен для обозримости, листинг 3).
Листинг 3
// массив идентификаторов пользователей, из блогов которых
// будут выбираться записи
$arEntityUsersID = array();
// массив идентификаторов групп, из блогов которых будут
// выбираться записи
$arEntityGroupsID = array();
if($arResult['ENTITY_TYPE'] == 'U') {
//лента пользователя
//можно ли смотреть друзей для данного пользователя,
// доступны ли вообще блоги и друзья
$getFriends = false;
if($arResult['CURRENT_ACCESS']['canViewUserFriends']) {
$getFriends = CPTK_SocialNetwork::IsAllowedFeature
(SONET_ENTITY_USER, 'blog') &&
CSocNetUser::IsFriendsAllowed();
}
// можно ли смотреть группы для данного пользователя
// и доступны ли вообще блоги для групп
if($arResult['CURRENT_ACCESS']['canViewUserGroups']) {
$getGroups = CPTK_SocialNetwork::IsAllowedFeature
(SONET_ENTITY_GROUP, 'blog');
}
Определим ID друзей пользователя (листинг 4).
Листинг 4
if($getFriends) {
$arOrderUR = array();
$arFilterUR = array(
'RELATION' => SONET_RELATIONS_FRIEND,
'USER_ID' => $arResult['ENTITY_ID'],
);
$arGroupByUR = false;
$arNavigationUR = false;
$arSelectFieldsUR = array(
'FIRST_USER_ID',
'SECOND_USER_ID'
);
$rsItems = CSocNetUserRelations::GetList($arOrderUR,
$arFilterUR, $arGroupByUR, $arNavigationUR,
$arSelectFieldsUR);
while($arItem = $rsItems->Fetch())
{
$id_ = $arItem['FIRST_USER_ID'] ==
$arResult['ENTITY_ID'] ?
intval($arItem['SECOND_USER_ID']) :
intval($arItem['FIRST_USER_ID']);
$arEntityUsersID[$id_] = $id_;
}
unset($rsItems, $id_, $arOrderUR, $arGroupByUR,
$arNavigationUR, $arSelectFieldsUR);
}
Код исключения «закрытых» пользователей вынесен в конец модуля (он общий для двух типов лент). Схема довольно проста: добавим ID владельца в массив $arEntityUsersID, если включен режим вывода сообщений из блога владельца ленты. Здесь же важно заполнить записью массив $arEntityUsersID на случай, если пользователь запретил смотреть своих друзей (листинг 5).
Листинг 5
if($arParams['INC_SELF_MESSAGES']) {
$arEntityUsersID[$arResult['ENTITY_ID']] =
$arResult['ENTITY_ID'];
}
// Определим ID видимых и НЕзакрытых групп пользователя
if($getGroups)
{
$arOrderGR = array();
$arFilterGR = array(
'USER_ID' => $arResult['ENTITY_ID'],
'<=ROLE' => SONET_ROLES_USER,
'GROUP_SITE_ID' => SITE_ID,
'GROUP_ACTIVE' => 'Y',
'GROUP_VISIBLE' => 'Y'
);
$arGroupByGR = false;
$arNavigationGR = false;
$arSelectFieldsGR = array(
'GROUP_ID',
'GROUP_NAME'
);
$rsItems = CSocNetUserToGroup::GetList($arOrderGR,
$arFilterGR, $arGroupByGR, $arNavigationGR,
$arSelectFieldsGR);
while($arItem = $rsItems->GetNext(false, false)) {
$id_ = intval($arItem['GROUP_ID']);
$arEntityGroupsID[$id_] = array(
'ID' => $id_,
'NAME' => $arItem['GROUP_NAME']
);
}
unset($rsItems, $id_, $arOrderGR, $arGroupByGR,
$arNavigationGR, $arSelectFieldsGR);
if(!empty($arEntityGroupsID)) {
//Определим группы, у которых блоги имеют статус
// приватных, и исключим их из списка
$arExceptGroupEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_GROUP, 'blog',
'view_post', array('!ROLE' => SONET_ROLES_ALL),
array('!ROLE' => SONET_ROLES_ALL));
$arTmp = array_intersect_key($arExceptGroupEntity,
$arEntityGroupsID);
unset($arExceptGroupEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key) {
unset($arEntityGroupsID[$key]);
}
}
unset($arTmp);
//Определим группы, в которых вообще отключены блоги,
// и исключим их из списка
$arExceptGroupEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_GROUP, 'blog',
'view_post', array('FEATURE_ACTIVE' => 'N'),
array('FEATURE_ACTIVE' => 'N'));
$arTmp = array_intersect_key($arExceptGroupEntity,
$arEntityGroupsID);
unset($arExceptGroupEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key) {
unset($arEntityGroupsID[$key]);
}
}
unset($arTmp);
}
}
Кроме того, необходимо обработать ситуацию, когда мы имеем дело с лентой для группы. Определяем, открыта ли группа для просмотра текущему пользователю, и вообще могут ли пользователи иметь блоги (листинг 6).
Листинг 6
$getMembers = false;
if($arResult['CURRENT_ACCESS']['canViewGroup']) {
$getMembers = CPTK_SocialNetwork::IsAllowedFeature
(SONET_ENTITY_USER, 'blog');
}
// Определим ID участников группы
if($getMembers) {
$arOrderGM = array();
$arFilterGM = array(
'<=ROLE' => SONET_ROLES_USER,
'GROUP_ID' => $arResult['ENTITY_ID'],
);
$arGroupByGM = false;
$arNavigationGM = false;
$arSelectFieldsGM = array(
'USER_ID'
);
$rsItems = CSocNetUserToGroup::GetList($arOrderGM,
$arFilterGM, $arGroupByGM, $arNavigationGM,
$arSelectFieldsGM);
while($arItem = $rsItems->Fetch()) {
$id_ = intval($arItem['USER_ID']);
$arEntityUsersID[$id_] = $id_;
}
unset($rsItems, $id_, $arOrderGM, $arGroupByGM,
$arNavigationGM, $arSelectFieldsGM);
// код исключения "закрытых" пользователей вынесен в конец
// модуля (он общий для двух типов лент)
}
// Добавим ID группы в $arEntityGroupsID, если включен вывод
// сообщений из блога группы
if($arParams['INC_SELF_MESSAGES'] &&
$arResult['CURRENT_ACCESS']['canViewGroup'] &&
$arResult['CURRENT_ACCESS']['canViewGroupSelfMessages']) {
//если блоги в группе не отключены
if(CSocNetFeatures::IsActiveFeature(SONET_ENTITY_GROUP,
$arResult['ENTITY_ID'], 'blog')) {
$arEntityGroupsID[$arResult['ENTITY_ID']] = array(
'ID' => $arResult['ENTITY_ID'],
'NAME' => $arResult['GROUP_INFO']['NAME']
);
}
}
Чуть выше мы отметили, что код исключения «закрытых» пользователей вынесен в конец компонента. Теперь пришло время определить пользователей, которые запретили просмотр блогов. Уберем их из списка (раз уж они сами этого хотят; листинг 7).
Листинг 7
if(!empty($arEntityUsersID)) {
$arExceptUserEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_USER, 'blog',
'view_post', array('!ROLE' => SONET_RELATIONS_TYPE_ALL),
array('!ROLE' => SONET_RELATIONS_TYPE_ALL));
$arTmp = array_intersect_key($arExceptUserEntity,
$arEntityUsersID);
unset($arExceptUserEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key)
{
unset($arEntityUsersID[$key]);
}
}
unset($arTmp);
// Если активный пользователь – владелец ленты, повторно
// включим его ID, даже если он закрыл свой блог – ему
// заведомо можно просматривать свои записи
if($arParams['INC_SELF_MESSAGES'] && $arResult
['CURRENT_ACCESS']['canViewUserSelfMessages']) {
$arEntityUsersID[$arResult['ENTITY_ID']] =
$arResult['ENTITY_ID'];
}
// Определим пользователей, которые вообще отключили свои
// блоги, и исключим их из списка
$arExceptUserEntity = CPTK_SocialNetwork::
GetByRoleFeaturesIdArray(SONET_ENTITY_USER, 'blog',
'view_post', array('FEATURE_ACTIVE' => 'N'),
array('FEATURE_ACTIVE' => 'N'));
$arTmp = array_intersect_key($arExceptUserEntity,
$arEntityUsersID);
unset($arExceptUserEntity);
if(!empty($arTmp)) {
foreach($arTmp as $key) {
unset($arEntityUsersID[$key]);
}
}
unset($arTmp);
}
Здесь надо обратить внимание на две дополнительные функции, которые не входят в стандартный API ядра «1С-Битрикс: Управление сайтом»: CPTK_SocialNetwork::
GetByRoleFeaturesIdArray() и CPTK_SocialNetwork::IsAllowedFeature(). Они представляют собой часть библиотеки автора, их текст здесь не приводится (при желании библиотеку можно запросить у автора статьи). Первая, CPTK_SocialNetwork::GetByRoleFeaturesIdArray(), возвращает массив идентификаторов объектов по типу объекта, сигнатуре, функциональности и операциям (с возможностью установки дополнительного фильтра). Функция имеет внутреннее кэширование результатов, в ее основе лежит метод CSocNetFeaturesPerms::GetList(). Вторая, CPTK_SocialNetwork::IsAllowedFeature(), проверяет, доступна ли для заданного объекта затребованная возможность (это небольшая оптимизация часто выполняемой операции, опытному разработчику не составит труда реализовать ее самостоятельно).
На этом этапе возникает проблема: при организации группы социальной сети система не создает соответствующие записи в таблицах БД, где хранятся настройки прав доступа к заданным функциям. Эти записи автоматически создаются только после первого изменения прав доступа, до того они определяются системой, средствами PHP. Причем по умолчанию используется режим не «разрешено всем», а «разрешено только участникам». Следовательно, попытка выбрать все записи, у которых в поле ROLE не установлено значение константы SONET_RELATIONS_TYPE_ALL («разрешено всем»; в нашем случае это и будет вызов CPTK_SocialNetwork::GetByRoleFeaturesIdArray() с установленным дополнительным фильтром array('!ROLE' => SONET_RELATIONS_TYPE_ALL), приведет к получению неверного результата. Этот нюанс можно считать досадной ошибкой в архитектуре модуля, но, к счастью, дело поправимо без вмешательства в ядро системы. В нашем случае оказалось достаточно добавить обработчик события OnSocNetGroupAdd, где и выполняются необходимые для корректной записи в БД структуры прав операции. В группах, которые были созданы ранее, эта ошибка была исправлена с помощью «Мастера».
Двигаемся дальше – получаем массив блогов, из которых будут выбираться новые записи (листинг 8).
Листинг 8
$arBlogID = array(); // массив блогов
if(!empty($arEntityUsersID)) { // по владельцу блога
$arOrderBlog = array();
$arFilterBlog = array(
'OWNER_ID' => array_keys($arEntityUsersID),
'GROUP_ID' => $arParams['BLOG_GROUP_ID'],
'GROUP_SITE_ID' => SITE_ID,
'ACTIVE' => 'Y'
);
unset($arEntityUsersID);
$arGroupByBlog = false; $arNavigationBlog = false;
$arSelectFieldsBlog = array('ID', 'SONET_GROUP_ID');
$rsItems = CBlog::GetList($arOrderBlog, $arFilterBlog,
$arGroupByBlog, $arNavigationBlog, $arSelectFieldsBlog);
while($arItem = $rsItems->Fetch()) {
if(intval($arItem['SONET_GROUP_ID']) <= 0) {
$id_ = intval($arItem['ID']);
$arBlogID[$id_] = $id_;
}
}
unset($rsItems, $id_, $arOrderBlog, $arGroupByBlog,
$arNavigationBlog, $arSelectFieldsBlog);
}
if(!empty($arEntityGroupsID)) { // по группе блога в соцсети
$arOrderBlog = array();
$arFilterBlog = array(
'SOCNET_GROUP_ID' => array_keys($arEntityGroupsID),
'GROUP_ID' => $arParams['BLOG_GROUP_ID'],
'GROUP_SITE_ID' => SITE_ID,
'ACTIVE' => 'Y'
);
$arGroupByBlog = false; $arNavigationBlog = false;
$arSelectFieldsBlog = array('ID', 'OWNER_ID');
$rsItems = CBlog::GetList($arOrderBlog, $arFilterBlog,
$arGroupByBlog, $arNavigationBlog, $arSelectFieldsBlog);
while($arItem = $rsItems->Fetch()) {
if(intval($arItem['OWNER_ID']) <= 0) {
$id_ = intval($arItem['ID']);
$arBlogID[$id_] = $id_;
}
}
unset($rsItems, $id_, $arOrderBlog, $arGroupByBlog,
$arNavigationBlog, $arSelectFieldsBlog);
}
Здесь все вполне прозрачно, комментарии не требуются. И наконец, выбираем записи из блогов, которые и станут основой для формирования конечного результата – «Ленты друзей» (листинг 9).
Листинг 9
if(!empty($arBlogID)) {
$arFilter = array(
'BLOG_ACTIVE' => 'Y',
'BLOG_GROUP_SITE_ID' => SITE_ID,
'PUBLISH_STATUS' => BLOG_PUBLISH_STATUS_PUBLISH,
'BLOG_ID' => array_keys($arBlogID),
'ACTIVE' => 'Y'
);
unset($arBlogID);
if($arParams['MAX_DAYS_COUNT'] > 0) {
// задан промежуток времени для выборки сообщений 86400 —
// кэшируем на сутки
$from = intval(time() – $arParams['MAX_DAYS_COUNT']*86400);
$arFilter['>=DATE_PUBLISH'] = ConvertTimeStamp($from,
'FULL');
}
$arGroupBy = false;
$arSelectFields = array(
'ID',
'BLOG_ID',
'TITLE',
'DATE_PUBLISH',
'AUTHOR_ID',
'DETAIL_TEXT',
'BLOG_ACTIVE',
'BLOG_URL',
'BLOG_GROUP_ID',
'BLOG_GROUP_SITE_ID',
'AUTHOR_LOGIN',
'AUTHOR_NAME',
'AUTHOR_LAST_NAME',
'BLOG_USER_ALIAS',
'BLOG_OWNER_ID',
'BLOG_USER_AVATAR',
'NUM_COMMENTS',
'VIEWS',
'ATTACH_IMG',
'BLOG_SOCNET_GROUP_ID'
);
$rsItems = CBlogPost::GetList($arOrder, $arFilter,
$arGroupBy, $arNavParams, $arSelectFields);
$rsItems->bShowAll = $arParams['PAGER_SHOW_ALL'];
//создаем объект парсера сообщений блогов
$obParser = new blogTextParser(false,
$arParams['PATH_TO_SMILE']);
while($arItem = $rsItems->GetNext()) {
// здесь код разбора записи блога – ссылки, аватары,
// картинки, выполняем парсинг текста сообщения и т. д.
}
unset($obParser, $arOrder, $arGroupBy, $arSelectFields);
unset($arEntityGroupsID);
}
На этом содержательная часть работы заканчивается. Далее подключаем шаблон для оформления полученных данных HTML-кодом, и лента сообщений «как в ЖЖ» готова.
Архитектура «Ленты друзей»: проблемы и решения
Легко заметить, что данный компонент не столь совершенен, каким мог бы быть. Скажем, напрашивается вопрос: а нельзя ли в «Ленте друзей» учитывать структуру связей пользователя с группами и другими пользователями социальной сети? Теоретически можно, на практике нагрузка на сервер возрастет в разы (если не на порядки), причем кэшировать что-либо будет невозможно. Причина – необходимость учитывать огромное количество комбинаций настроек, слишком много факторов будут определять итоговый результат.
Посмотрим, как будет влиять на содержимое «Ленты друзей» сложная конфигурация прав доступа. Предположим, что в профиле пользователя имеются настройки:
• «просматривать список друзей могут только друзья» – это означает, что прежде чем включать записи, опубликованные друзьями владельца ленты в блогах социальной сети, нужно выяснить, не является ли «текущий пользователь» («текущий пользователь» – пользователь, который в данный момент смотрит ленту) другом владельца ленты;
• «просматривать список друзей могут только друзья и друзья друзей» – прежде чем включать записи, опубликованные друзьями владельца ленты, нам нужно выяснить, не является ли текущий пользователь другом или другом друга владельца ленты;
• «просматривать список друзей могут все пользователи» – включаем записи, опубликованные друзьями владельца ленты в блогах социальной сети;
• «полный запрет на просмотр друзей» – не включаем записи, опубликованные друзьями владельца ленты в блогах социальной сети.
Настройки доступа к блогам пользователя:
• «просматривать сообщения могут все пользователи» – включаем записи блога в какую-либо ленту;
• «просматривать сообщения могут только друзья пользователя» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь другом владельца блога;
• «просматривать сообщения могут только друзья и друзья друзей пользователя» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь другом или другом друга владельца блога;
• «просматривать сообщения может только владелец блога» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь владельцем этого блога.
Настройки приватности группы:
• «группа видима всем посетителям» – включаем записи, опубликованные в блоге группы без проверки членства текущего пользователя в данной группе;
• «группа не видима всем посетителям» – прежде чем включить записи, опубликованные в блоге группы, нужно проверить, является ли текущий пользователь ее членом.
Настройки доступа к блогам группы:
• «просматривать сообщения блога могут только члены группы» – это означает, что, прежде чем включать записи, опубликованные в блоге группы, нам нужно выяснить, не является ли текущий пользователь ее членом;
• «просматривать сообщения блога могут только владелец группы и модераторы» – прежде чем включать записи, опубликованные в блоге группы, нужно выяснить, является ли пользователь ее владельцем или модератором;
• «просматривать сообщения блога могут все пользователи» – включаем записи блога группы без дополнительной проверки прав;
• «просматривать сообщения блога может только владелец группы» – прежде чем включать записи, опубликованные в блоге группы, нам нужно выяснить, не является ли текущий пользователь ее владельцем.
Все это придется проверять для каждого (!) блога, который будет попадать в чью-либо ленту. Наглядный пример. Допустим, Иван состоит в группе «Любители виски», которая видима всем посетителям сайта, но сообщения блогов могут читать только члены группы, и свой блог Иван разрешает читать только своим друзьям. Петя состоит в группе «Любители молока», которая видима всем посетителям сайта, и сообщения блогов открыты для всех. Петя – друг Ивана и читать сообщения из своего блога тоже разрешает только друзьям. Маша не состоит в указанных группах и сообщения из своего блога разрешает читать всем посетителям сайта. При этом Маша – друг Пети.
Теперь, если Петя захочет почитать ленту Ивана, то ему должны быть доступны только сообщения из блога Ивана. Если же Иван будет читать ленту Пети, то он должен видеть сообщения из блога Пети и из блога группы «Любителей молока». Маша, посетив ленту Ивана, вообще не должна видеть сообщений, а в ленте Пети – видеть только сообщения из группы «Любителей молока». Если Иван или Маша посетят ленту группы «Любителей молока», то они должны видеть сообщения из блога группы и сообщения из блога Пети. В ленте Маши, Вася и Петя должны будут видеть только сообщения из блога Маши.
Таким образом, для каждого посетителя каждой ленты придется генерировать уникальный кэш, что совершенно противопоказано для метода «полного кэширования результата» (когда сохраняется полностью готовый результат и на время жизни кэша он выдается без единого запроса к базе данных и вычислений в рамках логики компонента). Если предположить, что каждая лента будет состоять, скажем, из 10 страниц, а всего активных участников социальной сети (без учета групп!), например, 1000, то только для лент пользователей будет генерироваться 10 страниц ленты ? 1000 лент ? 1000 пользователей = 10 000 000 кэш-файлов. Если каждый кэш-файл будет занимать порядка 30 000 байт дискового пространства, то суммарный объем кэш-файлов только лент будет составлять 10 000 000 ? 30 000 = 300 000 000 000 байт (?279 Гбайт)! Мягко говоря, немало.
Как вариант, можно было бы использовать метод генерации страниц ленты с частичным, но не значимым кэшированием. Но полумеры не спасают. Нагрузка на сервер возрастает, несущественно снижая объем кэша (в экспериментах – примерно в три раза) и радикально усложняя логику обработки (как минимум придется тщательно заботиться о сбросе некорректного кэша), отладку и сопровождение модуля.
В итоге при создании социальной сети для сайта www.pcmag.ru было принято решение реализовать ленту примерно в том виде, как описано в данной статье (ряд мелких деталей опущен для удобочитаемости). Да и, как показывает практика, особенной надобности в подключении к «Ленте друзей» дополнительных сущностей в общем-то не возникает. При необходимости (например, если все пользователи вдруг дружно возжелали видеть фотографии друг друга) компонент может быть доработан и расширен.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК