We have written the following code to sync contacts between two gmail ids, such that if a new contact is being added to one email id and the Google Apps Script is run, the new contacts get synced to the other email id too:
const CLIENT_ID = 'client_id'; const CLIENT_SECRET = 'client_secret';
// Replace with the initial refresh tokens for both users
const USER1_REFRESH_TOKEN = 'Reftoken1';
const USER2_REFRESH_TOKEN = 'Reftoken2';
function syncContacts() {
try {
// Get fresh access tokens for both users
const user1AccessToken = refreshAccessToken(USER1_REFRESH_TOKEN);
const user2AccessToken = refreshAccessToken(USER2_REFRESH_TOKEN);
// Fetch contacts from both accounts
const user1Contacts = fetchContacts(user1AccessToken);
const user2Contacts = fetchContacts(user2AccessToken);
// Sync contacts between accounts
addUniqueContacts(user1Contacts, user2AccessToken);
addUniqueContacts(user2Contacts, user1AccessToken);
Logger.log('Contacts synced successfully!');
} catch (error) {
Logger.log(`Error during sync: ${error.message}`);
}
}
function fetchContacts(accessToken) {
const url = 'https://people.googleapis.com/v1/people/me/connections?pageSize=2000&personFields=names,emailAddresses,phoneNumbers';
const options = {
method: 'get',
headers: {
Authorization: 'Bearer ' + accessToken,
},
muteHttpExceptions: true,
};
try {
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
if (response.getResponseCode() === 200 && result.connections) {
Logger.log(`Fetched ${result.connections.length} contacts.`);
return result;
} else {
Logger.log('Error fetching contacts: ' + response.getContentText());
return null;
}
} catch (error) {
Logger.log('Error fetching contacts: ' + error.message);
return null;
}
}
function addUniqueContacts(sourceAccessToken, targetAccessToken) {
const sourceContacts = fetchContacts(sourceAccessToken);
const targetContacts = fetchContacts(targetAccessToken);
if (!sourceContacts || !targetContacts) {
Logger.log('Failed to fetch contacts. Aborting synchronization.');
return;
}
const targetPhoneNumbers = new Set(
targetContacts.connections.flatMap(contact =>
contact.phoneNumbers?.map(phone => formatPhoneNumber(phone.value)) || []
)
);
const uniqueContacts = sourceContacts.connections.filter(contact =>
(contact.phoneNumbers || []).some(phone =>
!targetPhoneNumbers.has(formatPhoneNumber(phone.value))
)
);
Logger.log(`Found ${uniqueContacts.length} unique contacts.`);
uniqueContacts.forEach(contact => {
const sanitizedContact = sanitizeContact(contact); // Sanitize before creating
try {
createContact(targetAccessToken, sanitizedContact);
} catch (error) {
Logger.log('Error adding unique contact: ' + error.message);
}
});
}
function sanitizeContact(contact) {
// Remove metadata, resourceName, etag, and other unnecessary fields
return {
names: contact.names?.map(name => ({
displayName: name.displayName,
givenName: name.givenName,
})),
emailAddresses: contact.emailAddresses?.map(email => ({
value: email.value,
})),
phoneNumbers: contact.phoneNumbers?.map(phone => ({
value: phone.value,
})),
};
}
function createContact(accessToken, contact) {
const url = 'https://people.googleapis.com/v1/people:createContact';
const options = {
method: 'post',
headers: {
Authorization: 'Bearer ' + accessToken,
},
contentType: 'application/json',
muteHttpExceptions: true,
payload: JSON.stringify(contact),
};
try {
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
if (response.getResponseCode() === 200) {
Logger.log('Contact Created: ' + JSON.stringify(result));
} else {
Logger.log('Error creating contact: ' + response.getContentText());
}
} catch (error) {
Logger.log('Error creating contact: ' + error.message);
throw new Error('Failed to create contact.');
}
}
function refreshAccessToken(refreshToken) {
const url = 'https://oauth2.googleapis.com/token';
const payload = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken,
grant_type: 'refresh_token',
};
const options = {
method: 'post',
contentType: 'application/x-www-form-urlencoded',
payload: Object.keys(payload)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(payload[key])}`)
.join('&'),
muteHttpExceptions: true, // Capture full error details
};
try {
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
// Log the response for debugging
Logger.log(`Token refresh response: ${JSON.stringify(result)}`);
if (result.access_token) {
Logger.log('Access token refreshed successfully.');
return result.access_token;
} else {
throw new Error(result.error_description || 'Failed to refresh access token.');
}
} catch (error) {
Logger.log(`Error refreshing access token: ${error.message}`);
throw new Error('Access token refresh failed.');
}
}
function formatPhoneNumber(phone) {
// Standardize phone numbers for comparison
return phone.replace(/[^d+]/g, ''); // Removes non-digit and non-'+' characters
}
However, I keep getting this error:
Error fetching contacts: {
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
}
These are the following ways I have tried trouble shooting:
-
Check if People API is enabled for both email ids.
-
Set up Credentials from one account and share with the other.
-
Set up these following redirect URI’s:
http://localhost, https://script.google.com/oauthcallback, https://developers.google.com/oauthplayground -
Have authorised API in Google playground from both id’s and stored correct refresh tokens
I am not sure how to rectify this error. Would be really helpful if someone could guide?
1