lex-microsoft_teams 0.6.36 → 0.6.40
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/.rubocop.yml +5 -0
- data/CHANGELOG.md +31 -0
- data/README.md +8 -0
- data/lib/legion/extensions/microsoft_teams/absorbers/channel.rb +226 -0
- data/lib/legion/extensions/microsoft_teams/absorbers/chat.rb +181 -0
- data/lib/legion/extensions/microsoft_teams/actors/absorb_channel.rb +60 -0
- data/lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb +60 -0
- data/lib/legion/extensions/microsoft_teams/client.rb +2 -0
- data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +0 -8
- data/lib/legion/extensions/microsoft_teams/helpers/client.rb +2 -0
- data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/runners/loop.rb +99 -0
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +2 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6f9d39d6ddf7f53a0d22f727e6ca9cfede2aa3487cef4c8ffee064b549a158c
|
|
4
|
+
data.tar.gz: efbe44cedda75981daae45b173f795c968f2883e17d8526b512c0324a9269e73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2147849085641ca61d971a3f7b37009a481285cf9edb1dead731d03f6007dac3845168d2aeeb19f6737cd8673144e0673b860e924676136b7c6213ef61240114
|
|
7
|
+
data.tar.gz: 297f2da23ebd6a9db658ca27eecb1504376cee05745f4dea3e5b9b5d4a29d33923c28375a766b99de9a5e58d935dc6688c3552cb55442514b4a187085335f7a2
|
data/.rubocop.yml
CHANGED
|
@@ -16,3 +16,8 @@ Legion/Extension/EveryActorRequiresTime:
|
|
|
16
16
|
# RunnerReturnHash fires on private helper methods inside runner modules (false positives)
|
|
17
17
|
Legion/Extension/RunnerReturnHash:
|
|
18
18
|
Enabled: false
|
|
19
|
+
|
|
20
|
+
Metrics/CyclomaticComplexity:
|
|
21
|
+
Max: 25
|
|
22
|
+
Metrics/PerceivedComplexity:
|
|
23
|
+
Max: 25
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.40] - 2026-04-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `Helpers::BrowserAuth` — removed private `log` method that was shadowing the `log` accessor provided by `Legion::Extensions::Helpers::Lex`. The `include Helpers::Lex` guard was already present but the manual fallback overrode it; all `log.debug/info/warn/error` calls now correctly route through the Lex helper (structured logging, context propagation, etc.)
|
|
9
|
+
|
|
10
|
+
## [0.6.39] - 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Absorbers::Chat` — absorbs a Teams chat thread into Apollo by URL (`teams.microsoft.com/l/chat/19:*@*`). Extracts the chat ID from the URL, fetches metadata, pulls messages (with inline replies, HTML stripped), and ingests participants. Content types: `teams_chat_thread`, `teams_chat_participants`
|
|
14
|
+
- `Absorbers::Channel` — absorbs a Teams channel or specific thread into Apollo by URL (`teams.microsoft.com/l/channel/*`, `teams.microsoft.com/l/message/*`). Extracts `team_id` from `groupId` query param and `channel_id` from path. When a `message_id` is present (deep-link to a specific thread) only that thread is ingested; otherwise the top 50 channel messages are ingested. Replies are fetched and inlined. Content types: `teams_channel_thread`, `teams_channel_members`
|
|
15
|
+
- `Actors::AbsorbChat` — Subscription actor delegating to `Absorbers::Chat#absorb`, mirrors the `AbsorbMeeting` actor pattern
|
|
16
|
+
- `Actors::AbsorbChannel` — Subscription actor delegating to `Absorbers::Channel#absorb`, mirrors the `AbsorbMeeting` actor pattern
|
|
17
|
+
- Both new absorbers and actors are conditionally required (gated on `Legion::Extensions::Absorbers::Base` presence) in `microsoft_teams.rb`
|
|
18
|
+
|
|
19
|
+
## [0.6.38] - 2026-04-23
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- `Helpers::Client#graph_connection` now falls back to `Legion::Settings[:microsoft_teams]&.dig(:auth, :delegated, :token)` when no `token:` is explicitly passed — fixes unauthenticated Graph API calls when runners are invoked as standalone modules via Lex tool dispatch (not via an instantiated `Client` object where `@opts` carries the token)
|
|
23
|
+
- `Helpers::Client#bot_connection` applies the same fallback using `Legion::Settings[:microsoft_teams]&.dig(:auth, :bot, :token)`
|
|
24
|
+
|
|
25
|
+
## [0.7.0] - unreleased
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- New `Runners::Loop` module with four functions for Microsoft Loop component support via `fluidEmbedCard`:
|
|
29
|
+
- `create_loop_file` — Creates a new `.loop` file in a user's OneDrive via the Graph API; returns drive item metadata including `webUrl`
|
|
30
|
+
- `loop_attachment` — Builds the `fluidEmbedCard` + placeholder attachment pair required to embed a Loop component in a Teams message
|
|
31
|
+
- `post_loop_to_chat` — Posts a Loop component inline into a Teams chat thread
|
|
32
|
+
- `post_loop_to_channel` — Posts a Loop component inline into a Teams channel thread
|
|
33
|
+
- Requires `Files.ReadWrite` and `Sites.ReadWrite.All` Graph API permissions for `create_loop_file`; `Chat.ReadWrite` and `ChannelMessage.Send` are already required by existing runners
|
|
34
|
+
- **Note:** Programmatic write access to Loop page *content* (Fluid Framework) is not yet available via Microsoft Graph; Loop files must be opened in Teams to initialize the collaborative session
|
|
35
|
+
|
|
5
36
|
## [0.6.36] - 2026-04-13
|
|
6
37
|
|
|
7
38
|
### Fixed
|
data/README.md
CHANGED
|
@@ -113,6 +113,14 @@ gem install lex-microsoft_teams
|
|
|
113
113
|
- `action_submit` — Create a Submit action
|
|
114
114
|
- `message_attachment` — Wrap a card as a message attachment
|
|
115
115
|
|
|
116
|
+
### Loop Components
|
|
117
|
+
- `create_loop_file` — Create a new `.loop` file in a user's OneDrive; returns drive item metadata including `webUrl`
|
|
118
|
+
- `loop_attachment` — Build a `fluidEmbedCard` attachment array for embedding an existing Loop component URL in a Teams message
|
|
119
|
+
- `post_loop_to_chat` — Post a Loop component inline into a Teams chat thread
|
|
120
|
+
- `post_loop_to_channel` — Post a Loop component inline into a Teams channel thread
|
|
121
|
+
|
|
122
|
+
> **Note:** Creating a `.loop` file provisions the OneDrive item; the Fluid Framework collaborative session is initialized by Teams on first open. Programmatic write access to Loop page *content* is not yet available via Microsoft Graph.
|
|
123
|
+
|
|
116
124
|
### Bot Framework
|
|
117
125
|
- `send_activity` — Send an activity to a conversation
|
|
118
126
|
- `reply_to_activity` — Reply to an existing activity
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Absorbers
|
|
7
|
+
class Channel < Legion::Extensions::Absorbers::Base
|
|
8
|
+
pattern :url, 'teams.microsoft.com/l/channel/*'
|
|
9
|
+
pattern :url, 'teams.microsoft.com/l/message/*'
|
|
10
|
+
description 'Absorbs a Teams channel thread (messages, replies, members) into Apollo'
|
|
11
|
+
|
|
12
|
+
def absorb(url: nil, content: nil, metadata: {}, context: {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
+
report_progress(message: 'extracting ids from url')
|
|
14
|
+
ids = extract_ids(url)
|
|
15
|
+
return { success: false, error: 'could not extract team/channel ids from url' } unless ids
|
|
16
|
+
|
|
17
|
+
team_id = ids[:team_id]
|
|
18
|
+
channel_id = ids[:channel_id]
|
|
19
|
+
message_id = ids[:message_id]
|
|
20
|
+
|
|
21
|
+
report_progress(message: 'fetching channel metadata', percent: 10)
|
|
22
|
+
channel = resolve_channel(team_id, channel_id)
|
|
23
|
+
return { success: false, error: 'could not resolve channel' } unless channel
|
|
24
|
+
|
|
25
|
+
channel_name = channel['displayName'] || channel[:displayName] || 'untitled channel'
|
|
26
|
+
results = { team_id: team_id, channel_id: channel_id, channel_name: channel_name, chunks: 0 }
|
|
27
|
+
|
|
28
|
+
if message_id
|
|
29
|
+
# Scoped to a specific thread
|
|
30
|
+
ingest_thread(team_id, channel_id, message_id, channel_name, results)
|
|
31
|
+
else
|
|
32
|
+
# Full channel ingest
|
|
33
|
+
ingest_messages(team_id, channel_id, channel_name, results)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
ingest_members(team_id, channel_id, channel_name, results)
|
|
37
|
+
|
|
38
|
+
report_progress(message: 'done', percent: 100)
|
|
39
|
+
results.merge(success: true)
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
log.error("Channel absorber failed: #{e.message}")
|
|
42
|
+
{ success: false, error: e.message }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def channels_runner
|
|
48
|
+
@channels_runner ||= Object.new.extend(Runners::Channels)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def channel_messages_runner
|
|
52
|
+
@channel_messages_runner ||= Object.new.extend(Runners::ChannelMessages)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def graph_token
|
|
56
|
+
return @graph_token if defined?(@graph_token)
|
|
57
|
+
|
|
58
|
+
@graph_token = begin
|
|
59
|
+
Helpers::TokenCache.instance.cached_delegated_token if defined?(Helpers::TokenCache)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
log.warn("graph_token unavailable: #{e.message}")
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Teams channel URL formats:
|
|
67
|
+
# /l/channel/<encoded_channel_id>/<channel_name>?groupId=<team_id>&...
|
|
68
|
+
# /l/message/<encoded_channel_id>/<message_id>?groupId=<team_id>&...
|
|
69
|
+
def extract_ids(url)
|
|
70
|
+
return nil unless url.is_a?(String)
|
|
71
|
+
|
|
72
|
+
uri = URI.parse(url)
|
|
73
|
+
params = URI.decode_www_form(uri.query.to_s).to_h
|
|
74
|
+
|
|
75
|
+
team_id = params['groupId'] || params['groupid']
|
|
76
|
+
return nil unless team_id
|
|
77
|
+
|
|
78
|
+
path_parts = uri.path.split('/')
|
|
79
|
+
# path: ["", "l", "channel"|"message", <encoded_id>, ...]
|
|
80
|
+
encoded_id = path_parts[3]
|
|
81
|
+
return nil unless encoded_id
|
|
82
|
+
|
|
83
|
+
channel_id = URI.decode_uri_component(encoded_id)
|
|
84
|
+
|
|
85
|
+
message_id = (path_parts[4] if uri.path.include?('/l/message/'))
|
|
86
|
+
|
|
87
|
+
{ team_id: team_id, channel_id: channel_id, message_id: message_id }
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
log.debug("extract_ids failed: #{e.message}")
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def resolve_channel(team_id, channel_id)
|
|
94
|
+
response = channels_runner.get_channel(team_id: team_id, channel_id: channel_id, token: graph_token)
|
|
95
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
96
|
+
return nil unless body.is_a?(Hash) && !body['error'] && !body[:error]
|
|
97
|
+
|
|
98
|
+
body
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
log.warn("resolve_channel failed: #{e.message}")
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ingest_messages(team_id, channel_id, channel_name, results)
|
|
105
|
+
report_progress(message: 'fetching channel messages', percent: 25)
|
|
106
|
+
response = channel_messages_runner.list_channel_messages(
|
|
107
|
+
team_id: team_id, channel_id: channel_id, top: 50, token: graph_token
|
|
108
|
+
)
|
|
109
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
110
|
+
items = body.is_a?(Hash) ? (body['value'] || body[:value]) : nil
|
|
111
|
+
return unless items.is_a?(Array) && items.any?
|
|
112
|
+
|
|
113
|
+
items.reverse_each do |msg|
|
|
114
|
+
ingest_single_message(team_id, channel_id, msg, channel_name, results)
|
|
115
|
+
end
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
log.warn("Channel message ingest failed: #{e.message}")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ingest_thread(team_id, channel_id, message_id, channel_name, results)
|
|
121
|
+
report_progress(message: 'fetching thread root message', percent: 20)
|
|
122
|
+
response = channel_messages_runner.get_channel_message(
|
|
123
|
+
team_id: team_id, channel_id: channel_id, message_id: message_id, token: graph_token
|
|
124
|
+
)
|
|
125
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
126
|
+
return unless body.is_a?(Hash) && !body['error']
|
|
127
|
+
|
|
128
|
+
ingest_single_message(team_id, channel_id, body, channel_name, results, scoped_thread: true)
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
log.warn("Thread ingest failed: #{e.message}")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def ingest_single_message(team_id, channel_id, msg, channel_name, results, scoped_thread: false)
|
|
134
|
+
return unless msg.is_a?(Hash)
|
|
135
|
+
return if msg['deletedDateTime'] || msg[:deletedDateTime]
|
|
136
|
+
return if (msg['messageType'] || msg[:messageType]) == 'unknownFutureValue'
|
|
137
|
+
|
|
138
|
+
msg_id = msg['id'] || msg[:id]
|
|
139
|
+
sender = msg.dig('from', 'user', 'displayName') ||
|
|
140
|
+
msg.dig(:from, :user, :displayName) ||
|
|
141
|
+
'unknown'
|
|
142
|
+
body_content = msg.dig('body', 'content') || msg.dig(:body, :content) || ''
|
|
143
|
+
text = body_content.gsub(/<[^>]+>/, '').strip
|
|
144
|
+
return if text.empty? && !scoped_thread
|
|
145
|
+
|
|
146
|
+
subject = msg['subject'] || msg[:subject]
|
|
147
|
+
timestamp = msg['createdDateTime'] || msg[:createdDateTime]
|
|
148
|
+
lines = []
|
|
149
|
+
lines << "Subject: #{subject}" if subject && !subject.empty?
|
|
150
|
+
lines << "[#{timestamp}] #{sender}: #{text}" unless text.empty?
|
|
151
|
+
|
|
152
|
+
reply_lines = fetch_reply_lines(team_id, channel_id, msg_id)
|
|
153
|
+
lines.concat(reply_lines)
|
|
154
|
+
|
|
155
|
+
return if lines.empty?
|
|
156
|
+
|
|
157
|
+
percent = scoped_thread ? 60 : nil
|
|
158
|
+
report_progress(message: "ingesting message #{msg_id}", percent: percent) if percent
|
|
159
|
+
|
|
160
|
+
absorb_to_knowledge(
|
|
161
|
+
content: lines.join("\n"),
|
|
162
|
+
tags: ['teams', 'channel', 'thread', channel_name],
|
|
163
|
+
source_file: "teams://teams/#{team_id}/channels/#{channel_id}/messages/#{msg_id}",
|
|
164
|
+
heading: "Channel Thread: #{channel_name}#{" — #{subject}" if subject}",
|
|
165
|
+
content_type: 'teams_channel_thread'
|
|
166
|
+
)
|
|
167
|
+
results[:chunks] += 1
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
log.warn("ingest_single_message failed: #{e.message}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def fetch_reply_lines(team_id, channel_id, message_id)
|
|
173
|
+
return [] unless message_id
|
|
174
|
+
|
|
175
|
+
response = channel_messages_runner.list_channel_message_replies(
|
|
176
|
+
team_id: team_id, channel_id: channel_id, message_id: message_id, top: 50, token: graph_token
|
|
177
|
+
)
|
|
178
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
179
|
+
items = body.is_a?(Hash) ? (body['value'] || body[:value]) : nil
|
|
180
|
+
return [] unless items.is_a?(Array) && items.any?
|
|
181
|
+
|
|
182
|
+
items.filter_map do |reply|
|
|
183
|
+
next if reply['deletedDateTime'] || reply[:deletedDateTime]
|
|
184
|
+
|
|
185
|
+
sender = reply.dig('from', 'user', 'displayName') ||
|
|
186
|
+
reply.dig(:from, :user, :displayName) ||
|
|
187
|
+
'unknown'
|
|
188
|
+
body_content = reply.dig('body', 'content') || reply.dig(:body, :content) || ''
|
|
189
|
+
text = body_content.gsub(/<[^>]+>/, '').strip
|
|
190
|
+
next if text.empty?
|
|
191
|
+
|
|
192
|
+
timestamp = reply['createdDateTime'] || reply[:createdDateTime]
|
|
193
|
+
" ↳ [#{timestamp}] #{sender}: #{text}"
|
|
194
|
+
end
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
log.debug("fetch_reply_lines failed: #{e.message}")
|
|
197
|
+
[]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def ingest_members(team_id, channel_id, channel_name, results)
|
|
201
|
+
report_progress(message: 'fetching channel members', percent: 85)
|
|
202
|
+
response = channels_runner.list_channel_members(
|
|
203
|
+
team_id: team_id, channel_id: channel_id, token: graph_token
|
|
204
|
+
)
|
|
205
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
206
|
+
items = body.is_a?(Hash) ? (body['value'] || body[:value]) : nil
|
|
207
|
+
return unless items.is_a?(Array) && items.any?
|
|
208
|
+
|
|
209
|
+
names = items.filter_map { |m| m['displayName'] || m[:displayName] }
|
|
210
|
+
return if names.empty?
|
|
211
|
+
|
|
212
|
+
absorb_raw(
|
|
213
|
+
content: "Channel members for '#{channel_name}': #{names.join(', ')}",
|
|
214
|
+
tags: ['teams', 'channel', 'members', channel_name],
|
|
215
|
+
content_type: 'teams_channel_members',
|
|
216
|
+
metadata: { team_id: team_id, channel_id: channel_id, member_count: names.length }
|
|
217
|
+
)
|
|
218
|
+
results[:chunks] += 1
|
|
219
|
+
rescue StandardError => e
|
|
220
|
+
log.warn("Member ingest failed: #{e.message}")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Absorbers
|
|
7
|
+
class Chat < Legion::Extensions::Absorbers::Base
|
|
8
|
+
pattern :url, 'teams.microsoft.com/l/chat/19:*@*'
|
|
9
|
+
pattern :url, 'teams.microsoft.com/l/chat/19:*_*@*'
|
|
10
|
+
description 'Absorbs a Teams chat thread (messages, replies, participants) into Apollo'
|
|
11
|
+
|
|
12
|
+
def absorb(url: nil, content: nil, metadata: {}, context: {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
13
|
+
report_progress(message: 'extracting chat id from url')
|
|
14
|
+
chat_id = extract_chat_id(url)
|
|
15
|
+
return { success: false, error: 'could not extract chat id from url' } unless chat_id
|
|
16
|
+
|
|
17
|
+
report_progress(message: 'fetching chat metadata', percent: 10)
|
|
18
|
+
chat = resolve_chat(chat_id)
|
|
19
|
+
return { success: false, error: 'could not resolve chat' } unless chat
|
|
20
|
+
|
|
21
|
+
topic = chat['topic'] || chat[:topic] || 'untitled chat'
|
|
22
|
+
results = { chat_id: chat_id, topic: topic, chunks: 0 }
|
|
23
|
+
|
|
24
|
+
ingest_messages(chat_id, topic, results)
|
|
25
|
+
ingest_members(chat_id, topic, results)
|
|
26
|
+
|
|
27
|
+
report_progress(message: 'done', percent: 100)
|
|
28
|
+
results.merge(success: true)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
log.error("Chat absorber failed: #{e.message}")
|
|
31
|
+
{ success: false, error: e.message }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def chats_runner
|
|
37
|
+
@chats_runner ||= Object.new.extend(Runners::Chats)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def messages_runner
|
|
41
|
+
@messages_runner ||= Object.new.extend(Runners::Messages)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def graph_token
|
|
45
|
+
return @graph_token if defined?(@graph_token)
|
|
46
|
+
|
|
47
|
+
@graph_token = begin
|
|
48
|
+
Helpers::TokenCache.instance.cached_delegated_token if defined?(Helpers::TokenCache)
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
log.warn("graph_token unavailable: #{e.message}")
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_chat_id(url)
|
|
56
|
+
return nil unless url.is_a?(String)
|
|
57
|
+
|
|
58
|
+
# teams.microsoft.com/l/chat/19:XXXXX@unq.gbl.spaces/...
|
|
59
|
+
match = url.match(%r{/l/chat/(19:[^/?#]+)})
|
|
60
|
+
return unless match
|
|
61
|
+
|
|
62
|
+
URI.decode_uri_component(match[1])
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
log.debug("extract_chat_id failed: #{e.message}")
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resolve_chat(chat_id)
|
|
69
|
+
response = chats_runner.get_chat(chat_id: chat_id, token: graph_token)
|
|
70
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
71
|
+
return nil unless body.is_a?(Hash) && !body['error'] && !body[:error]
|
|
72
|
+
|
|
73
|
+
body
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
log.warn("resolve_chat failed: #{e.message}")
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ingest_messages(chat_id, topic, results)
|
|
80
|
+
report_progress(message: 'fetching messages', percent: 25)
|
|
81
|
+
response = messages_runner.list_chat_messages(chat_id: chat_id, top: 50, token: graph_token)
|
|
82
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
83
|
+
return unless body.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
items = body['value'] || body[:value]
|
|
86
|
+
return unless items.is_a?(Array) && items.any?
|
|
87
|
+
|
|
88
|
+
# Filter out system/deleted messages and build a readable transcript
|
|
89
|
+
lines = []
|
|
90
|
+
items.reverse_each do |msg|
|
|
91
|
+
next if msg['messageType'] != 'message' && !msg['messageType'].nil?
|
|
92
|
+
next if msg['deletedDateTime'] || msg[:deletedDateTime]
|
|
93
|
+
|
|
94
|
+
sender = msg.dig('from', 'user', 'displayName') ||
|
|
95
|
+
msg.dig(:from, :user, :displayName) ||
|
|
96
|
+
'unknown'
|
|
97
|
+
body_content = msg.dig('body', 'content') || msg.dig(:body, :content) || ''
|
|
98
|
+
# Strip HTML tags for plain text
|
|
99
|
+
text = body_content.gsub(/<[^>]+>/, '').strip
|
|
100
|
+
next if text.empty?
|
|
101
|
+
|
|
102
|
+
timestamp = msg['createdDateTime'] || msg[:createdDateTime]
|
|
103
|
+
lines << "[#{timestamp}] #{sender}: #{text}"
|
|
104
|
+
|
|
105
|
+
# Pull replies for this message
|
|
106
|
+
reply_lines = fetch_reply_lines(chat_id, msg['id'] || msg[:id], topic)
|
|
107
|
+
lines.concat(reply_lines) if reply_lines.any?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return if lines.empty?
|
|
111
|
+
|
|
112
|
+
report_progress(message: 'ingesting message thread', percent: 60)
|
|
113
|
+
absorb_to_knowledge(
|
|
114
|
+
content: lines.join("\n"),
|
|
115
|
+
tags: ['teams', 'chat', 'messages', topic],
|
|
116
|
+
source_file: "teams://chats/#{chat_id}/messages",
|
|
117
|
+
heading: "Chat: #{topic}",
|
|
118
|
+
content_type: 'teams_chat_thread'
|
|
119
|
+
)
|
|
120
|
+
results[:chunks] += 1
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
log.warn("Message ingest failed: #{e.message}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def fetch_reply_lines(chat_id, message_id, _topic)
|
|
126
|
+
return [] unless message_id
|
|
127
|
+
|
|
128
|
+
response = messages_runner.list_message_replies(
|
|
129
|
+
chat_id: chat_id, message_id: message_id, top: 50, token: graph_token
|
|
130
|
+
)
|
|
131
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
132
|
+
items = body.is_a?(Hash) ? (body['value'] || body[:value]) : nil
|
|
133
|
+
return [] unless items.is_a?(Array) && items.any?
|
|
134
|
+
|
|
135
|
+
items.filter_map do |reply|
|
|
136
|
+
next if reply['deletedDateTime'] || reply[:deletedDateTime]
|
|
137
|
+
|
|
138
|
+
sender = reply.dig('from', 'user', 'displayName') ||
|
|
139
|
+
reply.dig(:from, :user, :displayName) ||
|
|
140
|
+
'unknown'
|
|
141
|
+
body_content = reply.dig('body', 'content') || reply.dig(:body, :content) || ''
|
|
142
|
+
text = body_content.gsub(/<[^>]+>/, '').strip
|
|
143
|
+
next if text.empty?
|
|
144
|
+
|
|
145
|
+
timestamp = reply['createdDateTime'] || reply[:createdDateTime]
|
|
146
|
+
" ↳ [#{timestamp}] #{sender}: #{text}"
|
|
147
|
+
end
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
log.debug("fetch_reply_lines failed: #{e.message}")
|
|
150
|
+
[]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def ingest_members(chat_id, topic, results)
|
|
154
|
+
report_progress(message: 'fetching members', percent: 80)
|
|
155
|
+
response = chats_runner.list_chat_members(chat_id: chat_id, token: graph_token)
|
|
156
|
+
body = response.is_a?(Hash) ? response[:result] : nil
|
|
157
|
+
return unless body.is_a?(Hash)
|
|
158
|
+
|
|
159
|
+
items = body['value'] || body[:value]
|
|
160
|
+
return unless items.is_a?(Array) && items.any?
|
|
161
|
+
|
|
162
|
+
names = items.filter_map do |m|
|
|
163
|
+
m['displayName'] || m[:displayName]
|
|
164
|
+
end
|
|
165
|
+
return if names.empty?
|
|
166
|
+
|
|
167
|
+
absorb_raw(
|
|
168
|
+
content: "Chat participants for '#{topic}': #{names.join(', ')}",
|
|
169
|
+
tags: ['teams', 'chat', 'participants', topic],
|
|
170
|
+
content_type: 'teams_chat_participants',
|
|
171
|
+
metadata: { chat_id: chat_id, participant_count: names.length }
|
|
172
|
+
)
|
|
173
|
+
results[:chunks] += 1
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
log.warn("Member ingest failed: #{e.message}")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
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 AbsorbChannel < Legion::Extensions::Actors::Subscription
|
|
8
|
+
def runner_class = 'Legion::Extensions::MicrosoftTeams::Absorbers::Channel'
|
|
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::Channel)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
log.debug("AbsorbChannel#enabled?: #{e.message}")
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def work(payload)
|
|
22
|
+
parsed = parse_payload(payload)
|
|
23
|
+
absorber = Absorbers::Channel.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("AbsorbChannel 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("AbsorbChannel 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("AbsorbChannel#parse_payload: #{e.message}")
|
|
54
|
+
{}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
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 AbsorbChat < Legion::Extensions::Actors::Subscription
|
|
8
|
+
def runner_class = 'Legion::Extensions::MicrosoftTeams::Absorbers::Chat'
|
|
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::Chat)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
log.debug("AbsorbChat#enabled?: #{e.message}")
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def work(payload)
|
|
22
|
+
parsed = parse_payload(payload)
|
|
23
|
+
absorber = Absorbers::Chat.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("AbsorbChat 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("AbsorbChat 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("AbsorbChat#parse_payload: #{e.message}")
|
|
54
|
+
{}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -15,6 +15,7 @@ require 'legion/extensions/microsoft_teams/runners/meetings'
|
|
|
15
15
|
require 'legion/extensions/microsoft_teams/runners/transcripts'
|
|
16
16
|
require 'legion/extensions/microsoft_teams/runners/people'
|
|
17
17
|
require 'legion/extensions/microsoft_teams/runners/ownership'
|
|
18
|
+
require 'legion/extensions/microsoft_teams/runners/loop'
|
|
18
19
|
|
|
19
20
|
module Legion
|
|
20
21
|
module Extensions
|
|
@@ -37,6 +38,7 @@ module Legion
|
|
|
37
38
|
include Runners::CacheIngest
|
|
38
39
|
include Runners::People
|
|
39
40
|
include Runners::Ownership
|
|
41
|
+
include Runners::Loop
|
|
40
42
|
|
|
41
43
|
attr_reader :opts
|
|
42
44
|
|
|
@@ -106,14 +106,6 @@ module Legion
|
|
|
106
106
|
|
|
107
107
|
private
|
|
108
108
|
|
|
109
|
-
def log
|
|
110
|
-
return Legion::Logging if defined?(Legion::Logging) # rubocop:disable Legion/HelperMigration/LoggingGuard
|
|
111
|
-
|
|
112
|
-
@log ||= Object.new.tap do |nl|
|
|
113
|
-
%i[debug info warn error fatal].each { |m| nl.define_singleton_method(m) { |*| nil } }
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
109
|
def host_os
|
|
118
110
|
RbConfig::CONFIG['host_os']
|
|
119
111
|
end
|
|
@@ -8,6 +8,7 @@ module Legion
|
|
|
8
8
|
module Helpers
|
|
9
9
|
module Client
|
|
10
10
|
def graph_connection(token: nil, api_url: 'https://graph.microsoft.com/v1.0', **_opts)
|
|
11
|
+
token ||= Legion::Settings[:microsoft_teams]&.dig(:auth, :delegated, :token)
|
|
11
12
|
Faraday.new(url: api_url) do |conn|
|
|
12
13
|
conn.request :json
|
|
13
14
|
conn.response :json, content_type: /\bjson$/
|
|
@@ -17,6 +18,7 @@ module Legion
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def bot_connection(token: nil, service_url: 'https://smba.trafficmanager.net/teams/', **_opts)
|
|
21
|
+
token ||= Legion::Settings[:microsoft_teams]&.dig(:auth, :bot, :token)
|
|
20
22
|
Faraday.new(url: service_url) do |conn|
|
|
21
23
|
conn.request :json
|
|
22
24
|
conn.response :json, content_type: /\bjson$/
|
|
@@ -143,7 +143,7 @@ module Legion
|
|
|
143
143
|
|
|
144
144
|
# Extract individual messages from a messageMap record.
|
|
145
145
|
# These contain multiple messages in a reply chain.
|
|
146
|
-
def extract_message_map(strings, messages, seen_hashes)
|
|
146
|
+
def extract_message_map(strings, messages, seen_hashes)
|
|
147
147
|
conversation_id = nil
|
|
148
148
|
i = 0
|
|
149
149
|
|
|
@@ -232,7 +232,7 @@ module Legion
|
|
|
232
232
|
)
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
-
def apply_filters(messages, since:, channels:, senders:, skip_bots:)
|
|
235
|
+
def apply_filters(messages, since:, channels:, senders:, skip_bots:)
|
|
236
236
|
messages.select do |msg|
|
|
237
237
|
next false if since && msg.compose_time && Time.parse(msg.compose_time) < since
|
|
238
238
|
next false if channels&.none? { |c| msg.thread_topic&.downcase&.include?(c.downcase) }
|
|
@@ -21,7 +21,7 @@ module Legion
|
|
|
21
21
|
# (same format as CacheIngest) with dedup by content hash.
|
|
22
22
|
#
|
|
23
23
|
# Requires a delegated token with Chat.Read and People.Read scopes.
|
|
24
|
-
def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/
|
|
24
|
+
def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
25
25
|
return error_result('lex-memory not loaded') unless memory_available?
|
|
26
26
|
return error_result('no token provided') unless token && !token.empty?
|
|
27
27
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'legion/extensions/microsoft_teams/helpers/client'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module MicrosoftTeams
|
|
9
|
+
module Runners
|
|
10
|
+
module Loop
|
|
11
|
+
include Legion::Extensions::MicrosoftTeams::Helpers::Client
|
|
12
|
+
include Legion::JSON::Helper
|
|
13
|
+
|
|
14
|
+
# Creates a new .loop file in the user's OneDrive via the Graph API.
|
|
15
|
+
# The Fluid Framework collaborative session is initialized by Teams on first open.
|
|
16
|
+
# Returns the drive item metadata including +webUrl+, which can be passed to
|
|
17
|
+
# +post_loop_to_chat+ or +post_loop_to_channel+.
|
|
18
|
+
#
|
|
19
|
+
# @param filename [String] Name of the file (e.g. "incident-status" or "incident-status.loop")
|
|
20
|
+
# @param folder_path [String] OneDrive folder path relative to root (default: root)
|
|
21
|
+
# @param user_id [String] Graph user ID or 'me' (default: 'me')
|
|
22
|
+
def create_loop_file(filename:, folder_path: nil, user_id: 'me', **)
|
|
23
|
+
filename = "#{filename}.loop" unless filename.end_with?('.loop')
|
|
24
|
+
|
|
25
|
+
path = if folder_path.nil? || folder_path.empty?
|
|
26
|
+
"users/#{user_id}/drive/root:/#{filename}:/content"
|
|
27
|
+
else
|
|
28
|
+
"users/#{user_id}/drive/root:/#{folder_path}/#{filename}:/content"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
response = graph_connection(**).put(path, '', 'Content-Type' => 'application/octet-stream')
|
|
32
|
+
{ result: response.body }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Builds the fluidEmbedCard attachment array required to embed a Loop component
|
|
36
|
+
# in a Teams message. Pass the resulting array as +attachments:+ to
|
|
37
|
+
# +send_chat_message+ / +send_channel_message+, or use the convenience methods
|
|
38
|
+
# +post_loop_to_chat+ and +post_loop_to_channel+.
|
|
39
|
+
#
|
|
40
|
+
# @param component_url [String] SharePoint/OneDrive URL of the .loop file
|
|
41
|
+
# (the +webUrl+ from +create_loop_file+)
|
|
42
|
+
# @param source_type [String] 'Compose' (default) or 'Loop'
|
|
43
|
+
def loop_attachment(component_url:, source_type: 'Compose', **)
|
|
44
|
+
attachment_id = SecureRandom.hex(16)
|
|
45
|
+
{
|
|
46
|
+
result: [
|
|
47
|
+
{
|
|
48
|
+
id: attachment_id,
|
|
49
|
+
contentType: 'application/vnd.microsoft.card.fluidEmbedCard',
|
|
50
|
+
contentUrl: nil,
|
|
51
|
+
content: json_generate({ componentUrl: component_url, sourceType: source_type }),
|
|
52
|
+
teamsAppId: 'FluidEmbedCard'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'placeholderCard',
|
|
56
|
+
contentType: 'application/vnd.microsoft.card.codesnippet',
|
|
57
|
+
content: '{}',
|
|
58
|
+
teamsAppId: 'FLUID_PLACEHOLDER_CARD'
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Posts a message into a Teams chat thread with a Loop component embedded inline.
|
|
65
|
+
#
|
|
66
|
+
# @param chat_id [String] Teams chat thread ID (e.g. 19:...@thread.v2)
|
|
67
|
+
# @param component_url [String] SharePoint/OneDrive URL of the .loop file
|
|
68
|
+
# @param body_text [String] Optional plain-text preamble shown above the component
|
|
69
|
+
def post_loop_to_chat(chat_id:, component_url:, body_text: '', **)
|
|
70
|
+
attachments = loop_attachment(component_url: component_url, **)[:result]
|
|
71
|
+
content = body_text.empty? ? '<p></p>' : "<p>#{body_text}</p>"
|
|
72
|
+
payload = { body: { contentType: 'html', content: content }, attachments: attachments }
|
|
73
|
+
response = graph_connection(**).post("chats/#{chat_id}/messages", payload)
|
|
74
|
+
{ result: response.body }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Posts a message into a Teams channel thread with a Loop component embedded inline.
|
|
78
|
+
#
|
|
79
|
+
# @param team_id [String] Teams team ID
|
|
80
|
+
# @param channel_id [String] Teams channel ID
|
|
81
|
+
# @param component_url [String] SharePoint/OneDrive URL of the .loop file
|
|
82
|
+
# @param body_text [String] Optional plain-text preamble shown above the component
|
|
83
|
+
# @param subject [String] Optional thread subject line
|
|
84
|
+
def post_loop_to_channel(team_id:, channel_id:, component_url:, body_text: '', subject: nil, **)
|
|
85
|
+
attachments = loop_attachment(component_url: component_url, **)[:result]
|
|
86
|
+
content = body_text.empty? ? '<p></p>' : "<p>#{body_text}</p>"
|
|
87
|
+
payload = { body: { contentType: 'html', content: content }, attachments: attachments }
|
|
88
|
+
payload[:subject] = subject if subject
|
|
89
|
+
response = graph_connection(**).post("teams/#{team_id}/channels/#{channel_id}/messages", payload)
|
|
90
|
+
{ result: response.body }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
94
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -48,6 +48,8 @@ if defined?(Legion::Extensions) &&
|
|
|
48
48
|
Legion::Extensions.const_defined?(:Absorbers, false) &&
|
|
49
49
|
Legion::Extensions::Absorbers.const_defined?(:Base, false)
|
|
50
50
|
require_relative 'microsoft_teams/absorbers/meeting'
|
|
51
|
+
require_relative 'microsoft_teams/absorbers/chat'
|
|
52
|
+
require_relative 'microsoft_teams/absorbers/channel'
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
module Legion
|
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.40
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -175,7 +175,11 @@ 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/channel.rb
|
|
179
|
+
- lib/legion/extensions/microsoft_teams/absorbers/chat.rb
|
|
178
180
|
- lib/legion/extensions/microsoft_teams/absorbers/meeting.rb
|
|
181
|
+
- lib/legion/extensions/microsoft_teams/actors/absorb_channel.rb
|
|
182
|
+
- lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb
|
|
179
183
|
- lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb
|
|
180
184
|
- lib/legion/extensions/microsoft_teams/actors/api_ingest.rb
|
|
181
185
|
- lib/legion/extensions/microsoft_teams/actors/auth_validator.rb
|
|
@@ -218,6 +222,7 @@ files:
|
|
|
218
222
|
- lib/legion/extensions/microsoft_teams/runners/channels.rb
|
|
219
223
|
- lib/legion/extensions/microsoft_teams/runners/chats.rb
|
|
220
224
|
- lib/legion/extensions/microsoft_teams/runners/local_cache.rb
|
|
225
|
+
- lib/legion/extensions/microsoft_teams/runners/loop.rb
|
|
221
226
|
- lib/legion/extensions/microsoft_teams/runners/meetings.rb
|
|
222
227
|
- lib/legion/extensions/microsoft_teams/runners/messages.rb
|
|
223
228
|
- lib/legion/extensions/microsoft_teams/runners/ownership.rb
|