lex-microsoft_teams 0.5.0 → 0.5.3

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -2
  3. data/CHANGELOG.md +36 -0
  4. data/CLAUDE.md +2 -2
  5. data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +1 -5
  6. data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +2 -2
  7. data/lib/legion/extensions/microsoft_teams/actors/message_processor.rb +1 -1
  8. data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +1 -1
  9. data/lib/legion/extensions/microsoft_teams/client.rb +5 -2
  10. data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +1 -1
  11. data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +14 -6
  12. data/lib/legion/extensions/microsoft_teams/helpers/client.rb +4 -0
  13. data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +2 -5
  14. data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +1 -0
  15. data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +58 -3
  16. data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +1 -1
  17. data/lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb +1 -1
  18. data/lib/legion/extensions/microsoft_teams/runners/bot.rb +4 -4
  19. data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +8 -9
  20. data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +5 -5
  21. data/lib/legion/extensions/microsoft_teams/runners/channels.rb +6 -6
  22. data/lib/legion/extensions/microsoft_teams/runners/chats.rb +5 -5
  23. data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +16 -16
  24. data/lib/legion/extensions/microsoft_teams/runners/messages.rb +5 -5
  25. data/lib/legion/extensions/microsoft_teams/runners/presence.rb +2 -2
  26. data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +5 -5
  27. data/lib/legion/extensions/microsoft_teams/runners/teams.rb +3 -3
  28. data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +6 -6
  29. data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
  30. metadata +1 -2
  31. data/Gemfile.lock +0 -103
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9f65eaac56b416e05eb5d96d914d6e3a88b86c91fdd083162fa09e5a6c887f4
4
- data.tar.gz: ba6cd5f7d0d1b08cc8bc880cd8ab0db61d799ba66afaca8ae1938df057149a16
3
+ metadata.gz: 6c23c0559f6761c3c19f3aea4a372e7bc4c07e0c271795cca4d8ac73a984cac4
4
+ data.tar.gz: affd2bdc5020080b320816044999b23403e463290cdabdbb625bc9bf831ffb8e
5
5
  SHA512:
6
- metadata.gz: e996a958fbffca134f301089fcc9c3be7abd03ef73f7b756f975198d3653dcbefaaa53a3ddf26c3afd33c92aea574f33336200604912088383e7ac6acdff54d5
7
- data.tar.gz: 800d036ddcb4a99ee833b3abb3ff255a60f1102625ed8ef1ec0ea9ad90c0e0195b654b8b14a0fff41835eb4288191c62fd0aaf03fcb8b58b087c752b1c32f20e
6
+ metadata.gz: a2de35d93f2e6377c8eb1f078df5093c676c575fe1a65bc70ce454bd0ba94261821d771df585209d780624bb037f9a21c12a3bf729a2cff306b73628487994ab
7
+ data.tar.gz: 6e739d4ab5d13bd78230860dd62920e9921138c89062afa9f113158bf1a14764727981ec666b22bb62d2dd40ef911e9c85f4399f46a1398a27bf0d5f544dff77
data/.gitignore CHANGED
@@ -10,6 +10,6 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
 
13
- # local credentials
13
+ # local credentials and test scripts
14
14
  .env
