Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.75% covered (warning)
74.75%
74 / 99
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ActivityManager
74.75% covered (warning)
74.75%
74 / 99
50.00% covered (danger)
50.00%
4 / 8
68.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getActivityFormat
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
156
 triggerEvent
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 createEvent
75.00% covered (warning)
75.00%
24 / 32
0.00% covered (danger)
0.00%
0 / 1
14.25
 sendToUsers
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 findObjectForEntity
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
7.77
 findDetailsForBill
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 findDetailsForProject
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
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}