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