Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(email): Recognize guests invited via email #13499

Merged
merged 14 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading