GoogleカレンダーとSalesforceのEventを同期する方法。簡易版と完全版

      2016/05/20

追記:php版書きました

Salesforce使っている組織でGoogleApps使っているというのはザラにあるんじゃないでしょうか。

そこで一番頭を悩ませるのがスケジュールの管理です。

GoogleAppsを使っていれば、Googleカレンダーを活用するでしょう。UIも使いやすいですし、これが一番だと思います。

一方で、Salesforceには各個人の活動状況を登録する事が出来ます。

営業活動なんかのデータをきちんと蓄積して分析したいよねというニーズは当然のように出てくるでしょう。

そこで課題が出てきます

簡単に同期出来ないんですよね。無料でやれるかどうかというと、そういうのは皆無です。ありません。

しょうがないなと思って、色々と調べるじゃないですか。

以下では、GoogleAppsとSalesforce両方の管理者権限を持っている場合に出来ることを紹介します。

少しお金を払って同期する方法

Zapierというサービスがあります。
$20/month払えばGoogleAppsとSalesforceを接続する事が出来ます。

設定方法はこんな感じです。

z1.jpg

ここで課題が出てきます。現在のZapierでは、新規のカレンダー登録がされたというトリガーしか選べません。つまり、Googleカレンダーの情報が更新された場合(日付や時間が変更になった場合)にそれをSalesfordeに同期する術がありません。とは言え、簡単にできるのでそれを差し引いても取り敢えず同期したいという場合にはこれがオススメです。設定の続き

z2.jpg

GoogleカレンダーとSalesforceの管理者アカウントを登録します。これ必須です。

どのカレンダーを同期するのかを選びましょう

z3.jpg

あとは、どの項目を紐付けるのかを選びます。

z5.jpg

DescriptionにはDescriptionを入れます

 

z6.jpg

終了日時にEvents Ends

 

z7.jpg

Locationも同じでいいですね

 

z8.jpg

担当者に、カレンダーと同じ人を設定しましょう

 

z11.jpg

開始日時・SubjectにSammary・関連先に適当な商談のIDを紐付けています。

SalesforceのEventは、何かの情報に紐付いていない場合は、自分しか見ることが出来ないデータとなっていますので、弊社では適当な商談を一つ立ち上げ、それに全ての予定を紐付けています。

こんな感じで設定終わり。これを人数分作ります。大きな組織では地獄ですね。

以上が安くて簡単だけど、ちょっとかゆいところに手が届かないパターンです。

無料だし、情報の更新も出来るけど、ちょっとむずかしい方法

環境:Mac

使うもの

  • Ruby
  • Talend
  • cron

バックグラウンドで実行し続ける必要があるので、デスクトップが良いと思う。

まずはTalendで更新ファイルを作ろう

本当はTalendで全部やってしまえば良いのですが、GoogleAppsへの接続が難しいので諦めました。GoogleAppsへの接続とデータの取得をRubyで処理して、TalendがSalesforceにデータを同期するという形にしています。

処理の図はこんな感じ

talend1.jpg

大きく分けて2つの処理が走っています。片方は、新規に出来たカレンダーデータをSalesforceに挿入するパターン。(図の上の処理)

もう一つは、GoogleカレンダーからSalesforceに一度データの同期をかけたもので、Googleカレンダー上で情報が更新された場合に、Salesforceのデータを更新する処理になっています。

Talendで処理を書いておけば、Zapierの時に出来なかった更新処理が出来るようになって便利ですね。

noID3と書かれたデータは、csvファイルです。

新規にGoogleカレンダーに登録されたデータがここに入っています。(中身の取得の方法は下部のRubyスクリプトを参照)

userListはEmailアドレスと、SalesforceのUserIDを紐付けたCSVファイルです。

"ID","Email"
"00xxlkjdalkfjsldkfjalXXX1","[email protected]"
"00xxlkjdalkfjsldkfjalXXX2","[email protected]p"
"00xxlkjdalkfjsldkfjalXXX3","[email protected]"
"00xxlkjdalkfjsldkfjalXXX4","[email protected]"

こんな感じのCSVファイルです。IDはSalesforceからエクスポートしてきてください。

tMap_4の設定がこれです。

tmap1.jpg

tMap_2の設定がこれ。

tmap2.jpg

上部の処理は何をしているかというと

新規のカレンダーデータであるnoID3を展開して、userListのEmailからUserIDを照合します。

tMap_4でやっているのはuserIDを入れることだけですね。userIDが合致したら、そのデータをEventに挿入します。

