id:otn:20150901 「MS Exchange/Outlook のカレンダーとGoogle Calendarの同期」のその後。
google-api-client が 0.9 になってそのままでは動かなくなった。0.8のまま使っていたのだが、認証がおかしくなった(毎回認証しないとタイムスタンプがおかしいというエラーになる)ので、0.9 対応に修正。いろいろあった。
認証については、googleauth の README.md を参考に。しかし、以前のように「ブラウザが自動的に開いて、認証するとアプリが続行」という風には出来ず、自分でブラウザを開いて、認証したあと、そこに表示された文字列を入力しないといけない。これは不便だがしょうが無い。
あともAPIが色々変わっており、ソースを追ったり、gemにデバッグプリントを入れたりしてなんとかなった。手こずったのがリマインダ。リマインダメソッド指定のキーが、"method" から :reminder_method に変わっている!
#! ruby CAL_ID = "xxxxxxxxxxxxxxxxx@gmail.com" #GOOGLEアカウントのメールアドレス DAYS = 30 AUTH_FILE = "authfile.yaml" SECRET_FILE = "client_secret.json" ENV["http_proxy"] = "http://user:pass@proxyserver:port" # proxyの設定 ENV["https_proxy"] = "http://user:pass@proxyserver:port" # proxyの設定 Dir.chdir File.dirname($0) require "googleauth" require "googleauth/stores/file_token_store" require "google/apis/calendar_v3" require "google/api_client/client_secrets" require "win32ole" class Event def to_s # デバッグ用に書いたもの [@start.strftime("%Y-%m-%d %H:%M"), @end.strftime("%Y-%m-%d %H:%M"), @allday.to_s[0], @reminder.to_s, @title, @body.gsub(/\s/," "), @location].join(",") end def ==(other) @start == other.instance_variable_get(:@start) and @end == other.instance_variable_get(:@end) and @allday == other.instance_variable_get(:@allday) and reminder_eql?(@start, @reminder, other.instance_variable_get(:@reminder)) and @title == other.instance_variable_get(:@title) and @body == other.instance_variable_get(:@body) and @location == other.instance_variable_get(:@location) end def delete Gcal.delete(@id) end def add event = { summary: @title, description: @body, start: start, end: ende, location: @location, reminders: reminder, source: {title: "Exchange", url: "http://localhost"} } Gcal.add(event) end def is_holiday @allday and @location=="日本" end private def start if @allday {date: @start.strftime("%Y-%m-%d")} else {date_time: @start.iso8601} end end def ende if @allday {date: @end.strftime("%Y-%m-%d")} else {date_time: @end.iso8601} end end def reminder if @reminder {use_default: false, overrides: [{reminder_method: :popup, minutes: @reminder}]} else {use_default: false} end end def reminder_eql?(start, x, y) @@now ||= Time.now ( x == y ) or # リマインダー時刻を過ぎている場合はリマインダーの有無を無視 ( x and not y and start - x < @@now ) or ( y and not x and start - y < @@now ) end end # Googleカレンダー class Gcal < Event def self.get_auth(calendar_id, auth_filename, secret_filename) @@cal_id = calendar_id client_id = Google::Auth::ClientId.from_file(SECRET_FILE) scope = "https://www.googleapis.com/auth/calendar" token_store = Google::Auth::Stores::FileTokenStore.new(file: AUTH_FILE) authorizer = Google::Auth::UserAuthorizer.new(client_id, scope, token_store) credentials = authorizer.get_credentials(@@cal_id) if credentials.nil? url = authorizer.get_authorization_url(base_url: "urn:ietf:wg:oauth:2.0:oob") system(%Q|CMD /c start "" "#{url}"|) print "Enter the resulting code: " code = gets credentials = authorizer.get_and_store_credentials_from_code( user_id: @@cal_id, code: code, base_url: "urn:ietf:wg:oauth:2.0:oob") end @@client = Google::Apis::CalendarV3::CalendarService.new @@client.authorization = credentials end def self.delete(id) @@client.delete_event(@@cal_id, id) end def self.add(event) @@client.insert_event(@@cal_id, Google::Apis::CalendarV3::Event.new(event)) end def self.is_exchange(item) item.source and item.source.title == "Exchange" end def self.list(from, to) @@client.list_events(@@cal_id, order_by: "startTime", time_min: from.iso8601, time_max: to.iso8601, single_events: "True" ).items end def initialize(event) if event.start.date @start = Time.parse(event.start.date) @end = Time.parse(event.end.date) @allday = true else @start = event.start.date_time.to_time @end = event.end.date_time.to_time @allday = false end if event.reminders.use_default @reminder = 10 # 正確にはそのユーザーのデフォルト値を取得する必要がある elsif event.reminders.overrides @reminder = event.reminders.overrides[0].minutes else @reminder = nil end @title = event.summary || "" @body = event.description || "" @location = event.location || "" @id = event.id end end # Exchangeカレンダー class Ecal < Event FolderCalendar = 9 def self.list(from, to) @@calendar ||= WIN32OLE.new("Outlook.Application") .GetNamespace("MAPI").GetDefaultFolder(FolderCalendar) items = @@calendar.Items items.Sort "[Start]" items.IncludeRecurrences = true item = items.Find(%Q/[Start] < "#{to.strftime("%Y-%m-%d %H:%M")}" AND [End] >= "#{from.strftime("%Y-%m-%d %H:%M")}"/) Enumerator.new do |y| while item y << item item = items.FindNext end end end def initialize(event) @start = event.Start @end = event.End @allday = event.AllDayEvent @reminder = event.ReminderSet ? event.ReminderMinutesBeforeStart : nil @title = event.Subject.encode(Encoding::UTF_8) @body = event.Body.encode(Encoding::UTF_8) @location = event.Location.encode(Encoding::UTF_8) end end # 期間設定 today = Time.now time_min = today - 3600*24*DAYS time_max = today + 3600*24*DAYS # Exchangeイベント取得 e_events = Ecal.list(time_min, time_max).map{|ev| Ecal.new(ev)}.reject(&:is_holiday) # Google認証 Gcal.get_auth(CAL_ID, AUTH_FILE, SECRET_FILE) # Googleイベント取得 g_events = Gcal.list(time_min, time_max) .select{|ev| Gcal.is_exchange(ev)}.map{|ev| Gcal.new(ev)} # イベント削除 g_events.reject{|ev| e_events.include?(ev)}.each{|ev| ev.delete} # イベント追加 e_events.reject{|ev| g_events.include?(ev)}.each{|ev| ev.add}