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;
}
public static function createFeed($feedData)
{
if(!is_array($feedData)){
throw new \Exception('Invalid data, The provided data must be an array');
}
$acceptedKeys = ['message', 'title', 'user_id', 'space_id', 'type', 'media_preview'];
$feedData = Arr::only($feedData, $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()) {
throw new \Exception('Validation failed', $validation->errors());
}
$sanitizedData = self::sanitizeAndValidateData($feedData);
$feedData = wp_parse_args($sanitizedData, $feedData);
$user = User::findOrFail($feedData['user_id']);
$user->syncXProfile();
if($user->xprofile->status != 'active'){
throw new \Exception(esc_html__('User status is not active', 'fluent-community'));
}
$markdown = $feedData['message'];
$mentions = null;
// Extra Validaton for space_id
if (!empty($feedData['space_id'])) {
if (!Helper::isUserInSpace($feedData['user_id'], $feedData['space_id'])) {
throw new \Exception(esc_html__('User is not in the space', 'fluent-community'));
}
$mentions = FeedsHelper::getMentions($markdown, Arr::get($feedData, 'space_id'));
if ($mentions) {
$markdown = $mentions['text'];
}
} else if (!Helper::hasGlobalPost()) {
throw new \Exception(esc_html__('User is not allowed to post in global', 'fluent-community'));
}
$feedData['message_rendered'] = wp_kses_post(selff::mdToHtml($markdown));
$feedData['status'] = 'published';
$firstUrl = FeedsHelper::findFirstUrl($feedData['message_rendered']);
if ($firstUrl) {
$metaData = RemoteUrlParser::parse($firstUrl);
if ($metaData && !is_wp_error($metaData) && (!empty($metaData['image']) || !empty($metaData['html']))) {
$feedData['meta'] = [
'media_preview' => $metaData
];
}
}
$data = apply_filters('fluent_community/feed/new_feed_data', $feedData, $feedData);
$feed = new Feed();
$feed->fill($data);
$feed->save();
if ($mentions) {
do_action('fluent_community/feed_mentioned', $feed, $mentions['users']);
}
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')));
$type = sanitize_text_field(Arr::get($data, 'type', 'text'));
$processedData = [
'message' => $message,
'type' => $type
];
$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;
}
}