lex-microsoft_teams 0.6.20 → 0.6.24
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/CHANGELOG.md +33 -0
- data/CLAUDE.md +1 -1
- data/lib/legion/extensions/microsoft_teams/absorbers/meeting.rb +168 -0
- data/lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb +60 -0
- data/lib/legion/extensions/microsoft_teams/helpers/graph_client.rb +75 -0
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +7 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89010807fc32d72ba6fcefdd2239508ae1d384a14162265d3f97b6098d3114ea
|
|
4
|
+
data.tar.gz: 9b7909be2dfafd3242fb8407acfa2f0cf0d81365bb4d9606e475e0a40061ffa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32d5556151443500353755e7584ca526f778ebff2bdfeb19cd76ef53fc0f660e4ffc4b4ba6468834e06e5841edfc9b3fd3203f36acf03dc2436e40a82d0bf167
|
|
7
|
+
data.tar.gz: 5f107a3b5ea8cc0afdd662bf170fc06c74b4ba982e966d57b9d876b0e39bc3f422c43b36dc1f3ede9c502acf914f23f83bf7861ddf10f367df09941032832ae6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.6.24] - 2026-03-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `Actors::AbsorbMeeting` — Subscription actor that listens on `lex.microsoft_teams.absorbers.meeting.absorb` and delegates to `Absorbers::Meeting#absorb`
|
|
9
|
+
- `Helpers::GraphClient` — mixin module wrapping `Helpers::Client#graph_connection` with `graph_get`, `graph_post`, `graph_paginate`, and an inline `GraphError` class for responses other than 200, 201, 204, or 404; 401/403 raise with descriptive messages including the Graph error body when available
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `Absorbers::Meeting#graph_token` — rescue now captures the exception as `=> e` and logs a warning, satisfying the rescue-logging lint rule
|
|
13
|
+
|
|
14
|
+
## [0.6.23] - 2026-03-27
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `Absorbers::Meeting` — all Graph API runner calls now pass `token: graph_token` so requests carry an `Authorization` header in production. `graph_token` resolves from `Helpers::TokenCache.instance.cached_graph_token` when available, falling back to `nil` (unauthenticated) with a rescued `StandardError` to prevent test-environment boot failures
|
|
18
|
+
- `CLAUDE.md` — version field updated to 0.6.23
|
|
19
|
+
|
|
20
|
+
## [0.6.22] - 2026-03-27
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `Absorbers::Meeting#handle` now fails fast with `{ success: false, error: 'meeting has no id' }` when the resolved meeting item has no `id` field, preventing subsequent runner calls from building invalid URLs
|
|
24
|
+
- `spec/legion/extensions/microsoft_teams/absorbers/meeting_spec.rb` — added spec covering the blank `meeting_id` guard path
|
|
25
|
+
|
|
26
|
+
## [0.6.21] - 2026-03-27
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `Absorbers::Meeting` — reference implementation of the absorber framework for Teams meetings. Resolves a Teams join URL to a meeting via Graph API, then ingests transcripts (VTT), AI insights, and participant lists into Apollo knowledge store. Two URL patterns registered: `teams.microsoft.com/l/meetup-join/*` and `*.teams.microsoft.com/meet/*`. Guard on `Legion::Extensions::Absorbers` ensures the absorber only loads when the framework base class is available.
|
|
30
|
+
- `spec/spec_helper.rb` — inline stubs for `Legion::Extensions::Absorbers::Base` and `Matchers::Url` so absorber specs run without the full `legionio` gem in the test environment
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- `lib/legion/extensions/microsoft_teams/absorbers/meeting.rb` — runner calls now go through `meetings_runner`, `transcripts_runner`, and `ai_insights_runner` instance accessors (`Object.new.extend(Runners::*)`) instead of calling runner modules directly as class methods, which would raise `NoMethodError` at runtime
|
|
34
|
+
- `spec/legion/extensions/microsoft_teams/absorbers/meeting_spec.rb` — specs stub runner instances via `absorber.meetings_runner` / `absorber.transcripts_runner` / `absorber.ai_insights_runner` rather than the module constants; `.patterns` spec no longer relies on `patterns.first` ordering; now asserts both expected pattern values are present in the set
|
|
35
|
+
|
|
3
36
|
## [0.6.19] - 2026-03-26
|
|
4
37
|
|
|
5
38
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Absorbers
|
|
7
|
+
class Meeting < Legion::Extensions::Absorbers::Base
|
|
8
|
+
pattern :url, 'teams.microsoft.com/l/meetup-join/*'
|
|
9
|
+
pattern :url, '*.teams.microsoft.com/meet/*'
|
|
10
|
+
description 'Absorbs Teams meeting transcripts, AI insights, and participants into Apollo'
|
|
11
|
+
|
|
12
|
+
def handle(url: nil, content: nil, metadata: {}, context: {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
+
report_progress(message: 'resolving meeting from link')
|
|
14
|
+
meeting = resolve_meeting(url)
|
|
15
|
+
return { success: false, error: 'could not resolve meeting' } unless meeting
|
|
16
|
+
|
|
17
|
+
subject = meeting['subject'] || meeting[:subject] || 'untitled meeting'
|
|
18
|
+
meeting_id = meeting['id'] || meeting[:id]
|
|
19
|
+
return { success: false, error: 'meeting has no id' } if meeting_id.nil? || meeting_id.to_s.empty?
|
|
20
|
+
|
|
21
|
+
results = { meeting_id: meeting_id, subject: subject, chunks: 0 }
|
|
22
|
+
|
|
23
|
+
ingest_transcript(meeting_id, subject, results)
|
|
24
|
+
ingest_ai_insights(meeting_id, subject, results)
|
|
25
|
+
ingest_participants(meeting, subject, results)
|
|
26
|
+
|
|
27
|
+
report_progress(message: 'done', percent: 100)
|
|
28
|
+
results.merge(success: true)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
log.error("Meeting absorber failed: #{e.message}")
|
|
31
|
+
{ success: false, error: e.message }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def meetings_runner
|
|
37
|
+
@meetings_runner ||= Object.new.extend(Runners::Meetings)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def transcripts_runner
|
|
41
|
+
@transcripts_runner ||= Object.new.extend(Runners::Transcripts)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ai_insights_runner
|
|
45
|
+
@ai_insights_runner ||= Object.new.extend(Runners::AiInsights)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def graph_token
|
|
49
|
+
return @graph_token if defined?(@graph_token)
|
|
50
|
+
|
|
51
|
+
@graph_token = begin
|
|
52
|
+
Helpers::TokenCache.instance.cached_graph_token if defined?(Helpers::TokenCache)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
log.warn("graph_token unavailable: #{e.message}")
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_meeting(url)
|
|
60
|
+
report_progress(message: 'looking up meeting by join URL', percent: 5)
|
|
61
|
+
response = meetings_runner.get_meeting_by_join_url(join_url: url, token: graph_token)
|
|
62
|
+
return nil unless response.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
body = response[:result]
|
|
65
|
+
return nil unless body.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
items = body['value'] || body[:value]
|
|
68
|
+
return nil unless items.is_a?(Array) && !items.empty?
|
|
69
|
+
|
|
70
|
+
items.first
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
log.warn("Could not resolve meeting: #{e.message}")
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ingest_transcript(meeting_id, subject, results)
|
|
77
|
+
report_progress(message: 'fetching transcripts', percent: 20)
|
|
78
|
+
transcripts_response = transcripts_runner.list_transcripts(meeting_id: meeting_id, token: graph_token)
|
|
79
|
+
transcripts_body = transcripts_response.is_a?(Hash) ? transcripts_response[:result] : nil
|
|
80
|
+
return unless transcripts_body.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
transcript_items = transcripts_body['value'] || transcripts_body[:value]
|
|
83
|
+
return unless transcript_items.is_a?(Array) && transcript_items.any?
|
|
84
|
+
|
|
85
|
+
transcript_items.each do |t|
|
|
86
|
+
transcript_id = t['id'] || t[:id]
|
|
87
|
+
next unless transcript_id
|
|
88
|
+
|
|
89
|
+
report_progress(message: "pulling transcript #{transcript_id}", percent: 40)
|
|
90
|
+
vtt_result = transcripts_runner.get_transcript_content(
|
|
91
|
+
meeting_id: meeting_id, transcript_id: transcript_id, format: :vtt, token: graph_token
|
|
92
|
+
)
|
|
93
|
+
vtt = vtt_result.is_a?(Hash) ? vtt_result[:result] : vtt_result
|
|
94
|
+
next unless vtt.is_a?(String) && !vtt.empty?
|
|
95
|
+
|
|
96
|
+
absorb_to_knowledge(
|
|
97
|
+
content: vtt,
|
|
98
|
+
tags: ['meeting', 'transcript', subject],
|
|
99
|
+
source_file: "teams://meetings/#{meeting_id}/transcripts/#{transcript_id}",
|
|
100
|
+
heading: "Transcript: #{subject}",
|
|
101
|
+
content_type: 'meeting_transcript'
|
|
102
|
+
)
|
|
103
|
+
results[:chunks] += 1
|
|
104
|
+
end
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
log.warn("Transcript ingest failed: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ingest_ai_insights(meeting_id, subject, results)
|
|
110
|
+
report_progress(message: 'fetching AI insights', percent: 60)
|
|
111
|
+
insights = ai_insights_runner.list_meeting_ai_insights(meeting_id: meeting_id, token: graph_token)
|
|
112
|
+
return unless insights.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
body = insights[:result] || insights
|
|
115
|
+
items = body.is_a?(Hash) ? (body['value'] || body[:value]) : nil
|
|
116
|
+
return unless items.is_a?(Array) && items.any?
|
|
117
|
+
|
|
118
|
+
items.each { |item| absorb_insight_item(item, meeting_id, subject, results) }
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
log.warn("AI insights ingest failed: #{e.message}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def absorb_insight_item(item, meeting_id, subject, results)
|
|
124
|
+
return unless item.is_a?(Hash)
|
|
125
|
+
|
|
126
|
+
insight_id = item['id'] || item[:id]
|
|
127
|
+
action_items = item['actionItems'] || item[:actionItems] || []
|
|
128
|
+
return if action_items.empty?
|
|
129
|
+
|
|
130
|
+
content = action_items.filter_map { |a| a.is_a?(Hash) ? (a['text'] || a[:text]) : a.to_s }.join("\n")
|
|
131
|
+
return if content.empty?
|
|
132
|
+
|
|
133
|
+
absorb_to_knowledge(
|
|
134
|
+
content: content,
|
|
135
|
+
tags: ['meeting', 'ai-insight', 'action-item', subject],
|
|
136
|
+
source_file: "teams://meetings/#{meeting_id}/insights/#{insight_id}",
|
|
137
|
+
heading: "AI Insight: #{subject}",
|
|
138
|
+
content_type: 'meeting_insight'
|
|
139
|
+
)
|
|
140
|
+
results[:chunks] += 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def ingest_participants(meeting, subject, results)
|
|
144
|
+
report_progress(message: 'recording participants', percent: 80)
|
|
145
|
+
participants = meeting.dig('participants', 'attendees') || meeting.dig(:participants, :attendees)
|
|
146
|
+
return unless participants.is_a?(Array) && participants.any?
|
|
147
|
+
|
|
148
|
+
names = participants.filter_map do |p|
|
|
149
|
+
p.dig('identity', 'user', 'displayName') || p.dig(:identity, :user, :displayName)
|
|
150
|
+
end
|
|
151
|
+
return if names.empty?
|
|
152
|
+
|
|
153
|
+
meeting_id = meeting['id'] || meeting[:id]
|
|
154
|
+
absorb_raw(
|
|
155
|
+
content: "Meeting participants for '#{subject}': #{names.join(', ')}",
|
|
156
|
+
tags: ['meeting', 'participants', subject],
|
|
157
|
+
content_type: 'meeting_participants',
|
|
158
|
+
metadata: { meeting_id: meeting_id, participant_count: names.length }
|
|
159
|
+
)
|
|
160
|
+
results[:chunks] += 1
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
log.warn("Participant ingest failed: #{e.message}")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Actor
|
|
7
|
+
class AbsorbMeeting < Legion::Extensions::Actors::Subscription
|
|
8
|
+
def runner_class = 'Legion::Extensions::MicrosoftTeams::Absorbers::Meeting'
|
|
9
|
+
def runner_function = 'absorb'
|
|
10
|
+
def check_subtask? = false
|
|
11
|
+
def generate_task? = false
|
|
12
|
+
|
|
13
|
+
def enabled?
|
|
14
|
+
defined?(Legion::Extensions::Absorbers::Base) &&
|
|
15
|
+
defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Meeting)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
log.debug("AbsorbMeeting#enabled?: #{e.message}")
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def work(payload)
|
|
22
|
+
parsed = parse_payload(payload)
|
|
23
|
+
absorber = Absorbers::Meeting.new
|
|
24
|
+
result = absorber.absorb(
|
|
25
|
+
url: parsed[:url],
|
|
26
|
+
metadata: parsed[:metadata] || {},
|
|
27
|
+
context: parsed[:context] || {}
|
|
28
|
+
)
|
|
29
|
+
if result.respond_to?(:[]) && result.key?(:success)
|
|
30
|
+
if result[:success]
|
|
31
|
+
ack!
|
|
32
|
+
else
|
|
33
|
+
log.error("AbsorbMeeting actor absorb failed: #{result.inspect}")
|
|
34
|
+
reject!(requeue: false)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
ack!
|
|
38
|
+
end
|
|
39
|
+
result
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
log.error("AbsorbMeeting actor error: #{e.message}")
|
|
42
|
+
reject!(requeue: false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def parse_payload(payload)
|
|
48
|
+
data = payload.is_a?(String) ? json_load(payload) : payload
|
|
49
|
+
return {} unless data.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
data.transform_keys(&:to_sym)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
log.debug("AbsorbMeeting#parse_payload: #{e.message}")
|
|
54
|
+
{}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Helpers
|
|
7
|
+
module GraphClient
|
|
8
|
+
class GraphError < StandardError; end
|
|
9
|
+
|
|
10
|
+
def graph_get(path, token:, params: {})
|
|
11
|
+
connection = graph_connection(token: token)
|
|
12
|
+
response = connection.get(path, params)
|
|
13
|
+
handle_graph_response(response, path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def graph_paginate(path, token:, params: {}, max_pages: 10)
|
|
17
|
+
results = []
|
|
18
|
+
next_link = nil
|
|
19
|
+
page = 0
|
|
20
|
+
|
|
21
|
+
loop do
|
|
22
|
+
current_path = next_link || path
|
|
23
|
+
data = graph_get(current_path, token: token, params: page.zero? ? params : {})
|
|
24
|
+
break if data.nil?
|
|
25
|
+
|
|
26
|
+
items = data['value'] || data[:value]
|
|
27
|
+
results.concat(Array(items)) if items
|
|
28
|
+
|
|
29
|
+
next_link = data['@odata.nextLink'] || data[:'@odata.nextLink']
|
|
30
|
+
page += 1
|
|
31
|
+
break if next_link.nil? || page >= max_pages
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
results
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def graph_post(path, token:, body: {})
|
|
38
|
+
connection = graph_connection(token: token)
|
|
39
|
+
response = connection.post(path) do |req|
|
|
40
|
+
req.body = body
|
|
41
|
+
end
|
|
42
|
+
handle_graph_response(response, path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def handle_graph_response(response, path)
|
|
48
|
+
error_message =
|
|
49
|
+
if response.body.respond_to?(:dig)
|
|
50
|
+
response.body.dig('error', 'message') ||
|
|
51
|
+
response.body.dig(:error, :message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
case response.status
|
|
55
|
+
when 200, 201
|
|
56
|
+
response.body
|
|
57
|
+
when 204, 404
|
|
58
|
+
nil
|
|
59
|
+
when 401
|
|
60
|
+
detail = error_message || 'Access token is missing, expired, or invalid.'
|
|
61
|
+
raise GraphError, "Graph API 401 Unauthorized on #{path}: #{detail}"
|
|
62
|
+
when 403
|
|
63
|
+
detail = error_message || 'Caller does not have sufficient permissions to perform this action.'
|
|
64
|
+
raise GraphError, "Graph API 403 Forbidden on #{path}: #{detail}"
|
|
65
|
+
else
|
|
66
|
+
base_message = "Graph API #{response.status} on #{path}"
|
|
67
|
+
base_message = "#{base_message}: #{error_message}" if error_message
|
|
68
|
+
raise GraphError, base_message
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -33,6 +33,7 @@ require 'legion/extensions/microsoft_teams/helpers/callback_server'
|
|
|
33
33
|
require 'legion/extensions/microsoft_teams/helpers/browser_auth'
|
|
34
34
|
require 'legion/extensions/microsoft_teams/helpers/permission_guard'
|
|
35
35
|
require 'legion/extensions/microsoft_teams/helpers/transform_definitions'
|
|
36
|
+
require 'legion/extensions/microsoft_teams/helpers/graph_client'
|
|
36
37
|
|
|
37
38
|
# Transport
|
|
38
39
|
if defined?(Legion::Transport)
|
|
@@ -43,6 +44,12 @@ end
|
|
|
43
44
|
|
|
44
45
|
require 'legion/extensions/microsoft_teams/client'
|
|
45
46
|
|
|
47
|
+
if defined?(Legion::Extensions) &&
|
|
48
|
+
Legion::Extensions.const_defined?(:Absorbers, false) &&
|
|
49
|
+
Legion::Extensions::Absorbers.const_defined?(:Base, false)
|
|
50
|
+
require_relative 'microsoft_teams/absorbers/meeting'
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
module Legion
|
|
47
54
|
module Extensions
|
|
48
55
|
module MicrosoftTeams
|
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.6.
|
|
4
|
+
version: 0.6.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -175,6 +175,8 @@ files:
|
|
|
175
175
|
- docs/plans/2026-03-19-teams-token-lifecycle-implementation.md
|
|
176
176
|
- lex-microsoft_teams.gemspec
|
|
177
177
|
- lib/legion/extensions/microsoft_teams.rb
|
|
178
|
+
- lib/legion/extensions/microsoft_teams/absorbers/meeting.rb
|
|
179
|
+
- lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb
|
|
178
180
|
- lib/legion/extensions/microsoft_teams/actors/api_ingest.rb
|
|
179
181
|
- lib/legion/extensions/microsoft_teams/actors/auth_validator.rb
|
|
180
182
|
- lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb
|
|
@@ -193,6 +195,7 @@ files:
|
|
|
193
195
|
- lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb
|
|
194
196
|
- lib/legion/extensions/microsoft_teams/helpers/callback_server.rb
|
|
195
197
|
- lib/legion/extensions/microsoft_teams/helpers/client.rb
|
|
198
|
+
- lib/legion/extensions/microsoft_teams/helpers/graph_client.rb
|
|
196
199
|
- lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb
|
|
197
200
|
- lib/legion/extensions/microsoft_teams/helpers/permission_guard.rb
|
|
198
201
|
- lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb
|