3 * @copyright Copyright (C) 2010-2022, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Module\Notifications;
24 use Friendica\BaseModule;
25 use Friendica\Content\ForumManager;
26 use Friendica\Content\Text\BBCode;
27 use Friendica\Core\Cache\Enum\Duration;
28 use Friendica\Core\Hook;
29 use Friendica\Core\Renderer;
30 use Friendica\Database\DBA;
32 use Friendica\Model\Contact;
33 use Friendica\Model\Group;
34 use Friendica\Model\Notification;
35 use Friendica\Model\Post;
36 use Friendica\Model\Verb;
37 use Friendica\Module\Register;
38 use Friendica\Navigation\Notifications\Entity;
39 use Friendica\Network\HTTPException;
40 use Friendica\Protocol\Activity;
41 use Friendica\Util\DateTimeFormat;
42 use Friendica\Util\Proxy;
43 use Friendica\Util\Temporal;
45 class Ping extends BaseModule
47 protected function rawContent(array $request = [])
62 $all_events_today = 0;
70 $notifications = $this->getNotificationList(local_user());
73 "`unseen` AND `uid` = ? AND NOT `origin` AND (`vid` != ? OR `vid` IS NULL)",
74 local_user(), Verb::getID(Activity::FOLLOW)
76 $items = Post::selectForUser(local_user(), ['wall', 'uid', 'uri-id'], $condition, ['limit' => 1000]);
77 if (DBA::isResult($items)) {
78 $items_unseen = Post::toArray($items, false);
79 $arr = ['items' => $items_unseen];
80 Hook::callAll('network_ping', $arr);
82 foreach ($items_unseen as $item) {
93 // Find out how unseen network posts are spread across groups
94 $group_counts = Group::countUnseen();
95 if (DBA::isResult($group_counts)) {
96 foreach ($group_counts as $group_count) {
97 if ($group_count['count'] > 0) {
98 $groups_unseen[] = $group_count;
103 $forum_counts = ForumManager::countUnseenItems();
104 if (DBA::isResult($forum_counts)) {
105 foreach ($forum_counts as $forum_count) {
106 if ($forum_count['count'] > 0) {
107 $forums_unseen[] = $forum_count;
113 $intros1 = DBA::toArray(DBA::p(
114 "SELECT `intro`.`id`, `intro`.`datetime`,
115 `contact`.`name`, `contact`.`url`, `contact`.`photo`
116 FROM `intro` INNER JOIN `contact` ON `intro`.`suggest-cid` = `contact`.`id`
117 WHERE `intro`.`uid` = ? AND NOT `intro`.`blocked` AND NOT `intro`.`ignore` AND `intro`.`suggest-cid` != 0",
120 $intros2 = DBA::toArray(DBA::p(
121 "SELECT `intro`.`id`, `intro`.`datetime`,
122 `contact`.`name`, `contact`.`url`, `contact`.`photo`
123 FROM `intro` INNER JOIN `contact` ON `intro`.`contact-id` = `contact`.`id`
124 WHERE `intro`.`uid` = ? AND NOT `intro`.`blocked` AND NOT `intro`.`ignore` AND `intro`.`contact-id` != 0 AND (`intro`.`suggest-cid` = 0 OR `intro`.`suggest-cid` IS NULL)",
128 $intro_count = count($intros1) + count($intros2);
129 $intros = $intros1 + $intros2;
131 $myurl = DI::baseUrl() . '/profile/' . DI::app()->getLoggedInUserNickname();
132 $mail_count = DBA::count('mail', ["`uid` = ? AND NOT `seen` AND `from-url` != ?", local_user(), $myurl]);
134 if (intval(DI::config()->get('config', 'register_policy')) === Register::APPROVE && DI::app()->isSiteAdmin()) {
135 $regs = \Friendica\Model\Register::getPending();
137 if (DBA::isResult($regs)) {
138 $register_count = count($regs);
142 $cachekey = "ping_init:" . local_user();
143 $ev = DI::cache()->get($cachekey);
145 $ev = DBA::selectToArray('event', ['type', 'start'],
146 ["`uid` = ? AND `start` < ? AND `finish` > ? AND NOT `ignore`",
147 local_user(), DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utcNow()]);
148 if (DBA::isResult($ev)) {
149 DI::cache()->set($cachekey, $ev, Duration::HOUR);
153 if (DBA::isResult($ev)) {
154 $all_events = count($ev);
157 $str_now = DateTimeFormat::localNow('Y-m-d');
158 foreach ($ev as $x) {
160 if ($x['type'] === 'birthday') {
166 if (DateTimeFormat::local($x['start'], 'Y-m-d') === $str_now) {
167 $all_events_today ++;
179 foreach ($notifications as $notification) {
180 if ($notification['seen'] == 0) {
185 // merge all notification types in one array
186 if (DBA::isResult($intros)) {
187 foreach ($intros as $intro) {
189 'href' => DI::baseUrl() . '/notifications/intros/' . $intro['id'],
191 'name' => strip_tags(BBCode::convert($intro['name'])),
192 'url' => $intro['url'],
194 'message' => DI::l10n()->t('{0}} wants to follow you'),
195 'date' => $intro['datetime'],
201 if (DBA::isResult($regs)) {
202 if (count($regs) <= 1 || DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) {
203 foreach ($regs as $reg) {
205 'href' => DI::baseUrl()->get(true) . '/admin/users/pending',
207 'name' => $reg['name'],
208 'url' => $reg['url'],
210 'message' => DI::l10n()->t('{0} requested registration'),
211 'date' => $reg['created'],
217 'href' => DI::baseUrl()->get(true) . '/admin/users/pending',
219 'name' => $regs[0]['name'],
220 'url' => $regs[0]['url'],
222 'message' => DI::l10n()->t('{0} and %d others requested registration', count($regs) - 1),
223 'date' => $regs[0]['created'],
229 // sort notifications by $[]['date']
230 $sort_function = function ($a, $b) {
231 $adate = strtotime($a['date']);
232 $bdate = strtotime($b['date']);
234 // Unseen messages are kept at the top
235 // The value 31536000 means one year. This should be enough :-)
243 if ($adate == $bdate) {
246 return ($adate < $bdate) ? 1 : -1;
248 usort($notifications, $sort_function);
254 if (!empty($_SESSION['sysmsg'])) {
255 $sysmsgs = $_SESSION['sysmsg'];
256 unset($_SESSION['sysmsg']);
259 if (!empty($_SESSION['sysmsg_info'])) {
260 $sysmsgs_info = $_SESSION['sysmsg_info'];
261 unset($_SESSION['sysmsg_info']);
264 $notification_count = $sysnotify_count + $intro_count + $register_count;
266 $tpl = Renderer::getMarkupTemplate('notifications/nav/notify.tpl');
269 $data['intro'] = $intro_count;
270 $data['mail'] = $mail_count;
271 $data['net'] = ($network_count < 1000) ? $network_count : '999+';
272 $data['home'] = ($home_count < 1000) ? $home_count : '999+';
273 $data['register'] = $register_count;
275 $data['all-events'] = $all_events;
276 $data['all-events-today'] = $all_events_today;
277 $data['events'] = $events;
278 $data['events-today'] = $events_today;
279 $data['birthdays'] = $birthdays;
280 $data['birthdays-today'] = $birthdays_today;
281 $data['groups'] = $groups_unseen;
282 $data['forums'] = $forums_unseen;
283 $data['notification'] = ($notification_count < 50) ? $notification_count : '49+';
284 $data['notifications'] = array_map(function ($navNotification) use ($tpl) {
285 $navNotification['contact']['photo'] = Contact::getAvatarUrlForUrl($navNotification['contact']['url'], local_user(), Proxy::SIZE_MICRO);
287 $navNotification['timestamp'] = strtotime($navNotification['date']);
288 $navNotification['localdate'] = DateTimeFormat::local($navNotification['date']);
289 $navNotification['ago'] = Temporal::getRelativeDate($navNotification['date']);
290 $navNotification['richtext'] = Entity\Notify::formatMessage($navNotification['contact']['name'], $navNotification['message']);
291 $navNotification['plaintext'] = strip_tags($navNotification['richtext']);
292 $navNotification['html'] = Renderer::replaceMacros($tpl, [
293 'notify' => $navNotification,
296 return $navNotification;
299 'notice' => $sysmsgs,
300 'info' => $sysmsgs_info
303 $json_payload = json_encode(["result" => $data]);
305 if (isset($_GET['callback'])) {
307 header("Content-type: application/javascript");
308 echo $_GET['callback'] . '(' . $json_payload . ')';
310 header("Content-type: application/json");
318 * Retrieves the notifications array for the given user ID
320 * @param int $uid User id
321 * @return array Associative array of notifications
322 * @throws HTTPException\InternalServerErrorException
324 private function getNotificationList(int $uid): array
334 $notifies = DBA::toArray(DBA::p(
335 "SELECT `notify`.*, `post`.`visible`, `post`.`deleted`
337 LEFT JOIN `post` ON `post`.`uri-id` = `notify`.`uri-id`
338 WHERE `notify`.`uid` = ? AND `notify`.`msg` != ''
339 AND NOT (`notify`.`type` IN (?, ?))
340 AND $seensql `notify`.`seen` ORDER BY `notify`.`date` $order LIMIT ?, 50",
342 Notification\Type::INTRO,
343 Notification\Type::MAIL,
347 if (!$notifies && !$seen) {
352 } elseif (!$notifies) {
358 foreach ($notifies as $notify) {
359 $notify['visible'] = $notify['visible'] ?? true;
360 $notify['deleted'] = $notify['deleted'] ?? 0;
362 if ($notify['msg_cache']) {
363 $notify['name'] = $notify['name_cache'];
364 $notify['message'] = $notify['msg_cache'];
366 $notify['name'] = strip_tags(BBCode::convert($notify['name']));
367 $notify['message'] = BBCode::toPlaintext($notify['msg']);
369 // @todo Replace this with a call of the Notify model class
370 DBA::update('notify', ['name_cache' => $notify['name'], 'msg_cache' => $notify['message']], ['id' => $notify['id']]);
373 if ($notify['visible']
374 && !$notify['deleted']
375 && empty($result['p:' . $notify['parent']])
378 'href' => DI::baseUrl() . '/notify/' . $notify['id'],
380 'name' => $notify['name'],
381 'url' => $notify['url'],
383 'message' => $notify['message'],
384 'date' => $notify['date'],
385 'seen' => $notify['seen'],
388 // Should we condense the notifications or show them all?
389 if (($notify['verb'] != Activity::POST) || DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) {
390 $result[] = $notification;
392 $result['p:' . $notify['parent']] = $notification;
396 } while ((count($result) < 50) && !$quit);