1<?php 2/** 3 * @copyright Copyright (c) 2019 Julien Veyssier <eneiluj@posteo.net> 4 * 5 * @author Julien Veyssier <eneiluj@posteo.net> 6 * 7 * @license GNU AGPL version 3 or any later version 8 * 9 * This program is free software: you can redistribute it and/or modify 10 * it under the terms of the GNU Affero General Public License as 11 * published by the Free Software Foundation, either version 3 of the 12 * License, or (at your option) any later version. 13 * 14 * This program is distributed in the hope that it will be useful, 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 * GNU Affero General Public License for more details. 18 * 19 * You should have received a copy of the GNU Affero General Public License 20 * along with this program. If not, see <http://www.gnu.org/licenses/>. 21 * 22 */ 23 24namespace OCA\Cospend\Activity; 25 26use Exception; 27use InvalidArgumentException; 28use OCA\Cospend\Service\UserService; 29use OCA\Cospend\Db\BillMapper; 30use OCA\Cospend\Db\Bill; 31use OCA\Cospend\Db\ProjectMapper; 32use OCA\Cospend\Db\Project; 33 34use OCP\AppFramework\Db\Entity; 35use Psr\Log\LoggerInterface; 36use OCP\Activity\IEvent; 37use OCP\Activity\IManager; 38use OCP\AppFramework\Db\DoesNotExistException; 39use OCP\AppFramework\Db\MultipleObjectsReturnedException; 40use OCP\IL10N; 41use function get_class; 42 43class ActivityManager { 44 45 private $manager; 46 private $userId; 47 private $projectMapper; 48 private $billMapper; 49 private $l10n; 50 51 const COSPEND_OBJECT_BILL = 'cospend_bill'; 52 const COSPEND_OBJECT_PROJECT = 'cospend_project'; 53 54 const SUBJECT_BILL_CREATE = 'bill_create'; 55 const SUBJECT_BILL_UPDATE = 'bill_update'; 56 const SUBJECT_BILL_DELETE = 'bill_delete'; 57 58 const SUBJECT_PROJECT_SHARE = 'project_share'; 59 const SUBJECT_PROJECT_UNSHARE = 'project_unshare'; 60 /** 61 * @var UserService 62 */ 63 private $userService; 64 /** 65 * @var LoggerInterface 66 */ 67 private $logger; 68 69 public function __construct(IManager $manager, 70 UserService $userService, 71 ProjectMapper $projectMapper, 72 BillMapper $billMapper, 73 IL10N $l10n, 74 LoggerInterface $logger, 75 ?string $userId) { 76 $this->manager = $manager; 77 $this->userService = $userService; 78 $this->projectMapper = $projectMapper; 79 $this->billMapper = $billMapper; 80 $this->l10n = $l10n; 81 $this->userId = $userId; 82 $this->logger = $logger; 83 } 84 85 /** 86 * @param string $subjectIdentifier 87 * @param array $subjectParams 88 * @param bool $ownActivity 89 * @return string 90 */ 91 public function getActivityFormat(string $subjectIdentifier, array $subjectParams = [], bool $ownActivity = false): string { 92 $subject = ''; 93 switch ($subjectIdentifier) { 94 case self::SUBJECT_BILL_CREATE: 95 $subject = $ownActivity ? $this->l10n->t('You have created a new bill {bill} in project {project}'): $this->l10n->t('{user} has created a new bill {bill} in project {project}'); 96 break; 97 case self::SUBJECT_BILL_DELETE: 98 $subject = $ownActivity ? $this->l10n->t('You have deleted the bill {bill} of project {project}') : $this->l10n->t('{user} has deleted the bill {bill} of project {project}'); 99 break; 100 case self::SUBJECT_PROJECT_SHARE: 101 $subject = $ownActivity ? $this->l10n->t('You have shared the project {project} with {who}') : $this->l10n->t('{user} has shared the project {project} with {who}'); 102 break; 103 case self::SUBJECT_PROJECT_UNSHARE: 104 $subject = $ownActivity ? $this->l10n->t('You have removed {who} from the project {project}') : $this->l10n->t('{user} has removed {who} from the project {project}'); 105 break; 106 case self::SUBJECT_BILL_UPDATE: 107 $subject = $ownActivity ? $this->l10n->t('You have updated the bill {bill} of project {project}') : $this->l10n->t('{user} has updated the bill {bill} of project {project}'); 108 break; 109 default: 110 break; 111 } 112 return $subject; 113 } 114 115 /** 116 * @param string $objectType 117 * @param Entity $entity 118 * @param string $subject 119 * @param array $additionalParams 120 * @param string|null $author 121 */ 122 public function triggerEvent(string $objectType, Entity $entity, string $subject, array $additionalParams = [], ?string $author = null) { 123 try { 124 $event = $this->createEvent($objectType, $entity, $subject, $additionalParams, $author); 125 if ($event !== null) { 126 $this->sendToUsers($event); 127 } 128 } catch (Exception $e) { 129 // Ignore exception for undefined activities on update events 130 } 131 } 132 133 /** 134 * @param string $objectType 135 * @param Entity $entity 136 * @param string $subject 137 * @param array $additionalParams 138 * @param string|null $author 139 * @return IEvent|null 140 * @throws Exception 141 */ 142 private function createEvent(string $objectType, Entity $entity, string $subject, array $additionalParams = [], ?string $author = null): ?IEvent { 143 if ($subject === self::SUBJECT_BILL_DELETE) { 144 $object = $entity; 145 } else { 146 try { 147 $object = $this->findObjectForEntity($objectType, $entity); 148 } catch (DoesNotExistException $e) { 149 $this->logger->error('Could not create activity entry for ' . $subject . '. Entity not found.', (array)$entity); 150 return null; 151 } catch (MultipleObjectsReturnedException $e) { 152 $this->logger->error('Could not create activity entry for ' . $subject . '. Entity not found.', (array)$entity); 153 return null; 154 } 155 } 156 157 /** 158 * Automatically fetch related details for subject parameters 159 * depending on the subject 160 */ 161 $eventType = 'cospend'; 162 $subjectParams = []; 163 $message = null; 164 $objectName = null; 165 switch ($subject) { 166 // No need to enhance parameters since entity already contains the required data 167 case self::SUBJECT_BILL_CREATE: 168 case self::SUBJECT_BILL_UPDATE: 169 case self::SUBJECT_BILL_DELETE: 170 $subjectParams = $this->findDetailsForBill($object); 171 $objectName = $object->getWhat(); 172 $eventType = 'cospend_bill_event'; 173 break; 174 case self::SUBJECT_PROJECT_SHARE: 175 case self::SUBJECT_PROJECT_UNSHARE: 176 $subjectParams = $this->findDetailsForProject($entity->getId()); 177 $objectName = $object->getId(); 178 break; 179 default: 180 throw new Exception('Unknown subject for activity.'); 181 } 182 $subjectParams['author'] = $this->l10n->t('A guest user'); 183 184 $event = $this->manager->generateEvent(); 185 $event->setApp('cospend') 186 ->setType($eventType) 187 ->setAuthor($author === null ? $this->userId ?? '' : $author) 188 ->setObject($objectType, (int)$object->getId(), $objectName) 189 ->setSubject($subject, array_merge($subjectParams, $additionalParams)) 190 ->setTimestamp(time()); 191 192 if ($message !== null) { 193 $event->setMessage($message); 194 } 195 return $event; 196 } 197 198 /** 199 * Publish activity to all users that are part of the project of a given object 200 * 201 * @param IEvent $event 202 */ 203 private function sendToUsers(IEvent $event) { 204 $projectId = ''; 205 switch ($event->getObjectType()) { 206 case self::COSPEND_OBJECT_BILL: 207 $projectId = $event->getSubjectParameters()['project']['id']; 208 break; 209 case self::COSPEND_OBJECT_PROJECT: 210 $projectId = $event->getObjectName(); 211 break; 212 } 213 foreach ($this->userService->findUsers($projectId) as $user) { 214 $event->setAffectedUser($user); 215 /** @noinspection DisconnectedForeachInstructionInspection */ 216 $this->manager->publish($event); 217 } 218 } 219 220 /** 221 * @param $objectType 222 * @param $entity 223 * @return Entity 224 */ 225 private function findObjectForEntity($objectType, $entity): Entity { 226 $className = get_class($entity); 227 if ($objectType === self::COSPEND_OBJECT_BILL) { 228 switch ($className) { 229 case Bill::class: 230 $objectId = $entity->getId(); 231 break; 232 default: 233 throw new InvalidArgumentException('No entity relation present for '. $className . ' to ' . $objectType); 234 } 235 return $this->billMapper->find($objectId); 236 } 237 if ($objectType === self::COSPEND_OBJECT_PROJECT) { 238 switch ($className) { 239 case Project::class: 240 $objectId = $entity->getId(); 241 break; 242 default: 243 throw new InvalidArgumentException('No entity relation present for '. $className . ' to ' . $objectType); 244 } 245 return $this->projectMapper->find($objectId); 246 } 247 throw new InvalidArgumentException('No entity relation present for '. $className . ' to ' . $objectType); 248 } 249 250 /** 251 * @param object $bill 252 * @return array[] 253 */ 254 private function findDetailsForBill(object $bill): array { 255 $project = $this->projectMapper->find($bill->getProjectid()); 256 $bill = [ 257 'id' => $bill->getId(), 258 'name' => $bill->getWhat(), 259 'amount' => $bill->getAmount() 260 ]; 261 $project = [ 262 'id' => $project->getId(), 263 'name' => $project->getName() 264 ]; 265 return [ 266 'bill' => $bill, 267 'project' => $project 268 ]; 269 } 270 271 /** 272 * @param string $projectId 273 * @return array[] 274 */ 275 private function findDetailsForProject(string $projectId): array { 276 $project = $this->projectMapper->find($projectId); 277 $project = [ 278 'id' => $project->getId(), 279 'name' => $project->getName() 280 ]; 281 return [ 282 'project' => $project 283 ]; 284 } 285 286} 287