15
- test_device_code.rb
15
+ test_*.rb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.3] - 2026-03-19
4
+
5
+ ### Added
6
+ - `user_path` helper in `Helpers::Client` for Graph API `/me/` vs `/users/{id}/` flexibility
7
+ - `user_id: 'me'` default on all meeting, transcript, presence, chat, and team runner methods
8
+ - `user_id:` parameter on `Client` constructor for application-permission workflows
9
+
10
+ ### Fixed
11
+ - RecordParser 3-byte varint decoding: added missing `& 0x7F` mask on third byte
12
+ - MessageProcessor actor namespace: `Actors` to `Actor` for consistency with all other actors
13
+ - `Client#authenticate!` nil guard preventing `NoMethodError` on failed token acquisition
14
+ - CallbackServer error handling: separate `IOError` (shutdown) from unexpected errors
15
+ - SubscriptionRegistry now calls `load` on initialization to restore persisted subscriptions
16
+ - Device code polling: collapsed duplicate case branches for cleaner error handling
17
+
18
+ ### Removed
19
+ - Dead `transport.rb` file (never required by any code path)
20
+ - Dead `.tap` block in CacheSync `args` method
21
+ - Dead `conversation_overrides` TODO stub in PromptResolver (simplified to nil return)
22
+
23
+ ### Changed
24
+ - `strip_html` in CacheIngest moved from public to private
25
+ - Token cache spec cleanup: atomic file operations, `Process.pid` over `$$`
26
+
27
+ ## [0.5.2] - 2026-03-18
28
+
29
+ ### Fixed
30
+ - CallbackServer Ruby 4.0 compatibility: replaced `CGI.parse` with `URI.decode_www_form` (avoids extracted cgi gem dependency)
31
+ - CallbackServer header drain loop: fixed infinite loop when client disconnects before sending empty line
32
+ - Broadened rescue in listen thread to `StandardError` to prevent silent thread death
33
+
34
+ ## [0.5.1] - 2026-03-17
35
+
36
+ ### Added
37
+ - `Transport` module extending `Legion::Extensions::Transport` to provide the `build` method expected by LegionIO's `build_transport`
38
+
3
39
  ## [0.5.0] - 2026-03-16
4
40
 
5
41
  ### Added
data/CLAUDE.md CHANGED
@@ -10,7 +10,7 @@ Legion Extension that connects LegionIO to Microsoft Teams via Graph API and Bot
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-microsoft_teams
12
12
  **License**: MIT
13
- **Version**: 0.5.0
13
+ **Version**: 0.5.2
14
14
 
15
15
  ## Architecture
16
16
 
@@ -197,7 +197,7 @@ Optional framework dependencies (guarded with `defined?`, not in gemspec):
197
197
 
198
198
  ```bash
199
199
  bundle install
200
- bundle exec rspec # 185 specs (as of v0.5.0)
200
+ bundle exec rspec # 185 specs (as of v0.5.2)
201
201
  bundle exec rubocop # Clean
202
202
  ```
203
203
 
@@ -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
 
@@ -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'socket'
4
- require 'cgi'
4
+ require 'uri'
5
5
 
6
6
  module Legion
7
7
  module Extensions
@@ -53,16 +53,19 @@ module Legion
53
53
  client = @server.accept
54
54
  request_line = client.gets
55
55
  # drain headers
56
- nil until client.gets&.strip&.empty?
56
+ loop do
57
+ line = client.gets
58
+ break if line.nil? || line.strip.empty?
59
+ end
57
60
 
58
61
  if request_line&.include?('/callback?')
59
62
  query = request_line.split[1].split('?', 2).last
60
- params = CGI.parse(query)
63
+ params = URI.decode_www_form(query).to_h
61
64
 
62
65
  @mutex.synchronize do
63
66
  @result = {
64
- code: params['code']&.first,
65
- state: params['state']&.first
67
+ code: params['code'],
68
+ state: params['state']
66
69
  }
67
70
  @cv.broadcast
68
71
  end
@@ -73,7 +76,12 @@ module Legion
73
76
  break if @result
74
77
  end
75
78
  rescue IOError
76
- nil # server closed
79
+ nil # server closed during shutdown
80
+ rescue StandardError => e
81
+ @mutex.synchronize do
82
+ @result ||= { error: e.message }
83
+ @cv.broadcast
84
+ end
77
85
  end
78
86
  end
79
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
@@ -59,10 +63,10 @@ module Legion
59
63
  end
60
64
 
61
65
  def load_from_vault
62
- return false unless defined?(Legion::Crypt)
66
+ return load_from_local unless defined?(Legion::Crypt)
63
67
 
64
68
  data = Legion::Crypt.get(vault_path)
65
- return false unless data && data[:access_token]
69
+ return load_from_local unless data && data[:access_token]
66
70
 
67
71
  @mutex.synchronize do
