spaces()->pluck('slug')->toArray();
}
public static function getLastFeedId()
{
$lastItem = Feed::where('status', 'published')
->byUserAccess(get_current_user_id())
->orderBy('id', 'DESC')
->first();
if ($lastItem) {
return $lastItem->id;
}
return 1;
}
public static function mdToHtml($text, $options = [])
{
if (!$text) {
return '';
}
$text = str_replace(' ', '', $text); // hide markdown empty content
$html = (new \FluentCommunity\App\Services\Parsedown([
]))
->setBreaksEnabled(true)
->text($text);
if (!Arr::get($options, 'disable_link_process')) {
// add nofollow to all links. But check if nofollow is already there
$html = self::addNoFollowToLinks($html);
}
return $html;
}
public static function addNoFollowToLinks($html)
{
if (!$html) {
return '';
}
$current_domain = parse_url(home_url(), PHP_URL_HOST);
// Regular expression to match tags
$pattern = '/]*href=("|\')(https?:\/\/(?!' . preg_quote($current_domain, '/') . ').*?)("|\')\s?([^>]*)>/i';
// Callback function to modify each matched tag
$callback = function ($matches) {
$url = $matches[2];
$attr = $matches[4];
// Remove existing rel attribute if present
$attr = preg_replace('/\srel=("|\').*?("|\')/i', '', $attr);
// Add nofollow
return '';
};
// Perform the replacement
return preg_replace_callback($pattern, $callback, $html);
}
public static function findFirstUrl($html)
{
// use regular expression to find the first URL in a href tag
// do not take the url which contains /u/ in it
$pattern = '/]*?\s+)?href=([\'"])(?!.*\/u\/)(.*?)\1/';
preg_match($pattern, $html, $matches);
if (isset($matches[2])) {
return $matches[2];
}
return '';
}
public static function extractHashTags($text, $limit = 5)
{
// Extract hashtag including - and _
preg_match_all('/#([a-zA-Z0-9_-]+)/', $text, $matches);
$tags = array_unique($matches[1]);
if (!$tags) {
return [];
}
$tags = array_slice($tags, 0, $limit);
$lowerCaseTags = array_map('strtolower', $tags);
$terms = Term::whereIn('slug', $lowerCaseTags)
->where('taxonomy_name', 'hashtag')
->get();
$termIds = [];
foreach ($terms as $term) {
$termIds[$term->slug] = $term->id;
}
if (count($termIds) == count($tags)) {
return array_values($termIds);
}
$excepts = array_diff($tags, array_keys($termIds));
foreach ($excepts as $except) {
$term = Term::create([
'taxonomy_name' => 'hashtag',
'slug' => strtolower($except),
'title' => $except
]);
$termIds[$term->slug] = $term->id;
}
return array_values($termIds);
}
public static function getMentions($text, $spaceId = null)
{
// the mention may have . or _ or - in the username
preg_match_all('/@([a-zA-Z0-9_.-]+)/', $text, $matches);
$mentions = array_unique($matches[1]);
if (!$mentions) {
return null;
}
if ($spaceId) {
$xProfiles = XProfile::whereIn('username', $mentions)
->whereHas('spaces', function ($query) use ($spaceId) {
$query->where('space_id', $spaceId);
})
->get();
} else {
$xProfiles = XProfile::whereIn('username', $mentions)
->get();
}
if ($xProfiles->isEmpty()) {
return null;
}
$userMentions = [];
$userIds = [];
foreach ($xProfiles as $xProfile) {
$userIds[] = $xProfile->user_id;
$html = '' . $xProfile->display_name . '';
$userMentions['@' . $xProfile->username] = $html;
}
$users = User::whereIn('ID', $userIds)->get();
return [
'users' => $users,
'text' => strtr($text, $userMentions)
];
}
public static function getLikedIdsByUserFeedId($feedId, $userId)
{
return Reaction::select('object_id')
->where('object_type', 'comment')
->where('parent_id', $feedId)
->where('user_id', $userId)
->get()
->pluck('object_id')
->toArray();
}
public static function castSurveyVote($newVoteIndexes, Feed $feed, $userId)
{
$surveyConfig = Arr::get($feed->meta, 'survey_config', []);
$slugs = array_map(function ($item) {
return $item['slug'];
}, $surveyConfig['options']);
$newVoteIndexes = array_filter(array_intersect($slugs, $newVoteIndexes));
$previousVotes = Reaction::where('type', 'survey_vote')
->where('user_id', $userId)
->where('object_id', $feed->id)
->get();
$removedIndexes = [];
$alreadyIndexes = [];
foreach ($previousVotes as $previousVote) {
if (!in_array($previousVote->object_type, $newVoteIndexes)) {
// This vote need to be deleted
$removedIndexes[] = $previousVote->object_type;
$previousVote->delete();
} else {
$alreadyIndexes[] = $previousVote->object_type;
}
}
$newSyncIndexes = array_diff($newVoteIndexes, $alreadyIndexes);
foreach ($newSyncIndexes as $newSyncIndex) {
Reaction::create([
'user_id' => $userId,
'object_id' => $feed->id,
'type' => 'survey_vote',
'object_type' => $newSyncIndex
]);
}
foreach ($surveyConfig['options'] as $index => $option) {
$slug = $option['slug'];
if (in_array($slug, $removedIndexes)) {
$newCount = (int)Arr::get($option, 'vote_counts', 0) - 1;
$option['vote_counts'] = $newCount > 0 ? $newCount : 0;
} else if (in_array($slug, $newSyncIndexes)) {
$newCount = (int)Arr::get($option, 'vote_counts', 0) + 1;
$option['vote_counts'] = $newCount > 0 ? $newCount : 0;
}
$surveyConfig['options'][$index] = $option;
}
$meta = $feed->meta;
$meta['survey_config'] = $surveyConfig;
$feed->meta = $meta;
$feed->save();
Utility::forgetCache('survey_cast_' . $feed->id . '_' . $userId);
return $feed;
}
/**
* Create a new feed programmatically
* @param array $allData
* @return \FluentCommunity\App\Models\Feed|\WP_Error
**/
public static function createFeed($allData)
{
if (!is_array($allData)) {
return new \WP_Error('invalid_data', 'Invalid data. The data need to be array', ['status' => 400]);
}
$acceptedKeys = ['message', 'title', 'user_id', 'space_id', 'type'];
$feedData = Arr::only($allData, $acceptedKeys);
// Let's validate the data
$validation = Validator::make($feedData, [
'message' => 'required',
'title' => 'nullable|string',
'user_id' => 'required|integer|exists:users,ID',
'space_id' => 'nullable|integer|exists:fcom_spaces,id'
]);
if ($validation->fails()) {
return new \WP_Error('validation_failed', 'Validation failed', $validation->errors());
}
$sanitizedData = self::sanitizeAndValidateData($feedData);
$feedData = wp_parse_args($sanitizedData, $feedData);
$user = User::find($feedData['user_id']);
if (!$user) {
return new \WP_Error('user_not_found', 'User not found', ['status' => 400]);
}
$user->syncXProfile();
if ($user->xprofile->status != 'active') {
return new \WP_Error('user_inactive', 'User status is not active', $validation->errors());
}
$markdown = $feedData['message'];
$mentions = null;
// Extra Validaton for space_id
if (!empty($feedData['space_id'])) {
if (!Helper::isUserInSpace($feedData['user_id'], $feedData['space_id'])) {
return new \WP_Error('invalid_space', 'User is not in the space', ['status' => 400]);
}
$mentions = FeedsHelper::getMentions($markdown, Arr::get($feedData, 'space_id'));
if ($mentions) {
$markdown = $mentions['text'];
}
} else if (!Helper::hasGlobalPost()) {
return new \WP_Error('global_post_disabled', 'User is not allowed to post in global', ['status' => 400]);
}
$feedData['message_rendered'] = wp_kses_post(self::mdToHtml($markdown));
$feedData['status'] = 'published';
[$feedData, $mediaItems] = self::processFeedMetaData($feedData, $allData);
$data = apply_filters('fluent_community/feed/new_feed_data', $feedData, $allData);
$feed = new Feed();
$feed->fill($data);
$feed->save();
if ($mentions) {
do_action('fluent_community/feed_mentioned', $feed, $mentions['users']);
}
if ($mediaItems) {
foreach ($mediaItems as $media) {
$media->feed_id = $feed->id;
$media->is_active = 1;
$media->object_source = 'feed';
$media->save();
}
}
do_action('fluent_community/feed/created', $feed);
if ($feed->space_id) {
do_action('fluent_community/space_feed/created', $feed);
}
return $feed;
}
public static function sanitizeAndValidateData($data)
{
$message = CustomSanitizer::unslashMarkdown(trim(Arr::get($data, 'message')));
$processedData = [
'message' => $message,
'type' => 'text'
];
$survey = Arr::get($data, 'survey', []);
if ($survey) {
$options = Arr::get($survey, 'options', []);
$formattedOptions = [];
foreach ($options as $index => $option) {
if (empty($option['label'])) {
continue;
}
$formattedOptions[] = [
'label' => sanitize_text_field($option['label']),
'slug' => 'opt_' . ($index + 1)
];
}
if ($formattedOptions) {
$processedData['survey'] = [
'type' => Arr::get($survey, 'type') == 'single_choice' ? 'single_choice' : 'multi_choice',
'options' => $formattedOptions
];
}
}
$maxlen = apply_filters('fluent_community/max_post_length', 15000);
if (\strlen($message) > $maxlen) {
throw new \Exception(esc_html__('Post message is too long', 'fluent-community'));
}
$titlePref = Utility::postTitlePref();
if ($titlePref) {
$processedData['title'] = sanitize_text_field(Arr::get($data, 'title'));
if ($titlePref == 'required' && empty($processedData['title'])) {
throw new \Exception(esc_html__('Title is required. Please provide a title', 'fluent-community'));
}
// trim the title if it's too long to 150 char
if (\strlen($processedData['title']) > 192) {
$processedData['title'] = substr($processedData['title'], 0, 192);
}
}
return $processedData;
}
public static function transformForEdit($feed)
{
$topicsConfig = Helper::getTopicsConfig();
$terms = $feed->terms;
$feed->topic_ids = $terms->where('taxonomy_name', 'post_topic')->pluck('id')->toArray();
if ($topicsConfig['max_topics_per_post'] == 1) {
if ($feed->topic_ids) {
$feed->topic_ids = Arr::first($feed->topic_ids);
} else {
$feed->topic_ids = '';
}
}
$surveyConfig = Arr::get($feed->meta, 'survey_config', []);
if ($surveyConfig) {
$feed->survey = [
'type' => Arr::get($surveyConfig, 'type'),
'options' => Arr::get($surveyConfig, 'options', [])
];
} else {
$mediaImages = Arr::get($feed->meta, 'media_items', []);
$meta = $feed->meta;
unset($feed->meta);
if ($mediaImages) {
$feed->media_images = $mediaImages;
} else if ($mediaPreview = Arr::get($meta, 'media_preview')) {
$type = Arr::get($mediaPreview, 'type');
if ($type == 'oembed') {
$feed->media = $mediaPreview;
}
$feedMedias = Media::where('object_source', 'feed')
->where('feed_id', $feed->id)
->where('is_active', 1)
->get();
if (!$feedMedias->isEmpty()) {
$mediaItems = [];
foreach ($feedMedias as $media) {
$mediaItems[] = [
'url' => $media->public_url,
'type' => 'image',
'media_id' => $media->id,
'width' => Arr::get($media->settings, 'width'),
'height' => Arr::get($media->settings, 'height'),
'provider' => Arr::get($media->settings, 'provider', 'uploader')
];
}
$feed->media_images = $mediaItems;
} else if ($type != 'meta_data') {
$feed->meta = $meta;
}
}
}
$feed->load('space');
return $feed;
}
public static function processFeedMetaData($data, $requestData, $existingFeed = null)
{
if (empty($data['meta'])) {
$data['meta'] = [];
}
$uplaodedDocs = [];
// Handle Survey
if (!empty($data['survey'])) {
$surveyConfig = $data['survey'];
if ($existingFeed) {
$surveyConfig = Arr::get($existingFeed->meta, 'survey_config', []);
if ($surveyConfig) {
$oldOptions = Arr::get($surveyConfig, 'options', []);
$newOptions = Arr::get($data['survey'], 'options', []);
$oldKeyedOptions = [];
foreach ($oldOptions as $option) {
$oldKeyedOptions[$option['slug']] = $option;
}
foreach ($newOptions as $index => $option) {
$slug = Arr::get($option, 'slug', '');
if (isset($oldKeyedOptions[$slug])) {
$newOptions[$index]['vote_counts'] = Arr::get($oldKeyedOptions[$slug], 'vote_counts', 0);
}
}
$surveyConfig['options'] = $newOptions;
}
}
$data['meta']['survey_config'] = $surveyConfig;
$data['content_type'] = 'survey';
unset($data['survey']);
return [$data, $uplaodedDocs];
}
// Handle Giphy
if (Arr::get($requestData, 'meta.media_preview.provider') == 'giphy') {
$data['meta']['media_preview'] = array_filter([
'image' => sanitize_url($requestData['meta']['media_preview']['image']),
'type' => sanitize_text_field(Arr::get($requestData, 'meta.media_preview.type', 'image')),
'provider' => 'giphy',
'height' => (int)Arr::get($requestData, 'meta.media_preview.height', 0),
'width' => (int)Arr::get($requestData, 'meta.media_preview.width', 0),
]);
return [$data, $uplaodedDocs];
}
// Handling Video Embed
if (Arr::get($requestData, 'media') && Arr::get($requestData, 'media.type') == 'oembed') {
$media = Arr::get($requestData, 'media');
$url = Arr::get($media, 'url');
$metaData = RemoteUrlParser::parse($url);
if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
$data['meta']['media_preview'] = $metaData;
}
return [$data, $uplaodedDocs];
}
// Let's handle the uploaded media
if ($mediaImages = Arr::get($requestData, 'media_images', [])) {
$uploadedImages = Helper::getMediaByProvider($mediaImages);
if (!$existingFeed) {
$uploadedMediaItems = Helper::getMediaItemsFromUrl($uploadedImages);
} else {
$uploadedMediaItems = [];
foreach ($mediaImages as $mediaImage) {
$url = sanitize_url(Arr::get($mediaImage, 'url', ''));
if (!$url) {
continue;
}
$mediaItem = Helper::getMediaFromUrl($mediaImage);
if ($mediaItem) {
$uploadedMediaItems[] = $mediaItem;
} else {
// maybe this is a previously uploaded image
$media = Media::where('media_url', $url)
->where('object_source', 'feed')
->where('feed_id', $existingFeed->id)
->where('is_active', 1)
->first();
if ($media) {
$uploadedMediaItems[] = $media;
}
}
}
}
if (count($uploadedMediaItems) == 1) {
$singleMedia = $uploadedMediaItems[0];
$data['meta']['media_preview'] = [
'is_uploaded' => true,
'image' => $singleMedia->public_url,
'type' => 'meta_data',
'provider' => 'uploader',
'width' => Arr::get($singleMedia->settings, 'width'),
'height' => Arr::get($singleMedia->settings, 'height'),
'media_id' => $singleMedia->id,
];
} else if ($uploadedMediaItems) {
$mediaPreviews = [];
foreach ($uploadedMediaItems as $mediaItem) {
$mediaData = [
'media_id' => $mediaItem->id,
'url' => $mediaItem->public_url,
'type' => 'image',
'width' => Arr::get($mediaItem->settings, 'width'),
'height' => Arr::get($mediaItem->settings, 'height'),
'provider' => Arr::get($mediaItem->settings, 'provider', 'uploader')
];
$mediaPreviews[] = array_filter($mediaData);
}
$data['meta']['media_items'] = $mediaPreviews;
}
return [$data, $uploadedMediaItems];
}
// Let's handle the fallback here
$firstUrl = FeedsHelper::findFirstUrl(Arr::get($data, 'message_rendered'));
if ($firstUrl) {
$metaData = RemoteUrlParser::parse($firstUrl);
if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
$data['meta']['media_preview'] = $metaData;
}
}
return [$data, $uplaodedDocs];
}
}