UPSTREAM BASELINE: nextcloud/spreed v22.0.12 (без изменений)
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Источник: https://github.com/nextcloud/spreed/archive/refs/tags/v22.0.12.tar.gz С этого коммита ветка официального Nextcloud Talk отрезана (решение владельца 2026-07-06). Все дальнейшие изменения — только наши; версии релизов: 22.0.12-f7.N.
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Chat;
|
||||
|
||||
use OCA\Talk\Exceptions\ParticipantNotFoundException;
|
||||
use OCA\Talk\Files\Util;
|
||||
use OCA\Talk\Model\Attendee;
|
||||
use OCA\Talk\Model\Session;
|
||||
use OCA\Talk\Model\ThreadAttendee;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\ThreadService;
|
||||
use OCA\Talk\Webinary;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Comments\IComment;
|
||||
use OCP\IConfig;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Notification\IManager as INotificationManager;
|
||||
use OCP\Notification\INotification;
|
||||
|
||||
/**
|
||||
* Helper class for notifications related to user mentions in chat messages.
|
||||
*
|
||||
* This class uses the NotificationManager to create and remove the
|
||||
* notifications as needed; OCA\Talk\Notification\Notifier is the one that
|
||||
* prepares the notifications for display.
|
||||
*/
|
||||
class Notifier {
|
||||
public const PRIORITY_NONE = 0;
|
||||
public const PRIORITY_NORMAL = 1;
|
||||
public const PRIORITY_IMPORTANT = 2;
|
||||
|
||||
public function __construct(
|
||||
private INotificationManager $notificationManager,
|
||||
private IUserManager $userManager,
|
||||
private IGroupManager $groupManager,
|
||||
private ParticipantService $participantService,
|
||||
private ThreadService $threadService,
|
||||
private IConfig $config,
|
||||
private ITimeFactory $timeFactory,
|
||||
private Util $util,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the user mentioned in the comment.
|
||||
*
|
||||
* The comment must be a chat message comment. That is, its "objectId" must
|
||||
* be the room ID.
|
||||
*
|
||||
* Not every user mentioned in the message is notified, but only those that
|
||||
* are able to participate in the room.
|
||||
*
|
||||
* @param Room $chat
|
||||
* @param IComment $comment
|
||||
* @param array[] $alreadyNotifiedUsers
|
||||
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}> $alreadyNotifiedUsers
|
||||
* @param bool $silent
|
||||
* @param Participant|null $participant
|
||||
* @return string[] Users that were mentioned
|
||||
* @psalm-return array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}>
|
||||
*/
|
||||
public function notifyMentionedUsers(Room $chat, IComment $comment, array $alreadyNotifiedUsers, bool $silent, ?Participant $participant = null, ?int $threadId = null): array {
|
||||
$usersToNotify = $this->getUsersToNotify($chat, $comment, $alreadyNotifiedUsers, $participant);
|
||||
|
||||
if (!$usersToNotify) {
|
||||
return $alreadyNotifiedUsers;
|
||||
}
|
||||
|
||||
$shouldFlush = false;
|
||||
if (!$silent) {
|
||||
$notification = $this->createNotification($chat, $comment, 'mention', threadId: $threadId);
|
||||
$parameters = $notification->getSubjectParameters();
|
||||
$shouldFlush = $this->notificationManager->defer();
|
||||
}
|
||||
|
||||
foreach ($usersToNotify as $mentionedUser) {
|
||||
$shouldMentionedUserBeNotified = $this->shouldMentionedUserBeNotified($mentionedUser['id'], $comment, $chat, $mentionedUser['attendee'] ?? null);
|
||||
if ($shouldMentionedUserBeNotified !== self::PRIORITY_NONE) {
|
||||
if (!$silent) {
|
||||
$notification->setUser($mentionedUser['id']);
|
||||
if (isset($mentionedUser['reason'])) {
|
||||
$notification->setSubject('mention_' . $mentionedUser['reason'], array_merge($parameters, [
|
||||
'sourceId' => $mentionedUser['sourceId'] ?? null,
|
||||
]));
|
||||
} else {
|
||||
$notification->setSubject('mention', $parameters);
|
||||
}
|
||||
$notification->setPriorityNotification($shouldMentionedUserBeNotified === self::PRIORITY_IMPORTANT);
|
||||
$this->notificationManager->notify($notification);
|
||||
}
|
||||
$alreadyNotifiedUsers[] = $mentionedUser;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldFlush) {
|
||||
$this->notificationManager->flush();
|
||||
}
|
||||
|
||||
return $alreadyNotifiedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $chat
|
||||
* @param IComment $comment
|
||||
* @param array $alreadyNotifiedUsers
|
||||
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}> $alreadyNotifiedUsers
|
||||
* @param Participant|null $participant
|
||||
* @return array
|
||||
* @psalm-return array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}>
|
||||
*/
|
||||
public function getUsersToNotify(Room $chat, IComment $comment, array $alreadyNotifiedUsers, ?Participant $participant = null): array {
|
||||
$usersToNotify = $this->getMentionedUsers($comment);
|
||||
$usersToNotify = $this->getMentionedGroupMembers($chat, $comment, $usersToNotify);
|
||||
$usersToNotify = $this->getMentionedTeamMembers($chat, $comment, $usersToNotify);
|
||||
$usersToNotify = $this->addMentionAllToList($chat, $usersToNotify, $participant);
|
||||
$usersToNotify = $this->removeAlreadyNotifiedUsers($usersToNotify, $alreadyNotifiedUsers);
|
||||
|
||||
return $usersToNotify;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $usersToNotify
|
||||
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}> $usersToNotify
|
||||
* @param array $alreadyNotifiedUsers
|
||||
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}> $alreadyNotifiedUsers
|
||||
* @return array
|
||||
* @psalm-return array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}>
|
||||
*/
|
||||
private function removeAlreadyNotifiedUsers(array $usersToNotify, array $alreadyNotifiedUsers): array {
|
||||
return array_filter($usersToNotify, static function (array $userToNotify) use ($alreadyNotifiedUsers): bool {
|
||||
foreach ($alreadyNotifiedUsers as $alreadyNotified) {
|
||||
if ($alreadyNotified['id'] === $userToNotify['id'] && $alreadyNotified['type'] === $userToNotify['type']) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $chat
|
||||
* @param array $list
|
||||
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string}> $list
|
||||
* @param Participant|null $participant
|
||||
* @return array
|
||||
* @psalm-return array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}>
|
||||
*/
|
||||
private function addMentionAllToList(Room $chat, array $list, ?Participant $participant = null): array {
|
||||
$usersToNotify = array_filter($list, static function (array $entry): bool {
|
||||
return $entry['type'] !== Attendee::ACTOR_USERS || $entry['id'] !== 'all';
|
||||
});
|
||||
|
||||
if (count($list) === count($usersToNotify)) {
|
||||
return $usersToNotify;
|
||||
}
|
||||
if ($chat->getMentionPermissions() === Room::MENTION_PERMISSIONS_MODERATORS && (!$participant instanceof Participant || !$participant->hasModeratorPermissions())) {
|
||||
return $usersToNotify;
|
||||
}
|
||||
|
||||
$attendees = $this->participantService->getActorsByType($chat, Attendee::ACTOR_USERS);
|
||||
foreach ($attendees as $attendee) {
|
||||
$alreadyAddedToNotify = array_filter($list, static function ($user) use ($attendee): bool {
|
||||
return $user['id'] === $attendee->getActorId();
|
||||
});
|
||||
if (!empty($alreadyAddedToNotify)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$usersToNotify[] = [
|
||||
'id' => $attendee->getActorId(),
|
||||
'type' => $attendee->getActorType(),
|
||||
'attendee' => $attendee,
|
||||
'reason' => 'all',
|
||||
];
|
||||
}
|
||||
|
||||
return $usersToNotify;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Notifies the author that wrote the comment which was replied to
|
||||
*
|
||||
* The comment must be a chat message comment. That is, its "objectId" must
|
||||
* be the room ID.
|
||||
*
|
||||
* The author of the message is notified only if they are still able to participate in the room
|
||||
*
|
||||
* @param Room $chat
|
||||
* @param IComment $comment
|
||||
* @param IComment $replyTo
|
||||
* @param bool $silent
|
||||
* @return array[] Actor that was replied to
|
||||
* @psalm-return array<int, array{id: string, type: string, reason: string}>
|
||||
*/
|
||||
public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $replyTo, bool $silent, ?int $threadId = null): array {
|
||||
if ($replyTo->getActorType() !== Attendee::ACTOR_USERS && $replyTo->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) {
|
||||
// No reply notification when the replyTo-author was not a user or federated user
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($replyTo->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
|
||||
return [
|
||||
[
|
||||
'id' => $replyTo->getActorId(),
|
||||
'type' => $replyTo->getActorType(),
|
||||
'reason' => 'reply',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$shouldMentionedUserBeNotified = $this->shouldMentionedUserBeNotified($replyTo->getActorId(), $comment, $chat);
|
||||
if ($shouldMentionedUserBeNotified === self::PRIORITY_NONE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$silent) {
|
||||
$notification = $this->createNotification($chat, $comment, 'reply', threadId: $threadId);
|
||||
$notification->setUser($replyTo->getActorId());
|
||||
$notification->setPriorityNotification($shouldMentionedUserBeNotified === self::PRIORITY_IMPORTANT);
|
||||
$this->notificationManager->notify($notification);
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'id' => $replyTo->getActorId(),
|
||||
'type' => $replyTo->getActorType(),
|
||||
'reason' => 'reply',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the user mentioned in the comment.
|
||||
*
|
||||
* The comment must be a chat message comment. That is, its "objectId" must
|
||||
* be the room ID.
|
||||
*
|
||||
* Not every user mentioned in the message is notified, but only those that
|
||||
* are able to participate in the room.
|
||||
*
|
||||
* @param Room $chat
|
||||
* @param IComment $comment
|
||||
* @param array[] $alreadyNotifiedUsers
|
||||
* @param bool $silent
|
||||
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}> $alreadyNotifiedUsers
|
||||
*/
|
||||
public function notifyOtherParticipant(Room $chat, IComment $comment, array $alreadyNotifiedUsers, bool $silent): void {
|
||||
if ($silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$participants = $this->participantService->getParticipantsByNotificationLevel($chat, Participant::NOTIFY_ALWAYS);
|
||||
$threadId = (int)$comment->getTopmostParentId();
|
||||
/** @var array<int, ThreadAttendee> $threadAttendees */
|
||||
$threadAttendees = [];
|
||||
if ($threadId !== 0) {
|
||||
$threadAttendees = $this->threadService->findAttendeesForNotificationByThreadId($chat->getId(), $threadId);
|
||||
}
|
||||
|
||||
// Handle participants that only subscribed with Participant::NOTIFY_ALWAYS to the thread, but not the conversation
|
||||
$threadAttendeeIds = array_map(static fn (ThreadAttendee $threadAttendee): int => $threadAttendee->getAttendeeId(),
|
||||
array_filter($threadAttendees, static fn (ThreadAttendee $threadAttendee): bool => $threadAttendee->getNotificationLevel() === Participant::NOTIFY_ALWAYS)
|
||||
);
|
||||
if (!empty($threadAttendeeIds)) {
|
||||
$participantIds = array_map(static fn (Participant $participant): int => $participant->getAttendee()->getId(), $participants);
|
||||
$missingParticipantIds = array_diff($threadAttendeeIds, $participantIds);
|
||||
if (!empty($missingParticipantIds)) {
|
||||
$missingParticipants = $this->participantService->getParticipantsByAttendeeId($chat, $missingParticipantIds);
|
||||
if (!empty($missingParticipants)) {
|
||||
$participants = array_merge($participants, $missingParticipants);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$notification = $this->createNotification($chat, $comment, 'chat', threadId: $threadId);
|
||||
foreach ($participants as $participant) {
|
||||
$attendeeId = $participant->getAttendee()->getId();
|
||||
$shouldParticipantBeNotified = $this->shouldParticipantBeNotified($participant, $comment, $alreadyNotifiedUsers);
|
||||
|
||||
if (isset($threadAttendees[$attendeeId])) {
|
||||
$threadAttendee = $threadAttendees[$attendeeId];
|
||||
if ($threadAttendee->getNotificationLevel() !== Participant::NOTIFY_ALWAYS) {
|
||||
// User unsubscribed from this thread
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldParticipantBeNotified === self::PRIORITY_NONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$notification->setUser($participant->getAttendee()->getActorId());
|
||||
$notification->setPriorityNotification($shouldParticipantBeNotified === self::PRIORITY_IMPORTANT);
|
||||
$this->notificationManager->notify($notification);
|
||||
}
|
||||
|
||||
// Also notify default participants in one-to-one chats or when the admin default is "always"
|
||||
if ($this->getDefaultGroupNotification() === Participant::NOTIFY_ALWAYS || $chat->getType() === Room::TYPE_ONE_TO_ONE) {
|
||||
$participants = $this->participantService->getParticipantsByNotificationLevel($chat, Participant::NOTIFY_DEFAULT);
|
||||
foreach ($participants as $participant) {
|
||||
$shouldParticipantBeNotified = $this->shouldParticipantBeNotified($participant, $comment, $alreadyNotifiedUsers);
|
||||
if ($shouldParticipantBeNotified === self::PRIORITY_NONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$notification->setUser($participant->getAttendee()->getActorId());
|
||||
$notification->setPriorityNotification($shouldParticipantBeNotified === self::PRIORITY_IMPORTANT);
|
||||
$this->notificationManager->notify($notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function notifyReacted(Room $chat, IComment $comment, IComment $reaction): void {
|
||||
if ($comment->getActorType() !== Attendee::ACTOR_USERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($comment->getActorType() === $reaction->getActorType() && $comment->getActorId() === $reaction->getActorId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$participant = $this->participantService->getParticipant($chat, $comment->getActorId(), false);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notificationLevel = $participant->getAttendee()->getNotificationLevel();
|
||||
if ($notificationLevel === Participant::NOTIFY_DEFAULT) {
|
||||
if ($chat->getType() === Room::TYPE_ONE_TO_ONE) {
|
||||
$notificationLevel = Participant::NOTIFY_ALWAYS;
|
||||
} else {
|
||||
$notificationLevel = $this->getDefaultGroupNotification();
|
||||
}
|
||||
}
|
||||
|
||||
if ($notificationLevel === Participant::NOTIFY_ALWAYS) {
|
||||
$notification = $this->createNotification($chat, $comment, 'reaction', [
|
||||
'reaction' => $reaction->getMessage(),
|
||||
], $reaction);
|
||||
$notification->setUser($comment->getActorId());
|
||||
$this->notificationManager->notify($notification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the pending notifications for the room with the given ID.
|
||||
*/
|
||||
public function removePendingNotificationsForRoom(Room $chat, bool $chatOnly = false): void {
|
||||
$notification = $this->notificationManager->createNotification();
|
||||
$shouldFlush = $this->notificationManager->defer();
|
||||
|
||||
// @todo this should be in the Notifications\Hooks
|
||||
$notification->setApp('spreed');
|
||||
|
||||
$objectTypes = [
|
||||
'chat',
|
||||
'reminder',
|
||||
];
|
||||
if (!$chatOnly) {
|
||||
$objectTypes = [
|
||||
'call',
|
||||
'chat',
|
||||
'room',
|
||||
'recording',
|
||||
'recording_information',
|
||||
'remote_talk_share',
|
||||
];
|
||||
}
|
||||
foreach ($objectTypes as $type) {
|
||||
$notification->setObject($type, $chat->getToken());
|
||||
$this->notificationManager->markProcessed($notification);
|
||||
}
|
||||
|
||||
if ($shouldFlush) {
|
||||
$this->notificationManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the pending mention notifications for the room
|
||||
*
|
||||
* @param Room $chat
|
||||
* @param ?string $userId
|
||||
*/
|
||||
public function markMentionNotificationsRead(Room $chat, ?string $userId): void {
|
||||
if ($userId === null || $userId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$shouldFlush = $this->notificationManager->defer();
|
||||
$notification = $this->notificationManager->createNotification();
|
||||
|
||||
$notification
|
||||
->setApp('spreed')
|
||||
->setObject('chat', $chat->getToken())
|
||||
->setUser($userId);
|
||||
|
||||
$this->notificationManager->markProcessed($notification);
|
||||
if ($shouldFlush) {
|
||||
$this->notificationManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all mention notifications of users that got their mention removed
|
||||
*
|
||||
* @param list<string> $userIds
|
||||
*/
|
||||
public function removeMentionNotificationAfterEdit(Room $chat, IComment $comment, array $userIds): void {
|
||||
$shouldFlush = $this->notificationManager->defer();
|
||||
$notification = $this->notificationManager->createNotification();
|
||||
|
||||
$notification
|
||||
->setApp('spreed')
|
||||
->setObject('chat', $chat->getToken())
|
||||
// FIXME message_parameters are not handled by notification app, so this removes all notifications :(
|
||||
->setMessage('comment', [
|
||||
'commentId' => $comment->getId(),
|
||||
]);
|
||||
|
||||
foreach (['mention_all', 'mention_direct'] as $subject) {
|
||||
$notification->setSubject($subject);
|
||||
foreach ($userIds as $userId) {
|
||||
$notification->setUser($userId);
|
||||
$this->notificationManager->markProcessed($notification);
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldFlush) {
|
||||
$this->notificationManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IDs of the users mentioned in the given comment.
|
||||
*
|
||||
* @param IComment $comment
|
||||
* @return string[] the mentioned user IDs
|
||||
*/
|
||||
public function getMentionedUserIds(IComment $comment): array {
|
||||
$mentionedUsers = $this->getMentionedUsers($comment);
|
||||
return array_map(static function ($mentionedUser) {
|
||||
return $mentionedUser['id'];
|
||||
}, $mentionedUsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cloud IDs of the federated users mentioned in the given comment.
|
||||
*
|
||||
* @param IComment $comment
|
||||
* @return string[] the mentioned cloud IDs
|
||||
*/
|
||||
public function getMentionedCloudIds(IComment $comment): array {
|
||||
$mentionedFederatedUsers = $this->getMentionedFederatedUsers($comment);
|
||||
return array_map(static function ($mentionedUser) {
|
||||
return $mentionedUser['id'];
|
||||
}, $mentionedFederatedUsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IComment $comment
|
||||
* @return array[]
|
||||
* @psalm-return array<int, array{type: string, id: string, reason: string}>
|
||||
*/
|
||||
private function getMentionedUsers(IComment $comment): array {
|
||||
$mentions = $comment->getMentions();
|
||||
|
||||
if (empty($mentions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$mentionedUsers = [];
|
||||
foreach ($mentions as $mention) {
|
||||
if ($mention['type'] !== 'user') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mentionedUsers[] = [
|
||||
'id' => $mention['id'],
|
||||
'type' => Attendee::ACTOR_USERS,
|
||||
'reason' => 'direct',
|
||||
];
|
||||
}
|
||||
return $mentionedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IComment $comment
|
||||
* @return array[]
|
||||
* @psalm-return array<int, array{type: string, id: string, reason: string}>
|
||||
*/
|
||||
private function getMentionedFederatedUsers(IComment $comment): array {
|
||||
$mentions = $comment->getMentions();
|
||||
|
||||
if (empty($mentions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$mentionedUsers = [];
|
||||
foreach ($mentions as $mention) {
|
||||
if ($mention['type'] !== 'federated_user') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mentionedUsers[] = [
|
||||
'id' => $mention['id'],
|
||||
'type' => Attendee::ACTOR_FEDERATED_USERS,
|
||||
'reason' => 'direct',
|
||||
];
|
||||
}
|
||||
return $mentionedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $chat
|
||||
* @param IComment $comment
|
||||
* @param array $list
|
||||
* @psalm-param array<int, array{id: string, type: string, reason: string}> $list
|
||||
* @return array[]
|
||||
* @psalm-return array<int, array{type: string, id: string, reason: string, sourceId?: string}>
|
||||
*/
|
||||
private function getMentionedGroupMembers(Room $chat, IComment $comment, array $list): array {
|
||||
$mentions = $comment->getMentions();
|
||||
|
||||
if (empty($mentions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$alreadyMentionedUserIds = array_filter(
|
||||
array_map(static fn (array $entry) => $entry['type'] === Attendee::ACTOR_USERS ? $entry['id'] : null, $list),
|
||||
static fn ($userId) => $userId !== null
|
||||
);
|
||||
$alreadyMentionedUserIds = array_flip($alreadyMentionedUserIds);
|
||||
|
||||
foreach ($mentions as $mention) {
|
||||
if ($mention['type'] !== 'group') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$group = $this->groupManager->get($mention['id']);
|
||||
if (!$group instanceof IGroup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->participantService->getParticipantByActor($chat, Attendee::ACTOR_GROUPS, $group->getGID());
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$members = $group->getUsers();
|
||||
foreach ($members as $member) {
|
||||
if (isset($alreadyMentionedUserIds[$member->getUID()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = [
|
||||
'id' => $member->getUID(),
|
||||
'type' => Attendee::ACTOR_USERS,
|
||||
'reason' => 'group',
|
||||
'sourceId' => $group->getGID(),
|
||||
];
|
||||
$alreadyMentionedUserIds[$member->getUID()] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $chat
|
||||
* @param IComment $comment
|
||||
* @param array $list
|
||||
* @psalm-param array<int, array{type: string, id: string, reason: string, sourceId?: string}> $list
|
||||
* @return array[]
|
||||
* @psalm-return array<int, array{type: string, id: string, reason: string, sourceId?: string}>
|
||||
*/
|
||||
private function getMentionedTeamMembers(Room $chat, IComment $comment, array $list): array {
|
||||
$mentions = $comment->getMentions();
|
||||
|
||||
if (empty($mentions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$alreadyMentionedUserIds = array_filter(
|
||||
array_map(static fn (array $entry) => $entry['type'] === Attendee::ACTOR_USERS ? $entry['id'] : null, $list),
|
||||
static fn ($userId) => $userId !== null
|
||||
);
|
||||
$alreadyMentionedUserIds = array_flip($alreadyMentionedUserIds);
|
||||
|
||||
foreach ($mentions as $mention) {
|
||||
if ($mention['type'] !== 'team') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->participantService->getParticipantByActor($chat, Attendee::ACTOR_CIRCLES, $mention['id']);
|
||||
} catch (ParticipantNotFoundException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$members = $this->participantService->getCircleMembers($mention['id']);
|
||||
if (empty($members)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($members as $member) {
|
||||
$list[] = [
|
||||
'id' => $member->getUserId(),
|
||||
'type' => Attendee::ACTOR_USERS,
|
||||
'reason' => 'team',
|
||||
'sourceId' => $mention['id'],
|
||||
];
|
||||
$alreadyMentionedUserIds[$member->getUserId()] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification for the given chat message comment and mentioned
|
||||
* user ID.
|
||||
*/
|
||||
private function createNotification(Room $chat, IComment $comment, string $subject, array $subjectData = [], ?IComment $reaction = null, ?int $threadId = null): INotification {
|
||||
$subjectData['userType'] = $reaction ? $reaction->getActorType() : $comment->getActorType();
|
||||
$subjectData['userId'] = $reaction ? $reaction->getActorId() : $comment->getActorId();
|
||||
|
||||
$messageData = [
|
||||
'commentId' => $comment->getId(),
|
||||
];
|
||||
|
||||
if ($threadId !== null && $threadId !== 0) {
|
||||
$messageData['threadId'] = $threadId;
|
||||
}
|
||||
|
||||
$notification = $this->notificationManager->createNotification();
|
||||
$notification
|
||||
->setApp('spreed')
|
||||
->setObject('chat', $chat->getToken())
|
||||
->setSubject($subject, $subjectData)
|
||||
->setMessage($comment->getVerb(), $messageData)
|
||||
->setDateTime($reaction ? $reaction->getCreationDateTime() : $comment->getCreationDateTime());
|
||||
|
||||
return $notification;
|
||||
}
|
||||
|
||||
protected function getDefaultGroupNotification(): int {
|
||||
return (int)$this->config->getAppValue('spreed', 'default_group_notification', (string)Participant::NOTIFY_MENTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a user should be notified about the mention:
|
||||
*
|
||||
* 1. The user did not mention themself
|
||||
* 2. The user must exist
|
||||
* 3. The user must be a participant of the room
|
||||
* 4. The user must not be active in the room
|
||||
*/
|
||||
protected function shouldMentionedUserBeNotified(string $userId, IComment $comment, Room $room, ?Attendee $attendee = null): int {
|
||||
if ($comment->getActorType() === Attendee::ACTOR_USERS && $userId === $comment->getActorId()) {
|
||||
// Do not notify the user if they mentioned themselves
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$attendee instanceof Attendee) {
|
||||
if (!$this->userManager->userExists($userId)) {
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
|
||||
$participant = $this->participantService->getParticipant($room, $userId, false);
|
||||
$attendee = $participant->getAttendee();
|
||||
} else {
|
||||
$participant = new Participant($room, $attendee, null);
|
||||
}
|
||||
|
||||
if ($room->getLobbyState() !== Webinary::LOBBY_NONE
|
||||
&& !($participant->getPermissions() & Attendee::PERMISSIONS_LOBBY_IGNORE)) {
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
|
||||
$notificationLevel = $attendee->getNotificationLevel();
|
||||
$threadId = (int)$comment->getTopmostParentId();
|
||||
if ($threadId !== 0) {
|
||||
$threadAttendees = $this->threadService->findAttendeeByThreadIds($attendee, [$threadId]);
|
||||
$threadAttendee = array_shift($threadAttendees);
|
||||
if ($threadAttendee !== null && $threadAttendee->getNotificationLevel() !== Participant::NOTIFY_DEFAULT) {
|
||||
$notificationLevel = $threadAttendee->getNotificationLevel();
|
||||
}
|
||||
}
|
||||
|
||||
if ($notificationLevel === Participant::NOTIFY_DEFAULT) {
|
||||
if ($room->getType() === Room::TYPE_ONE_TO_ONE) {
|
||||
$notificationLevel = Participant::NOTIFY_ALWAYS;
|
||||
} else {
|
||||
$notificationLevel = $this->getDefaultGroupNotification();
|
||||
}
|
||||
}
|
||||
if ($notificationLevel === Participant::NOTIFY_NEVER) {
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
|
||||
if ($attendee->isImportant()) {
|
||||
return self::PRIORITY_IMPORTANT;
|
||||
}
|
||||
return self::PRIORITY_NORMAL;
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
if ($room->getObjectType() === 'file' && $this->util->canUserAccessFile($room->getObjectId(), $userId)) {
|
||||
// Users are added on mentions in file-rooms,
|
||||
// so they can see the room in their room list and
|
||||
// the notification can be parsed and links to an existing room,
|
||||
// where they are a participant of.
|
||||
$userDisplayName = $this->userManager->getDisplayName($userId);
|
||||
$this->participantService->addUsers($room, [[
|
||||
'actorType' => Attendee::ACTOR_USERS,
|
||||
'actorId' => $userId,
|
||||
'displayName' => $userDisplayName ?? $userId,
|
||||
]]);
|
||||
return self::PRIORITY_NORMAL;
|
||||
}
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a participant should be notified about the message:
|
||||
*
|
||||
* 1. The participant is not a guest
|
||||
* 2. The participant is not the writing user
|
||||
* 3. The participant was not mentioned already
|
||||
* 4. The participant must not be active in the room
|
||||
*
|
||||
* @psalm-param array<int, array{type: string, id: string, reason: string, sourceId?: string, attendee?: Attendee}> $alreadyNotifiedUsers
|
||||
*/
|
||||
protected function shouldParticipantBeNotified(Participant $participant, IComment $comment, array $alreadyNotifiedUsers): int {
|
||||
if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS) {
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
|
||||
$userId = $participant->getAttendee()->getActorId();
|
||||
if ($comment->getActorType() === Attendee::ACTOR_USERS && $userId === $comment->getActorId()) {
|
||||
// Do not notify the author
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
|
||||
$actorType = $participant->getAttendee()->getActorType();
|
||||
foreach ($alreadyNotifiedUsers as $user) {
|
||||
if ($user['id'] === $userId && $user['type'] === $actorType) {
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if ($participant->getSession()?->getLastPing() >= $this->timeFactory->getTime() - Session::SESSION_TIMEOUT) {
|
||||
// User is online
|
||||
return self::PRIORITY_NONE;
|
||||
}
|
||||
|
||||
if ($participant->getAttendee()->isImportant()) {
|
||||
return self::PRIORITY_IMPORTANT;
|
||||
}
|
||||
|
||||
return self::PRIORITY_NORMAL;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user