PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.5.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.5.0
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.5.0, at app/Http/Controllers/FeedsController.php

1,536 lines 56.5 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 ($status = Arr::get($requestData, 'status')) {
556 if (in_array($status, $editableStatuses)) {
557 $data['status'] = $status;
558 }
559 }
560
561 $message = $data['message'];
562 $mentions = FeedsHelper::getMentions($data['message'], Arr::get($data, 'space_id'));
563 if ($mentions) {
564 $data['message'] = $message;
565 $message = $mentions['text'];
566 }
567
568 [$message, $inlineMedias] = FeedsHelper::replaceImageUrlsWithRealMediaArchive($message, $existingFeed);
569
570 // replace new line with br
571 $data['message_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($message));
572
573 [$data, $mediaItems] = FeedsHelper::processFeedMetaData($data, $requestData, $existingFeed);
574
575 if($inlineMedias) {
576 $mediaItems = array_merge($mediaItems, $inlineMedias);
577 }
578
579 if (isset($existingFeed->meta['comments_disabled'])) {
580 $data['meta']['comments_disabled'] = $existingFeed->meta['comments_disabled'];
581 }
582
583 $requestData['is_admin'] = $user->hasPermissionOrInCurrentSpace('community_moderator', $existingFeed->space);
584
585 if (Arr::get($requestData, 'send_announcement_email') == 'yes' && $requestData['is_admin']) {
586 $data['meta']['send_announcement_email'] = 'yes';
587 } else if (Arr::get($existingFeed->meta, 'send_announcement_email')) {
588 $data['meta']['send_announcement_email'] = Arr::get($existingFeed->meta, 'send_announcement_email');
589 }
590
591 $data = apply_filters('fluent_community/feed/update_feed_data', $data, $requestData);
592
593 if (is_wp_error($data)) {
594 return $this->sendError([
595 'message' => $data->get_error_message(),
596 'errors' => $data->get_error_data()
597 ]);
598 }
599
600 $newContentType = Arr::get($requestData, 'content_type', '');
601 $existingContentType = $existingFeed->content_type;
602
603 if (($newContentType === 'document' && empty($requestData['document_ids'])) || ($newContentType === '' && $existingContentType === 'document' && empty($requestData['survey']))) {
604 $newContentType = $data['content_type'] = 'text';
605 }
606
607 if ($newContentType != $existingContentType) {
608 // Content Type Changed
609 do_action('fluent_community/feed/updating_content_type_old_' . $existingContentType, $existingFeed, $newContentType, $requestData);
610 }
611
612 if ($newContentType != 'text') {
613 $data = apply_filters('fluent_community/feed/update_feed_data_type_' . $newContentType, $data, $requestData, $existingFeed);
614 if (is_wp_error($data)) {
615 return $this->sendError([
616 'message' => $data->get_error_message(),
617 'errors' => $data->get_error_data()
618 ]);
619 }
620 }
621
622 if ($message != $existingFeed->message) {
623 $data['meta']['last_edited'] = [
624 'user_id' => $user->ID,
625 'time' => current_time('mysql')
626 ];
627 }
628
629 if ($newSpaceId = $request->get('new_space_id')) {
630 if (!Helper::isUserInSpace($existingFeed->user_id, $newSpaceId)) {
631 return $this->sendError([
632 'message' => __('The author is not a member of the selected space', 'fluent-community')
633 ]);
634 }
635
636 $newSpace = Space::findOrFail($newSpaceId);
637
638 // check if the current user is admin
639 if (!$user->hasPermissionOrInCurrentSpace('community_admin', $newSpace)) {
640 return $this->sendError([
641 'message' => __('Sorry, you do not have permission to change the space for this post', 'fluent-community')
642 ]);
643 }
644
645 $data['space_id'] = $newSpaceId;
646
647 \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
648 ->update(['space_id' => $newSpaceId]);
649 } else if ($request->get('move_to_profile')) {
650 if (!$user->hasPermissionOrInCurrentSpace('community_admin', $existingFeed->space)) {
651 return $this->sendError([
652 'message' => __('Sorry, you do not have permission to move this post to a profile', 'fluent-community')
653 ]);
654 }
655
656 $data['space_id'] = null;
657
658 \FluentCommunity\App\Models\Activity::where('feed_id', $existingFeed->id)
659 ->update(['space_id' => null]);
660 }
661
662 $data = apply_filters('fluent_community/feed/update_data', $data, $existingFeed);
663 $existingFeed->fill($data);
664 $dirty = $existingFeed->getDirty();
665
666 $existingFeed->fill($data);
667 $existingFeed->save();
668
669 if ($message != $existingFeed->message) {
670 $editHistory = $existingFeed->getCustomMeta('_edit_history', []);
671 if (!$editHistory) {
672 $editHistory = [];
673 }
674
675 $editHistory[] = array_filter([
676 'user_id' => $user->ID,
677 'time' => current_time('mysql'),
678 'prev_message' => $existingFeed->message,
679 'prev_title' => $existingFeed->title
680 ]);
681
682 // get last 5 edit history
683 $editHistory = array_slice($editHistory, -5);
684 $existingFeed->updateCustomMeta('_edit_history', $editHistory);
685 }
686
687 $mediaItemIds = [];
688 foreach ($mediaItems as $mediaItem) {
689 $mediaItemIds[] = $mediaItem->id;
690 }
691
692 Media::where('object_source', 'feed')
693 ->where('feed_id', $existingFeed->id)
694 ->whereNotIn('id', $mediaItemIds)
695 ->update(['is_active' => 0]);
696
697 if ($mediaItems) {
698 $this->saveMediaItems($existingFeed, $mediaItems);
699 }
700
701 $existingFeed->load(['xprofile', 'comments.xprofile']);
702
703 if ($existingFeed->space_id) {
704 $existingFeed->load(['space']);
705 $space = $existingFeed->space;
706 $topicIds = (array)Arr::get($requestData, 'topic_ids', []);
707 $topicsConfig = Helper::getTopicsConfig();
708 // take only max topics per post
709 if ($topicIds) {
710 $topicIds = array_slice($topicIds, 0, $topicsConfig['max_topics_per_post']);
711 $existingFeed->attachTopics($topicIds, true);
712 } else {
713 if ($space && Arr::get($space->settings, 'topic_required') != 'yes') {
714 $existingFeed->terms()->where('taxonomy_name', 'post_topic')->detach();
715 }
716 }
717 }
718
719 if ($dirty) {
720 do_action('fluent_community/feed/updated', $existingFeed, $dirty);
721 if ($existingFeed->space_id) {
722 do_action('fluent_community/space_feed/updated', $existingFeed);
723 }
724 }
725
726 $data = [
727 'feed' => FeedsHelper::transformFeed($existingFeed),
728 'message' => __('Your post has been updated', 'fluent-community')
729 ];
730
731 return apply_filters('fluent_community/feed/update_feed_response', $data, $request->all());
732 }
733
734 public function patchFeed(Request $request, $feedId)
735 {
736 $feed = Feed::findOrFail($feedId);
737 $user = $this->getUser(true);
738
739 $isAuthor = $feed->user_id == $user->ID;
740 $isMod = $user->hasPermissionOrInCurrentSpace('community_moderator', $feed->space);
741 $isAdmin = $user->hasPermissionOrInCurrentSpace('community_admin', $feed->space);
742
743 if (!$isMod && !$isAuthor && !$isAdmin) {
744 return $this->sendError([
745 'message' => __('You do not have permission to perform this action', 'fluent-community')
746 ]);
747 }
748
749 $allData = $request->all();
750 $validKeys = ['is_sticky', 'priority', 'comments_disabled'];
751
752 if (!$isMod) {
753 $validKeys = ['comments_disabled'];
754 }
755
756 $data = Arr::only($allData, $validKeys);
757
758 $data = array_map('intval', $data);
759
760 if (isset($data['is_sticky'])) {
761 $data['is_sticky'] = $data['is_sticky'] ? 1 : 0;
762 if ($data['is_sticky'] && $feed->space_id) {
763 // remove all the sticky posts from the space
764 Feed::where('space_id', $feed->space_id)
765 ->where('is_sticky', 1)
766 ->update(['is_sticky' => 0]);
767 }
768 }
769
770 if (isset($data['comments_disabled'])) {
771 $meta = $feed->meta;
772 $meta['comments_disabled'] = $data['comments_disabled'] ? 'yes' : 'no';
773 $data['meta'] = $meta;
774 }
775
776 if ($data) {
777 $feed->fill($data);
778 $dirty = $feed->getDirty();
779 if ($dirty) {
780 $feed->save();
781 do_action('fluent_community/feed/updated', $feed, $dirty);
782 }
783 }
784
785 return apply_filters('fluent_community/feed/patch_feed_response', [
786 'feed' => $feed,
787 'message' => __('Feed updated', 'fluent-community')
788 ], $feed, $request->all());
789 }
790
791 public function getWelcomeBanner(Request $request)
792 {
793 $scope = get_current_user_id() ? 'login' : 'logout';
794
795 $data = [
796 'welcome_banner' => Helper::getWelcomeBanner($scope)
797 ];
798
799 return apply_filters('fluent_community/welcome_banner_api_response', $data, $request->all());
800 }
801
802 public function getLinks(Request $request)
803 {
804 $scope = $request->getSafe('scope');
805
806 if ($scope == 'view') {
807 $data = [
808 'links' => Helper::getEnabledFeedLinks()
809 ];
810
811 return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
812 }
813
814 $data = [
815 'links' => Helper::getFeedLinks()
816 ];
817
818 return apply_filters('fluent_community/feed_links_api_response', $data, $request->all());
819 }
820
821 public function updateLinks(Request $request)
822 {
823 $links = $request->get('links', []);
824
825 $links = array_map(function ($link) {
826 return CustomSanitizer::santizeLinkItem($link);
827 }, $links);
828
829 Helper::updateFeedLinks($links);
830
831 return [
832 'message' => __('Links have been updated.', 'fluent-community'),
833 'links' => $links
834 ];
835 }
836
837 private function saveMediaItems($feed, $mediaItems)
838 {
839 foreach ($mediaItems as $media) {
840 $media->feed_id = $feed->id;
841 $media->is_active = 1;
842 $media->object_source = 'feed';
843 $media->save();
844 }
845 }
846
847 private function sanitizeAndValidateData($data)
848 {
849 $data['type'] = 'text';
850
851 $this->validate($data, [
852 'message' => 'required'
853 ], [
854 'message.required' => __('Message is required', 'fluent-community'),
855 ]);
856
857 return FeedsHelper::sanitizeAndValidateData($data);
858 }
859
860 private function checkForDuplicatePost($userId, $message, $spaceId = null)
861 {
862 if (apply_filters('fluent_community/disable_duplicate_post_check', false, $userId, $spaceId)) {
863 return false;
864 }
865
866 $message = trim($message);
867
868 $exist = Feed::where('user_id', $userId)
869 ->where('message', $message)
870 ->where('created_at', '>', gmdate('Y-m-d H:i:s', current_time('timestamp') - 7 * 24 * 60 * 60))
871 ->when($spaceId, function ($query) use ($spaceId) {
872 $query->where('space_id', $spaceId);
873 })
874 ->first();
875
876 if ($exist) {
877 return $this->sendError(['message' => __('No duplicate post please!', 'fluent-community')]);
878 }
879
880 return false;
881 }
882
883 private function validateAndSetSpace($spaceSlug, $user)
884 {
885 if ($spaceSlug == '__self__post__') {
886 if (!Helper::hasGlobalPost()) {
887 throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
888 }
889
890 return null;
891 }
892
893 $space = Space::where('slug', $spaceSlug)->first();
894
895 if (!$space) {
896 throw new \Exception(esc_html__('Please select a valid space to post in', 'fluent-community'));
897 }
898
899 $user->verifySpacePermission('can_create_post', $space);
900
901 return $space->id;
902 }
903
904 public function deleteFeed(Request $request, $feed_id)
905 {
906 $feed = Feed::findOrFail($feed_id);
907
908 $user = User::find(get_current_user_id());
909 $user->canDeleteFeed($feed, true);
910 do_action('fluent_community/feed/before_deleted', $feed);
911 $feed->delete();
912
913 do_action('fluent_community/feed/deleted', $feed_id);
914
915 return [
916 'message' => 'Feed has been deleted successfully'
917 ];
918 }
919
920 public function deleteMediaPreview(Request $request, $feed_id)
921 {
922 $feed = Feed::findOrFail($feed_id);
923 $user = User::find(get_current_user_id());
924 $user->canDeleteFeed($feed, true);
925
926 //do_action('fluent_community/feed/media_deleted', $feed->media);
927
928 $meta = $feed->meta;
929 $meta['media_preview'] = null;
930
931 $feed->meta = $meta;
932 $feed->save();
933
934 return [
935 'message' => __('Media preview image has been removed successfully.', 'fluent-community')
936 ];
937 }
938
939 public function handleMediaUpload(Request $request)
940 {
941 if ($error = Helper::checkUploadSizeError()) {
942 return $this->sendError($error, 413);
943 }
944
945 $user = $this->getUser(true);
946
947 do_action('fluent_community/check_rate_limit/media_upload', $user);
948
949 $allowedMimeTypesArray = apply_filters('fluent_community/support_attachment_types', [
950 'image/jpeg',
951 'image/pjpeg',
952 'image/png',
953 'image/gif',
954 'image/webp',
955 'image/heic',
956 ]);
957
958 $allowedTypes = implode(',', $allowedMimeTypesArray);
959
960 // Extensions eligible for WebP conversion (from allowed MIME types, excluding webp)
961 $convertibleExtensions = [];
962 foreach ($allowedMimeTypesArray as $mime) {
963 $element = explode('/', $mime);
964 $ext = end($element);
965 if ($ext === 'pjpeg') {
966 $ext = 'jpeg';
967 }
968 if ($ext && $ext !== 'webp' && !in_array($ext, $convertibleExtensions)) {
969 $convertibleExtensions[] = $ext;
970 }
971 }
972 // jpg is a common alias for jpeg — add only if jpeg is allowed
973 if (in_array('jpeg', $convertibleExtensions)) {
974 $convertibleExtensions[] = 'jpg';
975 }
976
977 $maxFileUnit = apply_filters('fluent_community/media_upload_max_file_unit', 'MB');
978 $maxFileSize = apply_filters('fluent_community/media_upload_max_file_size', 100);
979
980 $allowedFileSize = $maxFileSize;
981 if (strtoupper($maxFileUnit) == 'MB') {
982 $allowedFileSize = $maxFileSize * 1024;
983 } else if (strtoupper($maxFileUnit) == 'GB') {
984 $allowedFileSize = $maxFileSize * 1024 * 1024;
985 }
986
987 $files = $this->validate($this->request->files(), [
988 'file' => 'mimetypes:' . $allowedTypes . '|max:' . $allowedFileSize,
989 ], [
990 'file.mimetypes' => __('The file must be an image type.', 'fluent-community'),
991 /* translators: %$1s is replaced by the maximum allowed file size, %2$s is replaced by the file size unit (e.g. MB) */
992 'file.max' => sprintf(__('The file size must be less than %1$s%2$s.', 'fluent-community'), $maxFileSize, $maxFileUnit)
993 ]);
994
995 if (Arr::get($files, 'file.type') === 'image/heic'
996 && (!extension_loaded('imagick') || !class_exists('Imagick') || !in_array('HEIC', \Imagick::queryFormats('HEIC')))
997 ) {
998 return $this->sendError([
999 'message' => __('HEIC image format is not supported on this system.', 'fluent-community')
1000 ]);
1001 }
1002
1003 add_filter('wp_handle_upload', [$this, 'fixImageOrientation']);
1004 $uploadedFiles = FileSystem::put($files);
1005 remove_filter('wp_handle_upload', [$this, 'fixImageOrientation']);
1006
1007 $file = $uploadedFiles[0];
1008
1009 $upload_dir = wp_upload_dir();
1010
1011 $originalUrl = $file['url'];
1012 $orginalPath = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1013 $originalFileType = $file['type'];
1014 $originalFileName = $file['file'];
1015
1016 $willWebPConvert = $request->get('disable_convert') != 'yes';
1017
1018 $willWebPConvert = apply_filters('fluent_community/convert_image_to_webp', $willWebPConvert, $file);
1019 $willResize = $request->get('resize');
1020 $maxWidth = $request->get('max_width');
1021
1022 $willResize = apply_filters('fluent_community/media_upload_resize', $willResize, $file);
1023
1024 if ($context = $request->get('context')) {
1025 $maxWidth = apply_filters('fluent_community/media_upload_max_width_' . $context, $maxWidth, $file);
1026 }
1027
1028 if ($willResize && $maxWidth) {
1029 $upload_dir = wp_upload_dir();
1030 $fileUrl = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file['url']);
1031
1032 $editor = wp_get_image_editor($fileUrl);
1033
1034 if (!is_wp_error($editor) && $editor->get_size()['width'] > $maxWidth) {
1035 // Current file extension
1036 $ext = pathinfo($file['url'], PATHINFO_EXTENSION);
1037 $willConvert = in_array($ext, $convertibleExtensions) && $willWebPConvert;
1038
1039 if ($willConvert) {
1040 $dottedExtensions = array_map(function ($ext) {
1041 return '.' . $ext;
1042 }, $convertibleExtensions);
1043
1044 $fileUrl = str_replace($dottedExtensions, '.webp', $fileUrl);
1045 $file['file'] = str_replace($dottedExtensions, '.webp', $file['file']);
1046 $file['url'] = str_replace($dottedExtensions, '.webp', $file['url']);
1047 $file['type'] = 'image/webp';
1048 }
1049
1050 // resize the image
1051 $editor->resize($maxWidth, null, false);
1052 $editor->set_quality(90);
1053 if ($willConvert) {
1054 $result = $editor->save($fileUrl, 'image/webp');
1055 if ($result['mime-type'] == 'image/webp') {
1056 // remove original file now
1057 wp_delete_file(str_replace('.webp', '.' . $ext, $fileUrl));
1058 }
1059 $file['is_converted'] = true;
1060 } else {
1061 $result = $editor->save($fileUrl);
1062 }
1063
1064 if ($result['mime-type'] != 'image/webp') {
1065 $file['file'] = $originalFileName;
1066 $file['url'] = $originalUrl;
1067 $file['type'] = $result['mime-type'];
1068 }
1069
1070 $file['meta'] = [
1071 'width' => $editor->get_size()['width'],
1072 'height' => $editor->get_size()['height']
1073 ];
1074 }
1075 $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1076 } else {
1077 $upload_dir = wp_upload_dir();
1078 $file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file'];
1079 }
1080
1081 if ($willWebPConvert && empty($file['is_converted']) && !$request->get('skip_convert')) {
1082 $path = $file['path'];
1083 $extension = pathinfo($path, PATHINFO_EXTENSION);
1084
1085 if ($extension != 'webp' && in_array($extension, $convertibleExtensions)) {
1086 // Let's convert to webp
1087 $editor = wp_get_image_editor($file['path']);
1088 if (!is_wp_error($editor)) {
1089 $file['path'] = str_replace('.' . $extension, '.webp', $file['path']);
1090 $file['url'] = str_replace('.' . $extension, '.webp', $file['url']);
1091 $file['type'] = 'image/webp';
1092 $result = $editor->save($file['path'], 'image/webp');
1093
1094 if ($result['mime-type'] != 'image/webp') {
1095 $file['path'] = $orginalPath;
1096 $file['url'] = $originalUrl;
1097 $file['type'] = $result['mime-type'];
1098 } else {
1099 wp_delete_file($orginalPath);
1100 }
1101
1102 $file['meta'] = [
1103 'width' => $editor->get_size()['width'],
1104 'height' => $editor->get_size()['height']
1105 ];
1106 }
1107 }
1108 }
1109
1110 $mediaData = [
1111 'media_type' => $file['type'],
1112 'driver' => 'local',
1113 'media_path' => $file['path'],
1114 'media_url' => $file['url'],
1115 'settings' => Arr::get($file, 'meta', [])
1116 ];
1117
1118 $mediaData = apply_filters('fluent_community/media_upload_data', $mediaData, $file);
1119
1120 if (is_wp_error($mediaData)) {
1121 return $this->sendError([
1122 'message' => $mediaData->get_error_message(),
1123 'errors' => $mediaData->get_error_data()
1124 ]);
1125 }
1126
1127 if (!$mediaData) {
1128 return $this->sendError([
1129 'message' => __('Error while uploading the media', 'fluent-community')
1130 ]);
1131 }
1132
1133 // Let's create the media now
1134 $media = Media::create($mediaData);
1135
1136 $mediaUrl = $media->public_url;
1137
1138 $mediaUrl = add_query_arg([
1139 'media_key' => $media->media_key,
1140 ], $mediaUrl);
1141
1142 return [
1143 'media' => [
1144 'url' => $mediaUrl,
1145 'media_key' => $media->media_key,
1146 'type' => $media->media_type,
1147 'width' => Arr::get($media->settings, 'width'),
1148 'height' => Arr::get($media->settings, 'height')
1149 ]
1150 ];
1151 }
1152
1153 public function fixImageOrientation($file)
1154 {
1155 // Only process JPEG images (since they typically have EXIF data)
1156 $image_types = array('image/jpeg', 'image/jpg');
1157 if (!in_array($file['type'], $image_types)) {
1158 return $file;
1159 }
1160
1161 // Check if the EXIF extension is available
1162 if (!function_exists('exif_read_data')) {
1163 return $file;
1164 }
1165
1166 // Read EXIF data from the uploaded image
1167 $exif = @exif_read_data($file['file']);
1168
1169 if (!$exif || !isset($exif['Orientation'])) {
1170 return $file;
1171 }
1172
1173 $orientation = $exif['Orientation'];
1174
1175 // Load the image based on the available library (Imagick or GD)
1176 if (extension_loaded('imagick') && class_exists('Imagick')) {
1177 // Use Imagick if available
1178 try {
1179 $image = new \Imagick($file['file']);
1180 switch ($orientation) {
1181 case 3: // 180°
1182 $image->rotateImage(new \ImagickPixel(), 180);
1183 break;
1184 case 6: // 90° clockwise
1185 $image->rotateImage(new \ImagickPixel(), 90);
1186 break;
1187 case 8: // 90° counter-clockwise
1188 $image->rotateImage(new \ImagickPixel(), -90);
1189 break;
1190 }
1191 // Strip EXIF data to prevent further issues
1192 $image->stripImage();
1193 // Save the rotated image
1194 $image->writeImage($file['file']);
1195 $image->destroy();
1196 } catch (\Exception $e) {
1197
1198 }
1199 } elseif (function_exists('imagecreatefromjpeg')) {
1200 // Use GD if Imagick is not available
1201 $image = @imagecreatefromjpeg($file['file']);
1202 if ($image === false) {
1203 return $file;
1204 }
1205
1206 switch ($orientation) {
1207 case 3: // 180°
1208 $image = imagerotate($image, 180, 0);
1209 break;
1210 case 6: // 90° clockwise
1211 $image = imagerotate($image, -90, 0);
1212 break;
1213 case 8: // 90° counter-clockwise
1214 $image = imagerotate($image, 90, 0);
1215 break;
1216 }
1217
1218 // Save the rotated image
1219 imagejpeg($image, $file['file'], 100);
1220 imagedestroy($image);
1221 }
1222
1223 return $file;
1224 }
1225
1226 public function getTicker(Request $request)
1227 {
1228 $start = microtime(true);
1229
1230 $userId = get_current_user_id();
1231 if (!$userId) {
1232 return [
1233 'timestamp' => current_time('mysql', true),
1234 'has_changes' => false,
1235 'error' => 'User not authenticated',
1236 'feeds' => []
1237 ];
1238 }
1239
1240 do_action('fluent_community/track_activity');
1241
1242
1243 // Support both old and new format
1244 $since = $request->get('since');
1245 if (!$since) {
1246 $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1247 } else {
1248 $timestamp = strtotime($since);
1249 if (current_time('timestamp') - $timestamp > 300) {
1250 $since = date('Y-m-d H:i:s', current_time('timestamp') - 60); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1251 }
1252 }
1253
1254 $feedUpdates = [];
1255 $hasChanges = false;
1256
1257 // Get feed updates if since timestamp provided
1258 if ($since) {
1259 // Get all updated/created feeds with full data (including relationships)
1260 $currentUserModel = Helper::getCurrentUser();
1261 $updatedFeeds = Feed::where('updated_at', '>', $since)
1262 ->where('status', 'published')
1263 ->byUserAccess($userId)
1264 ->with([
1265 'xprofile' => function ($q) {
1266 $q->select(ProfileHelper::getXProfilePublicFields());
1267 },
1268 'comments' => function ($q) use ($currentUserModel) {
1269 $q->byContentModerationAccessStatus($currentUserModel, null)
1270 ->with(['xprofile' => function ($q) {
1271 $q->select(ProfileHelper::getXProfilePublicFields());
1272 }])
1273 ->whereHas('xprofile', function ($q) {
1274 $q->where('status', 'active');
1275 });
1276 },
1277 'space' => function ($q) {
1278 $q->select(['id', 'title', 'slug', 'type']);
1279 },
1280 'reactions' => function ($q) {
1281 $q->with([
1282 'xprofile' => function ($query) {
1283 $query->select(['user_id', 'avatar', 'display_name']);
1284 }
1285 ])
1286 ->where('type', 'like')
1287 ->limit(3);
1288 },
1289 'terms' => function ($q) {
1290 $q->select(['title', 'slug'])
1291 ->where('taxonomy_name', 'post_topic');
1292 }
1293 ])
1294 ->orderBy('updated_at', 'desc')
1295 ->limit(20) // Reduced limit since we're sending full data
1296 ->get();
1297
1298 // Transform feeds to include all necessary data
1299 $transformedFeeds = FeedsHelper::transformFeedsCollection($updatedFeeds);
1300
1301 foreach ($transformedFeeds as $feed) {
1302 $isNew = $feed->created_at >= $since;
1303
1304 // Determine context (primary context)
1305 $context = 'global';
1306 if ($feed->space_id && $feed->space) {
1307 $context = 'space-' . $feed->space->slug;
1308 }
1309
1310 $feedUpdates[] = [
1311 'id' => $feed->id,
1312 'updated_at' => $feed->updated_at,
1313 'action' => $isNew ? 'created' : 'updated',
1314 'context' => $context,
1315 'user_id' => $feed->user_id,
1316 'feed_data' => $feed // Include full feed data
1317 ];
1318 }
1319
1320 $hasChanges = !empty($feedUpdates);
1321 }
1322
1323 // Get notification count
1324 $notificationCount = NotificationSubscriber::unread()->where('user_id', $userId)->count();
1325
1326 $response = [
1327 'timestamp' => current_time('mysql'),
1328 'has_changes' => $hasChanges,
1329 'feeds' => $feedUpdates,
1330 'notifications' => [
1331 'unread_count' => $notificationCount,
1332 'new_count' => 0 // Could track new since last check
1333 ],
1334 'spaces' => [], // For future use
1335 'execution_time' => microtime(true) - $start
1336 ];
1337
1338 return apply_filters('fluent_community/feed_ticker', $response, $request->all());
1339 }
1340
1341 public function batchFetch(Request $request)
1342 {
1343 $feedIds = $request->get('feed_ids', []);
1344
1345 if (empty($feedIds) || !is_array($feedIds)) {
1346 return [
1347 'feeds' => [],
1348 'error' => 'No feed IDs provided'
1349 ];
1350 }
1351
1352 $userId = get_current_user_id();
1353
1354 // Limit to 20 feeds per batch to prevent abuse
1355 $feedIds = array_slice($feedIds, 0, 20);
1356
1357 // Build query based on context
1358 $query = Feed::whereIn('id', $feedIds)
1359 ->where('status', 'published')
1360 ->byUserAccess($userId);
1361
1362 $currentUserModel = $this->getUser();
1363
1364 $feeds = $query
1365 ->with([
1366 'xprofile' => function ($q) {
1367 $q->select(ProfileHelper::getXProfilePublicFields());
1368 },
1369 'comments' => function ($q) use ($currentUserModel) {
1370 $q->byContentModerationAccessStatus($currentUserModel)
1371 ->with(['xprofile' => function ($q) {
1372 $q->select(ProfileHelper::getXProfilePublicFields());
1373 }])
1374 ->whereHas('xprofile', function ($q) {
1375 $q->where('status', 'active');
1376 });
1377 },
1378 'space' => function ($q) {
1379 $q->select(['id', 'title', 'slug', 'type']);
1380 },
1381 'reactions' => function ($q) {
1382 $q->with([
1383 'xprofile' => function ($query) {
1384 $query->select(['user_id', 'avatar', 'display_name']);
1385 }
1386 ])
1387 ->where('type', 'like')
1388 ->limit(3);
1389 },
1390 'terms' => function ($q) {
1391 $q->select(['title', 'slug'])
1392 ->where('taxonomy_name', 'post_topic');
1393 }
1394 ]
1395 )
1396 ->get();
1397
1398 $feeds = FeedsHelper::transformFeedsCollection($feeds);
1399
1400 return [
1401 'feeds' => $feeds,
1402 'count' => $feeds->count()
1403 ];
1404 }
1405
1406 public function getTickerUpdates(Request $request)
1407 {
1408 $context = $request->get('context', 'global');
1409 $since = $request->get('since'); // ISO 8601 timestamp
1410
1411 $userId = get_current_user_id();
1412 if (!$userId) {
1413 return [
1414 'updates' => [],
1415 'timestamp' => current_time('mysql', true),
1416 'has_changes' => false,
1417 'error' => 'User not authenticated'
1418 ];
1419 }
1420
1421 // Parse since timestamp
1422 try {
1423 $sinceDate = $since ? new \DateTime($since) : null;
1424 } catch (\Exception $e) {
1425 return [
1426 'updates' => [],
1427 'timestamp' => current_time('mysql', true),
1428 'has_changes' => false,
1429 'error' => 'Invalid timestamp format'
1430 ];
1431 }
1432
1433 // Build query based on context
1434 $query = Feed::query();
1435
1436 if ($context === 'global') {
1437 $query->where('type', 'feed');
1438 } elseif (str_starts_with($context, 'space-')) {
1439 $spaceSlug = str_replace('space-', '', $context);
1440 $space = Space::where('slug', $spaceSlug)->first();
1441 if ($space) {
1442 $query->where('space_id', $space->id);
1443 }
1444 } elseif (str_starts_with($context, 'user-')) {
1445 $targetUserId = str_replace('user-', '', $context);
1446 $query->where('user_id', $targetUserId);
1447 }
1448
1449 // Apply access control
1450 $query->byUserAccess($userId);
1451
1452 $updates = [];
1453
1454 // Get updated feeds (updated_at changed)
1455 if ($sinceDate) {
1456 $updatedFeeds = (clone $query)
1457 ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
1458 ->where('status', 'published')
1459 ->select(['id', 'updated_at', 'created_at'])
1460 ->orderBy('updated_at', 'desc')
1461 ->limit(100)
1462 ->get();
1463
1464 foreach ($updatedFeeds as $feed) {
1465 $isNew = $feed->created_at >= $sinceDate->format('Y-m-d H:i:s');
1466
1467 $updates[] = [
1468 'id' => $feed->id,
1469 'updated_at' => gmdate('c', strtotime($feed->updated_at)),
1470 'action' => $isNew ? 'created' : 'updated'
1471 ];
1472 }
1473
1474 // Check for deleted feeds (status changed to deleted)
1475 $deletedFeeds = (clone $query)
1476 ->where('updated_at', '>', $sinceDate->format('Y-m-d H:i:s'))
1477 ->whereIn('status', ['deleted', 'draft'])
1478 ->select(['id', 'updated_at'])
1479 ->limit(50)
1480 ->get();
1481
1482 foreach ($deletedFeeds as $feed) {
1483 $updates[] = [
1484 'id' => $feed->id,
1485 'updated_at' => gmdate('c', strtotime($feed->updated_at)),
1486 'action' => 'deleted'
1487 ];
1488 }
1489 }
1490
1491 return [
1492 'updates' => $updates,
1493 'timestamp' => current_time('mysql', true),
1494 'has_changes' => !empty($updates)
1495 ];
1496 }
1497
1498 public function getOembed(Request $request)
1499 {
1500 $url = $request->get('url');
1501 // check if the url is valid
1502 $metaData = RemoteUrlParser::parse($url);
1503
1504 if ($metaData && !is_wp_error($metaData)) {
1505 $data = [
1506 'oembed' => $metaData
1507 ];
1508 return apply_filters('fluent_community/feed_oembed_api_response', $data, $request->all());
1509 }
1510
1511 return $this->sendError([
1512 'message' => __('No oembed data found', 'fluent-community'),
1513 'url' => $url
1514 ]);
1515 }
1516
1517 public function markdownToHtml(Request $request)
1518 {
1519 $message = CustomSanitizer::unslashMarkdown($request->get('text', ''));
1520
1521 $html = wp_kses_post(FeedsHelper::mdToHtml($message));
1522
1523 $data = [
1524 'html' => $html
1525 ];
1526
1527 $data['message_rendered'] = $html;
1528
1529 if (in_array('meta', $request->get('with', [])) && $request->get('feed')) {
1530 [$data,] = FeedsHelper::processFeedMetaData($data, $request->get('feed'));
1531 }
1532
1533 return $data;
1534 }
1535 }
1536