lex-microsoft_teams 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -2
  3. data/CHANGELOG.md +34 -0
  4. data/CLAUDE.md +2 -2
  5. data/docs/plans/2026-03-19-teams-token-lifecycle-design.md +120 -0
  6. data/docs/plans/2026-03-19-teams-token-lifecycle-implementation.md +679 -0
  7. data/lib/legion/extensions/microsoft_teams/actors/auth_validator.rb +105 -0
  8. data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +1 -5
  9. data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +2 -2
  10. data/lib/legion/extensions/microsoft_teams/actors/message_processor.rb +1 -1
  11. data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +1 -1
  12. data/lib/legion/extensions/microsoft_teams/actors/token_refresher.rb +103 -0
  13. data/lib/legion/extensions/microsoft_teams/client.rb +5 -2
  14. data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +1 -1
  15. data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +7 -2
  16. data/lib/legion/extensions/microsoft_teams/helpers/client.rb +4 -0
  17. data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +2 -5
  18. data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +1 -0
  19. data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +66 -3
  20. data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +1 -1
  21. data/lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb +1 -1
  22. data/lib/legion/extensions/microsoft_teams/runners/bot.rb +4 -4
  23. data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +8 -9
  24. data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +5 -5
  25. data/lib/legion/extensions/microsoft_teams/runners/channels.rb +6 -6
  26. data/lib/legion/extensions/microsoft_teams/runners/chats.rb +5 -5
  27. data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +16 -16
  28. data/lib/legion/extensions/microsoft_teams/runners/messages.rb +5 -5
  29. data/lib/legion/extensions/microsoft_teams/runners/presence.rb +2 -2
  30. data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +5 -5
  31. data/lib/legion/extensions/microsoft_teams/runners/teams.rb +3 -3
  32. data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +6 -6
  33. data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
  34. metadata +5 -2
  35. data/lib/legion/extensions/microsoft_teams/transport.rb +0 -11
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MicrosoftTeams
6
+ module Actor
7
+ class AuthValidator < Legion::Extensions::Actors::Once
8
+ def use_runner? = false
9
+ def check_subtask? = false
10
+ def generate_task? = false
11
+
12
+ def delay
13
+ 2.0
14
+ end
15
+
16
+ def enabled?
17
+ defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
18
+ rescue StandardError
19
+ false
20
+ end
21
+
22
+ def token_cache
23
+ @token_cache ||= Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
24
+ end
25
+
26
+ def manual
27
+ loaded = token_cache.load_from_vault
28
+
29
+ if loaded
30
+ token = token_cache.cached_delegated_token
31
+ if token
32
+ log_info('Teams delegated auth restored')
33
+ elsif token_cache.previously_authenticated?
34
+ attempt_browser_reauth(token_cache)
35
+ end
36
+ elsif token_cache.previously_authenticated?
37
+ log_warn('Token file found but could not load, attempting re-authentication')
38
+ attempt_browser_reauth(token_cache)
39
+ else
40
+ log_debug('No Teams delegated auth configured, skipping')
41
+ end
42
+ rescue StandardError => e
43
+ log_error("AuthValidator: #{e.message}")
44
+ end
45
+
46
+ private
47
+
48
+ def attempt_browser_reauth(cache)
49
+ settings = teams_auth_settings
50
+ return false unless settings[:tenant_id] && settings[:client_id]
51
+
52
+ log_warn('Delegated token expired, opening browser for re-authentication...')
53
+
54
+ scopes = settings.dig(:delegated, :scopes) ||
55
+ Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth::DEFAULT_SCOPES
56
+ browser_auth = Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
57
+ tenant_id: settings[:tenant_id],
58
+ client_id: settings[:client_id],
59
+ scopes: scopes
60
+ )
61
+
62
+ result = browser_auth.authenticate
63
+ return false if result[:error]
64
+
65
+ body = result[:result]
66
+ cache.store_delegated_token(
67
+ access_token: body['access_token'],
68
+ refresh_token: body['refresh_token'],
69
+ expires_in: body['expires_in'],
70
+ scopes: scopes
71
+ )
72
+ cache.save_to_vault
73
+ log_info('Teams delegated auth restored via browser')
74
+ true
75
+ rescue StandardError => e
76
+ log_error("Browser re-auth failed: #{e.message}")
77
+ false
78
+ end
79
+
80
+ def teams_auth_settings
81
+ return {} unless defined?(Legion::Settings)
82
+
83
+ Legion::Settings.dig(:microsoft_teams, :auth) || {}
84
+ end
85
+
86
+ def log_info(msg)
87
+ Legion::Logging.info(msg) if defined?(Legion::Logging)
88
+ end
89
+
90
+ def log_warn(msg)
91
+ Legion::Logging.warn(msg) if defined?(Legion::Logging)
92
+ end
93
+
94
+ def log_debug(msg)
95
+ Legion::Logging.debug(msg) if defined?(Legion::Logging)
96
+ end
97
+
98
+ def log_error(msg)
99
+ Legion::Logging.error(msg) if defined?(Legion::Logging)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -29,11 +29,7 @@ module Legion
29
29
  end
