// Copyright (c) 2026 Duplicati Inc. All rights reserved. using Google.Apis.Auth.OAuth2; using Google.Apis.Auth.OAuth2.Flows; using Google.Apis.Gmail.v1; using Google.Apis.Admin.Directory.directory_v1; using Google.Apis.Calendar.v3; using Google.Apis.Drive.v3; using Google.Apis.PeopleService.v1; using Google.Apis.Tasks.v1; using Google.Apis.Keep.v1; using Google.Apis.Groupssettings.v1; using Google.Apis.HangoutsChat.v1; using Google.Apis.Services; using Google.Apis.Http; using Google.Apis.Util; namespace Duplicati.Proprietary.GoogleWorkspace; public class APIHelper { private readonly string? _clientId; private readonly string? _clientSecret; private readonly string? _refreshToken; private readonly string? _serviceAccountJson; private readonly string? _adminEmail; private readonly bool _isRestoreOperation; private DirectoryService? _directoryServiceForUsers; private DirectoryService? _directoryServiceForGroups; private DirectoryService? _directoryServiceForOrgUnits; private DriveService? _driveService; private GroupssettingsService? _groupsSettingsService; public APIHelper(OptionsHelper.GoogleWorkspaceOptions options, bool isRestoreOperation) { _clientId = options.ClientId; _clientSecret = options.ClientSecret; _refreshToken = options.RefreshToken; _serviceAccountJson = options.ServiceAccountJson; _adminEmail = options.AdminEmail; _isRestoreOperation = isRestoreOperation; } private BaseClientService.Initializer GetServiceInitializer(string? userId, IEnumerable scopes) { var backoffInitializer = new BackOffInitializer(); if (!string.IsNullOrEmpty(_serviceAccountJson)) { var credential = GoogleCredential.FromJson(_serviceAccountJson) .CreateScoped(scopes.ToArray()); var userToImpersonate = userId ?? _adminEmail; if (!string.IsNullOrEmpty(userToImpersonate)) { credential = credential.CreateWithUser(userToImpersonate); } return new BaseClientService.Initializer() { HttpClientInitializer = new CompositeHttpClientInitializer(credential, backoffInitializer), ApplicationName = "Duplicati", }; } else if (!string.IsNullOrEmpty(_clientId) && !string.IsNullOrEmpty(_clientSecret) && !string.IsNullOrEmpty(_refreshToken)) { var credential = new UserCredential(new GoogleAuthorizationCodeFlow( new GoogleAuthorizationCodeFlow.Initializer { ClientSecrets = new ClientSecrets { ClientId = _clientId, ClientSecret = _clientSecret } }), "user", new Google.Apis.Auth.OAuth2.Responses.TokenResponse { RefreshToken = _refreshToken }); return new BaseClientService.Initializer() { HttpClientInitializer = new CompositeHttpClientInitializer(credential, backoffInitializer), ApplicationName = "Duplicati", }; } else { throw new Exception("Missing credentials"); } } public void TestConnection() { if (_isRestoreOperation) { // Test connection by listing shared drives (Drive API is used for restore operations) var driveService = GetDriveService(); driveService.Drives.List().Execute(); } else { // Test connection by getting the Gmail service var gmailService = GetGmailService(_adminEmail ?? "me"); gmailService.Users.GetProfile(_adminEmail ?? "me").Execute(); } } public GmailService GetGmailService(string userId) { var scopes = _isRestoreOperation ? new[] { "https://www.googleapis.com/auth/gmail.modify" } : new[] { GmailService.Scope.GmailReadonly }; return new GmailService(GetServiceInitializer(userId, scopes)); } public DirectoryService GetDirectoryServiceForUsers() { if (_directoryServiceForUsers == null) { var scopes = _isRestoreOperation ? new[] { DirectoryService.Scope.AdminDirectoryUser } : new[] { DirectoryService.Scope.AdminDirectoryUserReadonly }; _directoryServiceForUsers = new DirectoryService(GetServiceInitializer(null, scopes)); } return _directoryServiceForUsers!; } public DirectoryService GetDirectoryServiceForGroups() { if (_directoryServiceForGroups == null) { var scopes = _isRestoreOperation ? new[] { DirectoryService.Scope.AdminDirectoryGroup } : new[] { DirectoryService.Scope.AdminDirectoryGroupReadonly }; _directoryServiceForGroups = new DirectoryService(GetServiceInitializer(null, scopes)); } return _directoryServiceForGroups!; } public DirectoryService GetDirectoryServiceForOrgUnits() { if (_directoryServiceForOrgUnits == null) { var scopes = _isRestoreOperation ? new[] { DirectoryService.Scope.AdminDirectoryOrgunit } : new[] { DirectoryService.Scope.AdminDirectoryOrgunitReadonly }; _directoryServiceForOrgUnits = new DirectoryService(GetServiceInitializer(null, scopes)); } return _directoryServiceForOrgUnits!; } public CalendarService GetCalendarService(string userId) { var scopes = _isRestoreOperation ? new[] { CalendarService.Scope.Calendar } : new[] { CalendarService.Scope.CalendarReadonly }; return new CalendarService(GetServiceInitializer(userId, scopes)); } public CalendarService GetCalendarAclService(string userId) { // ACL reading requires WRITE permissions var scopes = new[] { CalendarService.Scope.Calendar }; return new CalendarService(GetServiceInitializer(userId, scopes)); } public DriveService GetDriveService(string? userId = null) { var scopes = _isRestoreOperation ? new[] { DriveService.Scope.Drive } : new[] { DriveService.Scope.DriveReadonly }; if (userId != null) { return new DriveService(GetServiceInitializer(userId, scopes)); } if (_driveService == null) { _driveService = new DriveService(GetServiceInitializer(null, scopes)); } return _driveService!; } public PeopleServiceService GetPeopleService(string userId) { var scopes = _isRestoreOperation ? new[] { "https://www.googleapis.com/auth/contacts" } : new[] { PeopleServiceService.Scope.ContactsReadonly }; return new PeopleServiceService(GetServiceInitializer(userId, scopes)); } public TasksService GetTasksService(string userId) { var scopes = _isRestoreOperation ? new[] { TasksService.Scope.Tasks } : new[] { TasksService.Scope.TasksReadonly }; return new TasksService(GetServiceInitializer(userId, scopes)); } public KeepService GetKeepService(string userId) { var scopes = _isRestoreOperation ? new[] { KeepService.Scope.Keep } : new[] { KeepService.Scope.KeepReadonly }; return new KeepService(GetServiceInitializer(userId, scopes)); } public GroupssettingsService GetGroupsSettingsService() { if (_groupsSettingsService == null) { var scopes = new[] { GroupssettingsService.Scope.AppsGroupsSettings }; _groupsSettingsService = new GroupssettingsService(GetServiceInitializer(null, scopes)); } return _groupsSettingsService!; } public HangoutsChatService GetChatService(string userId) { var scopes = _isRestoreOperation ? new[] { HangoutsChatService.Scope.ChatMessages } : new[] { HangoutsChatService.Scope.ChatSpacesReadonly, HangoutsChatService.Scope.ChatMessagesReadonly, HangoutsChatService.Scope.ChatMembershipsReadonly }; return new HangoutsChatService(GetServiceInitializer(userId, scopes)); } public HangoutsChatService GetChatCreateSpaceService(string userId) { var scopes = new[] { HangoutsChatService.Scope.ChatSpaces }; return new HangoutsChatService(GetServiceInitializer(userId, scopes)); } private class CompositeHttpClientInitializer : IConfigurableHttpClientInitializer { private readonly IConfigurableHttpClientInitializer[] _initializers; public CompositeHttpClientInitializer(params IConfigurableHttpClientInitializer[] initializers) { _initializers = initializers; } public void Initialize(ConfigurableHttpClient httpClient) { foreach (var initializer in _initializers) { initializer.Initialize(httpClient); } } } private class BackOffInitializer : IConfigurableHttpClientInitializer { public void Initialize(ConfigurableHttpClient httpClient) { var backoffHandler = new BackOffHandler(new BackOffHandler.Initializer(new ExponentialBackOff()) { HandleUnsuccessfulResponseFunc = (r) => r.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable || r.StatusCode == (System.Net.HttpStatusCode)429 }); httpClient.MessageHandler.AddUnsuccessfulResponseHandler(backoffHandler); httpClient.MessageHandler.AddExceptionHandler(backoffHandler); } } }