| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/collaboration/internal/messaging/messaging_backend_service_impl.h" |
| |
| #include <sys/types.h> |
| |
| #include <memory> |
| #include <optional> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/collaboration/internal/messaging/data_sharing_change_notifier_impl.h" |
| #include "components/collaboration/internal/messaging/storage/collaboration_message_util.h" |
| #include "components/collaboration/internal/messaging/storage/messaging_backend_store.h" |
| #include "components/collaboration/internal/messaging/storage/protocol/message.pb.h" |
| #include "components/collaboration/internal/messaging/tab_group_change_notifier.h" |
| #include "components/collaboration/public/messaging/activity_log.h" |
| #include "components/collaboration/public/messaging/message.h" |
| #include "components/data_sharing/public/group_data.h" |
| #include "components/saved_tab_groups/public/types.h" |
| |
| namespace collaboration::messaging { |
| namespace { |
| collaboration_pb::Message CreateMessage( |
| const data_sharing::GroupId& collaboration_group_id, |
| collaboration_pb::EventType event_type, |
| DirtyType dirty_type, |
| const base::Time& event_time) { |
| collaboration_pb::Message message; |
| message.set_uuid(base::Uuid::GenerateRandomV4().AsLowercaseString()); |
| message.set_collaboration_id(collaboration_group_id.value()); |
| message.set_event_type(event_type); |
| message.set_dirty(static_cast<int>(dirty_type)); |
| message.set_event_timestamp(event_time.ToTimeT()); |
| return message; |
| } |
| |
| collaboration_pb::Message CreateTabGroupMessage( |
| data_sharing::GroupId collaboration_group_id, |
| const tab_groups::SavedTabGroup& tab_group, |
| collaboration_pb::EventType event_type, |
| DirtyType dirty_type) { |
| collaboration_pb::Message message = |
| CreateMessage(collaboration_group_id, event_type, dirty_type, |
| tab_group.update_time_windows_epoch_micros()); |
| message.mutable_tab_group_data()->set_sync_tab_group_id( |
| tab_group.saved_guid().AsLowercaseString()); |
| switch (event_type) { |
| case collaboration_pb::TAB_GROUP_ADDED: |
| message.set_triggering_user_gaia_id( |
| tab_group.shared_attribution().created_by.ToString()); |
| break; |
| case collaboration_pb::TAB_GROUP_REMOVED: |
| case collaboration_pb::TAB_GROUP_NAME_UPDATED: |
| case collaboration_pb::TAB_GROUP_COLOR_UPDATED: |
| message.set_triggering_user_gaia_id( |
| tab_group.shared_attribution().updated_by.ToString()); |
| break; |
| default: |
| break; |
| } |
| return message; |
| } |
| |
| CollaborationEvent ToCollaborationEvent( |
| collaboration_pb::EventType event_type) { |
| switch (event_type) { |
| case collaboration_pb::TAB_ADDED: |
| return CollaborationEvent::TAB_ADDED; |
| case collaboration_pb::TAB_REMOVED: |
| return CollaborationEvent::TAB_REMOVED; |
| case collaboration_pb::TAB_UPDATED: |
| return CollaborationEvent::TAB_UPDATED; |
| case collaboration_pb::TAB_GROUP_ADDED: |
| return CollaborationEvent::TAB_GROUP_ADDED; |
| case collaboration_pb::TAB_GROUP_REMOVED: |
| return CollaborationEvent::TAB_GROUP_REMOVED; |
| case collaboration_pb::TAB_GROUP_NAME_UPDATED: |
| return CollaborationEvent::TAB_GROUP_NAME_UPDATED; |
| case collaboration_pb::TAB_GROUP_COLOR_UPDATED: |
| return CollaborationEvent::TAB_GROUP_COLOR_UPDATED; |
| case collaboration_pb::COLLABORATION_ADDED: |
| return CollaborationEvent::COLLABORATION_ADDED; |
| case collaboration_pb::COLLABORATION_REMOVED: |
| return CollaborationEvent::COLLABORATION_REMOVED; |
| case collaboration_pb::COLLABORATION_MEMBER_ADDED: |
| return CollaborationEvent::COLLABORATION_MEMBER_ADDED; |
| case collaboration_pb::COLLABORATION_MEMBER_REMOVED: |
| return CollaborationEvent::COLLABORATION_MEMBER_REMOVED; |
| default: |
| return CollaborationEvent::UNDEFINED; |
| } |
| } |
| |
| RecentActivityAction GetRecentActivityActionFromCollaborationEvent( |
| CollaborationEvent event) { |
| switch (event) { |
| case CollaborationEvent::TAB_ADDED: |
| case CollaborationEvent::TAB_UPDATED: |
| return RecentActivityAction::kFocusTab; |
| case CollaborationEvent::TAB_REMOVED: |
| return RecentActivityAction::kReopenTab; |
| case CollaborationEvent::TAB_GROUP_ADDED: |
| case CollaborationEvent::TAB_GROUP_REMOVED: |
| return RecentActivityAction::kNone; |
| case CollaborationEvent::TAB_GROUP_NAME_UPDATED: |
| case CollaborationEvent::TAB_GROUP_COLOR_UPDATED: |
| return RecentActivityAction::kOpenTabGroupEditDialog; |
| case CollaborationEvent::COLLABORATION_ADDED: |
| case CollaborationEvent::COLLABORATION_REMOVED: |
| return RecentActivityAction::kNone; |
| case CollaborationEvent::COLLABORATION_MEMBER_ADDED: |
| case CollaborationEvent::COLLABORATION_MEMBER_REMOVED: |
| return RecentActivityAction::kManageSharing; |
| case CollaborationEvent::UNDEFINED: |
| return RecentActivityAction::kNone; |
| } |
| } |
| |
| std::optional<GaiaId> GetGaiaIdFromMessage( |
| const collaboration_pb::Message& message) { |
| switch (GetMessageCategory(message)) { |
| case MessageCategory::kTab: |
| case MessageCategory::kTabGroup: |
| if (message.triggering_user_gaia_id().empty()) { |
| return std::nullopt; |
| } |
| return GaiaId(message.triggering_user_gaia_id()); |
| case MessageCategory::kCollaboration: |
| if (message.affected_user_gaia_id().empty()) { |
| return std::nullopt; |
| } |
| return GaiaId(message.affected_user_gaia_id()); |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| std::optional<data_sharing::GroupId> GroupIdForTabGroup( |
| const tab_groups::SavedTabGroup& tab_group) { |
| if (!tab_group.collaboration_id()) { |
| return std::nullopt; |
| } |
| return data_sharing::GroupId(tab_group.collaboration_id().value().value()); |
| } |
| |
| tab_groups::CollaborationId ToCollaborationId( |
| const data_sharing::GroupId& group_id) { |
| return tab_groups::CollaborationId(group_id.value()); |
| } |
| |
| TabGroupMessageMetadata CreateTabGroupMessageMetadata( |
| const tab_groups::SavedTabGroup& tab_group) { |
| TabGroupMessageMetadata metadata; |
| metadata.local_tab_group_id = tab_group.local_group_id(); |
| metadata.sync_tab_group_id = tab_group.saved_guid(); |
| metadata.last_known_title = base::UTF16ToUTF8(tab_group.title()); |
| metadata.last_known_color = tab_group.color(); |
| return metadata; |
| } |
| |
| } // namespace |
| |
| MessagingBackendServiceImpl::MessagingBackendServiceImpl( |
| std::unique_ptr<TabGroupChangeNotifier> tab_group_change_notifier, |
| std::unique_ptr<DataSharingChangeNotifier> data_sharing_change_notifier, |
| std::unique_ptr<MessagingBackendStore> messaging_backend_store, |
| tab_groups::TabGroupSyncService* tab_group_sync_service, |
| data_sharing::DataSharingService* data_sharing_service) |
| : tab_group_change_notifier_(std::move(tab_group_change_notifier)), |
| data_sharing_change_notifier_(std::move(data_sharing_change_notifier)), |
| store_(std::move(messaging_backend_store)), |
| tab_group_sync_service_(tab_group_sync_service), |
| data_sharing_service_(data_sharing_service) { |
| store_->Initialize( |
| base::BindOnce(&MessagingBackendServiceImpl::OnStoreInitialized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| MessagingBackendServiceImpl::~MessagingBackendServiceImpl() = default; |
| |
| void MessagingBackendServiceImpl::SetInstantMessageDelegate( |
| InstantMessageDelegate* instant_message_delegate) { |
| instant_message_delegate_ = instant_message_delegate; |
| } |
| |
| void MessagingBackendServiceImpl::AddPersistentMessageObserver( |
| PersistentMessageObserver* observer) { |
| persistent_message_observers_.AddObserver(observer); |
| // TODO(345856704): Implement this and inform the observer if we have already |
| // initialized. |
| } |
| |
| void MessagingBackendServiceImpl::RemovePersistentMessageObserver( |
| PersistentMessageObserver* observer) { |
| persistent_message_observers_.RemoveObserver(observer); |
| } |
| |
| bool MessagingBackendServiceImpl::IsInitialized() { |
| return initialized_; |
| } |
| |
| std::vector<PersistentMessage> MessagingBackendServiceImpl::GetMessagesForTab( |
| tab_groups::EitherTabID tab_id, |
| std::optional<PersistentNotificationType> type) { |
| // TODO(345856704): Implement this and DCHECK(IsInitialized()) and update |
| // interface description. |
| return {}; |
| } |
| |
| std::vector<PersistentMessage> MessagingBackendServiceImpl::GetMessagesForGroup( |
| tab_groups::EitherGroupID group_id, |
| std::optional<PersistentNotificationType> type) { |
| // TODO(345856704): Implement this and DCHECK(IsInitialized()) and update |
| // interface description. |
| return {}; |
| } |
| |
| std::vector<PersistentMessage> MessagingBackendServiceImpl::GetMessages( |
| std::optional<PersistentNotificationType> type) { |
| // TODO(345856704): Implement this and DCHECK(IsInitialized()) and update |
| // interface description. |
| return {}; |
| } |
| |
| std::vector<ActivityLogItem> MessagingBackendServiceImpl::GetActivityLog( |
| const ActivityLogQueryParams& params) { |
| std::vector<ActivityLogItem> result; |
| std::vector<collaboration_pb::Message> messages = |
| store_->GetRecentMessagesForGroup(params.collaboration_id); |
| int message_count = 0; |
| for (const auto& message : messages) { |
| std::optional<ActivityLogItem> activity_log_item = |
| ConvertMessageToActivityLogItem(message); |
| if (!activity_log_item) { |
| continue; |
| } |
| result.emplace_back(*activity_log_item); |
| if (params.result_length == 0) { |
| continue; |
| } |
| if (++message_count >= params.result_length) { |
| break; |
| } |
| } |
| return result; |
| } |
| |
| void MessagingBackendServiceImpl::OnStoreInitialized(bool success) { |
| if (!success) { |
| DVLOG(2) << "Failed to initialize MessagingBackendServiceImpl."; |
| return; |
| } |
| data_sharing_change_notifier_observer_.Observe( |
| data_sharing_change_notifier_.get()); |
| data_sharing_flush_callback_ = data_sharing_change_notifier_->Initialize(); |
| } |
| |
| void MessagingBackendServiceImpl::OnDataSharingChangeNotifierInitialized() { |
| tab_group_change_notifier_observer_.Observe(tab_group_change_notifier_.get()); |
| tab_group_change_notifier_->Initialize(); |
| } |
| |
| void MessagingBackendServiceImpl::OnTabGroupChangeNotifierInitialized() { |
| initialized_ = true; |
| for (auto& observer : persistent_message_observers_) { |
| observer.OnMessagingBackendServiceInitialized(); |
| } |
| CHECK(data_sharing_flush_callback_); |
| std::move(data_sharing_flush_callback_).Run(); |
| } |
| |
| void MessagingBackendServiceImpl::OnTabGroupAdded( |
| const tab_groups::SavedTabGroup& added_group) { |
| std::optional<data_sharing::GroupId> collaboration_group_id = |
| GroupIdForTabGroup(added_group); |
| if (!collaboration_group_id) { |
| // Unable to find collaboration ID from tab group. |
| return; |
| } |
| |
| collaboration_pb::Message message = CreateTabGroupMessage( |
| *collaboration_group_id, added_group, collaboration_pb::TAB_GROUP_ADDED, |
| DirtyType::kNone); |
| store_->AddMessage(message); |
| } |
| |
| void MessagingBackendServiceImpl::OnTabGroupRemoved( |
| tab_groups::SavedTabGroup removed_group) { |
| std::optional<data_sharing::GroupId> collaboration_group_id = |
| GroupIdForTabGroup(removed_group); |
| if (!collaboration_group_id) { |
| // Unable to find collaboration ID from tab group. |
| return; |
| } |
| |
| collaboration_pb::Message message = CreateTabGroupMessage( |
| *collaboration_group_id, removed_group, |
| collaboration_pb::TAB_GROUP_REMOVED, DirtyType::kNone); |
| store_->AddMessage(message); |
| } |
| |
| void MessagingBackendServiceImpl::OnTabGroupNameUpdated( |
| const tab_groups::SavedTabGroup& updated_group) { |
| std::optional<data_sharing::GroupId> collaboration_group_id = |
| GroupIdForTabGroup(updated_group); |
| if (!collaboration_group_id) { |
| // Unable to find collaboration ID from tab group. |
| return; |
| } |
| |
| collaboration_pb::Message message = CreateTabGroupMessage( |
| *collaboration_group_id, updated_group, |
| collaboration_pb::TAB_GROUP_NAME_UPDATED, DirtyType::kNone); |
| store_->AddMessage(message); |
| } |
| |
| void MessagingBackendServiceImpl::OnTabGroupColorUpdated( |
| const tab_groups::SavedTabGroup& updated_group) { |
| std::optional<data_sharing::GroupId> collaboration_group_id = |
| GroupIdForTabGroup(updated_group); |
| if (!collaboration_group_id) { |
| // Unable to find collaboration ID from tab group. |
| return; |
| } |
| |
| collaboration_pb::Message message = CreateTabGroupMessage( |
| *collaboration_group_id, updated_group, |
| collaboration_pb::TAB_GROUP_COLOR_UPDATED, DirtyType::kNone); |
| store_->AddMessage(message); |
| } |
| |
| void MessagingBackendServiceImpl::OnTabAdded( |
| const tab_groups::SavedTabGroupTab& added_tab) {} |
| |
| void MessagingBackendServiceImpl::OnTabRemoved( |
| tab_groups::SavedTabGroupTab removed_tab) {} |
| |
| void MessagingBackendServiceImpl::OnTabUpdated( |
| const tab_groups::SavedTabGroupTab& updated_tab) {} |
| |
| void MessagingBackendServiceImpl::OnTabSelected( |
| std::optional<tab_groups::SavedTabGroupTab> selected_tab) {} |
| |
| void MessagingBackendServiceImpl::OnGroupAdded( |
| const data_sharing::GroupId& group_id, |
| const std::optional<data_sharing::GroupData>& group_data, |
| const base::Time& event_time) { |
| collaboration_pb::Message message = |
| CreateMessage(group_id, collaboration_pb::COLLABORATION_ADDED, |
| DirtyType::kNone, event_time); |
| store_->AddMessage(message); |
| } |
| |
| void MessagingBackendServiceImpl::OnGroupRemoved( |
| const data_sharing::GroupId& group_id, |
| const std::optional<data_sharing::GroupData>& group_data, |
| const base::Time& event_time) { |
| collaboration_pb::Message message = |
| CreateMessage(group_id, collaboration_pb::COLLABORATION_REMOVED, |
| DirtyType::kMessageOnly, event_time); |
| store_->AddMessage(message); |
| } |
| |
| void MessagingBackendServiceImpl::OnGroupMemberAdded( |
| const data_sharing::GroupData& group_data, |
| const GaiaId& member_gaia_id, |
| const base::Time& event_time) { |
| collaboration_pb::Message message = |
| CreateMessage(group_data.group_token.group_id, |
| collaboration_pb::COLLABORATION_MEMBER_ADDED, |
| DirtyType::kMessageOnly, event_time); |
| message.set_affected_user_gaia_id(member_gaia_id.ToString()); |
| std::optional<std::string> user_display_name = |
| GetDisplayNameForUserInGroup(group_data.group_token.group_id, |
| member_gaia_id, group_data, std::nullopt); |
| if (user_display_name) { |
| message.mutable_collaboration_data()->set_affected_user_name( |
| *user_display_name); |
| } |
| store_->AddMessage(message); |
| } |
| |
| void MessagingBackendServiceImpl::OnGroupMemberRemoved( |
| const data_sharing::GroupData& group_data, |
| const GaiaId& member_gaia_id, |
| const base::Time& event_time) { |
| collaboration_pb::Message message = |
| CreateMessage(group_data.group_token.group_id, |
| collaboration_pb::COLLABORATION_MEMBER_REMOVED, |
| DirtyType::kNone, event_time); |
| message.set_affected_user_gaia_id(member_gaia_id.ToString()); |
| std::optional<std::string> user_display_name = |
| GetDisplayNameForUserInGroup(group_data.group_token.group_id, |
| member_gaia_id, group_data, std::nullopt); |
| if (user_display_name) { |
| message.mutable_collaboration_data()->set_affected_user_name( |
| *user_display_name); |
| } |
| store_->AddMessage(message); |
| } |
| |
| std::optional<std::string> |
| MessagingBackendServiceImpl::GetDisplayNameForUserInGroup( |
| const data_sharing::GroupId& group_id, |
| const GaiaId& gaia_id, |
| const std::optional<data_sharing::GroupData>& group_data, |
| const std::optional<collaboration_pb::Message>& db_message) { |
| std::optional<data_sharing::GroupMemberPartialData> group_member_data = |
| data_sharing_service_->GetPossiblyRemovedGroupMember(group_id, gaia_id); |
| // Try given name from live data first. |
| if (group_member_data && !group_member_data->given_name.empty()) { |
| return group_member_data->given_name; |
| } |
| |
| // Then try given name from provided data. |
| if (group_data) { |
| for (const data_sharing::GroupMember& member : group_data->members) { |
| if (member.gaia_id == gaia_id) { |
| if (member.given_name.empty()) { |
| break; |
| } |
| return member.given_name; |
| } |
| } |
| } |
| |
| // Then try given name from stored data. |
| if (db_message) { |
| if (db_message->affected_user_gaia_id() == gaia_id.ToString()) { |
| if (!db_message->collaboration_data().affected_user_name().empty()) { |
| return db_message->collaboration_data().affected_user_name(); |
| } |
| } |
| } |
| |
| // Then try display name from live data. |
| if (group_member_data && !group_member_data->display_name.empty()) { |
| return group_member_data->display_name; |
| } |
| |
| // Then try display name from provided data. |
| if (group_data) { |
| for (const data_sharing::GroupMember& member : group_data->members) { |
| if (member.gaia_id == gaia_id) { |
| if (member.display_name.empty()) { |
| break; |
| } |
| return member.display_name; |
| } |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| std::optional<ActivityLogItem> |
| MessagingBackendServiceImpl::ConvertMessageToActivityLogItem( |
| const collaboration_pb::Message& message) { |
| switch (message.event_type()) { |
| case collaboration_pb::TAB_GROUP_ADDED: |
| case collaboration_pb::TAB_GROUP_REMOVED: |
| case collaboration_pb::COLLABORATION_ADDED: |
| case collaboration_pb::COLLABORATION_REMOVED: |
| return std::nullopt; |
| default: |
| break; |
| } |
| ActivityLogItem item; |
| item.collaboration_event = ToCollaborationEvent(message.event_type()); |
| data_sharing::GroupId collaboration_group_id(message.collaboration_id()); |
| |
| std::optional<GaiaId> gaia_id = GetGaiaIdFromMessage(message); |
| std::optional<data_sharing::GroupMember> group_member; |
| if (gaia_id) { |
| std::optional<std::string> user_name_for_display = |
| GetDisplayNameForUserInGroup(collaboration_group_id, *gaia_id, |
| std::nullopt, message); |
| if (user_name_for_display) { |
| item.user_display_name = *user_name_for_display; |
| } |
| std::optional<data_sharing::GroupMemberPartialData> group_member_data = |
| data_sharing_service_->GetPossiblyRemovedGroupMember( |
| collaboration_group_id, *gaia_id); |
| if (group_member_data) { |
| group_member = group_member_data->ToGroupMember(); |
| } |
| } |
| |
| // TODO(nyquist): Compare GaiaId with current user in this profile. |
| item.user_is_self = false; |
| |
| item.description = GetDescriptionTextForActivityLogItem(message); |
| item.time_delta = |
| base::Time::Now() - base::Time::FromTimeT(message.event_timestamp()); |
| item.action = |
| GetRecentActivityActionFromCollaborationEvent(item.collaboration_event); |
| |
| item.activity_metadata = MessageAttribution(); |
| item.activity_metadata.collaboration_id = collaboration_group_id; |
| |
| // The code below needs to fill in `activity_metadata`, and optionally |
| // `show_favicon` if it is true. |
| switch (GetMessageCategory(message)) { |
| case MessageCategory::kTab: |
| break; |
| case MessageCategory::kTabGroup: { |
| item.activity_metadata.triggering_user = group_member; |
| std::optional<tab_groups::SavedTabGroup> tab_group = |
| tab_group_sync_service_->GetGroup(base::Uuid::ParseLowercase( |
| message.tab_group_data().sync_tab_group_id())); |
| if (tab_group) { |
| item.activity_metadata.tab_group_metadata = |
| CreateTabGroupMessageMetadata(*tab_group); |
| } else { |
| item.activity_metadata.tab_group_metadata = TabGroupMessageMetadata(); |
| std::optional<std::u16string> previous_title = |
| tab_group_sync_service_ |
| ->GetTitleForPreviouslyExistingSharedTabGroup( |
| ToCollaborationId(collaboration_group_id)); |
| if (previous_title) { |
| item.activity_metadata.tab_group_metadata->last_known_title = |
| base::UTF16ToUTF8(*previous_title); |
| } |
| } |
| break; |
| } |
| case MessageCategory::kCollaboration: |
| item.activity_metadata.affected_user = group_member; |
| break; |
| default: |
| break; |
| } |
| return item; |
| } |
| |
| std::u16string |
| MessagingBackendServiceImpl::GetDescriptionTextForActivityLogItem( |
| const collaboration_pb::Message& message) { |
| std::optional<GaiaId> gaia_id = GetGaiaIdFromMessage(message); |
| std::optional<data_sharing::GroupMemberPartialData> group_member_data; |
| if (gaia_id) { |
| group_member_data = data_sharing_service_->GetPossiblyRemovedGroupMember( |
| data_sharing::GroupId(message.collaboration_id()), *gaia_id); |
| } |
| |
| switch (ToCollaborationEvent(message.event_type())) { |
| case CollaborationEvent::TAB_ADDED: |
| case CollaborationEvent::TAB_UPDATED: |
| case CollaborationEvent::TAB_REMOVED: |
| // TODO(nyquist): Update this to use real data. |
| return u""; // Current domain as eTLD+1 (format for security display). |
| case CollaborationEvent::TAB_GROUP_ADDED: |
| case CollaborationEvent::TAB_GROUP_REMOVED: |
| return u""; // Not defined. |
| case CollaborationEvent::TAB_GROUP_NAME_UPDATED: |
| // TODO(nyquist): Update this to use real data. |
| return u""; // Current tab group name. |
| case CollaborationEvent::TAB_GROUP_COLOR_UPDATED: |
| return u""; // Intentionally left blank. |
| case CollaborationEvent::COLLABORATION_ADDED: |
| case CollaborationEvent::COLLABORATION_REMOVED: |
| return u""; // Not defined. |
| case CollaborationEvent::COLLABORATION_MEMBER_ADDED: |
| case CollaborationEvent::COLLABORATION_MEMBER_REMOVED: |
| // Should use the email of the added / removed user. |
| if (group_member_data) { |
| return base::UTF8ToUTF16(group_member_data->email); |
| } else { |
| return u""; |
| } |
| case CollaborationEvent::UNDEFINED: |
| return u""; // Not defined. |
| } |
| } |
| |
| } // namespace collaboration::messaging |