30
30
 
31
31
  def args
32
- { since: @last_sync_time, skip_bots: true }.tap do |a|
33
- # After manual call returns, update high-water mark
34
- # This works because Base#manual calls runner_class.send(runner_function, **args)
35
- # and we update @last_sync_time in the overridden manual method
36
- end
32
+ { since: @last_sync_time, skip_bots: true }
37
33
  end
38
34
 
39
35
  def manual
@@ -50,13 +50,13 @@ module Legion
50
50
 
51
51
  def fetch_bot_chats(token:)
52
52
  conn = graph_connection(token: token)
53
- response = conn.get('/me/chats', { '$filter' => "chatType eq 'oneOnOne'", '$top' => 50 })
53
+ response = conn.get('me/chats', { '$filter' => "chatType eq 'oneOnOne'", '$top' => 50 })
54
54
  response.body&.dig('value') || []
55
55
  end
56
56
 
57
57
  def poll_chat(chat_id:, token:)
58
58
  conn = graph_connection(token: token)
59
- response = conn.get("/chats/#{chat_id}/messages",
59
+ response = conn.get("chats/#{chat_id}/messages",
60
60
  { '$top' => 10, '$orderby' => 'createdDateTime desc' })
61
61
  messages = response.body&.dig('value') || []
62
62
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MicrosoftTeams
6
- module Actors
6
+ module Actor
7
7
  class MessageProcessor < Legion::Extensions::Actors::Subscription
8
8
  def runner_class = 'Legion::Extensions::MicrosoftTeams::Runners::Bot'
9
9
  def runner_function = 'handle_message'
@@ -61,7 +61,7 @@ module Legion
61
61
 
62
62
  def poll_observed_chat(chat_id:, owner_id:, peer_name:, token:)
63
63
  conn = graph_connection(token: token)
64
- response = conn.get("/chats/#{chat_id}/messages",
64
+ response = conn.get("chats/#{chat_id}/messages",
65
65
  { '$top' => 10, '$orderby' => 'createdDateTime desc' })
66
66
  messages = response.body&.dig('value') || []
67
67
 
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MicrosoftTeams
6
+ module Actor
7
+ class TokenRefresher < Legion::Extensions::Actors::Every
8
+ DEFAULT_REFRESH_INTERVAL = 900
9
+
10
+ def runner_class = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache
11
+ def runner_function = 'cached_delegated_token'
12
+ def run_now? = false
13
+ def use_runner? = false
14
+ def check_subtask? = false
15
+ def generate_task? = false
16
+
17
+ def time
18
+ settings = teams_auth_settings
19
+ delegated = settings[:delegated]
20
+ return DEFAULT_REFRESH_INTERVAL unless delegated.is_a?(Hash)
21
+
22
+ delegated[:refresh_interval] || DEFAULT_REFRESH_INTERVAL
23
+ end
24
+
25
+ def enabled?
26
+ defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
27
+ rescue StandardError
28
+ false
29
+ end
30
+
31
+ def token_cache
32
+ @token_cache ||= Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
33
+ end
34
+
35
+ def manual
36
+ return unless token_cache.authenticated?
37
+
38
+ token = token_cache.cached_delegated_token
39
+ if token
40
+ token_cache.save_to_vault
41
+ elsif token_cache.previously_authenticated?
42
+ attempt_browser_reauth(token_cache)
43
+ end
44
+ rescue StandardError => e
45
+ log_error("TokenRefresher: #{e.message}")
46
+ end
47
+
48
+ private
49
+
50
+ def attempt_browser_reauth(cache)
51
+ settings = teams_auth_settings
52
+ return false unless settings[:tenant_id] && settings[:client_id]
53
+
54
+ log_warn('Delegated token expired, opening browser for re-authentication...')
55
+
56
+ scopes = settings.dig(:delegated, :scopes) ||
57
+ Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth::DEFAULT_SCOPES
58
+ browser_auth = Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
59
+ tenant_id: settings[:tenant_id],
60
+ client_id: settings[:client_id],
61
+ scopes: scopes
62
+ )
63
+
64
+ result = browser_auth.authenticate
65
+ return false if result[:error]
66
+
67
+ body = result[:result]
68
+ cache.store_delegated_token(
69
+ access_token: body['access_token'],
70
+ refresh_token: body['refresh_token'],
71
+ expires_in: body['expires_in'],
72
+ scopes: scopes
73
+ )
74
+ cache.save_to_vault
75
+ log_info('Teams delegated auth restored via browser')
76
+ true
77
+ rescue StandardError => e
78
+ log_error("Browser re-auth failed: #{e.message}")
79
+ false
80
+ end
81
+
82
+ def teams_auth_settings
83
+ return {} unless defined?(Legion::Settings)
84
+
85
+ Legion::Settings.dig(:microsoft_teams, :auth) || {}
86
+ end
87
+
88
+ def log_info(msg)
89
+ Legion::Logging.info(msg) if defined?(Legion::Logging)
90
+ end
91
+
92
+ def log_warn(msg)
93
+ Legion::Logging.warn(msg) if defined?(Legion::Logging)
94
+ end
95
+
96
+ def log_error(msg)
97
+ Legion::Logging.error(msg) if defined?(Legion::Logging)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -36,9 +36,10 @@ module Legion
36
36
 
37
37
  attr_reader :opts
38
38
 
39
- def initialize(tenant_id: nil, client_id: nil, client_secret: nil, token: nil, **extra)
39
+ def initialize(tenant_id: nil, client_id: nil, client_secret: nil, token: nil,
40
+ user_id: 'me', **extra)
40
41
  @opts = { tenant_id: tenant_id, client_id: client_id, client_secret: client_secret,
41
- token: token, **extra }
42
+ token: token, user_id: user_id, **extra }
42
43
  end
