PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.4.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.4.01
2.11.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 All 78 releases
fluent-community / app / Http / Controllers / FeedsController.php

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

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