diff --git a/appinfo/info.xml b/appinfo/info.xml
index 5d578679a..4b75b06a7 100755
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -14,7 +14,7 @@
More information is available in the Activity documentation.
- 2.13.0
+ 2.13.1
agpl
Frank Karlitschek
Joas Schilling
@@ -42,6 +42,7 @@
OCA\Activity\BackgroundJob\EmailNotification
OCA\Activity\BackgroundJob\ExpireActivities
+ OCA\Activity\BackgroundJob\DigestMail
diff --git a/lib/BackgroundJob/DigestMail.php b/lib/BackgroundJob/DigestMail.php
new file mode 100644
index 000000000..558f72560
--- /dev/null
+++ b/lib/BackgroundJob/DigestMail.php
@@ -0,0 +1,49 @@
+
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Activity\BackgroundJob;
+
+
+use OC\BackgroundJob\TimedJob;
+use OCA\Activity\DigestSender;
+use OCP\AppFramework\Utility\ITimeFactory;
+
+class DigestMail extends TimedJob {
+
+ /** @var DigestSender */
+ protected $digestSender;
+ /** @var ITimeFactory */
+ protected $timeFactory;
+
+ public function __construct(DigestSender $digestSender, ITimeFactory $timeFactory) {
+ // run hourly
+ $this->setInterval(60 * 60);
+
+ $this->digestSender = $digestSender;
+ $this->timeFactory = $timeFactory;
+ }
+
+ protected function run($argument) {
+ $this->digestSender->sendDigests($this->timeFactory->getTime());
+ }
+}
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 8173b56b8..fe1ae7b86 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -97,12 +97,15 @@ public function __construct($appName,
* @param int $notify_setting_batchtime
* @param bool $notify_setting_self
* @param bool $notify_setting_selfemail
+ * @param bool $activity_digest
* @return DataResponse
*/
public function personal(
$notify_setting_batchtime = UserSettings::EMAIL_SEND_HOURLY,
$notify_setting_self = false,
- $notify_setting_selfemail = false) {
+ $notify_setting_selfemail = false,
+ $activity_digest = false
+ ) {
$settings = $this->manager->getSettings();
foreach ($settings as $setting) {
@@ -145,6 +148,11 @@ public function personal(
'notify_setting_selfemail',
(int) $notify_setting_selfemail
);
+ $this->config->setUserValue(
+ $this->user, 'activity',
+ 'notify_setting_activity_digest',
+ (int) $activity_digest
+ );
return new DataResponse(array(
'data' => array(
diff --git a/lib/Data.php b/lib/Data.php
index 65f8752ae..eeed4d259 100755
--- a/lib/Data.php
+++ b/lib/Data.php
@@ -88,14 +88,14 @@ public function send(IEvent $event): int {
'type' => $event->getType(),
'affecteduser' => $event->getAffectedUser(),
'user' => $event->getAuthor(),
- 'timestamp' => (int) $event->getTimestamp(),
+ 'timestamp' => (int)$event->getTimestamp(),
'subject' => $event->getSubject(),
'subjectparams' => json_encode($event->getSubjectParameters()),
'message' => $event->getMessage(),
'messageparams' => json_encode($event->getMessageParameters()),
'priority' => IExtension::PRIORITY_MEDIUM,
'object_type' => $event->getObjectType(),
- 'object_id' => (int) $event->getObjectId(),
+ 'object_id' => (int)$event->getObjectId(),
'object_name' => $event->getObjectName(),
'link' => $event->getLink(),
])
@@ -108,7 +108,7 @@ public function send(IEvent $event): int {
* Send an event as email
*
* @param IEvent $event
- * @param int $latestSendTime Activity $timestamp + batch setting of $affectedUser
+ * @param int $latestSendTime Activity $timestamp + batch setting of $affectedUser
* @return bool
*/
public function storeMail(IEvent $event, int $latestSendTime): bool {
@@ -124,7 +124,7 @@ public function storeMail(IEvent $event, int $latestSendTime): bool {
'amq_subject' => $query->createNamedParameter($event->getSubject()),
'amq_subjectparams' => $query->createNamedParameter(json_encode($event->getSubjectParameters())),
'amq_affecteduser' => $query->createNamedParameter($affectedUser),
- 'amq_timestamp' => $query->createNamedParameter((int) $event->getTimestamp()),
+ 'amq_timestamp' => $query->createNamedParameter((int)$event->getTimestamp()),
'amq_type' => $query->createNamedParameter($event->getType()),
'amq_latest_send' => $query->createNamedParameter($latestSendTime),
'object_type' => $query->createNamedParameter($event->getObjectType()),
@@ -150,12 +150,11 @@ public function storeMail(IEvent $event, int $latestSendTime): bool {
* @param string $objectType Allows to filter the activities to a given object. May only appear together with $objectId
* @param int $objectId Allows to filter the activities to a given object. May only appear together with $objectType
*
+ * @param bool $returnEvents return only the events
* @return array
*
- * @throws \OutOfBoundsException if the user (Code: 1) or the since (Code: 2) is invalid
- * @throws \BadMethodCallException if the user has selected to display no types for this filter (Code: 3)
*/
- public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user, $since, $limit, $sort, $filter, $objectType = '', $objectId = 0) {
+ public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user, $since, $limit, $sort, $filter, $objectType = '', $objectId = 0, bool $returnEvents = false) {
// get current user
if ($user === '') {
throw new \OutOfBoundsException('Invalid user', 1);
@@ -252,13 +251,17 @@ public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user,
$hasMore = true;
break;
}
- $headers['X-Activity-Last-Given'] = (int) $row['activity_id'];
+ $headers['X-Activity-Last-Given'] = (int)$row['activity_id'];
$groupHelper->addActivity($row);
$limit--;
}
$result->closeCursor();
- return ['data' => $groupHelper->getActivities(), 'has_more' => $hasMore, 'headers' => $headers];
+ if ($returnEvents) {
+ return $groupHelper->getEvents();
+ } else {
+ return ['data' => $groupHelper->getActivities(), 'has_more' => $hasMore, 'headers' => $headers];
+ }
}
/**
@@ -276,7 +279,7 @@ protected function setOffsetFromSince(IQueryBuilder $query, $user, $since, $sort
$queryBuilder = $this->connection->getQueryBuilder();
$queryBuilder->select(['affecteduser', 'timestamp'])
->from('activity')
- ->where($queryBuilder->expr()->eq('activity_id', $queryBuilder->createNamedParameter((int) $since)));
+ ->where($queryBuilder->expr()->eq('activity_id', $queryBuilder->createNamedParameter((int)$since)));
$result = $queryBuilder->execute();
$activity = $result->fetch();
$result->closeCursor();
@@ -285,7 +288,7 @@ protected function setOffsetFromSince(IQueryBuilder $query, $user, $since, $sort
if ($activity['affecteduser'] !== $user) {
throw new \OutOfBoundsException('Invalid since', 2);
}
- $timestamp = (int) $activity['timestamp'];
+ $timestamp = (int)$activity['timestamp'];
if ($sort === 'DESC') {
$query->andWhere($query->expr()->lte('timestamp', $query->createNamedParameter($timestamp)));
@@ -313,7 +316,7 @@ protected function setOffsetFromSince(IQueryBuilder $query, $user, $since, $sort
if ($activity !== false) {
return [
- 'X-Activity-First-Known' => (int) $activity['activity_id'],
+ 'X-Activity-First-Known' => (int)$activity['activity_id'],
];
}
@@ -353,9 +356,9 @@ public function expire($expireDays = 365) {
$ttl = (60 * 60 * 24 * max(1, $expireDays));
$timelimit = time() - $ttl;
- $this->deleteActivities(array(
- 'timestamp' => array($timelimit, '<'),
- ));
+ $this->deleteActivities([
+ 'timestamp' => [$timelimit, '<'],
+ ]);
}
/**
@@ -367,7 +370,7 @@ public function expire($expireDays = 365) {
*/
public function deleteActivities($conditions) {
$sqlWhere = '';
- $sqlParameters = $sqlWhereList = array();
+ $sqlParameters = $sqlWhereList = [];
foreach ($conditions as $column => $comparison) {
$sqlWhereList[] = " `$column` " . ((is_array($comparison) && isset($comparison[1])) ? $comparison[1] : '=') . ' ? ';
$sqlParameters[] = (is_array($comparison)) ? $comparison[0] : $comparison;
@@ -379,7 +382,7 @@ public function deleteActivities($conditions) {
// Add galera safe delete chunking if using mysql
// Stops us hitting wsrep_max_ws_rows when large row counts are deleted
- if($this->connection->getDatabasePlatform() instanceof MySqlPlatform) {
+ if ($this->connection->getDatabasePlatform() instanceof MySqlPlatform) {
// Then use chunked delete
$max = 100000;
$query = $this->connection->prepare(
@@ -387,7 +390,7 @@ public function deleteActivities($conditions) {
do {
$query->execute($sqlParameters);
$deleted = $query->rowCount();
- } while($deleted === $max);
+ } while ($deleted === $max);
} else {
// Dont use chunked delete - let the DB handle the large row count natively
$query = $this->connection->prepare(
@@ -406,19 +409,63 @@ public function getById(int $activityId): ?IEvent {
$hasMore = false;
if ($row = $result->fetch()) {
$event = $this->activityManager->generateEvent();
- $event->setApp((string) $row['app'])
- ->setType((string) $row['type'])
- ->setAffectedUser((string) $row['affecteduser'])
- ->setAuthor((string) $row['user'])
- ->setTimestamp((int) $row['timestamp'])
- ->setSubject((string) $row['subject'], (array) json_decode($row['subjectparams'], true))
- ->setMessage((string) $row['message'], (array) json_decode($row['messageparams'], true))
- ->setObject((string) $row['object_type'], (int) $row['object_id'], (string) $row['file'])
- ->setLink((string) $row['link']);
+ $event->setApp((string)$row['app'])
+ ->setType((string)$row['type'])
+ ->setAffectedUser((string)$row['affecteduser'])
+ ->setAuthor((string)$row['user'])
+ ->setTimestamp((int)$row['timestamp'])
+ ->setSubject((string)$row['subject'], (array)json_decode($row['subjectparams'], true))
+ ->setMessage((string)$row['message'], (array)json_decode($row['messageparams'], true))
+ ->setObject((string)$row['object_type'], (int)$row['object_id'], (string)$row['file'])
+ ->setLink((string)$row['link']);
return $event;
} else {
return null;
}
}
+
+ /**
+ * Get the id of the first activity in the stream since a specified time
+ *
+ * @param string $user
+ * @param int $timestamp
+ * @return int
+ */
+ public function getFirstActivitySince(string $user, int $timestamp): int {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('activity_id')
+ ->from('activity')
+ ->where($query->expr()->eq('affecteduser', $query->createNamedParameter($user)))
+ ->andWhere($query->expr()->gt('timestamp', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)))
+ ->orderBy('timestamp', 'ASC')
+ ->setMaxResults(1);
+
+ $res = $query->execute()->fetch(\PDO::FETCH_COLUMN);
+ return (int)$res;
+ }
+
+ /**
+ * Get the number of activity items and the latest activity id since the specified activity
+ *
+ * @param string $user
+ * @param int $since
+ * @param bool $byOthers
+ * @return array
+ */
+ public function getActivitySince(string $user, int $since, bool $byOthers) {
+ $query = $this->connection->getQueryBuilder();
+ $nameParam = $query->createNamedParameter($user);
+ $query->select($query->func()->count('activity_id', 'count'))
+ ->selectAlias($query->func()->max('activity_id'), 'max')
+ ->from('activity')
+ ->where($query->expr()->eq('affecteduser', $nameParam))
+ ->andWhere($query->expr()->gt('activity_id', $query->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
+
+ if ($byOthers) {
+ $query->andWhere($query->expr()->neq('user', $nameParam));
+ }
+
+ return $query->execute()->fetch();
+ }
}
diff --git a/lib/DigestSender.php b/lib/DigestSender.php
new file mode 100644
index 000000000..13e677172
--- /dev/null
+++ b/lib/DigestSender.php
@@ -0,0 +1,230 @@
+
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\Activity;
+
+use OCP\Activity\IEvent;
+use OCP\Defaults;
+use OCP\IConfig;
+use OCP\IDateTimeFormatter;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use OCP\L10N\IFactory;
+use OCP\Mail\IMailer;
+use OCP\Util;
+use Psr\Log\LoggerInterface;
+
+class DigestSender {
+ public const ACTIVITY_LIMIT = 20;
+
+ private $config;
+ private $data;
+ private $userSettings;
+ private $groupHelper;
+ private $mailer;
+ private $userManager;
+ private $urlGenerator;
+ private $defaults;
+ private $l10nFactory;
+ private $dateFormatter;
+ private $logger;
+
+ public function __construct(
+ IConfig $config,
+ Data $data,
+ UserSettings $userSettings,
+ GroupHelper $groupHelper,
+ IMailer $mailer,
+ IUserManager $userManager,
+ IURLGenerator $urlGenerator,
+ Defaults $defaults,
+ IFactory $l10nFactory,
+ IDateTimeFormatter $dateTimeFormatter,
+ LoggerInterface $logger
+ ) {
+ $this->config = $config;
+ $this->data = $data;
+ $this->userSettings = $userSettings;
+ $this->groupHelper = $groupHelper;
+ $this->mailer = $mailer;
+ $this->userManager = $userManager;
+ $this->urlGenerator = $urlGenerator;
+ $this->defaults = $defaults;
+ $this->l10nFactory = $l10nFactory;
+ $this->dateFormatter = $dateTimeFormatter;
+ $this->logger = $logger;
+ }
+
+ public function sendDigests(int $now): void {
+ $users = $this->getDigestUsers();
+ $userLanguages = $this->config->getUserValueForUsers('core', 'lang', $users);
+ $userTimezones = $this->config->getUserValueForUsers('core', 'timezone', $users);
+ $digestDate = $this->config->getUserValueForUsers('activity', 'digest', $users);
+ $defaultLanguage = $this->config->getSystemValue('default_language', 'en');
+ $defaultTimeZone = date_default_timezone_get();
+ $timezoneDigestDay = [];
+
+ foreach ($users as $user) {
+ $language = (!empty($userLanguages[$user])) ? $userLanguages[$user] : $defaultLanguage;
+ $timezone = (!empty($userTimezones[$user])) ? $userTimezones[$user] : $defaultTimeZone;
+
+ // Check if the user's timezone is after 6am already
+ if (!isset($timezoneDigestDay[$timezone])) {
+ $timezoneDate = new \DateTime('now', new \DateTimeZone($timezone));
+ if ($timezoneDate->format('H') < 6) {
+ // Still before 6am, so dont send yet.
+ $timezoneDate->sub(new \DateInterval('P1D'));
+ }
+ $timezoneDigestDay[$timezone] = $timezoneDate->format('Y.m.d');
+ }
+
+ $userDigestDate = $digestDate[$user] ?? '';
+ if ($userDigestDate === $timezoneDigestDay[$timezone]) {
+ // User got todays digest already
+ continue;
+ }
+
+ $this->sendDigestForUser($user, $now, $timezone, $language);
+ $this->config->setUserValue($user, 'activity', 'digest', $timezoneDigestDay[$timezone]);
+ }
+ }
+
+ /**
+ * get all users who have activity digest enabled
+ *
+ * @return string[]
+ */
+ private function getDigestUsers(): array {
+ return $this->config->getUsersForUserValue('activity', 'notify_setting_activity_digest', 1);
+ }
+
+ private function getLastSendActivity(string $user, int $now): int {
+ $lastSend = (int)$this->config->getUserValue($user, 'activity', 'activity_digest_last_send', 0);
+ if ($lastSend > 0) {
+ return $lastSend;
+ }
+
+ // Don't flood on first email with old news, just consider the last 24h
+ return $this->data->getFirstActivitySince($user, $now - (24 * 60 * 60));
+ }
+
+ public function sendDigestForUser(string $uid, int $now, string $timezone, string $language) {
+ $l10n = $this->l10nFactory->get('activity', $language);
+ $lastSend = $this->getLastSendActivity($uid, $now);
+ $user = $this->userManager->get($uid);
+ if ($lastSend === 0) {
+ return;
+ }
+
+ ['count' => $count, 'max' => $lastActivityId] = $this->data->getActivitySince($uid, $lastSend, false);
+ $count = (int) $count;
+ $lastActivityId = (int) $lastActivityId;
+ if ($count === 0) {
+ return;
+ }
+
+ /** @var IEvent[] $activities */
+ $activities = $this->data->get(
+ $this->groupHelper,
+ $this->userSettings,
+ $uid,
+ $lastSend,
+ self::ACTIVITY_LIMIT,
+ 'asc',
+ 'by',
+ '',
+ 0,
+ true
+ );
+ $skippedCount = max(0, $count - self::ACTIVITY_LIMIT);
+
+ $template = $this->mailer->createEMailTemplate('activity.Notification', [
+ 'displayname' => $user->getDisplayName(),
+ 'url' => $this->urlGenerator->getAbsoluteURL('/'),
+ 'activityEvents' => $activities,
+ 'skippedCount' => $skippedCount,
+ ]);
+ $template->setSubject($l10n->t('Daily activity summary for ' . $this->defaults->getName()));
+ $template->addHeader();
+
+ foreach ($activities as $event) {
+ $relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
+ $event->getTimestamp(),
+ 'long',
+ 'short',
+ new \DateTimeZone($timezone),
+ $l10n
+ );
+
+ $template->addBodyListItem($this->getHTMLSubject($event), $relativeDateTime, $event->getIcon(), $event->getParsedSubject());
+ }
+
+ if ($skippedCount) {
+ $template->addBodyListItem($l10n->n('and %n more ', 'and %n more ', $skippedCount));
+ }
+
+ $template->addFooter();
+
+ $message = $this->mailer->createMessage();
+ $message->setTo([$user->getEMailAddress() => $user->getDisplayName()]);
+ $message->useTemplate($template);
+ $message->setFrom([Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
+
+ try {
+ $this->mailer->send($message);
+ $this->config->setUserValue($user->getUID(), 'activity', 'activity_digest_last_send', $lastActivityId);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage());
+ return;
+ }
+ }
+
+ /**
+ * @param IEvent $event
+ * @return string
+ */
+ protected function getHTMLSubject(IEvent $event): string {
+ if ($event->getRichSubject() === '') {
+ return htmlspecialchars($event->getParsedSubject());
+ }
+
+ $placeholders = $replacements = [];
+ foreach ($event->getRichSubjectParameters() as $placeholder => $parameter) {
+ $placeholders[] = '{' . $placeholder . '}';
+
+ if ($parameter['type'] === 'file') {
+ $replacement = $parameter['path'];
+ } else {
+ $replacement = $parameter['name'];
+ }
+
+ if (isset($parameter['link'])) {
+ $replacements[] = '' . htmlspecialchars($replacement) . '';
+ } else {
+ $replacements[] = '' . htmlspecialchars($replacement) . '';
+ }
+ }
+
+ return str_replace($placeholders, $replacements, $event->getRichSubject());
+ }
+}
diff --git a/lib/GroupHelper.php b/lib/GroupHelper.php
index c9766a4ad..fc5119e5a 100644
--- a/lib/GroupHelper.php
+++ b/lib/GroupHelper.php
@@ -139,6 +139,13 @@ public function getActivities() {
return $return;
}
+ /**
+ * @return IEvent[]
+ */
+ public function getEvents(): array {
+ return $this->event;
+ }
+
/**
* @param array $row
* @return IEvent
diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php
index cd9408c10..5bd866dbc 100644
--- a/lib/Settings/Personal.php
+++ b/lib/Settings/Personal.php
@@ -152,6 +152,8 @@ public function getForm() {
'notify_selfemail' => $this->userSettings->getUserSetting($this->user, 'setting', 'selfemail'),
'methods' => $methods,
+
+ 'activity_digest_enabled' => $this->userSettings->getUserSetting($this->user, 'setting', 'activity_digest')
], 'blank');
}
diff --git a/templates/settings/form.php b/templates/settings/form.php
index b59d85771..ddab62d1e 100644
--- a/templates/settings/form.php
+++ b/templates/settings/form.php
@@ -69,7 +69,7 @@
-
+