43
44
 
44
45
  def graph_connection(**override)
@@ -59,6 +60,8 @@ module Legion
59
60
  client_id: @opts[:client_id],
60
61
  client_secret: @opts[:client_secret]
61
62
  )
63
+ return result unless result&.dig(:result, 'access_token')
64
+
62
65
  @opts[:token] = result[:result]['access_token']
63
66
  result
64
67
  end
@@ -17,7 +17,7 @@ module Legion
17
17
 
18
18
  attr_reader :tenant_id, :client_id, :scopes
19
19
 
20
- def initialize(tenant_id:, client_id:, scopes: DEFAULT_SCOPES, auth: nil)
20
+ def initialize(tenant_id:, client_id:, scopes: DEFAULT_SCOPES, auth: nil, **)
21
21
  @tenant_id = tenant_id
22
22
  @client_id = client_id
23
23
  @scopes = scopes
@@ -75,8 +75,13 @@ module Legion
75
75
  client.close
76
76
  break if @result
77
77
  end
78
- rescue StandardError
79
- nil # server closed or unexpected error
78
+ rescue IOError
79
+ nil # server closed during shutdown
80
+ rescue StandardError => e
81
+ @mutex.synchronize do
82
+ @result ||= { error: e.message }
83
+ @cv.broadcast
84
+ end
80
85
  end