アクションを「挿入」にするのを忘れずに。

Eventに突っ込んだら、突っ込んだデータのIDを取得する必要があります。これを取ってこないと、後で更新するときに困りますので。Eventから出力を伸ばしてみるとtMap_2にあるように、salesforce_idというデータが取得できます。これがIDです。

insertedEventDataというのは、これもcsvファイルです。こんな感じで作っています

"ID","googleCalEventID__c"
"sfのID","メアドとGoogleカレンダーのイベントIDをくっつけたもの"

googleCalEventID__cがメアドとGoogleカレンダーのイベントIDをくっつけているのには理由があります。
Googleカレンダーでは、一つのイベントに他人を招待することができるのですが、イベントIDだけを取ってきてしまうと、それがユニークなキーとして使えません。そこでメアドと組み合わさえる事でuniqにしています。

 

次に下の処理を見てみましょう。

updatedEvent2というのはCSVファイルです。

こんな感じになってます。

"ID","Email","googleCalEventID__c","Deleted","StartDateTime","EndDateTime","Subject","Description","Location"
"00U10000014xxxxAM","[email protected]","[email protected]_lajsdkjfoaiwejflkasjdlfg30o0c","confirmed","2015-09-11T02:00:00.000Z","2015-09-11T03:00:00.000Z","カレンダーの件名","","場所",""

tMap_3がこれ

tmap3.jpg

userListは同じですね。tMap_3でやってるのは、IDをキーにしてカレンダーで更新された情報を上書きするよというものです。

こっちがわのEventのアウトプットは先ほどと違ってアクションに更新を選んでください。

これでTalendのスクリプトは完成です。

CSVファイルの読み込み方ですがこんな感じです。

ヘッダを列名として使っています。フィールドセパレータはCommaを選択しています。

ダブルクオーテーションで囲ったデータなので、テキストエンクロージャを選択してください。

全てのCSVをこれに合わせました。

Rubyスクリプトを作ってGoogleカレンダーの情報を取得してCSVを整形する

これ重要です。頑張りましょう。

前半部分はGoogleカレンダーとの接続です。やってることはここに書きました。

GoogleカレンダーのデータをAPI経由で取得する-Ruby- 

コード置き場
https://github.com/geeorgey/calendarsync

getcal.rb

#!/usr/bin/ruby

require 'rubygems'
require 'active_support'
require 'active_support/core_ext'
require 'google/api_client'
require 'google/api_client/client_secrets'
require 'google/api_client/auth/installed_app'
require 'google/api_client/auth/storage'
require 'google/api_client/auth/storages/file_store'
require 'fileutils'
require 'csv'
require 'open3'


class NilClass
  def method_missing(name, *args, &block)
    nil
  end
end

APPLICATION_NAME = 'Google Calendar API Quickstart'
CLIENT_SECRETS_PATH = '/Users/george/Downloads/client_secret.json'
CREDENTIALS_PATH = File.join(Dir.home, '.credentials',
                             "calendar-quickstart.json")
SCOPE = 'https://www.googleapis.com/auth/calendar.readonly'

##
# Ensure valid credentials, either by restoring from the saved credentials
# files or intitiating an OAuth2 authorization request via InstalledAppFlow.
# If authorization is required, the user's default browser will be launched
# to approve the request.
#
# @return [Signet::OAuth2::Client] OAuth2 credentials
def authorize
  FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))

  file_store = Google::APIClient::FileStore.new(CREDENTIALS_PATH)
  storage = Google::APIClient::Storage.new(file_store)
  auth = storage.authorize

  if auth.nil? || (auth.expired? && auth.refresh_token.nil?)
    app_info = Google::APIClient::ClientSecrets.load(CLIENT_SECRETS_PATH)
    flow = Google::APIClient::InstalledAppFlow.new({
      :client_id => app_info.client_id,
      :client_secret => app_info.client_secret,
      :scope => SCOPE})
    auth = flow.authorize(storage)
    puts "Credentials saved to #{CREDENTIALS_PATH}" unless auth.nil?
  end
  auth
end

userList = CSV.read("/Users/yourname/googleAppsUserList.csv") #メアドのリストです

# Initialize the API
client = Google::APIClient.new(:application_name => APPLICATION_NAME)
client.authorization = authorize
calendar_api = client.discovered_api('calendar', 'v3')

t = Time.now - 3.minutes #3分毎に更新するのでこの設定にした
t_max = Time.now + 3.month #三ヶ月先までにしましたが、おこのみで


