尽管有功能性过滤功能,但使用 appscript 重复创建联系人

Duplicate contact creation using appscript despite a functional filter function

上下文

在我们深入研究代码之前先了解一下上下文:我目前在一家保护猫的非营利组织工作。我不是专业开发人员,我没有报酬从事这项工作,但由于我是唯一愿意这样做的人并且知道如何编码,所以我自愿编写一个脚本来创建和更新采用者和我们猫的遗弃者联系方式。

组织中的其他志愿者正在使用 Google 表格来跟踪有关猫的大量信息,包括收养者和放弃者的联系信息。另一方面,该组织的负责人希望在她的 Google 联系人中以特定格式拥有每个采用者或放弃者的联系方式。在我写脚本之前,志愿者们习惯于在电子表格中输入所有信息,然后在老板的联系人中再次输入。

该脚本主要是功能性的,也处理联系信息的更新。但是,该脚本为某些人创建了重复的联系人,我真的不明白为什么(尽管我可能有线索)。这是一个只有在志愿者使用脚本时才会出现的错误,而在我使用它时不会出现;这让我觉得他们调用脚本时出了问题...

代码

该脚本创建一个 Person 对象数组。每个人都有一个方法来 return 自己的 contactObject 版本,与 Google 的 People API 兼容,我使用 People API 的 batchUpdateContacts 函数来创建联系人,通过当数组中有新联系人时,每批 200 个。

为了知道已经创建的联系人,我首先使用这个函数获取创建的连接:

/** Helper function to list all connections of the current google user
 * 
 * @returns {PeopleAPI.Person[]} connections - All of the connection objects from Google's People API
 */
/** Helper function to list all connections of the current google user
 * 
 * @returns {PeopleAPI.Person[]} connections - All of the connection objects from Google's People API
*/
function getAllConnections_() {
  var connections = [];
  var apiResponse;
  var nextPageToken;
  var firstPass = true;
  do {
    if (firstPass) {
      apiResponse = People.People.Connections.list('people/me', {'personFields': 'memberships,emailAddresses,phoneNumbers,names,addresses,biographies,userDefined'});
      firstPass = false;
    }
    else {
      apiResponse = People.People.Connections.list('people/me', {'personFields': 'memberships,emailAddresses,phoneNumbers,names,addresses,biographies,userDefined', 'pageToken': nextPageToken});
    }
    connections = connections.concat(apiResponse.connections);
    nextPageToken = apiResponse.nextPageToken;
  } while (nextPageToken);
  return connections;
}

然后,我使用过滤功能,根据联系人的电子邮件地址过滤掉已经存在的联系人(当领养一只猫时,我们总是要求提供 2 个电子邮件地址,所以我知道至少有一个):

/** Helper function to filter the existing contacts and avoid creating them
 * 
 * @param {Person[]} people - people to filter from
 * @param {connections[]} connections - existing contacts in person's address book
 * @returns {Person[]} filteredPeople - people who are not in connections
 */
function filterExistingContacts_(people, connections) {
  if (!connections[0]) {
    return people;
  }
  return people.filter(function (person) {
    for (contact of connections) {
      if (!contact.emailAddresses) {continue;}
      if (contact.emailAddresses.filter(function (email) {return email.value.toLowerCase().replace(/\s/g, '').includes(person.email)}).length > 0) {return false;}
    }
    return true;
  });
}

在上面的代码中,person.email被小写并且空格被替换为''。当我 运行 这些函数时,我无法重现错误,但是当脚本用户重现时,他们会创建 2 到 74 个重复联系人中的任意数字。

假设和线索

我的假设是,出于某种原因,“getAllConnections_”函数可能从 Google 的人员 API 那里得到了不好的响应,因此得到了不完整的现有连接数组。然后我的过滤器功能正确地过滤了联系人(因为我在这里的逻辑中没有发现错误),但是重新创建了一些现有的联系人,因为脚本不知道它们已经存在。

第一个想法

如果是这样,我认为 SQL 数据库可能可以解决问题(并降低算法的复杂性,这对于当前约 4000 个现有联系人来说非常慢)。但是我真的不知道在哪里可以找到一个可以与 Appscript 一起使用的免费数据库(因为该组织更愿意为兽医护理付费而不是为此付费);再加上这意味着要对代码本身进行大量工作以适应它...所以我想知道你们是否认为它可以解决问题,或者在我再花几个小时之前我是否完全错了。

第二个想法

此外,我考虑过使用此处描述的“ADDED”技巧之类的方法: 作为解决方法...但是电子表格不是按联系人构建的,而是按猫构建的。所以这会导致特定情况的问题,实际上很遗憾,这种情况非常频繁:

  1. Patrick Shmotherby 收养了猫 Smoochie → Smoochie 的收养者列被标记为“已添加”,并且创建了 Patrick 的联系人。
  2. Patrick Shmotherby 后来放弃了 Smoochie → Smoochie 的放弃者列被标记为“已添加”并且 Patrick 的联系方式已更新。
  3. Karen Klupstutsy 后来采用了 Smoochie → Smoochie 的采用者列已标记为“已添加”,因此不会创建 Karen 的联系人。

一个解决方案可能是要求志愿者手动删除“ADDED”标记,但我想你可以理解为什么在同一天更新大量联系人时容易出错并且对志愿者来说很耗时。

第三个想法

