\/**
\n * CONFIGURATION
\n *\/<\/SPAN>
\nconst<\/SPAN> CONFIG = {
\n SYNC_FIELD_NAME<\/SPAN>: “HubSpot Labels”<\/SPAN>, \/\/ Must match your HubSpot Property Name later<\/SPAN>
\n CHECKSUM_FIELD_NAME<\/SPAN>: “HS_Sync_Hash”<\/SPAN>, \/\/ Technical field, do not touch<\/SPAN>
\n IGNORED_LABELS<\/SPAN>: [‘contactGroups\/myContacts’<\/SPAN>, ‘contactGroups\/starred’<\/SPAN>],
\n SEPARATOR<\/SPAN>: “;”<\/SPAN>
\n};<\/p>\nfunction<\/SPAN> installTrigger<\/SPAN>() <\/SPAN>{
\n const<\/SPAN> triggers = ScriptApp.getProjectTriggers();
\n for<\/SPAN> (let<\/SPAN> t of<\/SPAN> triggers) {
\n if<\/SPAN> (t.getHandlerFunction() === ‘startSync’<\/SPAN>) {
\n console<\/SPAN>.log(“Sync is already running!”<\/SPAN>);
\n return<\/SPAN>;
\n }
\n }
\n \/\/ Runs every 15 minutes to handle large lists safely<\/SPAN>
\n ScriptApp.newTrigger(‘startSync’<\/SPAN>).timeBased().everyMinutes(15<\/SPAN>).create();
\n console<\/SPAN>.log(“\u2705<\/span> Installed! Sync runs every 15 mins.”<\/SPAN>);
\n}<\/p>\nfunction<\/SPAN> startSync<\/SPAN>() <\/SPAN>{
\n const<\/SPAN> groupMap = fetchGroupMap();
\n let<\/SPAN> pageToken = null<\/SPAN>;
\n do<\/SPAN> {
\n const<\/SPAN> response = People.People.Connections.list(‘people\/me’<\/SPAN>, {
\n personFields<\/SPAN>: ‘names,memberships,userDefined’<\/SPAN>,
\n pageSize<\/SPAN>: 1000<\/SPAN>,
\n pageToken<\/SPAN>: pageToken
\n });
\n const<\/SPAN> connections = response.connections || [];
\n if<\/SPAN> (connections.length > 0<\/SPAN>) {
\n for<\/SPAN> (const<\/SPAN> person of<\/SPAN> connections) {
\n try<\/SPAN> { processContact(person, groupMap); }
\n catch<\/SPAN> (e) { console<\/SPAN>.error(`Error: ${e.message}<\/SPAN>`<\/SPAN>); }
\n }
\n }
\n pageToken = response.nextPageToken;
\n } while<\/SPAN> (pageToken);
\n}<\/p>\nfunction<\/SPAN> processContact<\/SPAN>(person, groupMap<\/SPAN>) <\/SPAN>{
\n const<\/SPAN> resourceName = person.resourceName;
\n const<\/SPAN> currentMemberships = person.memberships || [];
\n const<\/SPAN> realLabelNames = [];<\/p>\n currentMemberships.forEach(mem<\/SPAN> =><\/SPAN> {
\n if<\/SPAN> (mem.contactGroupMembership) {
\n const<\/SPAN> res = mem.contactGroupMembership.contactGroupResourceName;
\n const<\/SPAN> name = groupMap.idToName[res];
\n if<\/SPAN> (name && !CONFIG.IGNORED_LABELS.includes(res)) realLabelNames.push(name);
\n }
\n });<\/p>\n realLabelNames.sort();
\n const<\/SPAN> realLabelString = realLabelNames.join(CONFIG.SEPARATOR);<\/p>\n const<\/SPAN> userDefined = person.userDefined || [];
\n let<\/SPAN> syncFieldVal = “”<\/SPAN>, checksumVal = “”<\/SPAN>;
\n userDefined.forEach(f<\/SPAN> =><\/SPAN> {
\n if<\/SPAN> (f.key === CONFIG.SYNC_FIELD_NAME) syncFieldVal = f.value;
\n if<\/SPAN> (f.key === CONFIG.CHECKSUM_FIELD_NAME) checksumVal = f.value;
\n });<\/p>\n const<\/SPAN> syncFieldArr = syncFieldVal ? syncFieldVal.split(CONFIG.SEPARATOR).map(s<\/SPAN> =><\/SPAN> s.trim()).filter(s<\/SPAN>=><\/SPAN>s).sort() : [];
\n const<\/SPAN> normalizedSyncString = syncFieldArr.join(CONFIG.SEPARATOR);<\/p>\n const<\/SPAN> googleChanged = (realLabelString !== checksumVal);
\n const<\/SPAN> hubspotChanged = (normalizedSyncString !== checksumVal);<\/p>\n if<\/SPAN> (!googleChanged && !hubspotChanged) return<\/SPAN>;<\/p>\n const<\/SPAN> contactToUpdate = { etag<\/SPAN>: person.etag };
\n const<\/SPAN> fieldsToUpdate = [];<\/p>\n if<\/SPAN> (googleChanged) {
\n const<\/SPAN> newUserDefined = rebuildUserDefined(userDefined, {
\n [CONFIG.SYNC_FIELD_NAME]: realLabelString,
\n [CONFIG.CHECKSUM_FIELD_NAME]: realLabelString
\n });
\n contactToUpdate.userDefined = newUserDefined;
\n fieldsToUpdate.push(‘userDefined’<\/SPAN>);
\n } else<\/SPAN> if<\/SPAN> (hubspotChanged) {
\n const<\/SPAN> newUserDefined = rebuildUserDefined(userDefined, {
\n [CONFIG.CHECKSUM_FIELD_NAME]: normalizedSyncString
\n });
\n contactToUpdate.userDefined = newUserDefined;
\n fieldsToUpdate.push(‘userDefined’<\/SPAN>);<\/p>\n const<\/SPAN> newMemberships = [];
\n currentMemberships.forEach(mem<\/SPAN> =><\/SPAN> {
\n if<\/SPAN> (mem.contactGroupMembership) {
\n const<\/SPAN> res = mem.contactGroupMembership.contactGroupResourceName;
\n if<\/SPAN> (CONFIG.IGNORED_LABELS.includes(res)) newMemberships.push({ contactGroupMembership<\/SPAN>: { contactGroupResourceName<\/SPAN>: res } });
\n }
\n });
\n syncFieldArr.forEach(labelName<\/SPAN> =><\/SPAN> {
\n let<\/SPAN> groupId = groupMap.nameToId[labelName];
\n if<\/SPAN> (!groupId) {
\n groupId = People.ContactGroups.create({ contactGroup<\/SPAN>: { name<\/SPAN>: labelName } }).resourceName;
\n groupMap.nameToId[labelName] = groupId;
\n }
\n newMemberships.push({ contactGroupMembership<\/SPAN>: { contactGroupResourceName<\/SPAN>: groupId } });
\n });
\n contactToUpdate.memberships = newMemberships;
\n fieldsToUpdate.push(‘memberships’<\/SPAN>);
\n }<\/p>\n if<\/SPAN> (fieldsToUpdate.length > 0<\/SPAN>) {
\n People.People.updateContact(contactToUpdate, resourceName, { updatePersonFields<\/SPAN>: fieldsToUpdate.join(‘,’<\/SPAN>) });
\n }
\n}<\/p>\nfunction<\/SPAN> fetchGroupMap<\/SPAN>() <\/SPAN>{
\n let<\/SPAN> pageToken = null<\/SPAN>;
\n const<\/SPAN> map = { nameToId<\/SPAN>: {}, idToName<\/SPAN>: {} };
\n do<\/SPAN> {
\n const<\/SPAN> response = People.ContactGroups.list({ groupFields<\/SPAN>: ‘name’<\/SPAN>, pageSize<\/SPAN>: 1000<\/SPAN>, pageToken<\/SPAN>: pageToken });
\n (response.contactGroups || []).forEach(g<\/SPAN> =><\/SPAN> {
\n const<\/SPAN> name = g.formattedName || g.name;
\n const<\/SPAN> id = g.resourceName;
\n if<\/SPAN> (name && id) { map.nameToId[name] = id; map.idToName[id] = name; }
\n });
\n pageToken = response.nextPageToken;
\n } while<\/SPAN> (pageToken);
\n return<\/SPAN> map;
\n}<\/p>\nfunction<\/SPAN> rebuildUserDefined<\/SPAN>(cur, updates<\/SPAN>) <\/SPAN>{
\n let<\/SPAN> arr = cur ? […cur] : [];