68
72
  @delegated_cache = {
@@ -75,10 +79,12 @@ module Legion
75
79
  true
76
80
  rescue StandardError => e
77
81
  log_error("Failed to load delegated token from Vault: #{e.message}")
78
- false
82
+ load_from_local
79
83
  end
80
84
 
81
85
  def save_to_vault
86
+ save_to_local
87
+
82
88
  return false unless defined?(Legion::Crypt)
83
89
 
84
90
  data = @mutex.synchronize { @delegated_cache&.dup }
@@ -95,6 +101,47 @@ module Legion
95
101
  false
96
102
  end
97
103
 
104
+ def load_from_local
105
+ path = local_token_path
106
+ return false unless File.exist?(path)
107
+
108
+ raw = File.read(path)
109
+ data = ::JSON.parse(raw)
110
+ return false unless data['access_token'] && data['refresh_token']
111
+
112
+ @mutex.synchronize do
113
+ @delegated_cache = {
114
+ token: data['access_token'],
115
+ refresh_token: data['refresh_token'],
116
+ expires_at: Time.parse(data['expires_at']),
117
+ scopes: data['scopes']
118
+ }
119
+ end
120
+ true
121
+ rescue StandardError => e
122
+ log_error("Failed to load delegated token from local file: #{e.message}")
123
+ false
124
+ end
125
+
126
+ def save_to_local
127
+ data = @mutex.synchronize { @delegated_cache&.dup }
128
+ return false unless data
129
+
130
+ path = local_token_path
131
+ FileUtils.mkdir_p(File.dirname(path))
132
+ File.write(path, ::JSON.pretty_generate(
133
+ 'access_token' => data[:token],
134
+ 'refresh_token' => data[:refresh_token],
135
+ 'expires_at' => data[:expires_at].utc.iso8601,
136
+ 'scopes' => data[:scopes]
137
+ ))
138
+ File.chmod(0o600, path)
139
+ true
140
+ rescue StandardError => e
141
+ log_error("Failed to save delegated token to local file: #{e.message}")
142
+ false
143
+ end
144
+
98
145
  private
99
146
 
100
147
  def token_expired?(cache_entry)
@@ -120,6 +167,14 @@ module Legion
120
167
  delegated[:vault_path] || DEFAULT_VAULT_PATH
121
168
  end
122
169
 
170
+ def local_token_path
171
+ settings = teams_auth_settings
172
+ delegated = settings[:delegated]
173
+ return DEFAULT_LOCAL_FILE unless delegated.is_a?(Hash)
174
+
175
+ delegated[:local_token_path] || DEFAULT_LOCAL_FILE
176
+ end
177
+
123
178
  def refresh_app_token
124
179
  result = acquire_fresh_token
125
180
  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
 
@@ -9,53 +9,53 @@ module Legion
9
9
  module Meetings
10
10
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
11
 
12
- def list_meetings(user_id:, **)
13
- response = graph_connection(**).get("/users/#{user_id}/onlineMeetings")
12
+ def list_meetings(user_id: 'me', **)
13
+ response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings")
14
14
  { result: response.body }
15
15
  end
16
16
 
17
- def get_meeting(user_id:, meeting_id:, **)
18
- response = graph_connection(**).get("/users/#{user_id}/onlineMeetings/#{meeting_id}")
17
+ def get_meeting(meeting_id:, user_id: 'me', **)
18
+ response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}")
19
19
  { result: response.body }
20
20
  end
21
21
 
22
- def create_meeting(user_id:, subject:, start_time:, end_time:, **)
22
+ def create_meeting(subject:, start_time:, end_time:, user_id: 'me', **)
23
23
  payload = {
24
24
  subject: subject,
25
25
  startDateTime: start_time,
26
26
  endDateTime: end_time
27
27
  }
28
- response = graph_connection(**).post("/users/#{user_id}/onlineMeetings", payload)
28
+ response = graph_connection(**).post("#{user_path(user_id)}/onlineMeetings", payload)
29
29
  { result: response.body }
30
30
  end
31
31
 
32
- def update_meeting(user_id:, meeting_id:, subject: nil, start_time: nil, end_time: nil, **)
32
+ def update_meeting(meeting_id:, user_id: 'me', subject: nil, start_time: nil, end_time: nil, **)
33
33
  payload = {}
34
34
  payload[:subject] = subject if subject
35
35
  payload[:startDateTime] = start_time if start_time
36
36
  payload[:endDateTime] = end_time if end_time
37
- response = graph_connection(**).patch("/users/#{user_id}/onlineMeetings/#{meeting_id}", payload)
37
+ response = graph_connection(**).patch("#{user_path(user_id)}/onlineMeetings/#{meeting_id}", payload)
38
38
  { result: response.body }
