space; if (!$space) { return false; } $types = ['np_by_member_mail']; $spaceRole = $feed->user->getSpaceRole($feed->space); if (in_array($spaceRole, ['admin', 'moderator'])) { $types[] = 'np_by_admin_mail'; } $hasSubscribers = User::query()->where(function ($query) use ($types, $space, $feed) { $query->whereHas('notificationSubscriptions', function ($query) use ($types, $space) { $query->whereIn('notification_type', $types) ->where('object_id', $space->id) ->where('is_read', 1); }); do_action_ref_array('fluent_community/space_feed/email_notify_sub_query', [&$query, $feed, $space, $types]); return $query; })->exists(); if ($hasSubscribers || Arr::get($feed->meta, 'mentioned_user_ids', [])) { // We are scheduling this after 2 minutes of the post publish for performance as_schedule_single_action(time() + 120, 'fluent_community/email_notify_new_posts', [ $feed->id ], 'fluent-community'); } } public function notifyOnPostCreatedAsync($feedId) { if (!$this->maxRunTime) { $this->maxRunTime = Utility::getMaxRunTime(); } if (is_numeric($feedId)) { $feed = Feed::find($feedId); } else { $feed = $feedId; } if (!$feed || !$feed instanceof Feed) { return; } $space = $feed->space; if (!$space || !$feed->user) { return; } $types = ['np_by_member_mail']; $spaceRole = $feed->user->getSpaceRole($feed->space); if (in_array($spaceRole, ['admin', 'moderator'])) { $types[] = 'np_by_admin_mail'; } $lastSendUserId = (int)$feed->getCustomMeta('_last_email_user_id', 0); $usersQuery = User::query()->where(function ($query) use ($types, $space, $feed) { $query->whereHas('notificationSubscriptions', function ($query) use ($types, $space) { $query->whereIn('notification_type', $types) ->where('object_id', $space->id) ->where('is_read', 1); }); $mentionedUserIds = Arr::get($feed->meta, 'mentioned_user_ids', []); if ($mentionedUserIds) { $query->orWhereIn('ID', $mentionedUserIds); } do_action_ref_array('fluent_community/space_feed/email_notify_sub_query', [&$query, $feed, $space, $types]); return $query; }) ->whereHas('space_pivot', function ($query) use ($space) { $query->where('space_id', $space->id) ->where('status', 'active'); }) ->when($lastSendUserId, function ($q) use ($lastSendUserId) { $q->where('ID', '>', $lastSendUserId); }) ->whereHas('xprofile', function ($query) { return $query->where('status', 'active'); }) ->orderBy('ID', 'ASC') ->limit(60); $users = $usersQuery->get(); if ($users->isEmpty()) { return; // It's done } $emailSubject = \sprintf( /* translators: %1$s is the author name and %2$s is the post excerpt (max 30 chars) */ __('New Post By %1$s: %2$s', 'fluent-community'), $feed->user->getDisplayName(), $feed->getHumanExcerpt(30) ); $emailBody = $feed->getFeedHtml(true); /* * must need to replace these two strings * ##feed_permalink## * ##email_notification_url## */ $feedLink = $feed->getPermalink(); $startTime = microtime(true); $maxSendPerSecond = 10; // max 10 emails per second foreach ($users as $index => $user) { $feed->updateCustomMeta('_last_email_user_id', $user->ID); if ($user->ID == $feed->user_id) { continue; } $newEmailBody = str_replace([ '##feed_permalink##', '##email_notification_url##' ], [ ProfileHelper::signUserUrlWithAuthHash($feedLink, $user->ID), ProfileHelper::getSignedNotificationPrefUrl($user->ID) ], $emailBody); $notificationBagde = $this->getNotificationBadges($user->ID); if ($notificationBagde) { $newEmailBody = str_replace('', $notificationBagde, $newEmailBody); } $hooksSections = apply_filters('fluent_community/new_feed_notification/email_sections', [ 'before_content' => '', 'after_content' => '' ], $user, $feed); if (!empty($hooksSections['before_content'])) { $newEmailBody = str_replace('', $hooksSections['before_content'], $newEmailBody); } if (!empty($hooksSections['after_content'])) { $newEmailBody = str_replace('', $hooksSections['after_content'], $newEmailBody); } $mailer = new Mailer('', $emailSubject, $newEmailBody); $mailer->to($user->user_email, $user->display_name); $mailer->send(); if (microtime(true) - FLUENT_COMMUNITY_START_TIME > $this->maxRunTime) { // It's been 45 seconds, let's stop and schedule the next one and schedule a new one as_schedule_single_action(time(), 'fluent_community/email_notify_new_posts', [$feed->id], 'fluent-community'); return true; } if (($index + 1) % $maxSendPerSecond == 0) { $timeTaken = microtime(true) - $startTime; if ($timeTaken < 1) { usleep((int)(1000000 - ($timeTaken * 1000000))); } $startTime = microtime(true); } } return $this->notifyOnPostCreatedAsync($feed); } public function handleNewCommentEvent(Comment $comment, $feed) { // Check the comment mentioned users or not if (Arr::get($comment->meta, 'mentioned_user_ids', [])) { as_schedule_single_action(time(), 'fluent_community/comment_added_async', [$comment->id, 0], 'fluent-community'); return; } if ($comment->parent_id) { $globalCommentStatus = $this->isEnabled('reply_my_com_mail'); } else { $globalCommentStatus = $this->isEnabled('com_my_post_mail'); } $notificationUserIds = []; if ($comment->user_id != $feed->user_id && NotificationPref::willGetCommentEmail($feed->user_id, $globalCommentStatus)) { as_schedule_single_action(time(), 'fluent_community/comment_added_async', [$comment->id, 0], 'fluent-community'); return true; } if ($comment->parent_id) { $notificationUserIds = $comment->getCommentParentUserIds(); $notificationUserIds = array_filter($notificationUserIds, function ($userId) use ($globalCommentStatus) { return NotificationPref::willGetCommentReplyEmail($userId, $globalCommentStatus); }); } if (!$notificationUserIds) { return false; } as_schedule_single_action(time(), 'fluent_community/comment_added_async', [$comment->id, 0], 'fluent-community'); return true; } /* * This is for the async notification for new comment */ public function handleNewCommentNotificationAsync($commentId, $lastUserId = 0) { $comment = Comment::find($commentId); if (!$comment) { return true; } $feed = $comment->post; if (!$feed) { return; } if ($comment->parent_id) { $globalCommentStatus = $this->isEnabled('reply_my_com_mail'); } else { $globalCommentStatus = $this->isEnabled('com_my_post_mail'); } $notificationUserIds = $comment->getCommentParentUserIds($lastUserId); $notificationUserIds = array_filter($notificationUserIds, function ($userId) use ($globalCommentStatus) { return NotificationPref::willGetCommentReplyEmail($userId, $globalCommentStatus); }); $notificationUserIds = array_diff($notificationUserIds, [$comment->user_id]); if ($comment->user_id != $feed->user_id && NotificationPref::willGetCommentEmail($feed->user_id, $globalCommentStatus)) { // Add at the first $notificationUserIds[] = $feed->user_id; } // the mentioned user ids if ($mentionedUserIds = Arr::get($comment->meta, 'mentioned_user_ids', [])) { foreach ($mentionedUserIds as $mentionedUserId) { if (NotificationPref::willGetMentionEmail($mentionedUserId, $this->isEnabled('mention_mail'))) { $notificationUserIds[] = $mentionedUserId; } } } if (!$notificationUserIds) { return; } $notificationUserIds = array_unique($notificationUserIds); $users = User::query()->whereIn('ID', $notificationUserIds) ->whereHas('xprofile', function ($query) { return $query->where('status', 'active'); }) ->when($lastUserId, function ($q) use ($lastUserId) { $q->where('ID', '>', $lastUserId); }) ->orderBy('ID', 'ASC') ->get(); if ($users->isEmpty()) { return; // it's done } $emailBody = $comment->getCommentHtml(true); $emailSubject = $comment->getEmailSubject($feed); $feedPermalik = $feed->getPermalink() . '?comment_id=' . $comment->id; $usersCount = count($users); $startTime = microtime(true); $maxSendPerSecond = 10; // max 10 emails per second foreach ($users as $index => $user) { $lastUserId = $user->ID; if ($user->ID == $comment->user_id) { continue; } $newEmailBody = str_replace([ '##feed_permalink##', '##email_notification_url##' ], [ ProfileHelper::signUserUrlWithAuthHash($feedPermalik, $user->ID), ProfileHelper::getSignedNotificationPrefUrl($user->ID) ], $emailBody); $notificationBagde = $this->getNotificationBadges($user->ID); if ($notificationBagde) { $newEmailBody = str_replace('', $notificationBagde, $newEmailBody); } $hooksSections = apply_filters('fluent_community/comment_notification/email_sections', [ 'before_content' => '', 'after_content' => '' ], $user, $comment); if (!empty($hooksSections['before_content'])) { $newEmailBody = str_replace('', $hooksSections['before_content'], $newEmailBody); } if (!empty($hooksSections['after_content'])) { $newEmailBody = str_replace('', $hooksSections['after_content'], $newEmailBody); } $mailer = new Mailer('', $emailSubject, $newEmailBody); $mailer->to($user->user_email, $user->display_name); $mailer->send(); if (microtime(true) - FLUENT_COMMUNITY_START_TIME > 50 && $index < ($usersCount - 1)) { // It's been 45 seconds, let's stop and schedule the next one and schedule a new one as_schedule_single_action(time(), 'fluent_community/comment_added_async', [$comment->id, $lastUserId], 'fluent-community'); return true; } if (($index + 1) % $maxSendPerSecond == 0) { $timeTaken = microtime(true) - $startTime; if ($timeTaken < 1) { usleep((int)(1000000 - ($timeTaken * 1000000))); } $startTime = microtime(true); } } // It's done return true; } public function emailNotifyUsersForEveryoneTag($feedId, $lastSendUserId = 0) { if (!$this->maxRunTime) { $this->maxRunTime = Utility::getMaxRunTime(); } // Let's try to send email to all users of this space for this post $feed = Feed::find($feedId); if (!$feed || !$feed->space) { return true; } if (!$feed->isEnabledForEveryoneTag()) { return true; } $notification = Notification::where('action', 'space_feed/created') ->where('feed_id', $feed->id) ->first(); if (!$notification) { return true; } $users = User::whereDoesntHave('notificationSubscriptions', function ($query) { $query->where('notification_type', 'mention_mail') ->where('is_read', 0); }) ->whereHas('space_pivot', function ($query) use ($feed) { $query->where('space_id', $feed->space_id) ->where('status', 'active'); }) ->whereHas('xprofile', function ($query) { return $query->where('status', 'active'); }) ->orderBy('ID', 'ASC') ->when($lastSendUserId, function ($q) use ($lastSendUserId) { $q->where('ID', '>', $lastSendUserId); }) ->limit(100) ->get(); if ($users->isEmpty()) { return true; // it's done } $author = $feed->user; $emailSubject = \sprintf( /* translators: for admin post to send email all space members: %1$s is the feed title, %2$s is the author name and %3$s space name */ __('%1$s - %2$s [%3$s]', 'fluent-community'), $feed->getHumanExcerpt(30), $author->display_name, $feed->space->title ); $emailBody = $feed->getFeedHtml(true); $feedPermalink = $feed->getPermalink(); $startTime = microtime(true); $maxSendPerSecond = 10; // max 10 emails per second foreach ($users as $index => $user) { $lastSendUserId = $user->ID; if ($user->ID == $author->ID) { continue; } $newEmailBody = str_replace([ '##feed_permalink##', '##email_notification_url##' ], [ ProfileHelper::signUserUrlWithAuthHash($feedPermalink, $user->ID), ProfileHelper::getSignedNotificationPrefUrl($user->ID) ], $emailBody); $notificationBagde = $this->getNotificationBadges($user->ID); if ($notificationBagde) { $newEmailBody = str_replace('', $notificationBagde, $newEmailBody); } $hooksSections = apply_filters('fluent_community/new_feed_everybody_notification/email_sections', [ 'before_content' => '', 'after_content' => '' ], $user, $feed); if (!empty($hooksSections['before_content'])) { $newEmailBody = str_replace('', $hooksSections['before_content'], $newEmailBody); } if (!empty($hooksSections['after_content'])) { $newEmailBody = str_replace('', $hooksSections['after_content'], $newEmailBody); } $mailer = new Mailer('', $emailSubject, $newEmailBody); $mailer->to($user->user_email, $user->display_name); $mailer->send(); if (microtime(true) - FLUENT_COMMUNITY_START_TIME > $this->maxRunTime) { // It's been 45 seconds, let's stop and schedule the next one and schedule a new one as_schedule_single_action(time(), 'fluent_community/email_notify_users_everyone_tag', [$feedId, $lastSendUserId], 'fluent-community'); return true; } if (($index + 1) % $maxSendPerSecond == 0) { $timeTaken = microtime(true) - $startTime; if ($timeTaken < 1) { usleep((int)(1000000 - ($timeTaken * 1000000))); } $startTime = microtime(true); } } sleep(1); // sleeping for 1 second return $this->emailNotifyUsersForEveryoneTag($feedId, $lastSendUserId); } public function maybeSendDailyDigest() { if (!$this->maxRunTime) { $this->maxRunTime = Utility::getMaxRunTime(); } $settings = Utility::getEmailNotificationSettings(); $digestEmailDay = Arr::get($settings, 'digest_mail_day'); if (strtolower(gmdate('D', current_time('timestamp'))) != $digestEmailDay) { return false; } $lastSentDate = Utility::getOption('last_digest_sent_date'); if ($lastSentDate && gmdate('Y-m-d', strtotime($lastSentDate)) == gmdate('Y-m-d', current_time('timestamp'))) { return false; // already completed } $globalEnabled = Arr::get($settings, 'digest_email_status') === 'yes'; $lastSentUserId = Utility::getOption('last_digest_sent_user_id'); if ($globalEnabled) { $users = User::whereDoesntHave('notification_records', function ($query) { $query->where('notification_type', 'digest_mail') ->where('is_read', 0); }) ->whereHas('xprofile', function ($query) { $query->where('status', 'active'); }) ->when($lastSentUserId, function ($q) use ($lastSentUserId) { $q->where('ID', '>', $lastSentUserId); }) ->limit(100) ->orderBy('ID', 'ASC') ->get(); } else { $users = User::whereHas('notification_records', function ($query) { $query->where('notification_type', 'digest_mail') ->where('is_read', 1); }) ->whereHas('xprofile', function ($query) { $query->where('status', 'active'); }) ->when($lastSentUserId, function ($q) use ($lastSentUserId) { $q->where('ID', '>', $lastSentUserId); }) ->limit(100) ->orderBy('ID', 'ASC') ->get(); } if ($users->isEmpty()) { // It's done Utility::updateOption('last_digest_sent_date', current_time('mysql')); Utility::updateOption('last_digest_sent_user_id', 0); return false; } $startAt = microtime(true); $maxSendPerSecond = 10; $sentCount = 0; foreach ($users as $user) { Utility::updateOption('last_digest_sent_user_id', $user->ID); $emailDigest = new DailyDigest($user); if ($emailDigest->send()) { $sentCount++; } if (microtime(true) - FLUENT_COMMUNITY_START_TIME > $this->maxRunTime) { // It's been 45 seconds, let's stop and schedule the next one and schedule a new one as_schedule_single_action(time(), 'fluent_community_send_daily_digest', [], 'fluent-community', false); return true; } if ($sentCount % $maxSendPerSecond === 0) { $timeTaken = microtime(true) - $startAt; if ($timeTaken < 1) { usleep((int)(1000000 - ($timeTaken * 1000000))); } $startAt = microtime(true); } } return $this->maybeSendDailyDigest(); } private function isEnabled($key) { $settings = Utility::getEmailNotificationSettings(); return Arr::get($settings, $key) === 'yes'; } public function handleCommunityJoinRequest(BaseSpace $space, $userId) { $xProfile = XProfile::where('user_id', $userId)->first(); if (!$xProfile) { return; } $emailComposer = new \FluentCommunity\App\Services\Libs\EmailComposer(); $emailComposer->addBlock('paragraph', __('Hi Space Leader,', 'fluent-community')); /* translators: %1$s is replaced by the name of the user who requested to join the space, %2$s is replaced by the title of the space */ $emailComposer->addBlock('paragraph', sprintf(__('You have a new join request from %1$s to join %2$s.', 'fluent-community'), '' . $xProfile->display_name . '', '' . $space->title . '')); $emailComposer->addBlock('paragraph', __('Please review the request in the portal', 'fluent-community')); $emailComposer->addBlock('button', __('View Pending Join Requests', 'fluent-community'), [ 'link' => Helper::baseUrl('space/' . $space->slug . '/members?view_pending=yes') ]); $emailComposer->setDefaultLogo(); /* translators: %s is replaced by the title of the space */ $emailComposer->addFooterLine('paragraph', sprintf(__('You are getting this email because you are an admin/moderator at %s', 'fluent-community'), '' . $space->title . '')); $emailBody = $emailComposer->getHtml(); $emailSubject = \sprintf( /* translators: %1$s is replaced by the name of the user who requested to join the space, %2$s is replaced by the title of the space */ __('%1$s requested to join %2$s', 'fluent-community'), $xProfile->display_name, $space->title ); $moderatorsUserIds = SpaceUserPivot::where('space_id', $space->id) ->whereIn('role', ['moderator', 'admin']) ->pluck('user_id') ->toArray(); if (!$moderatorsUserIds) { return false; } $mailer = new Mailer('', $emailSubject, $emailBody); if (count($moderatorsUserIds) == 1) { $modUserId = $moderatorsUserIds[0]; $moderator = get_user_by('ID', $modUserId); if (!$moderator || !$moderator->user_email) { return; } $mailer->to($moderator->user_email, $moderator->display_name); return $mailer->send(); } // send by BCC by 12 chunks $chunks = array_chunk($moderatorsUserIds, 12); foreach ($chunks as $chunk) { $mailer = new Mailer('', $emailSubject, $emailBody); $users = User::whereIn('ID', $chunk)->get(); $first = null; foreach ($users as $user) { if (!$user || !$user->user_email) { continue; } if (!$first) { $mailer->to($user->user_email, $user->display_name); $first = $user; continue; } $mailer->addBCC($user->display_name . ' <' . $user->user_email . '>'); } if ($first) { $mailer->send(); sleep(1); // sleeping for 1 second } } return true; } private function getNotificationBadges($userId) { $unreadCount = Notification::byStatus('unread', $userId)->count(); $unreadMessages = apply_filters('fluent_messaging/get_unread_message_count', 0, $userId); $html = ''; if ($unreadCount) { $notificationUrl = ProfileHelper::signUserUrlWithAuthHash(Helper::baseUrl('notifications'), $userId); /* translators: %d is replaced by the number of unread notifications */ $html = '' . sprintf(__('🔔 %d Unread Notifications.', 'fluent-community'), $unreadCount) . ''; if ($unreadMessages) { $html .= ' | '; } } if ($unreadMessages) { $chatUrl = ProfileHelper::signUserUrlWithAuthHash(Helper::baseUrl('chat'), $userId); /* translators: %d is replaced by the number of unread messages */ $html .= '' . sprintf(__('✉️ %d Unread Messages', 'fluent-community'), $unreadMessages) . ''; } return $html; } }