File.open("/Users/yourname/output.csv","w") do |file| #output.csvは毎回空にしてからデータ入れる
  file.puts '"ID","Email","googleCalEventID__c","Deleted","StartDateTime","EndDateTime","Subject","Description","Location"'
end

  for uid in userList do
    # Fetch the next 10 events for the user
    results = client.execute!(
      :api_method => calendar_api.events.list,
      :parameters => {
        :calendarId => uid,
        :maxResults => 20,
        :orderBy => 'updated',
        :updatedMin => t.iso8601 ,
        :timeMin => Time.now.iso8601,
        :timeMax => t_max.iso8601
        }
        )

 
    File.open("/Users/yourname/output.csv","a") do |file|
    results.data.items.each do |event|
      start = (event.start.date || event.start.date_time) - 9.hours #GMT +9の解消
      end_time = (event.end.date || event.end.date_time) - 9.hours #GMT +9の解消
      start2 = start.iso8601
#ここの処理ですが、Googleカレンダー上のデータを削除した時に、Salesforceのデータも削除したかったのですが、ISDELETEDフラグはスクリプトからいじれないようです。その為、削除されたデータははるか昔の日付に飛ばす処理にしてあります
      if start.nil? then
        start2 = "2000-08-03T08:00:00.000Z"
      else
        start2 = start.iso8601
      end
      if end_time.nil? then
        end_time2 = "2000-08-04T08:00:00.000Z"
      else
        end_time2 = end_time.iso8601
      end
#ここまでが削除時の処理
#ここの処理ブサイクなので誰か教えてください…ダブルクオーテーションで囲むスマートな方法が知りたい
      file.print '""',",#{uid},#{uid}_#{event.id}",'","',"#{event.status}",'","',"#{start2}",'","',"#{end_time2}" ,'","',"#{event.summary}",'","',"#{event.description}",'","',"#{event.location}",'"',"\n"
    end

    end
  end

  #置換。余計な文字の削除と、Salesforceに取り込む際の時間データの型を整形しています。
  f = File.open("/Users/yourname/output.csv","r")
  buffer = f.read();
  #buffer.gsub!(/"|[|]| +0900| UTC/,'"' =>'','[' => '',']' => '','+09:00' => '.000Z',' UTC' => '')
  #buffer.gsub!('"',"").gsub!('[',"").gsub!(']',"").gsub!('+09:00',".000Z").gsub!(' UTC',"");
  buffer.gsub!('[',"").gsub!(']',"").gsub!('lne.st"_',"lne.st_").gsub!('+09:00',".000Z").gsub!(' UTC',"").gsub!('confirmed',"FALSE").gsub!('cancelled',"TRUE");
  f=File.open("/Users/george/GoogleDrive/forTalend/googleCal/output.csv","w")
  f.write(buffer)
  f.close()


  File.readlines("/Users/yourname/compare.csv").uniq
  compare = CSV.table('/Users/yourname/compare.csv', force_quotes: true)
  output = CSV.table('/Users/yourname/output.csv', force_quotes: true)

  # Talendに喰わせる更新用ファイルの初期化
  File.open("/Users/yourname/updatedEvents.csv","w") do |file|
    file.puts '"ID","Email","googleCalEventID__c","Deleted","StartDateTime","EndDateTime","Subject","Description","Location"'
  end
  File.open("/Users/yourname/noID.csv","w") do |file|
    file.puts '"ID","Email","googleCalEventID__c","Deleted","StartDateTime","EndDateTime","Subject","Description","Location"'
  end

  # ファイルへのデータ書き込み
  output.each{|googlecaleventid__c|
    if compare[:googlecaleventid__c].include?(googlecaleventid__c[2]) then
      #存在データを更新するための出力
      index = compare.find_index{|x| #既存データのindexを取得してSFのIDを取ってくる処理
      x[:googlecaleventid__c] == googlecaleventid__c[2]
      }

      File.open("/Users/yourname/updatedEvents.csv","a") do |file|
      #  file.puts "ID,Email,googleCalEventID__c,Subject,StartDateTime,EndDateTime,googleCalEventLastUpdated__c,Description,Location"
#この出力処理もブサイクなので何とかしたい。SFのIDとGoogleカレンダーの更新情報を組み合わせています
        file.print '"', compare[index][:id] ,'","',  googlecaleventid__c[1] ,'","', googlecaleventid__c[2] ,'","', googlecaleventid__c[3] ,'","', googlecaleventid__c[4] ,'","', googlecaleventid__c[5]  ,'","',  googlecaleventid__c[6],'","',  googlecaleventid__c[7],'","',  googlecaleventid__c[8],'","', googlecaleventid__c[9] , '"' , "\n"
      end

    else
      #新規データを出力する
      File.open("/Users/yourname/noID.csv","a") do |file|
      #  file.puts "ID,Email,googleCalEventID__c,Subject,StartDateTime,EndDateTime,googleCalEventLastUpdated__c,Description,Location"
#ここもどうにかしたい
        file.print '"","' ,googlecaleventid__c[1] ,'","', googlecaleventid__c[2] ,'","', googlecaleventid__c[3] ,'","', googlecaleventid__c[4] ,'","', googlecaleventid__c[5]  ,'","',  googlecaleventid__c[6],'","',  googlecaleventid__c[7],'","',  googlecaleventid__c[8], '"' , "\n"
      end
    end
  }