我想我可以创建一个功能来自动删除 Google 帐户中的重复联系人,但我不想使用这个解决方案,因为我担心我可能会删除那里的一些重要数据,尤其是因为这是老板的专业、组织和个人账户。

你能帮我什么忙

我必须说,尽管有我的线索,但我对这些重复项感到有点迷茫和困惑,尤其是因为我无法调试任何东西,因为我自己无法重现错误。如果你有第四条线索,我会热烈欢迎。

另外,因为我是一个业余爱好者,所以很可能我没有以正确的方式做事,或者不知道我可以做其他事情(例如,我建议使用 SQL 数据库因为我知道关系数据库的存在,但也许还有其他我从未听说过的常用工具)。所以任何建议都会很好。

最后,如果您认为我自己的诊断是正确的,那么告诉我这样可以帮助我获得动力,在需要时几乎完全重写我的代码。如果你知道我在哪里可以找到一个可用于 Google Appscript 的免费数据库(我知道质量是有代价的,所以我对此不抱太大希望,但我们永远不知道),如果它不是“托管你的在你的地下室拥有自己的数据库”,那太棒了!

如果你需要更多信息,如果你想让我放一些其他代码或任何东西,请告诉我。

玩得开心day/afternoon/evening/night, 本杰明

这是我为使用我的电子邮件白名单所做的一些事情,以确保我没有收到重复的电子邮件。

function displayCurrentContacts() {
  const ss = SpreadsheetApp.getActive();
  const sh = ss.getSheetByName('Contacts');
  sh.clearContents();
  const vs = [['Name', 'Emails']];
  const resp = People.People.Connections.list('people/me', { personFields: "emailAddresses,names,organizations" });
  //Logger.log(resp);
  const data = JSON.parse(resp);
  let m = 0;
  let n = 0;
  data.connections.forEach((ob1, i) => {
    if (ob1.emailAddresses && ob1.emailAddresses.length > 0) {
      let emails = [... new Set(ob1.emailAddresses.map(ob2 => ob2.value))];//used set to insure I get unique list
      //let emails = ob1.emailAddresses.map(ob2 => ob2.value);
      let name;
      m += emails.length;
      //the following cases are derived from the way that I organize all of my contacts
      if (ob1.names && ob1.organizations) {
        name = ob1.names[0].displayName + '\n' + ob1.organizations[0].name;
        ++n;
      } else if (ob1.names) {
        name = ob1.names[0].displayName;
        ++n;
      } else if (ob1.organizations) {
        name = ob1.organizations[0].name;
        ++n;
      }
      vs.push([name, emails.sort().join('\n')])
    }
  });
  vs.push([n, m])
  sh.getRange(1, 1, vs.length, vs[0].length).setValues(vs)
  sh.getRange(2, 1, sh.getLastRow() - 2, sh.getLastColumn()).sort({ column: 1, sortAscending: true });
}

请注意,这并不意味着以任何方式替换插件。

好吧,我找到了问题出在哪里,感谢@OctaviaSima 向我指出了执行日志。 显然,出于某种我不知道的原因,有时,我的函数“getAllConnections_()”本应获取 Google 联系人簿中的所有联系人,但使用此代码无法获取某些联系人:

/** Helper function to list all connections of the current google user
 * 
 * @returns {PeopleAPI.Person[]} connections - All of the connection objects from Google's People API
 */
function getAllConnections_() {
  var connections = [];
  var apiResponse;
  var nextPageToken;
  var firstPass = true;
  do {
    if (firstPass) {
      apiResponse = People.People.Connections.list('people/me', {'personFields': 'memberships,emailAddresses,phoneNumbers,names,addresses,biographies,userDefined'});
      firstPass = false;
    }
    else {
      apiResponse = People.People.Connections.list('people/me', {'personFields': 'memberships,emailAddresses,phoneNumbers,names,addresses,biographies,userDefined', 'pageToken': nextPageToken});
    }
    connections = connections.concat(apiResponse.connections);
    nextPageToken = apiResponse.nextPageToken;
  } while (nextPageToken);
  return connections;
}

例如最后一次执行时,实际的联系人列表有 4061 个连接,但是脚本只有 4056 个连接,这导致创建了 5 个重复的联系人。 我添加了一个快速补丁,通过确保连接 table 与联系人数量一样长,如果不是这种情况,则通过递归调用该函数来确保连接数。

/** Helper function to list all connections of the current google user
 * 
 * @returns {PeopleAPI.Person[]} connections - All of the connection objects from Google's People API
 */
function getAllConnections_() {
  var connections = [];
  var apiResponse;
  var nextPageToken;
  var firstPass = true;
  do {
    if (firstPass) {
      apiResponse = People.People.Connections.list('people/me', {'personFields': 'memberships,emailAddresses,phoneNumbers,names,addresses,biographies,userDefined'});
      firstPass = false;
    }
    else {
      apiResponse = People.People.Connections.list('people/me', {'personFields': 'memberships,emailAddresses,phoneNumbers,names,addresses,biographies,userDefined', 'pageToken': nextPageToken});
    }
    connections = connections.concat(apiResponse.connections);
    nextPageToken = apiResponse.nextPageToken;
  } while (nextPageToken);
  if (connections.length != apiResponse.totalItems) {connections = getAllConnections_();} // Hopefully, the magic lies in this line of code
  return connections;
}

张贴在这里以防对其他人有帮助。

编辑:刚刚将魔术线上的测试从“==”更正为“!=”。