PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.6.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.6.01
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / app / Http / Controllers / FeedsController.php

FeedsController.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.6.01, at app/Http/Controllers/FeedsController.php

1,545 lines 57.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Http\Controllers;
4
5 use FluentCommunity\App\Functions\Utility;
6 use FluentCommunity\App\Models\Media;
7 use FluentCommunity\App\Models\NotificationSubscriber;
8 use FluentCommunity\App\Models\Space;
9 use FluentCommunity\App\Models\User;
10 use FluentCommunity\App\Services\CustomSanitizer;
11 use FluentCommunity\App\Services\FeedsHelper;
12 use FluentCommunity\App\Services\Helper;
13 use FluentCommunity\App\Services\Libs\FileSystem;
14 use FluentCommunity\App\Services\ProfileHelper;
15 use FluentCommunity\App\Services\RemoteUrlParser;
16 use FluentCommunity\Framework\Http\Request\Request;
17 use FluentCommunity\App\Models\Feed;
18 use FluentCommunity\App\Models\BaseSpace;
19 use FluentCommunity\Framework\Support\Arr;
20
21 class FeedsController extends Controller
22 {
23 public function get(Request $request)
24 {
25 $start = microtime(true);
26 $space = null;
27 $bySpace = $request->get('space');
28 $userId = $request->getSafe('user_id', 'intval', '');
29 $selectedTopic = $request->getSafe('topic_slug', 'sanitize_text_field', '');
30 $search = $request->getSafe('search', 'sanitize_text_field', '');
31 if ($bySpace) {
32 // just for validation
33 $space = BaseSpace::where('slug', $bySpace)->first();
34 if (!$space) {
35 return $this->sendError(__('Invalid space slug', 'fluent-community'));
36 }
37 }
38
39 $currentUserModel = $this->getUser();
40
41 $queryArgs = [
42 'selected_topic' => $selectedTopic,
43 'per_page' => (int)$request->get('per_page', 10),
44 'page' => (int)$request->get('page', 1),
45 'search' => $search,
46 ];
47
48 $feedsQuery = Feed::byContentModerationAccessStatus($currentUserModel, $space)
49 ->select(Feed::$publicColumns)
50 ->with([
51 'xprofile' => function ($q) {
52 $q->select(ProfileHelper::getXProfilePublicFields());
53 },
54 'comments' => function ($q) use ($space, $currentUserModel) {
55 $q->byContentModerationAccessStatus($currentUserModel, $space)
56 ->with(['xprofile' => function ($q) {
57 $q->select(ProfileHelper::getXProfilePublicFields());
58 }])
59 ->whereHas('xprofile', function ($q) {
60 $q->where('status', 'active');
61 });
62 },
63 'space' => function ($q) {
64 $q->select(['id', 'title', 'slug', 'type']);
65 },
66 'reactions' => function ($q) {
67 $q->with([
68 'xprofile' => function ($query) {
69 $query->select(['user_id', 'avatar', 'display_name']);
70 }
71 ])
72 ->where('type', 'like')
73 ->limit(3);
74 },
75 'terms' => function ($q) {
76 $q->select(['title', 'slug'])
77 ->where('taxonomy_name', 'post_topic');
78 }
79 ]
80 )
81 ->searchBy($search, (array)$request->get('search_in', ['post_content']))
82 ->byTopicSlug($selectedTopic)
83 ->customOrderBy($request->getSafe('order_by_type'));
84
85 $stickyFeed = null;
86
87 $disableSticky = $request->get('disable_sticky', '') == 'yes' || !!$search || !!$selectedTopic;
88
89 if ($bySpace) {
90 $feedsQuery = $feedsQuery->filterBySpaceSlug($bySpace);
91 $queryArgs['space_slug'] = $bySpace;
92 }
93
94 if ($bySpace && !$disableSticky) {
95 $feedsQuery = $feedsQuery->where('is_sticky', 0);
96 if ($queryArgs['page'] === 1) {
97 $stickyFeed = Feed::where('space_id', $space->id)
98 ->where('is_sticky', 1)
99 ->with([
100 'xprofile' => function ($q) {
101 $q->select(ProfileHelper::getXProfilePublicFields());
102 },
103 'comments' => function ($q) use ($space) {
104 $q->byContentModerationAccessStatus($this->getUser(), $space)
105 ->with(['xprofile' => function ($q) {
106 $q->select(ProfileHelper::getXProfilePublicFields());
107 }])
108 ->whereHas('xprofile', function ($q) {
109 $q->where('status', 'active');
110 });
111 },
112 'space' => function ($q) {
113 $q->select(['id', 'title', 'slug', 'type']);
114 },
115 'reactions' => function ($q) {
116 $q->with([
117 'xprofile' => function ($query) {
118 $query->select(['user_id', 'avatar', 'display_name']);
119 }
120 ])
121 ->where('type', 'like')
122 ->limit(3);
123 },
124 'terms' => function ($q) {
125 $q->select(['title', 'slug'])
126 ->where('taxonomy_name', 'post_topic');
127 }
128 ])
129 ->first();
130 }
131 }
132
133 $currentUserId = get_current_user_id();
134
135 if ($userId) {
136 $feedsQuery = $feedsQuery->where('user_id', $userId);
137
138 if (!Helper::isModerator()) {
139 $feedsQuery = $feedsQuery->whereHas('xprofile', function ($q) {
140 $q->where('status', 'active');
141 });
142 }
143
144 if ($userId != $currentUserId) {
145 $feedsQuery = $feedsQuery->byUserAccess($currentUserId);
146 }
147
148 $queryArgs['user_id'] = $userId;
149 } else {
150 $feedsQuery->byUserAccess($currentUserId)->whereHas('xprofile', function ($q) {
151 $q->where('status', 'active');
152 });
153 }
154
155 $queryArgs = array_filter($queryArgs);
156 $queryArgs['is_main_query'] = empty($queryArgs['space_slug']) && empty($queryArgs['user_id']) && empty($queryArgs['search']);
157
158 do_action_ref_array('fluent_community/feeds_query', [&$feedsQuery, $request->all(), $queryArgs]);
159
160 $feedsQuery->limit($queryArgs['per_page'])->offset(($queryArgs['page'] - 1) * $queryArgs['per_page']);
161 $feeds = $feedsQuery->get();
162
163 // add $stickyFeed to the first page
164 if ($stickyFeed) {
165 $stickyFeed = FeedsHelper::transformFeed($stickyFeed);
166 }
167
168 $feeds = FeedsHelper::transformFeedsCollection($feeds);
169
170 $currentCount = $feeds->count();
171 $to = ($queryArgs['page'] - 1) * $queryArgs['per_page'] + $currentCount;
172
173 $hasMore = $currentCount == $queryArgs['per_page'];
174
175 $data = [
176 'feeds' => [
177 'data' => $feeds,
178 'current_page' => $queryArgs['page'],
179 'per_page' => $queryArgs['per_page'],
180 'from' => $currentCount ? ($queryArgs['page'] - 1) * $queryArgs['per_page'] + 1 : 0,
181 'to' => $to,
182 'has_more' => $hasMore,
183 'total' => $currentCount == $queryArgs['per_page'] ? $to + $currentCount : $to
184 ],
185 'sticky' => $stickyFeed
186 ];
187
188 $isMainFeed = $queryArgs['page'] === 1 && !$search && !$userId;
189 if ($isMainFeed && $currentUserId) {
190 $data['last_fetched_timestamp'] = current_time('timestamp');
191 }
192
193 $data['execution_time'] = microtime(true) - $start;
194
195 $data = apply_filters('fluent_community/feeds_api_response', $data, $request->all());
196
197 return $data;
198 }
199
200 public function getFeedBySlug(Request $request, $feed_slug)
201 {
202 $start = microtime(true);
203 if ($request->get('context') == 'edit') {
204 $feed = Feed::where('slug', $feed_slug)->first();
205
206 if (!$feed || !$feed->hasEditAccess(get_current_user_id())) {
207 return $this->sendError([
208 'message' => __('You do not have permission to edit this feed', 'fluent-community')
209 ]);
210 }
211
212 $data = [
213 'feed' => FeedsHelper::transformForEdit($feed)
214 ];
215
216 return apply_filters('fluent_community/feed_api_response', $data, $request->all());
217 }
218
219 $feed = Feed::where('slug', $feed_slug)
220 ->select(Feed::$publicColumns)
221 ->with([
222 'xprofile' => function ($q) {
223 $q->select(ProfileHelper::getXProfilePublicFields());
224 },
225 'space' => function ($q) {
226 $q->select(['id', 'title', 'slug', 'type']);
227 },
228 'comments' => function ($q) {
229 $q->byContentModerationAccessStatus($this->getUser())
230 ->with(['xprofile' => function ($q) {
231 $q->select(ProfileHelper::getXProfilePublicFields());
232 }])
233 ->whereHas('xprofile', function ($q) {
234 $q->where('status', 'active');
235 });
236 },
237 'reactions' => function ($q) {
238 $q->with([
239 'xprofile' => function ($query) {
240 $query->select(['user_id', 'avatar', 'display_name']);
241 }
242 ])
243 ->where('type', 'like')
244 ->limit(3);
245 },
246 'terms' => function ($q) {
247 $q->select(['title', 'slug'])
248 ->where('taxonomy_name', 'post_topic');
249 }
250 ])
251 ->whereHas('xprofile', function ($q) {
252 $q->where('status', 'active');
253 })
254 ->byUserAccess($this->getUserId())
255 ->first();
256
257 if (!$feed) {
258 return $this->sendError([
259 'message' => __('The feed could not be found', 'fluent-community')
260 ], 404);
261 }
262
263 if ($feed->status != 'published' && !$feed->hasEditAccess($this->getUserId())) {
264 return $this->sendError([
265 'message' => __('Sorry, you do not have permission to view this post', 'fluent-community')
266 ], 404);
267 }
268
269 $feed = FeedsHelper::transformFeed($feed);
270
271 return apply_filters('fluent_community/feed_api_response', [
272 'feed' => $feed,
273 'execution_time' => microtime(true) - $start
274 ], $request->all());
275
276 }
277
278 public function getFeedById(Request $request, $feedId)
279 {
280 $feed = Feed::findOrFail($feedId);
281 return $this->getFeedBySlug($request, $feed->slug);
282 }
283
284 public function getBookmarks(Request $request)
285 {
286 $userId = $this->getUserId();
287
288 $feedsQuery = Feed::where('status', 'published')
289 ->select(Feed::$publicColumns)
290 ->with([
291 'xprofile' => function ($q) {
292 $q->select(ProfileHelper::getXProfilePublicFields());
293 },
294 'comments' => function ($q) {
295 $q->byContentModerationAccessStatus($this->getUser())
296 ->with(['xprofile' => function ($q) {
297 $q->select(ProfileHelper::getXProfilePublicFields());
298 }])
299 ->whereHas('xprofile', function ($q) {
300 $q->where('status', 'active');
301 });
302 },
303 'space',
304 'reactions' => function ($q) {
305 $q->with([
306 'xprofile' => function ($query) {
307 $query->select(['user_id', 'avatar', 'display_name']);
308 }
309 ])
310 ->where('type', 'like')
311 ->limit(3);
312 },
313 ]
314 )
315 ->byBookMarked($userId)
316 ->byUserAccess($userId)
317 ->byTopicSlug($request->getSafe('topic_slug'))
318 ->customOrderBy($request->getSafe('order_by_type'))
319 ->searchBy($request->getSafe('search'));
320
321 if ($type = $request->get('type')) {
322 $feedsQuery = $feedsQuery->where('type', $type);
323 }
324
325 $queryArgs = [
326 'per_page' => (int)$request->get('per_page', 10),
327 'page' => (int)$request->get('page', 1)
328 ];
329
330 $feeds = $feedsQuery->orderBy('id', 'DESC')
331 ->limit($queryArgs['per_page'])
332 ->offset(($queryArgs['page'] - 1) * $queryArgs['per_page'])
333 ->get();
334
335 $currentCount = $feeds->count();
336 $to = ($queryArgs['page'] - 1) * $queryArgs['per_page'] + $currentCount;
337
338 $hasMore = $currentCount == $queryArgs['per_page'];
339
340 $feeds = FeedsHelper::transformFeedsCollection($feeds);
341
342 $data = [
343 'feeds' => [
344 'data' => $feeds,
345 'current_page' => $queryArgs['page'],
346 'per_page' => $queryArgs['per_page'],
347 'from' => $currentCount ? ($queryArgs['page'] - 1) * $queryArgs['per_page'] + 1 : 0,
348 'to' => $to,
349 'has_more' => $hasMore,
350 'total' => $currentCount == $queryArgs['per_page'] ? $to + $currentCount : $to
351 ]
352 ];
353
354 if ($queryArgs['page'] === 1) {
355 $lastItem = FeedsHelper::getLastFeedId();
356 if ($lastItem) {
357 $data['last_id'] = $lastItem;
358 }
359 }
360
361 return apply_filters('fluent_community/bookmarks_api_response', $data, $request->all());
362 }
363
364 public function store(Request $request)
365 {
366 $user = $this->getUser(true);
367
368 do_action('fluent_community/check_rate_limit/create_post', $user);
369
370 $requestData = $request->all();
371
372 $data = $this->sanitizeAndValidateData($requestData);
373 $data['user_id'] = $user->ID;
374 $data['status'] = 'published';
375
376 $feed = new Feed();
377 $feed->user_id = $user->ID;
378 $space = null;
379
380 if ($spaceSlug = $request->get('space')) {
381 $data['space_id'] = $this->validateAndSetSpace($spaceSlug, $user);
382 if ($data['space_id']) {
383 $space = Space::where('id', $data['space_id'])->first();
384 if (!$space) {
385 return $this->sendError([
386 'message' => __('Please select a valid space to post in.', 'fluent-community')
387 ]);
388 }
389 }
390
391 if ($space && Arr::get($space->settings, 'topic_required') == 'yes') {
392 $topicIds = (array)$request->get('topic_ids', []);
393 $spaceTopics = Utility::getTopicsBySpaceId($space->id);
394 $spaceTopicsIds = [];
395
396 foreach ($spaceTopics as $topic) {
397 $spaceTopicsIds[] = $topic['id'];
398 }
399
400 $validTopicIds = array_intersect($topicIds, $spaceTopicsIds);
401
402 if (!$validTopicIds) {
403 return $this->sendError([
404 'message' => __('Please select at least one topic to post in this space.', 'fluent-community'),
405 'shakes' => [
406 'topic_ids' => true
407 ]
408 ]);
409 }
410 }
411
412 } else if (!Helper::hasGlobalPost()) {
413 return $this->sendError([
414 'message' => __('Please select a valid space to post in.', 'fluent-community')
415 ]);
416 }
417
418 $spaceId = Arr::get($data, 'space_id');
419 $message = Arr::get($data, 'message');
420
421 if ($isDulicate = $this->checkForDuplicatePost($user->ID, $message, $spaceId)) {
422 return $isDulicate;
423 }
424
425 $mentions = FeedsHelper::getMentions($data['message'], Arr::get($data, 'space_id'), true);
426 if ($mentions) {
427 $data['message'] = $message;
428 $message = $mentions['text'];
429 }
430
431 [$message, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($message);
432
433 // replace new line with br
434 $data['message_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($message));
435
436 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $space);
437
438 [$data, $mediaItems] = FeedsHelper::processFeedMetaData($data, $requestData);
439
440 if ($inlineMedias) {
441 $mediaItems = array_merge($mediaItems, $inlineMedias);
442 }
443
444 if (Arr::get($requestData, 'send_announcement_email') == 'yes' && $requestData['is_admin']) {
445 $data['meta']['send_announcement_email'] = 'yes';
446 } else if (isset($data['meta']['send_announcement_email'])) {
447 $data['meta']['send_announcement_email'] = 'no';
448 }
449
450 if ($mentions) {
451 $data['meta']['mentioned_user_ids'] = Arr::get($mentions, 'user_ids', []);
452 }
453
454 $data = apply_filters('fluent_community/feed/new_feed_data', $data, $requestData);
455
456 $formContentType = (string)Arr::get($requestData, 'content_type', '');
457
458 if ($formContentType) {
459 $data = apply_filters('fluent_community/feed/new_feed_data_type_' . $formContentType, $data, $requestData);
460 }
461
462 if (is_wp_error($data)) {
463 return $this->sendError([
464 'message' => $data->get_error_message(),
465 'errors' => $data->get_error_data()
466 ]);
467 }
468
469 $feed->fill($data);
470 $feed->save();
471
472 $feed = Feed::find($feed->id); // just renewing the feed
473
474 if ($mentions) {
475 do_action('fluent_community/feed_mentioned', $feed, Arr::get($mentions, 'users'));
476 }
477
478 if ($formContentType) {
479 do_action('fluent_community/feed/just_created_type_' . $formContentType, $feed, $requestData);
480 }
481
482 if ($mediaItems) {
483 $this->saveMediaItems($feed, $mediaItems);
484 }
485
486 $feed->load(['xprofile', 'comments.xprofile']);
487 if ($feed->space_id) {
488 $feed->load(['space']);
489 $topicIds = (array)$request->get('topic_ids', []);
490 // take only max topics per post
491 if ($topicIds) {
492 $topicsConfig = Helper::getTopicsConfig();
493 $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_post']);
494 $feed->attachTopics($topicIds, false);
495 $feed->load(['terms']);
496 }
497 }
498
499
500 if ($feed->status == 'scheduled') {
501 do_action('fluent_community/feed/scheduled', $feed);
502 /* translators: %s: The scheduled date and time for the post */
503 $message = sprintf(__('Your post has been scheduled for %s', 'fluent-community'), $feed->scheduled_at);
504 return [
505 'feed' => FeedsHelper::transformFeed($feed),
506 'scheduled_at' => $feed->scheduled_at,
507 'message' => $message,
508 'last_fetched_timestamp' => current_time('timestamp')
509 ];
510 }
511
512 if ($feed->status != 'published') {
513 do_action('fluent_community/feed/new_feed_' . $feed->status, $feed);
514 /* translators: %s: The status of the post */
515 $message = sprintf(__('Your post has been marked as %s', 'fluent-community'), $feed->status);
516 return apply_filters('fluent_community/feed/new_feed_response', [
517 'feed' => FeedsHelper::transformFeed($feed),
518 'message' => $message,
519 'last_fetched_timestamp' => current_time('timestamp')
520 ], $feed, $request->all());
521 }
522
523 do_action('fluent_community/feed/created', $feed);
524
525 if ($feed->space_id) {
526 do_action('fluent_community/space_feed/created', $feed);
527 } else {
528 do_action('fluent_community/profile_feed/created', $feed);
529 }
530
531 return apply_filters('fluent_community/feed/new_feed_response', [
532 'feed' => FeedsHelper::transformFeed($feed),
533 'message' => __('Your post has been published', 'fluent-community'),
534 'last_fetched_timestamp' => current_time('timestamp')
535 ], $feed, $request->all());
536 }
537
538 public function update(Request $request, $feedId)
539 {
540 $requestData = $request->all();
541 $data = $this->sanitizeAndValidateData($requestData);
542 $user = $this->getUser(true);
543 $existingFeed = Feed::findOrFail($feedId);
544
545 $editableStatuses = ['published', 'unlisted', 'scheduled', 'pending'];
546
547 if (!in_array($existingFeed->status, $editableStatuses)) {
548 return $this->sendError([
549 'message' => __('Sorry, this post is not in an editable state.', 'fluent-community')
550 ]);
551 }
552
553 $user->canEditFeed($existingFeed, true);
554
555 if ($surveyOptionError = FeedsHelper::getSurveyOptionsUpdateError(
556 Arr::get($existingFeed->meta, 'survey_config.options', []),
557 Arr::get($requestData, 'survey', [])
558 )) {
559 return $this->sendError([
560 'message' => $surveyOptionError
561 ]);
562 }
563
564 if ($status = Arr::get($requestData, 'status')) {
565 if (in_array($status, $editableStatuses)) {
566 $data['status'] = $status;
567 }
568 }
569
570 $message = $data['message'];
571 $mentions = FeedsHelper::getMentions($data['message'], Arr::get($data, 'space_id'));
572 if ($mentions) {
573 $data['message'] = $message;
574 $message = $mentions['text'];
575 }
576
577 [$message, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($message, $existingFeed);
578
579 // replace new line with br
580 $data['message_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($message));
581
582 [$data, $mediaItems] = FeedsHelper::processFeedMetaData($data, $requestData, $existingFeed);
583
584 if($inlineMedias) {
585 $mediaItems = array_merge($mediaItems, $inlineMedias);
586 }
587
588 if (isset($existingFeed->meta['comments_disabled'])) {
589 $data['meta']['comments_disabled'] = $existingFeed->meta['comments_disabled'];
590 }
591
592 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $existingFeed->space);
593
594 if (Arr::get($requestData, 'send_announcement_email') == 'yes' && $requestData['is_admin']) {
595 $data['meta']['send_announcement_email'] = 'yes';
596 } else if (Arr::get($existingFeed->meta, 'send_announcement_email')) {
597 $data['meta']['send_announcement_email'] = Arr::get($existingFeed->meta, 'send_announcement_email');
598 }
599
600 $data = apply_filters('fluent_community/feed/update_feed_data', $data, $requestData);
601
602 if (is_wp_error($data)) {
603 return $this->sendError([
604 'message' => $data->get_error_message(),
605 'errors' => $data->get_error_data()
606 ]);
607 }
608
609 $newContentType = Arr::get($requestData, 'content_type', '');
610 $existingContentType = $existingFeed->content_type;
611
612 if (($newContentType === 'document' && empty($requestData['document_ids'])) || ($newContentType === '' && $existingContentType === 'document' && empty($requestData['survey']))) {
613 $newContentType = $data['content_type'] = 'text';
614 }
615
616 if ($newContentType != $existingContentType) {
617 // Content Type Changed
618 do_action('fluent_community/feed/updating_content_type_old_' . $existingContentType, $existingFeed, $newContentType, $requestData);
619 }
620
621 if ($newContentType != 'text') {
622 $data = apply_filters('fluent_community/feed/update_feed_data_type_' . $newContentType, $data, $requestData, $existingFeed);
623 if (is_wp_error($data)) {
624 return $this->sendError([
625 'message' => $data->get_error_message(),
626 'errors' => $data->get_error_data()
627 ]);
628 }
629 }
630
631 if ($message != $existingFeed->message) {
632 $data['meta']['last_edited'] = [
633 'user_id' => $user->ID,
634 'time' => current_time('mysql')
635 ];
636 }
637
638 if ($newSpaceId = $request->get('new_space_id')) {
639 if (!Helper::isUserInSpace($existingFeed->user_id, $newSpaceId)) {
640 return $this->sendError([
641 'message' => __('The author is not a member of the selected space', 'fluent-community')
642 ]);
643 }
644
645 $newSpace = Space::findOrFail($newSpaceId);
646
647 // check if the current user is admin
648 if (!$user->hasPermissionOrInCurrentSpace('community_admin', $newSpace)) {
649 return $this->sendError([
650 'message' => __('Sorry, you do not have permission to change the space for this post', 'fluent-community')
651 ]);
652 }
653
654 $data['space_id'] = $newSpaceId;
655
656 \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
657 ->update(['space_id' => $newSpaceId]);
658 } else if ($request->get('move_to_profile')) {
659 if (!$user->hasPermissionOrInCurrentSpace('community_admin', $existingFeed->space)) {
660 return $this->sendError([
661 'message' => __('Sorry, you do not have permission to move this post to a profile', 'fluent-community')
662 ]);
663 }
664
665 $data['space_id'] = null;
666
667 \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
668 ->update(['space_id' => null]);
669 }
670
671 $data = apply_filters('fluent_community/feed/update_data', $data, $existingFeed);
672 $existingFeed->fill($data);
673 $dirty = $existingFeed->getDirty();
674
675 $existingFeed->fill($data);
676 $existingFeed->save();
677
678 if ($message != $existingFeed->message) {
679 $editHistory = $existingFeed->getCustomMeta('_edit_history', []);
680 if (!$editHistory) {
681 $editHistory = [];
682 }
683
684 $editHistory[] = array_filter([
685 'user_id' => $user->ID,
686 'time' => current_time('mysql'),
687 'prev_message' => $existingFeed->message,
688 'prev_title' => $existingFeed->title
689 ]);
690
691 // get last 5 edit history
692 $editHistory = array_slice($editHistory, -5);
693 $existingFeed->updateCustomMeta('_edit_history', $editHistory);
694 }
695
696 $mediaItemIds = [];
697 foreach ($mediaItems as $mediaItem) {
698 $mediaItemIds[] = $mediaItem->id;
699 }
700
701 Media::where('object_source', 'feed')
702 ->where('feed_id', $existingFeed->id)
703 ->whereNotIn('id', $mediaItemIds)
704 ->update(['is_active' => 0]);
705
706 if ($mediaItems) {
707 $this->saveMediaItems($existingFeed, $mediaItems);
708 }
709
710 $existingFeed->load(['xprofile', 'comments.xprofile']);
711
712 if ($existingFeed->space_id) {
713 $existingFeed->load(['space']);
714 $space = $existingFeed->space;
715 $topicIds = (array)Arr::get($requestData, 'topic_ids', []);
716 $topicsConfig = Helper::getTopicsConfig();
717 // take only max topics per post
718 if ($topicIds) {
719 $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_post']);
720 $existingFeed->attachTopics($topicIds, true);
721 } else {
722 if ($space && Arr::get($space->settings, 'topic_required') != 'yes') {
723 $existingFeed->terms()->where('taxonomy_name', 'post_topic')->detach();
724 }
725 }
726 }
727
728 if ($dirty) {
729 do_action('fluent_community/feed/updated', $existingFeed, $dirty);
730 if ($existingFeed->space_id) {
731 do_action('fluent_community/space_feed/updated', $existingFeed);
732 }
733 }
734
735 $data = [
736 'feed' => FeedsHelper::transformFeed($existingFeed),
737 'message' => __('Your post has been updated', 'fluent-community')
738 ];
739
740 return apply_filters('fluent_community/feed/update_feed_response', $data, $request->all());
741 }
742
743 public function patchFeed(Request $request, $feedId)
744 {
745 $feed = Feed::findOrFail($feedId);
746 $user = $this->getUser(true);
747
748 $isAuthor = $feed->user_id == $user->ID;
749 $isMod = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
750 $isAdmin = $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
751
752 if (!$isMod && !$isAuthor && !$isAdmin) {
753 return $this->sendError([
754 'message' => __('You do not have permission to perform this action', 'fluent-community')
755 ]);
756 }
757
758 $allData = $request->all();
759 $validKeys = ['is_sticky', 'priority', 'comments_disabled'];
760
761 if (!$isMod) {
762 $validKeys = ['comments_disabled'];
763 }
764
765 $data = Arr::only($allData, $validKeys);
766
767 $data = array_map('intval', $data);
768
769 if (isset($data['is_sticky'])) {
770 $data['is_sticky'] = $data['is_sticky'] ? 1 : 0;
771 if ($data['is_sticky'] && $feed->space_id) {
772 // remove all the sticky posts from the space
773 Feed::where('space_id', $feed->space_id)
774 ->where('is_sticky', 1)
775 ->update(['is_sticky' => 0]);
776 }
777 }
778
779 if (isset($data['comments_disabled'])) {
780 $meta = $feed->meta;
781 $meta['comments_disabled'] = $data['comments_disabled'] ? 'yes' : 'no';
782 $data['meta'] = $meta;
783 }
784
785 if ($data) {
786 $feed->fill($data);
787 $dirty = $feed->getDirty();
788 if ($dirty) {
789 $feed->save();
790 do_action('fluent_community/feed/updated', $feed, $dirty);
791 }
792 }
793
794 return apply_filters('fluent_community/feed/patch_feed_response', [
795 'feed' => $feed,
796 'message' => __('Feed updated', 'fluent-community')
797 ], $feed, $request->all());
798 }
799
800 public function getWelcomeBanner(Request $request)
801 {
802 $scope = get_current_user_id() ? 'login' : 'logout';
803
804 $data = [
805 'welcome_banner' => Helper::getWelcomeBanner($scope)
806 ];
807
808 return apply_filters('fluent_community/welcome_banner_api_response', $data, $request->all());
809 }
810
811 public function getLinks(Request $request)
812 {
813 $scope = $request->getSafe('scope');
814
815 if ($scope == 'view') {
816 $data = [
817 'links' => Helper::getEnabledFeedLinks()
818 ];
819
820 return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
821 }
822
823 $data = [
824 'links' => Helper::getFeedLinks()
825 ];
826
827 return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
828 }
829
830 public function updateLinks(Request $request)
831 {
832 $links = $request->get('links', []);
833
834 $links = array_map(function ($link) {
835 return CustomSanitizer::santizeLinkItem($link);
836 }, $links);
837
838 Helper::updateFeedLinks($links);
839
840 return [
841 'message' => __('Links have been updated.', 'fluent-community'),
842 'links' => $links
843 ];
844 }
845
846 private function saveMediaItems($feed, $mediaItems)
847 {
848 foreach ($mediaItems as $media) {
849 $media->feed_id = $feed->id;
850 $media->is_active = 1;
851 $media->object_source = 'feed';
852 $media->save();
853 }
854 }
855
856 private function sanitizeAndValidateData($data)
857 {
858 $data['type'] = 'text';
859
860 $this->validate($data, [
861 'message' => 'required'
862 ], [
863 'message.required' => __('Message is required', 'fluent-community'),
864 ]);
865
866 return FeedsHelper::sanitizeAndValidateData($data);
867 }
868
869 private function checkForDuplicatePost($userId, $message, $spaceId = null)
870 {
871 if (apply_filters('fluent_community/disable_duplicate_post_check', false, $userId, $spaceId)) {
872 return false;
873 }
874
875 $message = trim($message);
876
877 $exist = Feed::where('user_id', $userId)
878 ->where('message', $message)
879 ->where('created_at', '>', gmdate('Y-m-d H:i:s', current_time('timestamp') - 7 * 24 * 60 * 60))
880 ->when($spaceId, function ($query) use ($spaceId) {
881 $query->where('space_id', $spaceId);
882 })
883 ->first();
884
885 if ($exist) {
886 return $this->sendError(['message' => __('No duplicate post please!', 'fluent-community')]);
887 }
888
889 return false;
890 }
891
892 private function validateAndSetSpace($spaceSlug, $user)
893 {
894 if ($spaceSlug == '__self__post__') {
895 if (!Helper::hasGlobalPost()) {
896 throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
897 }
898
899 return null;
900 }
901
902 $space = Space::where('slug', $spaceSlug)->first();
903
904 if (!$space) {
905 throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
906 }
907
908 $user->verifySpacePermission('can_create_post', $space);
909
910 return $space->id;
911 }
912
913 public function deleteFeed(Request $request, $feed_id)
914 {
915 $feed = Feed::findOrFail($feed_id);
916
917 $user = User::find(get_current_user_id());
918 $user->canDeleteFeed($feed, true);
919 do_action('fluent_community/feed/before_deleted', $feed);
920 $feed->delete();
921
922 do_action('fluent_community/feed/deleted', $feed_id);
923
924 return [
925 'message' => __('Feed has been deleted successfully', 'fluent-community')
926 ];
927 }
928
929 public function deleteMediaPreview(Request $request, $feed_id)
930 {
931 $feed = Feed::findOrFail($feed_id);
932 $user = User::find(get_current_user_id());
933 $user->canDeleteFeed($feed, true);
934
935 //do_action('fluent_community/feed/media_deleted', $feed->media);
936
937 $meta = $feed->meta;
938 $meta['media_preview'] = null;
939
940 $feed->meta = $meta;
941 $feed->save();
942
943 return [
944 'message' => __('Media preview image has been removed successfully.', 'fluent-community')
945 ];
946 }
947
948 public function handleMediaUpload(Request $request)
949 {
950 if ($error = Helper::checkUploadSizeError()) {
951 return $this->sendError($error, 413);
952 }
953
954 $user = $this->getUser(true);
955
956 do_action('fluent_community/check_rate_limit/media_upload', $user);
957
958 $allowedMimeTypesArray = apply_filters('fluent_community/support_attachment_types', [
959 'image/jpeg',
960 'image/pjpeg',
961 'image/png',
962 'image/gif',
963 'image/webp',
964 'image/heic',
965 ]);
966
967 $allowedTypes = implode(',', $allowedMimeTypesArray);
968
969 // Extensions eligible for WebP conversion (from allowed MIME types, excluding webp)
970 $convertibleExtensions = [];
971 foreach ($allowedMimeTypesArray as $mime) {
972 $element = explode('/', $mime);
973 $ext = end($element);
974 if ($ext === 'pjpeg') {
975 $ext = 'jpeg';
976 }
977 if ($ext && $ext !== 'webp' && !in_array($ext, $convertibleExtensions)) {
978 $convertibleExtensions[] = $ext;
979 }
980 }
981 // jpg is a common alias for jpeg — add only if jpeg is allowed
982 if (in_array('jpeg', $convertibleExtensions)) {
983 $convertibleExtensions[] = 'jpg';
984 }
985
986 $maxFileUnit = apply_filters('fluent_community/media_upload_max_file_unit', 'MB');
987 $maxFileSize = apply_filters('fluent_community/media_upload_max_file_size', 100);
988
989 $allowedFileSize = $maxFileSize;
990 if (strtoupper($maxFileUnit) == 'MB') {
991 $allowedFileSize = $maxFileSize * 1024;
992 } else if (strtoupper($maxFileUnit) == 'GB') {
993 $allowedFileSize = $maxFileSize * 1024 * 1024;
994 }
995
996 $files = $this->validate($this->request->files(), [
997 'file' => 'mimetypes:' . $allowedTypes . '|max:' . $allowedFileSize,
998 ], [
999 'file.mimetypes' => __('The file must be an image type.', 'fluent-community'),
1000 /* translators: %$1s is replaced by the maximum allowed file size, %2$s is replaced by the file size unit (e.g. MB) */
1001 'file.max' => sprintf(__('The file size must be less than %1$s%2$s.', 'fluent-community'), $maxFileSize, $maxFileUnit)
1002 ]);
1003
1004 if (Arr::get($files, 'file.type') === 'image/heic'
1005 && (!extension_loaded('imagick') || !class_exists('Imagick') || !in_array('HEIC', \Imagick::queryFormats('HEIC')))
1006 ) {
1007 return $this->sendError([
1008 'message' => __('HEIC image format is not supported on this system.', 'fluent-community')
1009 ]);
1010 }
1011
1012 add_filter('wp_handle_upload', [$this, 'fixImageOrientation']);
1013 $uploadedFiles = FileSystem::put($files);
1014 remove_filter('wp_handle_upload', [$this, 'fixImageOrientation']);
1015
1016 $file = $uploadedFiles[0];
1017
1018 $upload_dir = wp_upload_dir();
1019
1020 $originalUrl = $file['url'];
1021 $orginalPath = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1022 $originalFileType = $file['type'];
1023 $originalFileName = $file['file'];
1024
1025 $willWebPConvert = $request->get('disable_convert') != 'yes';
1026
1027 $willWebPConvert = apply_filters('fluent_community/convert_image_to_webp', $willWebPConvert, $file);
1028 $willResize = $request->get('resize');
1029 $maxWidth = $request->get('max_width');
1030
1031 $willResize = apply_filters('fluent_community/media_upload_resize', $willResize, $file);
1032
1033 if ($context = $request->get('context')) {
1034 $maxWidth = apply_filters('fluent_community/media_upload_max_width_' . $context, $maxWidth, $file);
1035 }
1036
1037 if ($willResize && $maxWidth) {
1038 $upload_dir = wp_upload_dir();
1039 $fileUrl = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file['url']);
1040
1041 $editor = wp_get_image_editor($fileUrl);
1042
1043 if (!is_wp_error($editor) && $editor->get_size()['width'] > $maxWidth) {
1044 // Current file extension
1045 $ext = pathinfo($file['url'], PATHINFO_EXTENSION);
1046 $willConvert = in_array($ext, $convertibleExtensions) && $willWebPConvert;
1047
1048 if ($willConvert) {
1049 $dottedExtensions = array_map(function ($ext) {
1050 return '.' . $ext;
1051 }, $convertibleExtensions);
1052
1053 $fileUrl = str_replace($dottedExtensions, '.webp', $fileUrl);
1054 $file['file'] = str_replace($dottedExtensions, '.webp', $file['file']);
1055 $file['url'] = str_replace($dottedExtensions, '.webp', $file['url']);
1056 $file['type'] = 'image/webp';
1057 }
1058
1059 // resize the image
1060 $editor->resize($maxWidth, null, false);
1061 $editor->set_quality(90);
1062 if ($willConvert) {
1063 $result = $editor->save($fileUrl, 'image/webp');
1064 if ($result['mime-type'] == 'image/webp') {
1065 // remove original file now
1066 wp_delete_file(str_replace('.webp', '.' . $ext, $fileUrl));
1067 }
1068 $file['is_converted'] = true;
1069 } else {
1070 $result = $editor->save($fileUrl);
1071 }
1072
1073 if ($result['mime-type'] != 'image/webp') {
1074 $file['file'] = $originalFileName;
1075 $file['url'] = $originalUrl;
1076 $file['type'] = $result['mime-type'];
1077 }
1078
1079 $file['meta'] = [
1080 'width' => $editor->get_size()['width'],
1081 'height' => $editor->get_size()['height']
1082 ];
1083 }
1084 $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1085 } else {
1086 $upload_dir = wp_upload_dir();
1087 $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1088 }
1089
1090 if ($willWebPConvert && empty($file['is_converted']) && !$request->get('skip_convert')) {
1091 $path = $file['path'];
1092 $extension = pathinfo($path, PATHINFO_EXTENSION);
1093
1094 if ($extension != 'webp' && in_array($extension, $convertibleExtensions)) {
1095 // Let's convert to webp
1096 $editor = wp_get_image_editor($file['path']);
1097 if (!is_wp_error($editor)) {
1098 $file['path'] = str_replace('.' . $extension, '.webp', $file['path']);
1099 $file['url'] = str_replace('.' . $extension, '.webp', $file['url']);
1100 $file['type'] = 'image/webp';
1101 $result = $editor->save($file['path'], 'image/webp');
1102
1103 if ($result['mime-type'] != 'image/webp') {
1104 $file['path'] = $orginalPath;
1105 $file['url'] = $originalUrl;
1106 $file['type'] = $result['mime-type'];
1107 } else {
1108 wp_delete_file($orginalPath);
1109 }
1110
1111 $file['meta'] = [
1112 'width' => $editor->get_size()['width'],
1113 'height' => $editor->get_size()['height']
1114 ];
1115 }
1116 }
1117 }
1118
1119 $mediaData = [
1120 'media_type' => $file['type'],
1121 'driver' => 'local',
1122 'media_path' => $file['path'],
1123 'media_url' => $file['url'],
1124 'settings' => Arr::get($file, 'meta', [])
1125 ];
1126
1127 $mediaData = apply_filters('fluent_community/media_upload_data', $mediaData, $file);
1128
1129 if (is_wp_error($mediaData)) {
1130 return $this->sendError([
1131 'message' => $mediaData->get_error_message(),
1132 'errors' => $mediaData->get_error_data()
1133 ]);
1134 }
1135
1136 if (!$mediaData) {
1137 return $this->sendError([
1138 'message' => __('Error while uploading the media', 'fluent-community')
1139 ]);
1140 }
1141
1142 // Let's create the media now
1143 $media = Media::create($mediaData);
1144
1145 $mediaUrl = $media->public_url;
1146
1147 $mediaUrl = add_query_arg([
1148 'media_key' => $media->media_key,
1149 ], $mediaUrl);
1150
1151 return [
1152 'media' => [
1153 'url' => $mediaUrl,
1154 'media_key' => $media->media_key,
1155 'type' => $media->media_type,
1156 'width' => Arr::get($media->settings, 'width'),
1157 'height' => Arr::get($media->settings, 'height')
1158 ]
1159 ];
1160 }
1161
1162 public function fixImageOrientation($file)
1163 {
1164 // Only process JPEG images (since they typically have EXIF data)
1165 $image_types = array('image/jpeg', 'image/jpg');
1166 if (!in_array($file['type'], $image_types)) {
1167 return $file;
1168 }
1169
1170 // Check if the EXIF extension is available
1171 if (!function_exists('exif_read_data')) {
1172 return $file;
1173 }
1174
1175 // Read EXIF data from the uploaded image
1176 $exif = @exif_read_data($file['file']);
1177
1178 if (!$exif || !isset($exif['Orientation'])) {
1179 return $file;
1180 }
1181
1182 $orientation = $exif['Orientation'];
1183
1184 // Load the image based on the available library (Imagick or GD)
1185 if (extension_loaded('imagick') && class_exists('Imagick')) {
1186 // Use Imagick if available
1187 try {
1188 $image = new \Imagick($file['file']);
1189 switch ($orientation) {
1190 case 3: // 180°
1191 $image->rotateImage(new \ImagickPixel(), 180);
1192 break;
1193 case 6: // 90° clockwise
1194 $image->rotateImage(new \ImagickPixel(), 90);
1195 break;
1196 case 8: // 90° counter-clockwise
1197 $image->rotateImage(new \ImagickPixel(), -90);
1198 break;
1199 }
1200 // Strip EXIF data to prevent further issues
1201 $image->stripImage();
1202 // Save the rotated image
1203 $image->writeImage($file['file']);
1204 $image->destroy();
1205 } catch (\Exception $e) {
1206
1207 }
1208 } elseif (function_exists('imagecreatefromjpeg')) {
1209 // Use GD if Imagick is not available
1210 $image = @imagecreatefromjpeg($file['file']);
1211 if ($image === false) {
1212 return $file;
1213 }
1214
1215 switch ($orientation) {
1216 case 3: // 180°
1217 $image = imagerotate($image, 180, 0);
1218 break;
1219 case 6: // 90° clockwise
1220 $image = imagerotate($image, -90, 0);
1221 break;
1222 case 8: // 90° counter-clockwise
1223 $image = imagerotate($image, 90, 0);
1224 break;
1225 }
1226
1227 // Save the rotated image
1228 imagejpeg($image, $file['file'], 100);
1229 imagedestroy($image);
1230 }
1231
1232 return $file;
1233 }
1234
1235 public function getTicker(Request $request)
1236 {
1237 $start = microtime(true);
1238
1239 $userId = get_current_user_id();
1240 if (!$userId) {
1241 return [
1242 'timestamp' => current_time('mysql', true),
1243 'has_changes' => false,
1244 'error' => __('User not authenticated', 'fluent-community'),
1245 'feeds' => []
1246 ];
1247 }
1248
1249 do_action('fluent_community/track_activity');
1250
1251
1252 // Support both old and new format
1253 $since = $request->get('since');
1254 if (!$since) {
1255 $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1256 } else {
1257 $timestamp = strtotime($since);
1258 if (current_time('timestamp') - $timestamp > 300) {
1259 $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1260 }
1261 }
1262
1263 $feedUpdates = [];
1264 $hasChanges = false;
1265
1266 // Get feed updates if since timestamp provided
1267 if ($since) {
1268 // Get all updated/created feeds with full data (including relationships)
1269 $currentUserModel = Helper::getCurrentUser();
1270 $updatedFeeds = Feed::where('updated_at', '>', $since)
1271 ->where('status', 'published')
1272 ->byUserAccess($userId)
1273 ->with([
1274 'xprofile' => function ($q) {
1275 $q->select(ProfileHelper::getXProfilePublicFields());
1276 },
1277 'comments' => function ($q) use ($currentUserModel) {
1278 $q->byContentModerationAccessStatus($currentUserModel, null)
1279 ->with(['xprofile' => function ($q) {
1280 $q->select(ProfileHelper::getXProfilePublicFields());
1281 }])
1282 ->whereHas('xprofile', function ($q) {
1283 $q->where('status', 'active');
1284 });
1285 },
1286 'space' => function ($q) {
1287 $q->select(['id', 'title', 'slug', 'type']);
1288 },
1289 'reactions' => function ($q) {
1290 $q->with([
1291 'xprofile' => function ($query) {
1292 $query->select(['user_id', 'avatar', 'display_name']);
1293 }
1294 ])
1295 ->where('type', 'like')
1296 ->limit(3);
1297 },
1298 'terms' => function ($q) {
1299 $q->select(['title', 'slug'])
1300 ->where('taxonomy_name', 'post_topic');
1301 }
1302 ])
1303 ->orderBy('updated_at', 'desc')
1304 ->limit(20) // Reduced limit since we're sending full data
1305 ->get();
1306
1307 // Transform feeds to include all necessary data
1308 $transformedFeeds = FeedsHelper::transformFeedsCollection($updatedFeeds);
1309
1310 foreach ($transformedFeeds as $feed) {
1311 $isNew = $feed->created_at >= $since;
1312
1313 // Determine context (primary context)
1314 $context = 'global';
1315 if ($feed->space_id && $feed->space) {
1316 $context = 'space-' . $feed->space->slug;
1317 }
1318
1319 $feedUpdates[] = [
1320 'id' => $feed->id,
1321 'updated_at' => $feed->updated_at,
1322 'action' => $isNew ? 'created' : 'updated',
1323 'context' => $context,
1324 'user_id' => $feed->user_id,
1325 'feed_data' => $feed // Include full feed data
1326 ];
1327 }
1328
1329 $hasChanges = !empty($feedUpdates);
1330 }
1331
1332 // Get notification count
1333 $notificationCount = NotificationSubscriber::unread()->where('user_id', $userId)->count();
1334
1335 $response = [
1336 'timestamp' => current_time('mysql'),
1337 'has_changes' => $hasChanges,
1338 'feeds' => $feedUpdates,
1339 'notifications' => [
1340 'unread_count' => $notificationCount,
1341 'new_count' => 0 // Could track new since last check
1342 ],
1343 'spaces' => [], // For future use
1344 'execution_time' => microtime(true) - $start
1345 ];
1346
1347 return apply_filters('fluent_community/feed_ticker', $response, $request->all());
1348 }
1349
1350 public function batchFetch(Request $request)
1351 {
1352 $feedIds = $request->get('feed_ids', []);
1353
1354 if (empty($feedIds) || !is_array($feedIds)) {
1355 return [
1356 'feeds' => [],
1357 'error' => __('No feed IDs provided', 'fluent-community')
1358 ];
1359 }
1360
1361 $userId = get_current_user_id();
1362
1363 // Limit to 20 feeds per batch to prevent abuse
1364 $feedIds = array_slice($feedIds, 0, 20);
1365
1366 // Build query based on context
1367 $query = Feed::whereIn('id', $feedIds)
1368 ->where('status', 'published')
1369 ->byUserAccess($userId);
1370
1371 $currentUserModel = $this->getUser();
1372
1373 $feeds = $query
1374 ->with([
1375 'xprofile' => function ($q) {
1376 $q->select(ProfileHelper::getXProfilePublicFields());
1377 },
1378 'comments' => function ($q) use ($currentUserModel) {
1379 $q->byContentModerationAccessStatus($currentUserModel)
1380 ->with(['xprofile' => function ($q) {
1381 $q->select(ProfileHelper::getXProfilePublicFields());
1382 }])
1383 ->whereHas('xprofile', function ($q) {
1384 $q->where('status', 'active');
1385 });
1386 },
1387 'space' => function ($q) {
1388 $q->select(['id', 'title', 'slug', 'type']);
1389 },
1390 'reactions' => function ($q) {
1391 $q->with([
1392 'xprofile' => function ($query) {
1393 $query->select(['user_id', 'avatar', 'display_name']);
1394 }
1395 ])
1396 ->where('type', 'like')
1397 ->limit(3);
1398 },
1399 'terms' => function ($q) {
1400 $q->select(['title', 'slug'])
1401 ->where('taxonomy_name', 'post_topic');
1402 }
1403 ]
1404 )
1405 ->get();
1406
1407 $feeds = FeedsHelper::transformFeedsCollection($feeds);
1408
1409 return [
1410 'feeds' => $feeds,
1411 'count' => $feeds->count()
1412 ];
1413 }
1414
1415 public function getTickerUpdates(Request $request)
1416 {
1417 $context = $request->get('context', 'global');
1418 $since = $request->get('since'); // ISO 8601 timestamp
1419
1420 $userId = get_current_user_id();
1421 if (!$userId) {
1422 return [
1423 'updates' => [],
1424 'timestamp' => current_time('mysql', true),
1425 'has_changes' => false,
1426 'error' => __('User not authenticated', 'fluent-community')
1427 ];
1428 }
1429
1430 // Parse since timestamp
1431 try {
1432 $sinceDate = $since ? new \DateTime($since) : null;
1433 } catch (\Exception $e) {
1434 return [
1435 'updates' => [],
1436 'timestamp' => current_time('mysql', true),
1437 'has_changes' => false,
1438 'error' => __('Invalid timestamp format', 'fluent-community')
1439 ];
1440 }
1441
1442 // Build query based on context
1443 $query = Feed::query();
1444
1445 if ($context === 'global') {
1446 $query->where('type', 'feed');
1447 } elseif (strpos($context, 'space-') === 0) {
1448 $spaceSlug = str_replace('space-', '', $context);
1449 $space = Space::where('slug', $spaceSlug)->first();
1450 if ($space) {
1451 $query->where('space_id', $space->id);
1452 }
1453 } elseif (strpos($context, 'user-') === 0) {
1454 $targetUserId = str_replace('user-', '', $context);
1455 $query->where('user_id', $targetUserId);
1456 }
1457
1458 // Apply access control
1459 $query->byUserAccess($userId);
1460
1461 $updates = [];
1462
1463 // Get updated feeds (updated_at changed)
1464 if ($sinceDate) {
1465 $updatedFeeds = (clone $query)
1466 ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
1467 ->where('status', 'published')
1468 ->select(['id', 'updated_at', 'created_at'])
1469 ->orderBy('updated_at', 'desc')
1470 ->limit(100)
1471 ->get();
1472
1473 foreach ($updatedFeeds as $feed) {
1474 $isNew = $feed->created_at >= $sinceDate->format('Y-m-d H:i:s');
1475
1476 $updates[] = [
1477 'id' => $feed->id,
1478 'updated_at' => gmdate('c', strtotime($feed->updated_at)),
1479 'action' => $isNew ? 'created' : 'updated'
1480 ];
1481 }
1482
1483 // Check for deleted feeds (status changed to deleted)
1484 $deletedFeeds = (clone $query)
1485 ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
1486 ->whereIn('status', ['deleted', 'draft'])
1487 ->select(['id', 'updated_at'])
1488 ->limit(50)
1489 ->get();
1490
1491 foreach ($deletedFeeds as $feed) {
1492 $updates[] = [
1493 'id' => $feed->id,
1494 'updated_at' => gmdate('c', strtotime($feed->updated_at)),
1495 'action' => 'deleted'
1496 ];
1497 }
1498 }
1499
1500 return [
1501 'updates' => $updates,
1502 'timestamp' => current_time('mysql', true),
1503 'has_changes' => !empty($updates)
1504 ];
1505 }
1506
1507 public function getOembed(Request $request)
1508 {
1509 $url = $request->get('url');
1510 // check if the url is valid
1511 $metaData = RemoteUrlParser::parse($url);
1512
1513 if ($metaData && !is_wp_error($metaData)) {
1514 $data = [
1515 'oembed' => $metaData
1516 ];
1517 return apply_filters('fluent_community/feed_oembed_api_response', $data, $request->all());
1518 }
1519
1520 return $this->sendError([
1521 'message' => __('No oembed data found', 'fluent-community'),
1522 'url' => $url
1523 ]);
1524 }
1525
1526 public function markdownToHtml(Request $request)
1527 {
1528 $message = CustomSanitizer::unslashMarkdown($request->get('text', ''));
1529
1530 $html = wp_kses_post(FeedsHelper::mdToHtml($message));
1531
1532 $data = [
1533 'html' => $html
1534 ];
1535
1536 $data['message_rendered'] = $html;
1537
1538 if (in_array('meta', $request->get('with', [])) && $request->get('feed')) {
1539 [$data,] = FeedsHelper::processFeedMetaData($data, $request->get('feed'));
1540 }
1541
1542 return $data;
1543 }
1544 }
1545