#RubyからTalendの出力ジョブを実行する処理

  DIR = "/Users/yourname/"

  cmd = 'cd ' + DIR + ";"
  cmd += 'ROOT_PATH=' + DIR + ";"
  cmd += 'java -Xms256M -Xmx1024M -cp $ROOT_PATH:$ROOT_PATH/../lib/systemRoutines.jar::$ROOT_PATH/../lib/userRoutines.jar::.:$ROOT_PATH/googlecalupdates_0_1.jar:$ROOT_PATH/../lib/activation-1.1.jar:$ROOT_PATH/../lib/advancedPersistentLookupLib-1.0.jar:$ROOT_PATH/../lib/axiom-api-1.2.13.jar:$ROOT_PATH/../lib/axiom-impl-1.2.13.jar:$ROOT_PATH/../lib/axis2-adb-1.6.2.jar:$ROOT_PATH/../lib/axis2-kernel-1.6.2.jar:$ROOT_PATH/../lib/axis2-transport-http-1.6.2.jar:$ROOT_PATH/../lib/axis2-transport-local-1.6.2.jar:$ROOT_PATH/../lib/commons-codec-1.3.jar:$ROOT_PATH/../lib/commons-collections-3.2.jar:$ROOT_PATH/../lib/commons-httpclient-3.1.jar:$ROOT_PATH/../lib/commons-logging-1.1.1.jar:$ROOT_PATH/../lib/dom4j-1.6.1.jar:$ROOT_PATH/../lib/geronimo-stax-api_1.0_spec-1.0.1.jar:$ROOT_PATH/../lib/httpcore-4.2.1.jar:$ROOT_PATH/../lib/jboss-serialization.jar:$ROOT_PATH/../lib/log4j-1.2.15.jar:$ROOT_PATH/../lib/mail-1.4.jar:$ROOT_PATH/../lib/neethi-3.0.1.jar:$ROOT_PATH/../lib/salesforceCRMManagement.jar:$ROOT_PATH/../lib/talend_file_enhanced_20070724.jar:$ROOT_PATH/../lib/talendcsv.jar:$ROOT_PATH/../lib/trove.jar:$ROOT_PATH/../lib/wsdl4j-1.6.3.jar:$ROOT_PATH/../lib/wstx-asl-3.2.9.jar:$ROOT_PATH/../lib/xmlschema-core-2.0.1.jar: salesforce.googlecalupdates_0_1.googleCalUpdates --context=Default "[email protected]" '

  out, err, status = Open3.capture3(cmd)

こんなかんじになります。
作業ディレクトリは、自分のフォルダ直下にしてあります。
/Users/yourname/
部分を適宜自分の環境にあわせてください。

各種CSVも全部/Users/yourname/に入るように書いています。

Talendで作ったジョブを出力してRubyから実行する方法についてはこちらを参照のこと。
RubyからTalendのジョブを呼び出してみる

DIRの部分でパス間違っててハマりました。

ハマりポイントがいくつか有ります。

Dateの形式については、SFでは型が決まってますので、それと逸脱するとエラーが出てしまいます。しっかりと整形しましょう。

ここまでやったらあとはこのRubyスクリプトをcronで動かすだけです。

本当はlaunchdを使うべきとの事でしたが、恐らくPathの設定が間違っているようで、うまく動きませんでした。今回は仕方が無いのでcrontabで実行します。

# crontab -e

で中身をこんなかんじに

*/3 * * * * /bin/bash -cl 'cd /Users/yourname/ && /usr/bin/ruby -Ku getcal.rb'

3分毎にgetcal.rbを実行します。

これで完成!

当然ですが、Macを止めたら更新とまりますのでお気をつけて。

おすすめ記事一覧

 - Google, Salesforce