Passed
Push — master ( 3c2aba...b2075d )
by Joas
14:49 queued 11s
created
apps/user_status/lib/Service/StatusService.php 1 patch
Indentation   +511 added lines, -511 removed lines patch added patch discarded remove patch
@@ -47,515 +47,515 @@
 block discarded – undo
47 47
  */
48 48
 class StatusService {
49 49
 
50
-	/** @var UserStatusMapper */
51
-	private $mapper;
52
-
53
-	/** @var ITimeFactory */
54
-	private $timeFactory;
55
-
56
-	/** @var PredefinedStatusService */
57
-	private $predefinedStatusService;
58
-
59
-	/** @var EmojiService */
60
-	private $emojiService;
61
-
62
-	/** @var bool */
63
-	private $shareeEnumeration;
64
-
65
-	/** @var bool */
66
-	private $shareeEnumerationInGroupOnly;
67
-
68
-	/** @var bool */
69
-	private $shareeEnumerationPhone;
70
-
71
-	/**
72
-	 * List of priorities ordered by their priority
73
-	 */
74
-	public const PRIORITY_ORDERED_STATUSES = [
75
-		IUserStatus::ONLINE,
76
-		IUserStatus::AWAY,
77
-		IUserStatus::DND,
78
-		IUserStatus::INVISIBLE,
79
-		IUserStatus::OFFLINE,
80
-	];
81
-
82
-	/**
83
-	 * List of statuses that persist the clear-up
84
-	 * or UserLiveStatusEvents
85
-	 */
86
-	public const PERSISTENT_STATUSES = [
87
-		IUserStatus::AWAY,
88
-		IUserStatus::DND,
89
-		IUserStatus::INVISIBLE,
90
-	];
91
-
92
-	/** @var int */
93
-	public const INVALIDATE_STATUS_THRESHOLD = 15 /* minutes */ * 60 /* seconds */;
94
-
95
-	/** @var int */
96
-	public const MAXIMUM_MESSAGE_LENGTH = 80;
97
-
98
-	/**
99
-	 * StatusService constructor.
100
-	 *
101
-	 * @param UserStatusMapper $mapper
102
-	 * @param ITimeFactory $timeFactory
103
-	 * @param PredefinedStatusService $defaultStatusService
104
-	 * @param EmojiService $emojiService
105
-	 * @param IConfig $config
106
-	 */
107
-	public function __construct(UserStatusMapper $mapper,
108
-								ITimeFactory $timeFactory,
109
-								PredefinedStatusService $defaultStatusService,
110
-								EmojiService $emojiService,
111
-								IConfig $config) {
112
-		$this->mapper = $mapper;
113
-		$this->timeFactory = $timeFactory;
114
-		$this->predefinedStatusService = $defaultStatusService;
115
-		$this->emojiService = $emojiService;
116
-		$this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
117
-		$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
118
-		$this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
119
-	}
120
-
121
-	/**
122
-	 * @param int|null $limit
123
-	 * @param int|null $offset
124
-	 * @return UserStatus[]
125
-	 */
126
-	public function findAll(?int $limit = null, ?int $offset = null): array {
127
-		// Return empty array if user enumeration is disabled or limited to groups
128
-		// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
129
-		//       groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
130
-		if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
131
-			return [];
132
-		}
133
-
134
-		return array_map(function ($status) {
135
-			return $this->processStatus($status);
136
-		}, $this->mapper->findAll($limit, $offset));
137
-	}
138
-
139
-	/**
140
-	 * @param int|null $limit
141
-	 * @param int|null $offset
142
-	 * @return array
143
-	 */
144
-	public function findAllRecentStatusChanges(?int $limit = null, ?int $offset = null): array {
145
-		// Return empty array if user enumeration is disabled or limited to groups
146
-		// TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
147
-		//       groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
148
-		if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
149
-			return [];
150
-		}
151
-
152
-		return array_map(function ($status) {
153
-			return $this->processStatus($status);
154
-		}, $this->mapper->findAllRecent($limit, $offset));
155
-	}
156
-
157
-	/**
158
-	 * @param string $userId
159
-	 * @return UserStatus
160
-	 * @throws DoesNotExistException
161
-	 */
162
-	public function findByUserId(string $userId):UserStatus {
163
-		return $this->processStatus($this->mapper->findByUserId($userId));
164
-	}
165
-
166
-	/**
167
-	 * @param array $userIds
168
-	 * @return UserStatus[]
169
-	 */
170
-	public function findByUserIds(array $userIds):array {
171
-		return array_map(function ($status) {
172
-			return $this->processStatus($status);
173
-		}, $this->mapper->findByUserIds($userIds));
174
-	}
175
-
176
-	/**
177
-	 * @param string $userId
178
-	 * @param string $status
179
-	 * @param int|null $statusTimestamp
180
-	 * @param bool $isUserDefined
181
-	 * @return UserStatus
182
-	 * @throws InvalidStatusTypeException
183
-	 */
184
-	public function setStatus(string $userId,
185
-							  string $status,
186
-							  ?int $statusTimestamp,
187
-							  bool $isUserDefined): UserStatus {
188
-		try {
189
-			$userStatus = $this->mapper->findByUserId($userId);
190
-		} catch (DoesNotExistException $ex) {
191
-			$userStatus = new UserStatus();
192
-			$userStatus->setUserId($userId);
193
-		}
194
-
195
-		// Check if status-type is valid
196
-		if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
197
-			throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
198
-		}
199
-		if ($statusTimestamp === null) {
200
-			$statusTimestamp = $this->timeFactory->getTime();
201
-		}
202
-
203
-		$userStatus->setStatus($status);
204
-		$userStatus->setStatusTimestamp($statusTimestamp);
205
-		$userStatus->setIsUserDefined($isUserDefined);
206
-		$userStatus->setIsBackup(false);
207
-
208
-		if ($userStatus->getId() === null) {
209
-			return $this->mapper->insert($userStatus);
210
-		}
211
-
212
-		return $this->mapper->update($userStatus);
213
-	}
214
-
215
-	/**
216
-	 * @param string $userId
217
-	 * @param string $messageId
218
-	 * @param int|null $clearAt
219
-	 * @return UserStatus
220
-	 * @throws InvalidMessageIdException
221
-	 * @throws InvalidClearAtException
222
-	 */
223
-	public function setPredefinedMessage(string $userId,
224
-										 string $messageId,
225
-										 ?int $clearAt): UserStatus {
226
-		try {
227
-			$userStatus = $this->mapper->findByUserId($userId);
228
-		} catch (DoesNotExistException $ex) {
229
-			$userStatus = new UserStatus();
230
-			$userStatus->setUserId($userId);
231
-			$userStatus->setStatus(IUserStatus::OFFLINE);
232
-			$userStatus->setStatusTimestamp(0);
233
-			$userStatus->setIsUserDefined(false);
234
-			$userStatus->setIsBackup(false);
235
-		}
236
-
237
-		if (!$this->predefinedStatusService->isValidId($messageId)) {
238
-			throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
239
-		}
240
-
241
-		// Check that clearAt is in the future
242
-		if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
243
-			throw new InvalidClearAtException('ClearAt is in the past');
244
-		}
245
-
246
-		$userStatus->setMessageId($messageId);
247
-		$userStatus->setCustomIcon(null);
248
-		$userStatus->setCustomMessage(null);
249
-		$userStatus->setClearAt($clearAt);
250
-
251
-		if ($userStatus->getId() === null) {
252
-			return $this->mapper->insert($userStatus);
253
-		}
254
-
255
-		return $this->mapper->update($userStatus);
256
-	}
257
-
258
-	/**
259
-	 * @param string $userId
260
-	 * @param string $status
261
-	 * @param string $messageId
262
-	 * @param bool $createBackup
263
-	 * @throws InvalidStatusTypeException
264
-	 * @throws InvalidMessageIdException
265
-	 */
266
-	public function setUserStatus(string $userId,
267
-										 string $status,
268
-										 string $messageId,
269
-										 bool $createBackup): void {
270
-		// Check if status-type is valid
271
-		if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
272
-			throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
273
-		}
274
-
275
-		if (!$this->predefinedStatusService->isValidId($messageId)) {
276
-			throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
277
-		}
278
-
279
-		if ($createBackup) {
280
-			if ($this->backupCurrentStatus($userId) === false) {
281
-				return; // Already a status set automatically => abort.
282
-			}
283
-
284
-			// If we just created the backup
285
-			$userStatus = new UserStatus();
286
-			$userStatus->setUserId($userId);
287
-		} else {
288
-			try {
289
-				$userStatus = $this->mapper->findByUserId($userId);
290
-			} catch (DoesNotExistException $ex) {
291
-				$userStatus = new UserStatus();
292
-				$userStatus->setUserId($userId);
293
-			}
294
-		}
295
-
296
-		$userStatus->setStatus($status);
297
-		$userStatus->setStatusTimestamp($this->timeFactory->getTime());
298
-		$userStatus->setIsUserDefined(true);
299
-		$userStatus->setIsBackup(false);
300
-		$userStatus->setMessageId($messageId);
301
-		$userStatus->setCustomIcon(null);
302
-		$userStatus->setCustomMessage(null);
303
-		$userStatus->setClearAt(null);
304
-
305
-		if ($userStatus->getId() !== null) {
306
-			$this->mapper->update($userStatus);
307
-			return;
308
-		}
309
-		$this->mapper->insert($userStatus);
310
-	}
311
-
312
-	/**
313
-	 * @param string $userId
314
-	 * @param string|null $statusIcon
315
-	 * @param string $message
316
-	 * @param int|null $clearAt
317
-	 * @return UserStatus
318
-	 * @throws InvalidClearAtException
319
-	 * @throws InvalidStatusIconException
320
-	 * @throws StatusMessageTooLongException
321
-	 */
322
-	public function setCustomMessage(string $userId,
323
-									 ?string $statusIcon,
324
-									 string $message,
325
-									 ?int $clearAt): UserStatus {
326
-		try {
327
-			$userStatus = $this->mapper->findByUserId($userId);
328
-		} catch (DoesNotExistException $ex) {
329
-			$userStatus = new UserStatus();
330
-			$userStatus->setUserId($userId);
331
-			$userStatus->setStatus(IUserStatus::OFFLINE);
332
-			$userStatus->setStatusTimestamp(0);
333
-			$userStatus->setIsUserDefined(false);
334
-		}
335
-
336
-		// Check if statusIcon contains only one character
337
-		if ($statusIcon !== null && !$this->emojiService->isValidEmoji($statusIcon)) {
338
-			throw new InvalidStatusIconException('Status-Icon is longer than one character');
339
-		}
340
-		// Check for maximum length of custom message
341
-		if (\mb_strlen($message) > self::MAXIMUM_MESSAGE_LENGTH) {
342
-			throw new StatusMessageTooLongException('Message is longer than supported length of ' . self::MAXIMUM_MESSAGE_LENGTH . ' characters');
343
-		}
344
-		// Check that clearAt is in the future
345
-		if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
346
-			throw new InvalidClearAtException('ClearAt is in the past');
347
-		}
348
-
349
-		$userStatus->setMessageId(null);
350
-		$userStatus->setCustomIcon($statusIcon);
351
-		$userStatus->setCustomMessage($message);
352
-		$userStatus->setClearAt($clearAt);
353
-
354
-		if ($userStatus->getId() === null) {
355
-			return $this->mapper->insert($userStatus);
356
-		}
357
-
358
-		return $this->mapper->update($userStatus);
359
-	}
360
-
361
-	/**
362
-	 * @param string $userId
363
-	 * @return bool
364
-	 */
365
-	public function clearStatus(string $userId): bool {
366
-		try {
367
-			$userStatus = $this->mapper->findByUserId($userId);
368
-		} catch (DoesNotExistException $ex) {
369
-			// if there is no status to remove, just return
370
-			return false;
371
-		}
372
-
373
-		$userStatus->setStatus(IUserStatus::OFFLINE);
374
-		$userStatus->setStatusTimestamp(0);
375
-		$userStatus->setIsUserDefined(false);
376
-
377
-		$this->mapper->update($userStatus);
378
-		return true;
379
-	}
380
-
381
-	/**
382
-	 * @param string $userId
383
-	 * @return bool
384
-	 */
385
-	public function clearMessage(string $userId): bool {
386
-		try {
387
-			$userStatus = $this->mapper->findByUserId($userId);
388
-		} catch (DoesNotExistException $ex) {
389
-			// if there is no status to remove, just return
390
-			return false;
391
-		}
392
-
393
-		$userStatus->setMessageId(null);
394
-		$userStatus->setCustomMessage(null);
395
-		$userStatus->setCustomIcon(null);
396
-		$userStatus->setClearAt(null);
397
-
398
-		$this->mapper->update($userStatus);
399
-		return true;
400
-	}
401
-
402
-	/**
403
-	 * @param string $userId
404
-	 * @return bool
405
-	 */
406
-	public function removeUserStatus(string $userId): bool {
407
-		try {
408
-			$userStatus = $this->mapper->findByUserId($userId, false);
409
-		} catch (DoesNotExistException $ex) {
410
-			// if there is no status to remove, just return
411
-			return false;
412
-		}
413
-
414
-		$this->mapper->delete($userStatus);
415
-		return true;
416
-	}
417
-
418
-	public function removeBackupUserStatus(string $userId): bool {
419
-		try {
420
-			$userStatus = $this->mapper->findByUserId($userId, true);
421
-		} catch (DoesNotExistException $ex) {
422
-			// if there is no status to remove, just return
423
-			return false;
424
-		}
425
-
426
-		$this->mapper->delete($userStatus);
427
-		return true;
428
-	}
429
-
430
-	/**
431
-	 * Processes a status to check if custom message is still
432
-	 * up to date and provides translated default status if needed
433
-	 *
434
-	 * @param UserStatus $status
435
-	 * @return UserStatus
436
-	 */
437
-	private function processStatus(UserStatus $status): UserStatus {
438
-		$clearAt = $status->getClearAt();
439
-
440
-		if ($status->getStatusTimestamp() < $this->timeFactory->getTime() - self::INVALIDATE_STATUS_THRESHOLD
441
-			&& (!$status->getIsUserDefined() || $status->getStatus() === IUserStatus::ONLINE)) {
442
-			$this->cleanStatus($status);
443
-		}
444
-		if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
445
-			$this->cleanStatusMessage($status);
446
-		}
447
-		if ($status->getMessageId() !== null) {
448
-			$this->addDefaultMessage($status);
449
-		}
450
-
451
-		return $status;
452
-	}
453
-
454
-	/**
455
-	 * @param UserStatus $status
456
-	 */
457
-	private function cleanStatus(UserStatus $status): void {
458
-		if ($status->getStatus() === IUserStatus::OFFLINE && !$status->getIsUserDefined()) {
459
-			return;
460
-		}
461
-
462
-		$status->setStatus(IUserStatus::OFFLINE);
463
-		$status->setStatusTimestamp($this->timeFactory->getTime());
464
-		$status->setIsUserDefined(false);
465
-
466
-		$this->mapper->update($status);
467
-	}
468
-
469
-	/**
470
-	 * @param UserStatus $status
471
-	 */
472
-	private function cleanStatusMessage(UserStatus $status): void {
473
-		$status->setMessageId(null);
474
-		$status->setCustomIcon(null);
475
-		$status->setCustomMessage(null);
476
-		$status->setClearAt(null);
477
-
478
-		$this->mapper->update($status);
479
-	}
480
-
481
-	/**
482
-	 * @param UserStatus $status
483
-	 */
484
-	private function addDefaultMessage(UserStatus $status): void {
485
-		// If the message is predefined, insert the translated message and icon
486
-		$predefinedMessage = $this->predefinedStatusService->getDefaultStatusById($status->getMessageId());
487
-		if ($predefinedMessage !== null) {
488
-			$status->setCustomMessage($predefinedMessage['message']);
489
-			$status->setCustomIcon($predefinedMessage['icon']);
490
-		}
491
-	}
492
-
493
-	/**
494
-	 * @return bool false if there is already a backup. In this case abort the procedure.
495
-	 */
496
-	public function backupCurrentStatus(string $userId): bool {
497
-		try {
498
-			$this->mapper->createBackupStatus($userId);
499
-			return true;
500
-		} catch (Exception $ex) {
501
-			if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
502
-				return false;
503
-			}
504
-			throw $ex;
505
-		}
506
-	}
507
-
508
-	public function revertUserStatus(string $userId, string $messageId, string $status): void {
509
-		try {
510
-			/** @var UserStatus $userStatus */
511
-			$backupUserStatus = $this->mapper->findByUserId($userId, true);
512
-		} catch (DoesNotExistException $ex) {
513
-			// No user status to revert, do nothing
514
-			return;
515
-		}
516
-
517
-		$deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId, $status);
518
-		if (!$deleted) {
519
-			// Another status is set automatically or no status, do nothing
520
-			return;
521
-		}
522
-
523
-		$backupUserStatus->setIsBackup(false);
524
-		// Remove the underscore prefix added when creating the backup
525
-		$backupUserStatus->setUserId(substr($backupUserStatus->getUserId(), 1));
526
-		$this->mapper->update($backupUserStatus);
527
-	}
528
-
529
-	public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void {
530
-		// Get all user statuses and the backups
531
-		$findById = $userIds;
532
-		foreach ($userIds as $userId) {
533
-			$findById[] = '_' . $userId;
534
-		}
535
-		$userStatuses = $this->mapper->findByUserIds($findById);
536
-
537
-		$backups = $restoreIds = $statuesToDelete = [];
538
-		foreach ($userStatuses as $userStatus) {
539
-			if (!$userStatus->getIsBackup()
540
-				&& $userStatus->getMessageId() === $messageId
541
-				&& $userStatus->getStatus() === $status) {
542
-				$statuesToDelete[$userStatus->getUserId()] = $userStatus->getId();
543
-			} else if ($userStatus->getIsBackup()) {
544
-				$backups[$userStatus->getUserId()] = $userStatus->getId();
545
-			}
546
-		}
547
-
548
-		// For users with both (normal and backup) delete the status when matching
549
-		foreach ($statuesToDelete as $userId => $statusId) {
550
-			$backupUserId = '_' . $userId;
551
-			if (isset($backups[$backupUserId])) {
552
-				$restoreIds[] = $backups[$backupUserId];
553
-			}
554
-		}
555
-
556
-		$this->mapper->deleteByIds(array_values($statuesToDelete));
557
-
558
-		// For users that matched restore the previous status
559
-		$this->mapper->restoreBackupStatuses($restoreIds);
560
-	}
50
+    /** @var UserStatusMapper */
51
+    private $mapper;
52
+
53
+    /** @var ITimeFactory */
54
+    private $timeFactory;
55
+
56
+    /** @var PredefinedStatusService */
57
+    private $predefinedStatusService;
58
+
59
+    /** @var EmojiService */
60
+    private $emojiService;
61
+
62
+    /** @var bool */
63
+    private $shareeEnumeration;
64
+
65
+    /** @var bool */
66
+    private $shareeEnumerationInGroupOnly;
67
+
68
+    /** @var bool */
69
+    private $shareeEnumerationPhone;
70
+
71
+    /**
72
+     * List of priorities ordered by their priority
73
+     */
74
+    public const PRIORITY_ORDERED_STATUSES = [
75
+        IUserStatus::ONLINE,
76
+        IUserStatus::AWAY,
77
+        IUserStatus::DND,
78
+        IUserStatus::INVISIBLE,
79
+        IUserStatus::OFFLINE,
80
+    ];
81
+
82
+    /**
83
+     * List of statuses that persist the clear-up
84
+     * or UserLiveStatusEvents
85
+     */
86
+    public const PERSISTENT_STATUSES = [
87
+        IUserStatus::AWAY,
88
+        IUserStatus::DND,
89
+        IUserStatus::INVISIBLE,
90
+    ];
91
+
92
+    /** @var int */
93
+    public const INVALIDATE_STATUS_THRESHOLD = 15 /* minutes */ * 60 /* seconds */;
94
+
95
+    /** @var int */
96
+    public const MAXIMUM_MESSAGE_LENGTH = 80;
97
+
98
+    /**
99
+     * StatusService constructor.
100
+     *
101
+     * @param UserStatusMapper $mapper
102
+     * @param ITimeFactory $timeFactory
103
+     * @param PredefinedStatusService $defaultStatusService
104
+     * @param EmojiService $emojiService
105
+     * @param IConfig $config
106
+     */
107
+    public function __construct(UserStatusMapper $mapper,
108
+                                ITimeFactory $timeFactory,
109
+                                PredefinedStatusService $defaultStatusService,
110
+                                EmojiService $emojiService,
111
+                                IConfig $config) {
112
+        $this->mapper = $mapper;
113
+        $this->timeFactory = $timeFactory;
114
+        $this->predefinedStatusService = $defaultStatusService;
115
+        $this->emojiService = $emojiService;
116
+        $this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
117
+        $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
118
+        $this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
119
+    }
120
+
121
+    /**
122
+     * @param int|null $limit
123
+     * @param int|null $offset
124
+     * @return UserStatus[]
125
+     */
126
+    public function findAll(?int $limit = null, ?int $offset = null): array {
127
+        // Return empty array if user enumeration is disabled or limited to groups
128
+        // TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
129
+        //       groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
130
+        if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
131
+            return [];
132
+        }
133
+
134
+        return array_map(function ($status) {
135
+            return $this->processStatus($status);
136
+        }, $this->mapper->findAll($limit, $offset));
137
+    }
138
+
139
+    /**
140
+     * @param int|null $limit
141
+     * @param int|null $offset
142
+     * @return array
143
+     */
144
+    public function findAllRecentStatusChanges(?int $limit = null, ?int $offset = null): array {
145
+        // Return empty array if user enumeration is disabled or limited to groups
146
+        // TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
147
+        //       groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
148
+        if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
149
+            return [];
150
+        }
151
+
152
+        return array_map(function ($status) {
153
+            return $this->processStatus($status);
154
+        }, $this->mapper->findAllRecent($limit, $offset));
155
+    }
156
+
157
+    /**
158
+     * @param string $userId
159
+     * @return UserStatus
160
+     * @throws DoesNotExistException
161
+     */
162
+    public function findByUserId(string $userId):UserStatus {
163
+        return $this->processStatus($this->mapper->findByUserId($userId));
164
+    }
165
+
166
+    /**
167
+     * @param array $userIds
168
+     * @return UserStatus[]
169
+     */
170
+    public function findByUserIds(array $userIds):array {
171
+        return array_map(function ($status) {
172
+            return $this->processStatus($status);
173
+        }, $this->mapper->findByUserIds($userIds));
174
+    }
175
+
176
+    /**
177
+     * @param string $userId
178
+     * @param string $status
179
+     * @param int|null $statusTimestamp
180
+     * @param bool $isUserDefined
181
+     * @return UserStatus
182
+     * @throws InvalidStatusTypeException
183
+     */
184
+    public function setStatus(string $userId,
185
+                                string $status,
186
+                              ?int $statusTimestamp,
187
+                                bool $isUserDefined): UserStatus {
188
+        try {
189
+            $userStatus = $this->mapper->findByUserId($userId);
190
+        } catch (DoesNotExistException $ex) {
191
+            $userStatus = new UserStatus();
192
+            $userStatus->setUserId($userId);
193
+        }
194
+
195
+        // Check if status-type is valid
196
+        if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
197
+            throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
198
+        }
199
+        if ($statusTimestamp === null) {
200
+            $statusTimestamp = $this->timeFactory->getTime();
201
+        }
202
+
203
+        $userStatus->setStatus($status);
204
+        $userStatus->setStatusTimestamp($statusTimestamp);
205
+        $userStatus->setIsUserDefined($isUserDefined);
206
+        $userStatus->setIsBackup(false);
207
+
208
+        if ($userStatus->getId() === null) {
209
+            return $this->mapper->insert($userStatus);
210
+        }
211
+
212
+        return $this->mapper->update($userStatus);
213
+    }
214
+
215
+    /**
216
+     * @param string $userId
217
+     * @param string $messageId
218
+     * @param int|null $clearAt
219
+     * @return UserStatus
220
+     * @throws InvalidMessageIdException
221
+     * @throws InvalidClearAtException
222
+     */
223
+    public function setPredefinedMessage(string $userId,
224
+                                            string $messageId,
225
+                                         ?int $clearAt): UserStatus {
226
+        try {
227
+            $userStatus = $this->mapper->findByUserId($userId);
228
+        } catch (DoesNotExistException $ex) {
229
+            $userStatus = new UserStatus();
230
+            $userStatus->setUserId($userId);
231
+            $userStatus->setStatus(IUserStatus::OFFLINE);
232
+            $userStatus->setStatusTimestamp(0);
233
+            $userStatus->setIsUserDefined(false);
234
+            $userStatus->setIsBackup(false);
235
+        }
236
+
237
+        if (!$this->predefinedStatusService->isValidId($messageId)) {
238
+            throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
239
+        }
240
+
241
+        // Check that clearAt is in the future
242
+        if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
243
+            throw new InvalidClearAtException('ClearAt is in the past');
244
+        }
245
+
246
+        $userStatus->setMessageId($messageId);
247
+        $userStatus->setCustomIcon(null);
248
+        $userStatus->setCustomMessage(null);
249
+        $userStatus->setClearAt($clearAt);
250
+
251
+        if ($userStatus->getId() === null) {
252
+            return $this->mapper->insert($userStatus);
253
+        }
254
+
255
+        return $this->mapper->update($userStatus);
256
+    }
257
+
258
+    /**
259
+     * @param string $userId
260
+     * @param string $status
261
+     * @param string $messageId
262
+     * @param bool $createBackup
263
+     * @throws InvalidStatusTypeException
264
+     * @throws InvalidMessageIdException
265
+     */
266
+    public function setUserStatus(string $userId,
267
+                                            string $status,
268
+                                            string $messageId,
269
+                                            bool $createBackup): void {
270
+        // Check if status-type is valid
271
+        if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
272
+            throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
273
+        }
274
+
275
+        if (!$this->predefinedStatusService->isValidId($messageId)) {
276
+            throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
277
+        }
278
+
279
+        if ($createBackup) {
280
+            if ($this->backupCurrentStatus($userId) === false) {
281
+                return; // Already a status set automatically => abort.
282
+            }
283
+
284
+            // If we just created the backup
285
+            $userStatus = new UserStatus();
286
+            $userStatus->setUserId($userId);
287
+        } else {
288
+            try {
289
+                $userStatus = $this->mapper->findByUserId($userId);
290
+            } catch (DoesNotExistException $ex) {
291
+                $userStatus = new UserStatus();
292
+                $userStatus->setUserId($userId);
293
+            }
294
+        }
295
+
296
+        $userStatus->setStatus($status);
297
+        $userStatus->setStatusTimestamp($this->timeFactory->getTime());
298
+        $userStatus->setIsUserDefined(true);
299
+        $userStatus->setIsBackup(false);
300
+        $userStatus->setMessageId($messageId);
301
+        $userStatus->setCustomIcon(null);
302
+        $userStatus->setCustomMessage(null);
303
+        $userStatus->setClearAt(null);
304
+
305
+        if ($userStatus->getId() !== null) {
306
+            $this->mapper->update($userStatus);
307
+            return;
308
+        }
309
+        $this->mapper->insert($userStatus);
310
+    }
311
+
312
+    /**
313
+     * @param string $userId
314
+     * @param string|null $statusIcon
315
+     * @param string $message
316
+     * @param int|null $clearAt
317
+     * @return UserStatus
318
+     * @throws InvalidClearAtException
319
+     * @throws InvalidStatusIconException
320
+     * @throws StatusMessageTooLongException
321
+     */
322
+    public function setCustomMessage(string $userId,
323
+                                     ?string $statusIcon,
324
+                                        string $message,
325
+                                     ?int $clearAt): UserStatus {
326
+        try {
327
+            $userStatus = $this->mapper->findByUserId($userId);
328
+        } catch (DoesNotExistException $ex) {
329
+            $userStatus = new UserStatus();
330
+            $userStatus->setUserId($userId);
331
+            $userStatus->setStatus(IUserStatus::OFFLINE);
332
+            $userStatus->setStatusTimestamp(0);
333
+            $userStatus->setIsUserDefined(false);
334
+        }
335
+
336
+        // Check if statusIcon contains only one character
337
+        if ($statusIcon !== null && !$this->emojiService->isValidEmoji($statusIcon)) {
338
+            throw new InvalidStatusIconException('Status-Icon is longer than one character');
339
+        }
340
+        // Check for maximum length of custom message
341
+        if (\mb_strlen($message) > self::MAXIMUM_MESSAGE_LENGTH) {
342
+            throw new StatusMessageTooLongException('Message is longer than supported length of ' . self::MAXIMUM_MESSAGE_LENGTH . ' characters');
343
+        }
344
+        // Check that clearAt is in the future
345
+        if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
346
+            throw new InvalidClearAtException('ClearAt is in the past');
347
+        }
348
+
349
+        $userStatus->setMessageId(null);
350
+        $userStatus->setCustomIcon($statusIcon);
351
+        $userStatus->setCustomMessage($message);
352
+        $userStatus->setClearAt($clearAt);
353
+
354
+        if ($userStatus->getId() === null) {
355
+            return $this->mapper->insert($userStatus);
356
+        }
357
+
358
+        return $this->mapper->update($userStatus);
359
+    }
360
+
361
+    /**
362
+     * @param string $userId
363
+     * @return bool
364
+     */
365
+    public function clearStatus(string $userId): bool {
366
+        try {
367
+            $userStatus = $this->mapper->findByUserId($userId);
368
+        } catch (DoesNotExistException $ex) {
369
+            // if there is no status to remove, just return
370
+            return false;
371
+        }
372
+
373
+        $userStatus->setStatus(IUserStatus::OFFLINE);
374
+        $userStatus->setStatusTimestamp(0);
375
+        $userStatus->setIsUserDefined(false);
376
+
377
+        $this->mapper->update($userStatus);
378
+        return true;
379
+    }
380
+
381
+    /**
382
+     * @param string $userId
383
+     * @return bool
384
+     */
385
+    public function clearMessage(string $userId): bool {
386
+        try {
387
+            $userStatus = $this->mapper->findByUserId($userId);
388
+        } catch (DoesNotExistException $ex) {
389
+            // if there is no status to remove, just return
390
+            return false;
391
+        }
392
+
393
+        $userStatus->setMessageId(null);
394
+        $userStatus->setCustomMessage(null);
395
+        $userStatus->setCustomIcon(null);
396
+        $userStatus->setClearAt(null);
397
+
398
+        $this->mapper->update($userStatus);
399
+        return true;
400
+    }
401
+
402
+    /**
403
+     * @param string $userId
404
+     * @return bool
405
+     */
406
+    public function removeUserStatus(string $userId): bool {
407
+        try {
408
+            $userStatus = $this->mapper->findByUserId($userId, false);
409
+        } catch (DoesNotExistException $ex) {
410
+            // if there is no status to remove, just return
411
+            return false;
412
+        }
413
+
414
+        $this->mapper->delete($userStatus);
415
+        return true;
416
+    }
417
+
418
+    public function removeBackupUserStatus(string $userId): bool {
419
+        try {
420
+            $userStatus = $this->mapper->findByUserId($userId, true);
421
+        } catch (DoesNotExistException $ex) {
422
+            // if there is no status to remove, just return
423
+            return false;
424
+        }
425
+
426
+        $this->mapper->delete($userStatus);
427
+        return true;
428
+    }
429
+
430
+    /**
431
+     * Processes a status to check if custom message is still
432
+     * up to date and provides translated default status if needed
433
+     *
434
+     * @param UserStatus $status
435
+     * @return UserStatus
436
+     */
437
+    private function processStatus(UserStatus $status): UserStatus {
438
+        $clearAt = $status->getClearAt();
439
+
440
+        if ($status->getStatusTimestamp() < $this->timeFactory->getTime() - self::INVALIDATE_STATUS_THRESHOLD
441
+            && (!$status->getIsUserDefined() || $status->getStatus() === IUserStatus::ONLINE)) {
442
+            $this->cleanStatus($status);
443
+        }
444
+        if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
445
+            $this->cleanStatusMessage($status);
446
+        }
447
+        if ($status->getMessageId() !== null) {
448
+            $this->addDefaultMessage($status);
449
+        }
450
+
451
+        return $status;
452
+    }
453
+
454
+    /**
455
+     * @param UserStatus $status
456
+     */
457
+    private function cleanStatus(UserStatus $status): void {
458
+        if ($status->getStatus() === IUserStatus::OFFLINE && !$status->getIsUserDefined()) {
459
+            return;
460
+        }
461
+
462
+        $status->setStatus(IUserStatus::OFFLINE);
463
+        $status->setStatusTimestamp($this->timeFactory->getTime());
464
+        $status->setIsUserDefined(false);
465
+
466
+        $this->mapper->update($status);
467
+    }
468
+
469
+    /**
470
+     * @param UserStatus $status
471
+     */
472
+    private function cleanStatusMessage(UserStatus $status): void {
473
+        $status->setMessageId(null);
474
+        $status->setCustomIcon(null);
475
+        $status->setCustomMessage(null);
476
+        $status->setClearAt(null);
477
+
478
+        $this->mapper->update($status);
479
+    }
480
+
481
+    /**
482
+     * @param UserStatus $status
483
+     */
484
+    private function addDefaultMessage(UserStatus $status): void {
485
+        // If the message is predefined, insert the translated message and icon
486
+        $predefinedMessage = $this->predefinedStatusService->getDefaultStatusById($status->getMessageId());
487
+        if ($predefinedMessage !== null) {
488
+            $status->setCustomMessage($predefinedMessage['message']);
489
+            $status->setCustomIcon($predefinedMessage['icon']);
490
+        }
491
+    }
492
+
493
+    /**
494
+     * @return bool false if there is already a backup. In this case abort the procedure.
495
+     */
496
+    public function backupCurrentStatus(string $userId): bool {
497
+        try {
498
+            $this->mapper->createBackupStatus($userId);
499
+            return true;
500
+        } catch (Exception $ex) {
501
+            if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
502
+                return false;
503
+            }
504
+            throw $ex;
505
+        }
506
+    }
507
+
508
+    public function revertUserStatus(string $userId, string $messageId, string $status): void {
509
+        try {
510
+            /** @var UserStatus $userStatus */
511
+            $backupUserStatus = $this->mapper->findByUserId($userId, true);
512
+        } catch (DoesNotExistException $ex) {
513
+            // No user status to revert, do nothing
514
+            return;
515
+        }
516
+
517
+        $deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId, $status);
518
+        if (!$deleted) {
519
+            // Another status is set automatically or no status, do nothing
520
+            return;
521
+        }
522
+
523
+        $backupUserStatus->setIsBackup(false);
524
+        // Remove the underscore prefix added when creating the backup
525
+        $backupUserStatus->setUserId(substr($backupUserStatus->getUserId(), 1));
526
+        $this->mapper->update($backupUserStatus);
527
+    }
528
+
529
+    public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void {
530
+        // Get all user statuses and the backups
531
+        $findById = $userIds;
532
+        foreach ($userIds as $userId) {
533
+            $findById[] = '_' . $userId;
534
+        }
535
+        $userStatuses = $this->mapper->findByUserIds($findById);
536
+
537
+        $backups = $restoreIds = $statuesToDelete = [];
538
+        foreach ($userStatuses as $userStatus) {
539
+            if (!$userStatus->getIsBackup()
540
+                && $userStatus->getMessageId() === $messageId
541
+                && $userStatus->getStatus() === $status) {
542
+                $statuesToDelete[$userStatus->getUserId()] = $userStatus->getId();
543
+            } else if ($userStatus->getIsBackup()) {
544
+                $backups[$userStatus->getUserId()] = $userStatus->getId();
545
+            }
546
+        }
547
+
548
+        // For users with both (normal and backup) delete the status when matching
549
+        foreach ($statuesToDelete as $userId => $statusId) {
550
+            $backupUserId = '_' . $userId;
551
+            if (isset($backups[$backupUserId])) {
552
+                $restoreIds[] = $backups[$backupUserId];
553
+            }
554
+        }
555
+
556
+        $this->mapper->deleteByIds(array_values($statuesToDelete));
557
+
558
+        // For users that matched restore the previous status
559
+        $this->mapper->restoreBackupStatuses($restoreIds);
560
+    }
561 561
 }
Please login to merge, or discard this patch.