Skip to content

Commit

Permalink
Merge pull request #13617 from nextcloud/backport/13499/stable30
Browse files Browse the repository at this point in the history
[stable30] feat(email): Recognize guests invited via email
  • Loading branch information
nickvergessen authored Oct 23, 2024
2 parents 8e5d778 + b0df6a3 commit 940f723
Show file tree
Hide file tree
Showing 39 changed files with 437 additions and 110 deletions.
28 changes: 23 additions & 5 deletions css/icons.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,44 +98,56 @@
* not accept several classes. */
.user-bubble__avatar .icon-group-forced-white.avatar-class-icon,
.user-bubble__avatar .icon-user-forced-white.avatar-class-icon,
.user-bubble__avatar .icon-mail-forced-white.avatar-class-icon,
.autocomplete-result .icon-group-forced-white.autocomplete-result__icon--,
.autocomplete-result .icon-user-forced-white.autocomplete-result__icon--,
.autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--,
.mention-bubble .icon-group-forced-white.mention-bubble__icon--,
.mention-bubble .icon-user-forced-white.mention-bubble__icon-- {
.mention-bubble .icon-user-forced-white.mention-bubble__icon--,
.mention-bubble .icon-mail-forced-white.mention-bubble__icon-- {
background-color: #6B6B6B;
}

/* System default: dark theme */
@media (prefers-color-scheme: dark) {
body[data-theme-default] .user-bubble__avatar .icon-group-forced-white.avatar-class-icon,
body[data-theme-default] .user-bubble__avatar .icon-user-forced-white.avatar-class-icon,
body[data-theme-default] .user-bubble__avatar .icon-mail-forced-white.avatar-class-icon,
body[data-theme-default] .autocomplete-result .icon-group-forced-white.autocomplete-result__icon--,
body[data-theme-default] .autocomplete-result .icon-user-forced-white.autocomplete-result__icon--,
body[data-theme-default] .autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--,
body[data-theme-default] .mention-bubble .icon-group-forced-white.mention-bubble__icon--,
body[data-theme-default] .mention-bubble .icon-user-forced-white.mention-bubble__icon-- {
body[data-theme-default] .mention-bubble .icon-user-forced-white.mention-bubble__icon--,
body[data-theme-default] .mention-bubble .icon-mail-forced-white.mention-bubble__icon-- {
background-color: #3B3B3B;
}
}

/* Manually set dark theme */
body[data-theme-dark] .user-bubble__avatar .icon-group-forced-white.avatar-class-icon,
body[data-theme-dark] .user-bubble__avatar .icon-user-forced-white.avatar-class-icon,
body[data-theme-dark] .user-bubble__avatar .icon-mail-forced-white.avatar-class-icon,
body[data-theme-dark] .autocomplete-result .icon-group-forced-white.autocomplete-result__icon--,
body[data-theme-dark] .autocomplete-result .icon-user-forced-white.autocomplete-result__icon--,
body[data-theme-dark] .autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--,
body[data-theme-dark] .mention-bubble .icon-group-forced-white.mention-bubble__icon--,
body[data-theme-dark] .mention-bubble .icon-user-forced-white.mention-bubble__icon-- {
body[data-theme-dark] .mention-bubble .icon-user-forced-white.mention-bubble__icon--,
body[data-theme-dark] .mention-bubble .icon-mail-forced-white.mention-bubble__icon-- {
background-color: #3B3B3B;
}

.user-bubble__avatar .icon-group-forced-white.avatar-class-icon,
.user-bubble__avatar .icon-user-forced-white.avatar-class-icon,
.user-bubble__avatar .icon-mail-forced-white.avatar-class-icon,
.mention-bubble .icon-group-forced-white.mention-bubble__icon--,
.mention-bubble .icon-user-forced-white.mention-bubble__icon-- {
.mention-bubble .icon-user-forced-white.mention-bubble__icon--,
.mention-bubble .icon-mail-forced-white.mention-bubble__icon-- {
background-size: 75%;
}

.autocomplete-result .icon-group-forced-white.autocomplete-result__icon--,
.autocomplete-result .icon-user-forced-white.autocomplete-result__icon-- {
.autocomplete-result .icon-user-forced-white.autocomplete-result__icon--,
.autocomplete-result .icon-mail-forced-white.autocomplete-result__icon-- {
background-size: 50% !important;
}

Expand All @@ -145,6 +157,12 @@ body[data-theme-dark] .mention-bubble .icon-user-forced-white.mention-bubble__ic
background-image: url(../img/icon-user-white.svg);
}

.user-bubble__avatar .icon-mail-forced-white,
.autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--,
.mention-bubble .icon-mail-forced-white.mention-bubble__icon-- {
background-image: url(../img/icon-mail-white.svg);
}

.user-bubble__avatar .icon-group-forced-white,
.autocomplete-result .icon-group-forced-white.autocomplete-result__icon--,
.mention-bubble .icon-group-forced-white.mention-bubble__icon-- {
Expand Down
2 changes: 1 addition & 1 deletion docs/bots.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The content format follows the [Activity Streams 2.0 Vocabulary](https://www.w3.

| Path | Description |
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| actor.id | One of the known [attendee types](constants.md#attendee-types) followed by the `/` slash character and a unique identifier within the given type. For users it is the Nextcloud user ID, for guests a sha1 value. |
| actor.id | One of the known [attendee types](constants.md#attendee-types) followed by the `/` slash character and a unique identifier within the given type. For users it is the Nextcloud user ID, for guests and email invited guests a random hash value. |
| actor.name | The display name of the attendee sending the message. |
| object.id | The message ID of the given message on the origin server. It can be used to react or reply to the given message. |
| object.name | For normal written messages `message`, otherwise one of the known [system message identifiers](chat.md#system-messages). |
Expand Down
3 changes: 2 additions & 1 deletion lib/Activity/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ protected function generateCallActivity(ACallEndedEvent $event): void {
$duration = $this->timeFactory->getTime() - $activeSince->getTimestamp();
$userIds = $this->participantService->getParticipantUserIds($room, $activeSince);
$cloudIds = $this->participantService->getParticipantActorIdsByActorType($room, [Attendee::ACTOR_FEDERATED_USERS], $activeSince);
$numGuests = $this->participantService->getGuestCount($room, $activeSince);
$numGuests = $this->participantService->getActorsCountByType($room, Attendee::ACTOR_GUESTS, $activeSince->getTimestamp());
$numGuests += $this->participantService->getActorsCountByType($room, Attendee::ACTOR_EMAILS, $activeSince->getTimestamp());

$message = 'call_ended';
if ($event instanceof CallEndedForEveryoneEvent) {
Expand Down
62 changes: 62 additions & 0 deletions lib/Chat/AutoComplete/SearchPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b
$groupIds = [];
/** @var array<string, string> $cloudIds */
$cloudIds = [];
/** @var array<string, string> $emailAttendees */
$emailAttendees = [];
/** @var list<Attendee> $guestAttendees */
$guestAttendees = [];

Expand All @@ -82,6 +84,8 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b
$attendee = $participant->getAttendee();
if ($attendee->getActorType() === Attendee::ACTOR_GUESTS) {
$guestAttendees[] = $attendee;
} elseif ($attendee->getActorType() === Attendee::ACTOR_EMAILS) {
$emailAttendees[$attendee->getActorId()] = $attendee->getDisplayName();
} elseif ($attendee->getActorType() === Attendee::ACTOR_USERS) {
$userIds[$attendee->getActorId()] = $attendee->getDisplayName();
} elseif ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
Expand All @@ -95,6 +99,7 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b
$this->searchUsers($search, $userIds, $searchResult);
$this->searchGroups($search, $groupIds, $searchResult);
$this->searchGuests($search, $guestAttendees, $searchResult);
$this->searchEmails($search, $emailAttendees, $searchResult);
$this->searchFederatedUsers($search, $cloudIds, $searchResult);

return false;
Expand Down Expand Up @@ -300,6 +305,53 @@ protected function searchGuests(string $search, array $attendees, ISearchResult
$searchResult->addResultSet($type, $matches, $exactMatches);
}

/**
* @param string $search
* @param array<string, string> $attendees
* @param ISearchResult $searchResult
*/
protected function searchEmails(string $search, array $attendees, ISearchResult $searchResult): void {
if (empty($attendees)) {
$type = new SearchResultType('emails');
$searchResult->addResultSet($type, [], []);
return;
}

$search = strtolower($search);
$currentSessionHash = null;
if (!$this->userId) {
// Best effort: Might not work on guests that reloaded but not worth too much performance impact atm.
$currentSessionHash = false; // FIXME sha1($this->talkSession->getSessionForRoom($this->room->getToken()));
}

$matches = $exactMatches = [];
foreach ($attendees as $actorId => $displayName) {
if ($currentSessionHash === $actorId) {
// Do not suggest the current guest
continue;
}

$displayName = $displayName ?: $this->l->t('Guest');
if ($search === '') {
$matches[] = $this->createEmailResult($actorId, $displayName);
continue;
}

if (strtolower($displayName) === $search) {
$exactMatches[] = $this->createEmailResult($actorId, $displayName);
continue;
}

if (stripos($displayName, $search) !== false) {
$matches[] = $this->createEmailResult($actorId, $displayName);
continue;
}
}

$type = new SearchResultType('emails');
$searchResult->addResultSet($type, $matches, $exactMatches);
}

protected function createResult(string $type, string $uid, string $name): array {
if ($type === 'user' && $name === '') {
$name = $this->userManager->getDisplayName($uid) ?? $uid;
Expand Down Expand Up @@ -333,4 +385,14 @@ protected function createGuestResult(string $actorId, string $name): array {
],
];
}

protected function createEmailResult(string $actorId, string $name): array {
return [
'label' => $name,
'value' => [
'shareType' => 'email',
'shareWith' => 'email/' . $actorId,
],
];
}
}
11 changes: 6 additions & 5 deletions lib/Chat/MessageParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,18 @@ protected function getActorInformation(Message $message, string $actorType, stri
} elseif ($actorType === Attendee::ACTOR_BRIDGED) {
$displayName = $actorId;
$actorId = MatterbridgeManager::BRIDGE_BOT_USERID;
} elseif ($actorType === Attendee::ACTOR_GUESTS
} elseif (($actorType === Attendee::ACTOR_GUESTS || $actorType === Attendee::ACTOR_EMAILS)
&& !in_array($actorId, [Attendee::ACTOR_ID_CLI, Attendee::ACTOR_ID_CHANGELOG], true)) {
if (isset($this->guestNames[$actorId])) {
$displayName = $this->guestNames[$actorId];
$cacheKey = $actorType . '/' . $actorId;
if (isset($this->guestNames[$cacheKey])) {
$displayName = $this->guestNames[$cacheKey];
} else {
try {
$participant = $this->participantService->getParticipantByActor($message->getRoom(), Attendee::ACTOR_GUESTS, $actorId);
$participant = $this->participantService->getParticipantByActor($message->getRoom(), $actorType, $actorId);
$displayName = $participant->getAttendee()->getDisplayName();
} catch (ParticipantNotFoundException) {
}
$this->guestNames[$actorId] = $displayName;
$this->guestNames[$cacheKey] = $displayName;
}
} elseif ($actorType === Attendee::ACTOR_BOTS) {
$displayName = $actorId . '-bot';
Expand Down
25 changes: 22 additions & 3 deletions lib/Chat/Parser/SystemMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ protected function parseMessage(Message $chatMessage): void {
$participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS &&
$currentActorId === $parsedParameters['actor']['id'] &&
empty($parsedParameters['actor']['server']);
} elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS) {
$currentActorType = $participant->getAttendee()->getActorType();
$currentActorId = $participant->getAttendee()->getActorId();
$currentUserIsActor = $parsedParameters['actor']['type'] === 'email' &&
$participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS &&
$participant->getAttendee()->getActorId() === $parsedParameters['actor']['id'];
} else {
$currentActorType = $participant->getAttendee()->getActorType();
$currentActorId = $participant->getAttendee()->getActorId();
Expand Down Expand Up @@ -457,7 +463,12 @@ protected function parseMessage(Message $chatMessage): void {
$parsedMessage = $this->l->t('An administrator demoted {user} from moderator');
}
} elseif ($message === 'guest_moderator_promoted') {
$parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']);
if (isset($parameters['type'], $parameters['id'])) {
$parsedParameters['user'] = $this->getGuest($room, $parameters['type'], $parameters['id']);
} else {
// Before Nextcloud 30
$parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']);
}
$parsedMessage = $this->l->t('{actor} promoted {user} to moderator');
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You promoted {user} to moderator');
Expand All @@ -470,7 +481,12 @@ protected function parseMessage(Message $chatMessage): void {
$parsedMessage = $this->l->t('An administrator promoted {user} to moderator');
}
} elseif ($message === 'guest_moderator_demoted') {
$parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']);
if (isset($parameters['type'], $parameters['id'])) {
$parsedParameters['user'] = $this->getGuest($room, $parameters['type'], $parameters['id']);
} else {
// Before Nextcloud 30
$parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']);
}
$parsedMessage = $this->l->t('{actor} demoted {user} from moderator');
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You demoted {user} from moderator');
Expand Down Expand Up @@ -847,6 +863,9 @@ protected function isCurrentParticipantChangedUser(?string $currentActorType, ?s
if ($currentActorType === Attendee::ACTOR_GUESTS) {
return $parameter['type'] === 'guest' && $currentActorId === $parameter['id'];
}
if ($currentActorType === Attendee::ACTOR_EMAILS) {
return $parameter['type'] === 'guest' && 'email/' . $currentActorId === $parameter['id'];
}

if (isset($parameter['server'])
&& $currentActorType === Attendee::ACTOR_FEDERATED_USERS
Expand Down Expand Up @@ -1019,7 +1038,7 @@ protected function getGuest(Room $room, string $actorType, string $actorId): arr

return [
'type' => 'guest',
'id' => 'guest/' . $actorId,
'id' => ($actorType === Attendee::ACTOR_GUESTS ? 'guest/' : 'email/') . $actorId,
'name' => $this->guestNames[$key],
];
}
Expand Down
15 changes: 15 additions & 0 deletions lib/Chat/Parser/UserMention.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ protected function parseMessage(Message $chatMessage): void {

$search = $mention['id'];
if (
$mention['type'] === 'email' ||
$mention['type'] === 'group' ||
// $mention['type'] === 'federated_group' ||
// $mention['type'] === 'team' ||
Expand All @@ -131,6 +132,7 @@ protected function parseMessage(Message $chatMessage): void {
$message = str_replace('@"' . $search . '"', '{' . $mentionParameterId . '}', $message);
if (!str_contains($search, ' ')
&& !str_starts_with($search, 'guest/')
&& !str_starts_with($search, 'email/')
&& !str_starts_with($search, 'group/')
// && !str_starts_with($search, 'federated_group/')
// && !str_starts_with($search, 'team/')
Expand Down Expand Up @@ -160,6 +162,19 @@ protected function parseMessage(Message $chatMessage): void {
$displayName = $this->l->t('Guest');
}

$messageParameters[$mentionParameterId] = [
'type' => $mention['type'],
'id' => $mention['id'],
'name' => $displayName,
];
} elseif ($mention['type'] === 'email') {
try {
$participant = $this->participantService->getParticipantByActor($chatMessage->getRoom(), Attendee::ACTOR_EMAILS, $mention['id']);
$displayName = $participant->getAttendee()->getDisplayName() ?: $this->l->t('Guest');
} catch (ParticipantNotFoundException) {
$displayName = $this->l->t('Guest');
}

$messageParameters[$mentionParameterId] = [
'type' => $mention['type'],
'id' => $mention['id'],
Expand Down
10 changes: 7 additions & 3 deletions lib/Chat/SystemMessage/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,11 @@ public function sendSystemMessageAboutPromoteOrDemoteModerator(ParticipantModifi
$room = $event->getRoom();
$attendee = $event->getParticipant()->getAttendee();

if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) {
if (!in_array($attendee->getActorType(), [
Attendee::ACTOR_USERS,
Attendee::ACTOR_EMAILS,
Attendee::ACTOR_GUESTS,
], true)) {
return;
}

Expand All @@ -324,9 +328,9 @@ public function sendSystemMessageAboutPromoteOrDemoteModerator(ParticipantModifi
$this->sendSystemMessage($room, 'moderator_demoted', ['user' => $attendee->getActorId()]);
}
} elseif ($event->getNewValue() === Participant::GUEST_MODERATOR) {
$this->sendSystemMessage($room, 'guest_moderator_promoted', ['session' => $attendee->getActorId()]);
$this->sendSystemMessage($room, 'guest_moderator_promoted', ['type' => $attendee->getActorType(), 'id' => $attendee->getActorId()]);
} elseif ($event->getNewValue() === Participant::GUEST) {
$this->sendSystemMessage($room, 'guest_moderator_demoted', ['session' => $attendee->getActorId()]);
$this->sendSystemMessage($room, 'guest_moderator_demoted', ['type' => $attendee->getActorType(), 'id' => $attendee->getActorId()]);
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Collaboration/Reference/TalkReferenceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ protected function fetchReference(Reference $reference): void {
}

$displayName = $message->getActorDisplayName();
if ($message->getActorType() === Attendee::ACTOR_GUESTS) {
if (in_array($message->getActorType(), [Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS], true)) {
if ($displayName === '') {
$displayName = $this->l->t('Guest');
} else {
Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/BanController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function __construct(
*
* Required capability: `ban-v1`
*
* @param 'users'|'guests'|'ip' $actorType Type of actor to ban, or `ip` when banning a clients remote address
* @param 'users'|'guests'|'emails'|'ip' $actorType Type of actor to ban, or `ip` when banning a clients remote address
* @param string $actorId Actor ID or the IP address or range in case of type `ip`
* @param string $internalNote Optional internal note (max. 4000 characters)
* @return DataResponse<Http::STATUS_OK, TalkBan, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'bannedActor'|'internalNote'|'moderator'|'self'|'room'}, array{}>
Expand Down
4 changes: 3 additions & 1 deletion lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ protected function getActorInfo(string $actorDisplayName = ''): array {
if ($actorDisplayName) {
$this->guestManager->updateName($this->room, $this->participant, $actorDisplayName);
}
return [Attendee::ACTOR_GUESTS, $this->participant->getAttendee()->getActorId()];
/** @var Attendee::ACTOR_GUESTS|Attendee::ACTOR_EMAILS $actorType */
$actorType = $this->participant->getAttendee()->getActorType();
return [$actorType, $this->participant->getAttendee()->getActorId()];
}

if ($this->userId === MatterbridgeManager::BRIDGE_BOT_USERID && $actorDisplayName) {
Expand Down
Loading

0 comments on commit 940f723

Please sign in to comment.