39
39
  end
40
40
 
41
- def delete_meeting(user_id:, meeting_id:, **)
42
- response = graph_connection(**).delete("/users/#{user_id}/onlineMeetings/#{meeting_id}")
41
+ def delete_meeting(meeting_id:, user_id: 'me', **)
42
+ response = graph_connection(**).delete("#{user_path(user_id)}/onlineMeetings/#{meeting_id}")
43
43
  { result: response.body }
44
44
  end
45
45
 
46
- def get_meeting_by_join_url(user_id:, join_url:, **)
46
+ def get_meeting_by_join_url(join_url:, user_id: 'me', **)
47
47
  params = { '$filter' => "joinWebUrl eq '#{join_url.gsub("'", "''")}'" }
48
- response = graph_connection(**).get("/users/#{user_id}/onlineMeetings", params)
48
+ response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings", params)
49
49
  { result: response.body }
50
50
  end
51
51
 
52
- def list_attendance_reports(user_id:, meeting_id:, **)
53
- response = graph_connection(**).get("/users/#{user_id}/onlineMeetings/#{meeting_id}/attendanceReports")
52
+ def list_attendance_reports(meeting_id:, user_id: 'me', **)
53
+ response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/attendanceReports")
54
54
  { result: response.body }
55
55
  end
56
56
 
57
- def get_attendance_report(user_id:, meeting_id:, report_id:, **)
58
- response = graph_connection(**).get("/users/#{user_id}/onlineMeetings/#{meeting_id}/attendanceReports/#{report_id}")
57
+ def get_attendance_report(meeting_id:, report_id:, user_id: 'me', **)
58
+ response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/attendanceReports/#{report_id}")
59
59
  { result: response.body }
60
60
  end
61
61
 
@@ -11,31 +11,31 @@ module Legion
11
11
 
12
12
  def list_chat_messages(chat_id:, top: 50, **)
13
13
  params = { '$top' => top }
14
- response = graph_connection(**).get("/chats/#{chat_id}/messages", params)
14
+ response = graph_connection(**).get("chats/#{chat_id}/messages", params)
15
15
  { result: response.body }
16
16
  end
17
17
 
18
18
  def get_chat_message(chat_id:, message_id:, **)
19
- response = graph_connection(**).get("/chats/#{chat_id}/messages/#{message_id}")
19
+ response = graph_connection(**).get("chats/#{chat_id}/messages/#{message_id}")
20
20
  { result: response.body }
21
21
  end
22
22
 
23
23
  def send_chat_message(chat_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("/chats/#{chat_id}/messages", payload)
26
+ response = graph_connection(**).post("chats/#{chat_id}/messages", payload)
27
27
  { result: response.body }
28
28
  end
29
29
 
30
30
  def reply_to_chat_message(chat_id:, message_id:, content:, content_type: 'text', **)
31
31
  payload = { body: { contentType: content_type, content: content } }
32
- response = graph_connection(**).post("/chats/#{chat_id}/messages/#{message_id}/replies", payload)
32
+ response = graph_connection(**).post("chats/#{chat_id}/messages/#{message_id}/replies", payload)
33
33
  { result: response.body }
34
34
  end
35
35
 
36
36
  def list_message_replies(chat_id:, message_id:, top: 50, **)
37
37
  params = { '$top' => top }
38
- response = graph_connection(**).get("/chats/#{chat_id}/messages/#{message_id}/replies", params)
38
+ response = graph_connection(**).get("chats/#{chat_id}/messages/#{message_id}/replies", params)
39
39
  { result: response.body }
40
40
  end
41
41
 
@@ -9,9 +9,9 @@ module Legion
9
9
  module Presence
10
10
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
11
 
12
- def get_presence(user_id:, **)
12
+ def get_presence(user_id: 'me', **)
13
13
  conn = graph_connection(**)
14
- response = conn.get("/users/#{user_id}/presence")
14
+ response = conn.get("#{user_path(user_id)}/presence")
15
15
  body = response.body || {}
16
16
  {
17
17
  availability: body['availability'],
@@ -10,12 +10,12 @@ module Legion
10
10
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
11
 
12
12
  def list_subscriptions(**)
13
- response = graph_connection(**).get('/subscriptions')
13
+ response = graph_connection(**).get('subscriptions')
14
14
  { result: response.body }
15
15
  end
16
16
 
17
17
  def get_subscription(subscription_id:, **)
18
- response = graph_connection(**).get("/subscriptions/#{subscription_id}")
18
+ response = graph_connection(**).get("subscriptions/#{subscription_id}")
19
19
  { result: response.body }
20
20
  end
21
21
 
@@ -29,18 +29,18 @@ module Legion
29
29
  includeResourceData: include_resource_data
30
30
  }
31
31
  payload[:clientState] = client_state if client_state
32
- response = graph_connection(**).post('/subscriptions', payload)
32
+ response = graph_connection(**).post('subscriptions', payload)
33
33
  { result: response.body }
34
34
  end
35
35
 
36
36
  def renew_subscription(subscription_id:, expiration:, **)
37
37
  payload = { expirationDateTime: expiration }
38
- response = graph_connection(**).patch("/subscriptions/#{subscription_id}", payload)
38
+ response = graph_connection(**).patch("subscriptions/#{subscription_id}", payload)
39
39
  { result: response.body }
40
40
  end
41
41
 
42
42
  def delete_subscription(subscription_id:, **)
43
- response = graph_connection(**).delete("/subscriptions/#{subscription_id}")
43
+ response = graph_connection(**).delete("subscriptions/#{subscription_id}")
44
44
  { result: response.body }
45
45
  end
46
46
 
@@ -10,17 +10,17 @@ module Legion
10
10
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
11
 
12
12
  def list_joined_teams(user_id: 'me', **)
13
- response = graph_connection(**).get("/#{user_id}/joinedTeams")
13
+ response = graph_connection(**).get("#{user_path(user_id)}/joinedTeams")
14
14
  { result: response.body }
15
15
  end
16
16
 
17
17
  def get_team(team_id:, **)
18
- response = graph_connection(**).get("/teams/#{team_id}")
18
+ response = graph_connection(**).get("teams/#{team_id}")
19
19
  { result: response.body }
20
20
  end
21
21
 
22
22
  def list_team_members(team_id:, **)
23
- response = graph_connection(**).get("/teams/#{team_id}/members")
23
+ response = graph_connection(**).get("teams/#{team_id}/members")
24
24
  { result: response.body }
25
25
  end
26
26
 
@@ -14,22 +14,22 @@ module Legion
14
14
  docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
15
15
  }.freeze
16
16
 
17
- def list_transcripts(user_id:, meeting_id:, **)
18
- response = graph_connection(**).get("/users/#{user_id}/onlineMeetings/#{meeting_id}/transcripts")
17
+ def list_transcripts(meeting_id:, user_id: 'me', **)
18
+ response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/transcripts")
19
19
  { result: response.body }
20
20
  end
21
21
 
22
- def get_transcript(user_id:, meeting_id:, transcript_id:, **)
22
+ def get_transcript(meeting_id:, transcript_id:, user_id: 'me', **)
23
23
  response = graph_connection(**).get(
24
- "/users/#{user_id}/onlineMeetings/#{meeting_id}/transcripts/#{transcript_id}"
24
+ "#{user_path(user_id)}/onlineMeetings/#{meeting_id}/transcripts/#{transcript_id}"
25
25
  )
26
26
  { result: response.body }
27
27
  end
28
28
 
29
- def get_transcript_content(user_id:, meeting_id:, transcript_id:, format: :vtt, **)
29
+ def get_transcript_content(meeting_id:, transcript_id:, user_id: 'me', format: :vtt, **)
30
30
  accept = CONTENT_TYPES.fetch(format)
31
31
  response = graph_connection(**).get(
32
- "/users/#{user_id}/onlineMeetings/#{meeting_id}/transcripts/#{transcript_id}/content"
32
+ "#{user_path(user_id)}/onlineMeetings/#{meeting_id}/transcripts/#{transcript_id}/content"
33
33
  ) do |req|
34
34
  req.headers['Accept'] = accept
35
35
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MicrosoftTeams
6
- VERSION = '0.5.0'
6
+ VERSION = '0.5.3'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-microsoft_teams
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -66,7 +66,6 @@ files:
66
66
  - CLAUDE.md
67
67
  - Dockerfile
68
68
  - Gemfile
69
- - Gemfile.lock
70
69
  - LICENSE
71
70
  - README.md
72
71
  - docs/plans/2026-03-15-meetings-transcripts-design.md
data/Gemfile.lock DELETED
@@ -1,103 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- lex-microsoft_teams (0.5.0)
5
- base64 (>= 0.1)
6
- faraday (>= 2.0)
7
- snappy (>= 0.5)
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- addressable (2.8.9)
13
- public_suffix (>= 2.0.2, < 8.0)
14
- ast (2.4.3)
15
- base64 (0.3.0)
16
- bigdecimal (4.0.1)
17
- diff-lcs (1.6.2)
18
- docile (1.4.1)
19
- faraday (2.14.1)
20
- faraday-net_http (>= 2.0, < 3.5)
21
- json
22
- logger
23
- faraday-net_http (3.4.2)
24
- net-http (~> 0.5)
25
- json (2.19.1)
26
- json-schema (6.2.0)
27
- addressable (~> 2.8)
28
- bigdecimal (>= 3.1, < 5)
29
- language_server-protocol (3.17.0.5)
30
- lint_roller (1.1.0)
31
- logger (1.7.0)
32
- mcp (0.8.0)
33
- json-schema (>= 4.1)
34
- net-http (0.9.1)
35
- uri (>= 0.11.1)
36
- parallel (1.27.0)
37
- parser (3.3.10.2)
38
- ast (~> 2.4.1)
39
- racc
40
- prism (1.9.0)
41
- public_suffix (7.0.5)
42
- racc (1.8.1)
43
- rainbow (3.1.1)
44
- rake (13.3.1)
45
- regexp_parser (2.11.3)
46
- rspec (3.13.2)
47
- rspec-core (~> 3.13.0)
48
- rspec-expectations (~> 3.13.0)
49
- rspec-mocks (~> 3.13.0)
50
- rspec-core (3.13.6)
51
- rspec-support (~> 3.13.0)
52
- rspec-expectations (3.13.5)
53
- diff-lcs (>= 1.2.0, < 2.0)
54
- rspec-support (~> 3.13.0)
55
- rspec-mocks (3.13.8)
56
- diff-lcs (>= 1.2.0, < 2.0)
57
- rspec-support (~> 3.13.0)
58
- rspec-support (3.13.7)
59
- rspec_junit_formatter (0.6.0)
60
- rspec-core (>= 2, < 4, != 2.12.0)
61
- rubocop (1.85.1)
62
- json (~> 2.3)
63
- language_server-protocol (~> 3.17.0.2)
64
- lint_roller (~> 1.1.0)
65
- mcp (~> 0.6)
66
- parallel (~> 1.10)
67
- parser (>= 3.3.0.2)
68
- rainbow (>= 2.2.2, < 4.0)
69
- regexp_parser (>= 2.9.3, < 3.0)
70
- rubocop-ast (>= 1.49.0, < 2.0)
71
- ruby-progressbar (~> 1.7)
72
- unicode-display_width (>= 2.4.0, < 4.0)
73
- rubocop-ast (1.49.1)
74
- parser (>= 3.3.7.2)
75
- prism (~> 1.7)
76
- ruby-progressbar (1.13.0)
77
- simplecov (0.22.0)
78
- docile (~> 1.1)
79
- simplecov-html (~> 0.11)
80
- simplecov_json_formatter (~> 0.1)
81
- simplecov-html (0.13.2)
82
- simplecov_json_formatter (0.1.4)
83
- snappy (0.5.0)
84
- unicode-display_width (3.2.0)
85
- unicode-emoji (~> 4.1)
86
- unicode-emoji (4.2.0)
87
- uri (1.1.1)
88
-
89
- PLATFORMS
90
- arm64-darwin-25
91
- ruby
92
- x86_64-linux
93
-
94
- DEPENDENCIES
95
- lex-microsoft_teams!
96
- rake
97
- rspec
98
- rspec_junit_formatter
99
- rubocop
100
- simplecov
101
-
102
- BUNDLED WITH
103
- 2.6.9