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.
- checksums.yaml +4 -4
- data/.gitignore +2 -2
- data/CHANGELOG.md +36 -0
- data/CLAUDE.md +2 -2
- data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +1 -5
- data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/actors/message_processor.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/client.rb +5 -2
- data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +14 -6
- data/lib/legion/extensions/microsoft_teams/helpers/client.rb +4 -0
- data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +2 -5
- data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +1 -0
- data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +58 -3
- data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/runners/bot.rb +4 -4
- data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +8 -9
- data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +5 -5
- data/lib/legion/extensions/microsoft_teams/runners/channels.rb +6 -6
- data/lib/legion/extensions/microsoft_teams/runners/chats.rb +5 -5
- data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +16 -16
- data/lib/legion/extensions/microsoft_teams/runners/messages.rb +5 -5
- data/lib/legion/extensions/microsoft_teams/runners/presence.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +5 -5
- data/lib/legion/extensions/microsoft_teams/runners/teams.rb +3 -3
- data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +6 -6
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- metadata +1 -2
- data/Gemfile.lock +0 -103
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c23c0559f6761c3c19f3aea4a372e7bc4c07e0c271795cca4d8ac73a984cac4
|
|
4
|
+
data.tar.gz: affd2bdc5020080b320816044999b23403e463290cdabdbb625bc9bf831ffb8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2de35d93f2e6377c8eb1f078df5093c676c575fe1a65bc70ce454bd0ba94261821d771df585209d780624bb037f9a21c12a3bf729a2cff306b73628487994ab
|
|
7
|
+
data.tar.gz: 6e739d4ab5d13bd78230860dd62920e9921138c89062afa9f113158bf1a14764727981ec666b22bb62d2dd40ef911e9c85f4399f46a1398a27bf0d5f544dff77
|
data/.gitignore
CHANGED
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.
|
|
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.
|
|
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 }
|
|
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('
|
|
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("
|
|
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
|
|
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("
|
|
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,
|
|
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 '
|
|
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
|
-
|
|
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 =
|
|
63
|
+
params = URI.decode_www_form(query).to_h
|
|
61
64
|
|
|
62
65
|
@mutex.synchronize do
|
|
63
66
|
@result = {
|
|
64
|
-
code: params['code']
|
|
65
|
-
state: params['state']
|
|
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
|
-
|
|
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:)
|
|
@@ -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
|
|
66
|
+
return load_from_local unless defined?(Legion::Crypt)
|
|
63
67
|
|
|
64
68
|
data = Legion::Crypt.get(vault_path)
|
|
65
|
-
return
|
|
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
|
-
|
|
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. "
|
|
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("
|
|
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(
|
|
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("
|
|
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(' ', ' ').gsub('&', '&')
|
|
15
|
-
.gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
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(' ', ' ').gsub('&', '&')
|
|
58
|
+
.gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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('
|
|
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("
|
|
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("
|
|
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("
|
|
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(
|
|
18
|
-
response = graph_connection(**).get("
|
|
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(
|
|
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("
|
|
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(
|
|
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("
|
|
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(
|
|
42
|
-
response = graph_connection(**).delete("
|
|
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(
|
|
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("
|
|
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(
|
|
53
|
-
response = graph_connection(**).get("
|
|
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(
|
|
58
|
-
response = graph_connection(**).get("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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('
|
|
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("
|
|
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('
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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(
|
|
18
|
-
response = graph_connection(**).get("
|
|
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(
|
|
22
|
+
def get_transcript(meeting_id:, transcript_id:, user_id: 'me', **)
|
|
23
23
|
response = graph_connection(**).get(
|
|
24
|
-
"
|
|
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(
|
|
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
|
-
"
|
|
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
|
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.
|
|
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
|