Class: Timely::Sources::Outlook

Inherits:
Object
  • Object
show all
Defined in:
lib/timely/sources/outlook.rb

Constant Summary collapse

GRAPH_BASE =
'https://graph.microsoft.com/v1.0'
AUTH_URL =
'https://login.microsoftonline.com/common/oauth2/v2.0'
SCOPES =
'Calendars.ReadWrite offline_access'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ Outlook

Returns a new instance of Outlook.



15
16
17
18
19
20
21
22
# File 'lib/timely/sources/outlook.rb', line 15

def initialize(config = {})
  @client_id = config['client_id']
  @tenant_id = config['tenant_id'] || 'common'
  @access_token = config['access_token']
  @refresh_token = config['refresh_token']
  @token_expires_at = 0
  @last_error = nil
end

Instance Attribute Details

#last_errorObject (readonly)

Returns the value of attribute last_error.



13
14
15
# File 'lib/timely/sources/outlook.rb', line 13

def last_error
  @last_error
end

Instance Method Details

#create_event(event_data) ⇒ Object

Create event



127
128
129
130
131
# File 'lib/timely/sources/outlook.rb', line 127

def create_event(event_data)
  body = to_outlook_format(event_data)
  data = api_post('/me/events', body)
  data ? data['id'] : nil
end

#delete_event(event_id) ⇒ Object

Delete event



140
141
142
# File 'lib/timely/sources/outlook.rb', line 140

def delete_event(event_id)
  api_delete("/me/events/#{event_id}")
end

#fetch_events(time_min, time_max) ⇒ Object

Fetch events for a date range



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/timely/sources/outlook.rb', line 110

def fetch_events(time_min, time_max)
  start_str = Time.at(time_min).utc.strftime('%Y-%m-%dT%H:%M:%SZ')
  end_str = Time.at(time_max).utc.strftime('%Y-%m-%dT%H:%M:%SZ')

  events = []
  url = "/me/calendarView?startDateTime=#{start_str}&endDateTime=#{end_str}&$top=250&$orderby=start/dateTime"

  while url
    data = api_get(url)
    break unless data && data['value']
    data['value'].each { |item| events << normalize_event(item) }
    url = data['@odata.nextLink']&.sub(GRAPH_BASE, '')
  end
  events
end

#list_calendarsObject

List calendars



101
102
103
104
105
106
107
# File 'lib/timely/sources/outlook.rb', line 101

def list_calendars
  data = api_get('/me/calendars')
  return [] unless data && data['value']
  data['value'].map do |cal|
    { id: cal['id'], name: cal['name'], color: cal['color'], can_edit: cal['canEdit'] }
  end
end

#poll_for_token(device_code) ⇒ Object

Device code flow - Step 2: poll for token



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/timely/sources/outlook.rb', line 44

def poll_for_token(device_code)
  uri = URI("#{AUTH_URL.sub('common', @tenant_id)}/token")
  loop do
    res = Net::HTTP.post_form(uri, {
      'client_id' => @client_id,
      'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
      'device_code' => device_code
    })
    data = JSON.parse(res.body)
    if data['access_token']
      @access_token = data['access_token']
      @refresh_token = data['refresh_token']
      @token_expires_at = Time.now.to_i + (data['expires_in'] || 3600).to_i - 60
      return { access_token: @access_token, refresh_token: @refresh_token }
    elsif data['error'] == 'authorization_pending'
      sleep 5
    elsif data['error'] == 'slow_down'
      sleep 10
    else
      @last_error = data['error_description'] || data['error']
      return nil
    end
  end
rescue => e
  @last_error = e.message
  nil
end

#refresh_access_tokenObject

Refresh access token



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/timely/sources/outlook.rb', line 73

def refresh_access_token
  return @access_token if @access_token && Time.now.to_i < @token_expires_at
  return nil unless @refresh_token && @client_id

  uri = URI("#{AUTH_URL.sub('common', @tenant_id)}/token")
  res = Net::HTTP.post_form(uri, {
    'client_id' => @client_id,
    'grant_type' => 'refresh_token',
    'refresh_token' => @refresh_token,
    'scope' => SCOPES
  })

  if res.is_a?(Net::HTTPSuccess)
    data = JSON.parse(res.body)
    @access_token = data['access_token']
    @refresh_token = data['refresh_token'] if data['refresh_token']
    @token_expires_at = Time.now.to_i + (data['expires_in'] || 3600).to_i - 60
    @access_token
  else
    @last_error = "Token refresh failed: #{res.code}"
    nil
  end
rescue => e
  @last_error = e.message
  nil
end

#respond_to_event(event_id, response) ⇒ Object

Accept/decline/tentative



145
146
147
148
149
150
151
152
153
# File 'lib/timely/sources/outlook.rb', line 145

def respond_to_event(event_id, response)
  endpoint = case response.to_s
             when 'accepted', 'accept' then 'accept'
             when 'declined', 'decline' then 'decline'
             when 'tentative', 'tentativelyAccept' then 'tentativelyAccept'
             else return false
             end
  api_post("/me/events/#{event_id}/#{endpoint}", { 'sendResponse' => true })
end

#start_device_authObject

Device code flow - Step 1: get device code Returns { user_code:, device_code:, verification_uri:, message: }



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/timely/sources/outlook.rb', line 26

def start_device_auth
  uri = URI("#{AUTH_URL.sub('common', @tenant_id)}/devicecode")
  res = Net::HTTP.post_form(uri, {
    'client_id' => @client_id,
    'scope' => SCOPES
  })
  if res.is_a?(Net::HTTPSuccess)
    JSON.parse(res.body)
  else
    @last_error = "Device auth failed: #{res.code}"
    nil
  end
rescue => e
  @last_error = e.message
  nil
end

#update_event(event_id, event_data) ⇒ Object

Update event



134
135
136
137
# File 'lib/timely/sources/outlook.rb', line 134

def update_event(event_id, event_data)
  body = to_outlook_format(event_data)
  api_patch("/me/events/#{event_id}", body)
end