81
86
  end
82
87
  end
@@ -25,6 +25,10 @@ module Legion
25
25
  end
26
26
  end
27
27
 
28
+ def user_path(user_id = 'me')
29
+ user_id == 'me' ? 'me' : "users/#{user_id}"
30
+ end
31
+
28
32
  def oauth_connection(tenant_id: 'common', **_opts)
29
33
  Faraday.new(url: "https://login.microsoftonline.com/#{tenant_id}") do |conn|
30
34
  conn.request :url_encoded
@@ -41,11 +41,8 @@ module Legion
41
41
  end
42
42
  end
43
43
 
44
- def conversation_overrides(conversation_id: nil)
45
- return nil unless conversation_id
46
- return nil unless defined?(Legion::Extensions::Memory::Runners::Traces)
47
-
48
- nil # TODO: query lex-memory for conversation_config by conversation_id
44
+ def conversation_overrides(conversation_id: nil) # rubocop:disable Lint/UnusedMethodArgument
45
+ nil
49
46
  end
50
47
 
51
48
  def preference_instructions_for(owner_id:)
@@ -12,6 +12,7 @@ module Legion
12
12
  def initialize
13
13
  @subscriptions = {}
14
14
  @mutex = Mutex.new
15
+ load
15
16
  end
16
17
 
17
18
  def subscribe(owner_id:, chat_id:, peer_name:)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'time'
4
+ require 'json'
5
+ require 'fileutils'
4
6
  require 'legion/extensions/microsoft_teams/runners/auth'
5
7
 
6
8
  module Legion
@@ -10,6 +12,8 @@ module Legion
10
12
  class TokenCache
11
13
  REFRESH_BUFFER = 60
12
14
  DEFAULT_VAULT_PATH = 'legionio/microsoft_teams/delegated_token'
15
+ DEFAULT_LOCAL_DIR = File.join(Dir.home, '.legionio', 'tokens')
16
+ DEFAULT_LOCAL_FILE = File.join(DEFAULT_LOCAL_DIR, 'microsoft_teams.json')
13
17
 
14
18
  def initialize
15
19
  @token_cache = nil
@@ -58,11 +62,19 @@ module Legion
58
62
  @mutex.synchronize { @delegated_cache = nil }
59
63
  end
60
64
 
65
+ def authenticated?
66
+ @mutex.synchronize { !@delegated_cache.nil? }
67
+ end
68
+
69
+ def previously_authenticated?
70
+ File.exist?(local_token_path)
71
+ end
72
+
61
73
  def load_from_vault
62
- return false unless defined?(Legion::Crypt)
74
+ return load_from_local unless defined?(Legion::Crypt)
63
75
 
64
76
  data = Legion::Crypt.get(vault_path)
65
- return false unless data && data[:access_token]
77
+ return load_from_local unless data && data[:access_token]
66
78
 
67
79
  @mutex.synchronize do
68
80
  @delegated_cache = {
@@ -75,10 +87,12 @@ module Legion
75
87
  true
76
88
  rescue StandardError => e
77
89
  log_error("Failed to load delegated token from Vault: #{e.message}")
78
- false
90
+ load_from_local
79
91
  end
80
92
 
81
93
  def save_to_vault
94
+ save_to_local
95
+
82
96
  return false unless defined?(Legion::Crypt)
83
97
 
84
98
  data = @mutex.synchronize { @delegated_cache&.dup }
@@ -95,6 +109,47 @@ module Legion
95
109
  false
96
110
  end
97
111
 
112
+ def load_from_local
113
+ path = local_token_path
114
+ return false unless File.exist?(path)
115
+
116
+ raw = File.read(path)
117
+ data = ::JSON.parse(raw)
118
+ return false unless data['access_token'] && data['refresh_token']
119
+
120
+ @mutex.synchronize do
121
+ @delegated_cache = {
122
+ token: data['access_token'],
123
+ refresh_token: data['refresh_token'],
124
+ expires_at: Time.parse(data['expires_at']),
125
+ scopes: data['scopes']
126
+ }
127
+ end
128
+ true
129
+ rescue StandardError => e
130
+ log_error("Failed to load delegated token from local file: #{e.message}")
131
+ false
132
+ end
133
+
134
+ def save_to_local
135
+ data = @mutex.synchronize { @delegated_cache&.dup }
136
+ return false unless data
137
+
138
+ path = local_token_path
139
+ FileUtils.mkdir_p(File.dirname(path))
140
+ File.write(path, ::JSON.pretty_generate(
141
+ 'access_token' => data[:token],
142
+ 'refresh_token' => data[:refresh_token],
143
+ 'expires_at' => data[:expires_at].utc.iso8601,
144
+ 'scopes' => data[:scopes]
145
+ ))
146
+ File.chmod(0o600, path)
147
+ true
148
+ rescue StandardError => e
149
+ log_error("Failed to save delegated token to local file: #{e.message}")
150
+ false
151
+ end
152
+
98
153
  private
99
154
 
100
155
  def token_expired?(cache_entry)
@@ -120,6 +175,14 @@ module Legion
120
175
  delegated[:vault_path] || DEFAULT_VAULT_PATH
121
176
  end
122
177
 
178
+ def local_token_path
179
+ settings = teams_auth_settings
180
+ delegated = settings[:delegated]
181
+ return DEFAULT_LOCAL_FILE unless delegated.is_a?(Hash)
182
+
183
+ delegated[:local_token_path] || DEFAULT_LOCAL_FILE
184
+ end
185
+
123
186
  def refresh_app_token
124
187
  result = acquire_fresh_token
125
188
  return nil unless result
@@ -17,7 +17,7 @@ module Legion
17
17
  class Extractor
18
18
  Message = Struct.new(
19
19
  :content, # HTML message body
20
- :sender, # display name (e.g. "Iverson, Matthew D")
20
+ :sender, # display name (e.g. "Doe, Jane A")
21
21
  :sender_id, # orgid URI (e.g. "8:orgid:uuid")
22
22
  :message_type, # RichText/Html, RichText/Media_Card, Text
23
23
  :content_type, # Text
@@ -178,7 +178,7 @@ module Legion
178
178
  third = data.getbyte(pos + 2)
179
179
  return nil unless third
180
180
 
181
- actual_len = (len & 0x7F) | ((next_byte & 0x7F) << 7) | (third << 14)
181
+ actual_len = (len & 0x7F) | ((next_byte & 0x7F) << 7) | ((third & 0x7F) << 14)
182
182
  str_start = pos + 3
183
183
  end
184
184
  end
@@ -194,7 +194,7 @@ module Legion
194
194
 
195
195
  def send_chat_message_via_graph(chat_id:, text:, token: nil, **)
196
196
  conn = graph_connection(token: token)
197
- response = conn.post("/chats/#{chat_id}/messages", { body: { contentType: 'text', content: text } })
197
+ response = conn.post("chats/#{chat_id}/messages", { body: { contentType: 'text', content: text } })
198
198
  { result: response.body }
199
199
  end
200
200
 
@@ -384,13 +384,13 @@ module Legion
384
384
  nil
385
385
  end
386
386
 
387
- def find_chat_with_person(name:, token: nil)
387
+ def find_chat_with_person(name:, user_id: 'me', token: nil)
388
388
  conn = graph_connection(token: token)
389
- response = conn.get('/me/chats', { '$filter' => "chatType eq 'oneOnOne'", '$top' => 50 })
389
+ response = conn.get("#{user_path(user_id)}/chats", { '$filter' => "chatType eq 'oneOnOne'", '$top' => 50 })
390
390
  chats = response.body&.dig('value') || []
391
391
 
392
392
  chats.each do |chat|
393
- members_resp = conn.get("/chats/#{chat['id']}/members")
393
+ members_resp = conn.get("chats/#{chat['id']}/members")
394
394
  members = members_resp.body&.dig('value') || members_resp.body || []
395
395
  return { id: chat['id'] } if members.any? { |m| m['displayName']&.downcase&.include?(name.downcase) }
396
396
  end
@@ -7,15 +7,6 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module CacheIngest
10
- # Strip HTML tags from message content for clean memory traces.
11
- def strip_html(html)
12
- return '' if html.nil? || html.empty?
13
-
14
- html.gsub(/<[^>]+>/, ' ').gsub('&nbsp;', ' ').gsub('&amp;', '&')
15
- .gsub('&lt;', '<').gsub('&gt;', '>').gsub('&quot;', '"')
16
- .gsub(/\s+/, ' ').strip
17
- end
18
-
19
10
  # Ingest Teams messages from local cache into lex-memory traces.
20
11
  # Returns count of new traces stored and the latest compose_time seen.
21
12
  def ingest_cache(since: nil, skip_bots: true, db_path: nil, imprint_active: false, **)
@@ -60,6 +51,14 @@ module Legion
60
51
 
61
52
  private
62
53
 
54
+ def strip_html(html)
55
+ return '' if html.nil? || html.empty?
56
+
57
+ html.gsub(/<[^>]+>/, ' ').gsub('&nbsp;', ' ').gsub('&amp;', '&')
58
+ .gsub('&lt;', '<').gsub('&gt;', '>').gsub('&quot;', '"')
59
+ .gsub(/\s+/, ' ').strip
60
+ end
61
+
63
62
  def memory_available?
64
63
  defined?(Legion::Extensions::Memory::Runners::Traces)
65
64
  end
@@ -11,26 +11,26 @@ module Legion
11
11
 
12
12
  def list_channel_messages(team_id:, channel_id:, top: 50, **)
13
13
  params = { '$top' => top }
14
- response = graph_connection(**).get("/teams/#{team_id}/channels/#{channel_id}/messages", params)
14
+ response = graph_connection(**).get("teams/#{team_id}/channels/#{channel_id}/messages", params)
15
15
  { result: response.body }
16
16
  end
17
17
 
18
18
  def get_channel_message(team_id:, channel_id:, message_id:, **)
19
- response = graph_connection(**).get("/teams/#{team_id}/channels/#{channel_id}/messages/#{message_id}")
19
+ response = graph_connection(**).get("teams/#{team_id}/channels/#{channel_id}/messages/#{message_id}")
20
20
  { result: response.body }
21
21
  end
22
22
 
23
23
  def send_channel_message(team_id:, channel_id:, content:, content_type: 'text', attachments: [], **)
24
24
  payload = { body: { contentType: content_type, content: content } }
25
25
  payload[:attachments] = attachments unless attachments.empty?
26
- response = graph_connection(**).post("/teams/#{team_id}/channels/#{channel_id}/messages", payload)
26
+ response = graph_connection(**).post("teams/#{team_id}/channels/#{channel_id}/messages", payload)
27
27
  { result: response.body }
28
28
  end
29
29
 
30
30
  def reply_to_channel_message(team_id:, channel_id:, message_id:, content:, content_type: 'text', **)
31
31
  payload = { body: { contentType: content_type, content: content } }
32
32
  response = graph_connection(**).post(
33
- "/teams/#{team_id}/channels/#{channel_id}/messages/#{message_id}/replies", payload
33
+ "teams/#{team_id}/channels/#{channel_id}/messages/#{message_id}/replies", payload
34
34
  )
35
35
  { result: response.body }
36
36
  end
@@ -38,7 +38,7 @@ module Legion
38
38
  def list_channel_message_replies(team_id:, channel_id:, message_id:, top: 50, **)
39
39
  params = { '$top' => top }
40
40
  response = graph_connection(**).get(
41
- "/teams/#{team_id}/channels/#{channel_id}/messages/#{message_id}/replies", params
41
+ "teams/#{team_id}/channels/#{channel_id}/messages/#{message_id}/replies", params
42
42
  )
43
43
  { result: response.body }
44
44
  end
@@ -10,19 +10,19 @@ module Legion
10
10
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
11
 
12
12
  def list_channels(team_id:, **)
13
- response = graph_connection(**).get("/teams/#{team_id}/channels")
13
+ response = graph_connection(**).get("teams/#{team_id}/channels")
14
14
  { result: response.body }
15
15
  end
16
16
 
17
17
  def get_channel(team_id:, channel_id:, **)
18
- response = graph_connection(**).get("/teams/#{team_id}/channels/#{channel_id}")
18
+ response = graph_connection(**).get("teams/#{team_id}/channels/#{channel_id}")
19
19
  { result: response.body }
20
20
  end
21
21
 
22
22
  def create_channel(team_id:, display_name:, description: nil, membership_type: 'standard', **)
23
23
  payload = { displayName: display_name, membershipType: membership_type }
24
24
  payload[:description] = description if description
25
- response = graph_connection(**).post("/teams/#{team_id}/channels", payload)
25
+ response = graph_connection(**).post("teams/#{team_id}/channels", payload)
26
26
  { result: response.body }
27
27
  end
28
28
 
@@ -30,17 +30,17 @@ module Legion
30
30
  payload = {}
31
31
  payload[:displayName] = display_name if display_name
32
32
  payload[:description] = description if description
33
- response = graph_connection(**).patch("/teams/#{team_id}/channels/#{channel_id}", payload)
33
+ response = graph_connection(**).patch("teams/#{team_id}/channels/#{channel_id}", payload)
34
34
  { result: response.body }
35
35
  end
36
36
 
37
37
  def delete_channel(team_id:, channel_id:, **)
38
- response = graph_connection(**).delete("/teams/#{team_id}/channels/#{channel_id}")
38
+ response = graph_connection(**).delete("teams/#{team_id}/channels/#{channel_id}")
39
39
  { result: response.body }
40
40
  end
41
41
 
42
42
  def list_channel_members(team_id:, channel_id:, **)
43
- response = graph_connection(**).get("/teams/#{team_id}/channels/#{channel_id}/members")
43
+ response = graph_connection(**).get("teams/#{team_id}/channels/#{channel_id}/members")
44
44
  { result: response.body }
45
45
  end
46
46
 
@@ -11,24 +11,24 @@ module Legion
11
11
 
12
12
  def list_chats(user_id: 'me', top: 50, **)
13
13
  params = { '$top' => top }
14
- response = graph_connection(**).get("/#{user_id}/chats", params)
14
+ response = graph_connection(**).get("#{user_path(user_id)}/chats", params)
15
15
  { result: response.body }
16
16
  end
17
17
 
18
18
  def get_chat(chat_id:, **)
19
- response = graph_connection(**).get("/chats/#{chat_id}")
19
+ response = graph_connection(**).get("chats/#{chat_id}")
20
20
  { result: response.body }
21
21
  end
22
22
 
23
23
  def create_chat(members:, chat_type: 'oneOnOne', topic: nil, **)
24
24
  payload = { chatType: chat_type, members: members }
25
25
  payload[:topic] = topic if topic
26
- response = graph_connection(**).post('/chats', payload)
26
+ response = graph_connection(**).post('chats', payload)
27
27
  { result: response.body }
28
28
  end
29
29
 
30
30
  def list_chat_members(chat_id:, **)
31
- response = graph_connection(**).get("/chats/#{chat_id}/members")
31
+ response = graph_connection(**).get("chats/#{chat_id}/members")
32
32
  { result: response.body }
33
33
  end
34
34
 
@@ -38,7 +38,7 @@ module Legion
38
38
  'roles' => roles,
39
39
  'user@odata.bind' => "https://graph.microsoft.com/v1.0/users('#{user_id}')"
40
40
  }
41
- response = graph_connection(**).post("/chats/#{chat_id}/members", payload)
41
+ response = graph_connection(**).post("chats/#{chat_id}/members", payload)
42
42
  { result: response.body }
43
43
  end
44
44