lex-microsoft_teams 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -2
- data/CHANGELOG.md +34 -0
- data/CLAUDE.md +2 -2
- data/docs/plans/2026-03-19-teams-token-lifecycle-design.md +120 -0
- data/docs/plans/2026-03-19-teams-token-lifecycle-implementation.md +679 -0
- data/lib/legion/extensions/microsoft_teams/actors/auth_validator.rb +105 -0
- 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/actors/token_refresher.rb +103 -0
- 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 +7 -2
- 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 +66 -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 +5 -2
- data/lib/legion/extensions/microsoft_teams/transport.rb +0 -11
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Actor
|
|
7
|
+
class AuthValidator < Legion::Extensions::Actors::Once
|
|
8
|
+
def use_runner? = false
|
|
9
|
+
def check_subtask? = false
|
|
10
|
+
def generate_task? = false
|
|
11
|
+
|
|
12
|
+
def delay
|
|
13
|
+
2.0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enabled?
|
|
17
|
+
defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
18
|
+
rescue StandardError
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def token_cache
|
|
23
|
+
@token_cache ||= Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def manual
|
|
27
|
+
loaded = token_cache.load_from_vault
|
|
28
|
+
|
|
29
|
+
if loaded
|
|
30
|
+
token = token_cache.cached_delegated_token
|
|
31
|
+
if token
|
|
32
|
+
log_info('Teams delegated auth restored')
|
|
33
|
+
elsif token_cache.previously_authenticated?
|
|
34
|
+
attempt_browser_reauth(token_cache)
|
|
35
|
+
end
|
|
36
|
+
elsif token_cache.previously_authenticated?
|
|
37
|
+
log_warn('Token file found but could not load, attempting re-authentication')
|
|
38
|
+
attempt_browser_reauth(token_cache)
|
|
39
|
+
else
|
|
40
|
+
log_debug('No Teams delegated auth configured, skipping')
|
|
41
|
+
end
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
log_error("AuthValidator: #{e.message}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def attempt_browser_reauth(cache)
|
|
49
|
+
settings = teams_auth_settings
|
|
50
|
+
return false unless settings[:tenant_id] && settings[:client_id]
|
|
51
|
+
|
|
52
|
+
log_warn('Delegated token expired, opening browser for re-authentication...')
|
|
53
|
+
|
|
54
|
+
scopes = settings.dig(:delegated, :scopes) ||
|
|
55
|
+
Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth::DEFAULT_SCOPES
|
|
56
|
+
browser_auth = Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
|
|
57
|
+
tenant_id: settings[:tenant_id],
|
|
58
|
+
client_id: settings[:client_id],
|
|
59
|
+
scopes: scopes
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
result = browser_auth.authenticate
|
|
63
|
+
return false if result[:error]
|
|
64
|
+
|
|
65
|
+
body = result[:result]
|
|
66
|
+
cache.store_delegated_token(
|
|
67
|
+
access_token: body['access_token'],
|
|
68
|
+
refresh_token: body['refresh_token'],
|
|
69
|
+
expires_in: body['expires_in'],
|
|
70
|
+
scopes: scopes
|
|
71
|
+
)
|
|
72
|
+
cache.save_to_vault
|
|
73
|
+
log_info('Teams delegated auth restored via browser')
|
|
74
|
+
true
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
log_error("Browser re-auth failed: #{e.message}")
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def teams_auth_settings
|
|
81
|
+
return {} unless defined?(Legion::Settings)
|
|
82
|
+
|
|
83
|
+
Legion::Settings.dig(:microsoft_teams, :auth) || {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def log_info(msg)
|
|
87
|
+
Legion::Logging.info(msg) if defined?(Legion::Logging)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def log_warn(msg)
|
|
91
|
+
Legion::Logging.warn(msg) if defined?(Legion::Logging)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def log_debug(msg)
|
|
95
|
+
Legion::Logging.debug(msg) if defined?(Legion::Logging)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def log_error(msg)
|
|
99
|
+
Legion::Logging.error(msg) if defined?(Legion::Logging)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -29,11 +29,7 @@ module Legion
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def args
|
|
32
|
-
{ since: @last_sync_time, skip_bots: true }
|
|
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
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Actor
|
|
7
|
+
class TokenRefresher < Legion::Extensions::Actors::Every
|
|
8
|
+
DEFAULT_REFRESH_INTERVAL = 900
|
|
9
|
+
|
|
10
|
+
def runner_class = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache
|
|
11
|
+
def runner_function = 'cached_delegated_token'
|
|
12
|
+
def run_now? = false
|
|
13
|
+
def use_runner? = false
|
|
14
|
+
def check_subtask? = false
|
|
15
|
+
def generate_task? = false
|
|
16
|
+
|
|
17
|
+
def time
|
|
18
|
+
settings = teams_auth_settings
|
|
19
|
+
delegated = settings[:delegated]
|
|
20
|
+
return DEFAULT_REFRESH_INTERVAL unless delegated.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
delegated[:refresh_interval] || DEFAULT_REFRESH_INTERVAL
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enabled?
|
|
26
|
+
defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def token_cache
|
|
32
|
+
@token_cache ||= Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def manual
|
|
36
|
+
return unless token_cache.authenticated?
|
|
37
|
+
|
|
38
|
+
token = token_cache.cached_delegated_token
|
|
39
|
+
if token
|
|
40
|
+
token_cache.save_to_vault
|
|
41
|
+
elsif token_cache.previously_authenticated?
|
|
42
|
+
attempt_browser_reauth(token_cache)
|
|
43
|
+
end
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
log_error("TokenRefresher: #{e.message}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def attempt_browser_reauth(cache)
|
|
51
|
+
settings = teams_auth_settings
|
|
52
|
+
return false unless settings[:tenant_id] && settings[:client_id]
|
|
53
|
+
|
|
54
|
+
log_warn('Delegated token expired, opening browser for re-authentication...')
|
|
55
|
+
|
|
56
|
+
scopes = settings.dig(:delegated, :scopes) ||
|
|
57
|
+
Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth::DEFAULT_SCOPES
|
|
58
|
+
browser_auth = Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
|
|
59
|
+
tenant_id: settings[:tenant_id],
|
|
60
|
+
client_id: settings[:client_id],
|
|
61
|
+
scopes: scopes
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
result = browser_auth.authenticate
|
|
65
|
+
return false if result[:error]
|
|
66
|
+
|
|
67
|
+
body = result[:result]
|
|
68
|
+
cache.store_delegated_token(
|
|
69
|
+
access_token: body['access_token'],
|
|
70
|
+
refresh_token: body['refresh_token'],
|
|
71
|
+
expires_in: body['expires_in'],
|
|
72
|
+
scopes: scopes
|
|
73
|
+
)
|
|
74
|
+
cache.save_to_vault
|
|
75
|
+
log_info('Teams delegated auth restored via browser')
|
|
76
|
+
true
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
log_error("Browser re-auth failed: #{e.message}")
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def teams_auth_settings
|
|
83
|
+
return {} unless defined?(Legion::Settings)
|
|
84
|
+
|
|
85
|
+
Legion::Settings.dig(:microsoft_teams, :auth) || {}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def log_info(msg)
|
|
89
|
+
Legion::Logging.info(msg) if defined?(Legion::Logging)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def log_warn(msg)
|
|
93
|
+
Legion::Logging.warn(msg) if defined?(Legion::Logging)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def log_error(msg)
|
|
97
|
+
Legion::Logging.error(msg) if defined?(Legion::Logging)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -36,9 +36,10 @@ module Legion
|
|
|
36
36
|
|
|
37
37
|
attr_reader :opts
|
|
38
38
|
|
|
39
|
-
def initialize(tenant_id: nil, client_id: nil, client_secret: nil, token: nil,
|
|
39
|
+
def initialize(tenant_id: nil, client_id: nil, client_secret: nil, token: nil,
|
|
40
|
+
user_id: 'me', **extra)
|
|
40
41
|
@opts = { tenant_id: tenant_id, client_id: client_id, client_secret: client_secret,
|
|
41
|
-
token: token, **extra }
|
|
42
|
+
token: token, user_id: user_id, **extra }
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def graph_connection(**override)
|
|
@@ -59,6 +60,8 @@ module Legion
|
|
|
59
60
|
client_id: @opts[:client_id],
|
|
60
61
|
client_secret: @opts[:client_secret]
|
|
61
62
|
)
|
|
63
|
+
return result unless result&.dig(:result, 'access_token')
|
|
64
|
+
|
|
62
65
|
@opts[:token] = result[:result]['access_token']
|
|
63
66
|
result
|
|
64
67
|
end
|
|
@@ -17,7 +17,7 @@ module Legion
|
|
|
17
17
|
|
|
18
18
|
attr_reader :tenant_id, :client_id, :scopes
|
|
19
19
|
|
|
20
|
-
def initialize(tenant_id:, client_id:, scopes: DEFAULT_SCOPES, auth: nil)
|
|
20
|
+
def initialize(tenant_id:, client_id:, scopes: DEFAULT_SCOPES, auth: nil, **)
|
|
21
21
|
@tenant_id = tenant_id
|
|
22
22
|
@client_id = client_id
|
|
23
23
|
@scopes = scopes
|
|
@@ -75,8 +75,13 @@ module Legion
|
|
|
75
75
|
client.close
|
|
76
76
|
break if @result
|
|
77
77
|
end
|
|
78
|
-
rescue
|
|
79
|
-
nil # server closed
|
|
78
|
+
rescue IOError
|
|
79
|
+
nil # server closed during shutdown
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
@mutex.synchronize do
|
|
82
|
+
@result ||= { error: e.message }
|
|
83
|
+
@cv.broadcast
|
|
84
|
+
end
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
87
|
end
|
|
@@ -25,6 +25,10 @@ module Legion
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def user_path(user_id = 'me')
|
|
29
|
+
user_id == 'me' ? 'me' : "users/#{user_id}"
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
def oauth_connection(tenant_id: 'common', **_opts)
|
|
29
33
|
Faraday.new(url: "https://login.microsoftonline.com/#{tenant_id}") do |conn|
|
|
30
34
|
conn.request :url_encoded
|
|
@@ -41,11 +41,8 @@ module Legion
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def conversation_overrides(conversation_id: nil)
|
|
45
|
-
|
|
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
|
|
@@ -58,11 +62,19 @@ module Legion
|
|
|
58
62
|
@mutex.synchronize { @delegated_cache = nil }
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
def authenticated?
|
|
66
|
+
@mutex.synchronize { !@delegated_cache.nil? }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def previously_authenticated?
|
|
70
|
+
File.exist?(local_token_path)
|
|
71
|
+
end
|
|
72
|
+
|
|
61
73
|
def load_from_vault
|
|
62
|
-
return
|
|
74
|
+
return load_from_local unless defined?(Legion::Crypt)
|
|
63
75
|
|
|
64
76
|
data = Legion::Crypt.get(vault_path)
|
|
65
|
-
return
|
|
77
|
+
return load_from_local unless data && data[:access_token]
|
|
66
78
|
|
|
67
79
|
@mutex.synchronize do
|
|
68
80
|
@delegated_cache = {
|
|
@@ -75,10 +87,12 @@ module Legion
|
|
|
75
87
|
true
|
|
76
88
|
rescue StandardError => e
|
|
77
89
|
log_error("Failed to load delegated token from Vault: #{e.message}")
|
|
78
|
-
|
|
90
|
+
load_from_local
|
|
79
91
|
end
|
|
80
92
|
|
|
81
93
|
def save_to_vault
|
|
94
|
+
save_to_local
|
|
95
|
+
|
|
82
96
|
return false unless defined?(Legion::Crypt)
|
|
83
97
|
|
|
84
98
|
data = @mutex.synchronize { @delegated_cache&.dup }
|
|
@@ -95,6 +109,47 @@ module Legion
|
|
|
95
109
|
false
|
|
96
110
|
end
|
|
97
111
|
|
|
112
|
+
def load_from_local
|
|
113
|
+
path = local_token_path
|
|
114
|
+
return false unless File.exist?(path)
|
|
115
|
+
|
|
116
|
+
raw = File.read(path)
|
|
117
|
+
data = ::JSON.parse(raw)
|
|
118
|
+
return false unless data['access_token'] && data['refresh_token']
|
|
119
|
+
|
|
120
|
+
@mutex.synchronize do
|
|
121
|
+
@delegated_cache = {
|
|
122
|
+
token: data['access_token'],
|
|
123
|
+
refresh_token: data['refresh_token'],
|
|
124
|
+
expires_at: Time.parse(data['expires_at']),
|
|
125
|
+
scopes: data['scopes']
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
true
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
log_error("Failed to load delegated token from local file: #{e.message}")
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def save_to_local
|
|
135
|
+
data = @mutex.synchronize { @delegated_cache&.dup }
|
|
136
|
+
return false unless data
|
|
137
|
+
|
|
138
|
+
path = local_token_path
|
|
139
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
140
|
+
File.write(path, ::JSON.pretty_generate(
|
|
141
|
+
'access_token' => data[:token],
|
|
142
|
+
'refresh_token' => data[:refresh_token],
|
|
143
|
+
'expires_at' => data[:expires_at].utc.iso8601,
|
|
144
|
+
'scopes' => data[:scopes]
|
|
145
|
+
))
|
|
146
|
+
File.chmod(0o600, path)
|
|
147
|
+
true
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
log_error("Failed to save delegated token to local file: #{e.message}")
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
98
153
|
private
|
|
99
154
|
|
|
100
155
|
def token_expired?(cache_entry)
|
|
@@ -120,6 +175,14 @@ module Legion
|
|
|
120
175
|
delegated[:vault_path] || DEFAULT_VAULT_PATH
|
|
121
176
|
end
|
|
122
177
|
|
|
178
|
+
def local_token_path
|
|
179
|
+
settings = teams_auth_settings
|
|
180
|
+
delegated = settings[:delegated]
|
|
181
|
+
return DEFAULT_LOCAL_FILE unless delegated.is_a?(Hash)
|
|
182
|
+
|
|
183
|
+
delegated[:local_token_path] || DEFAULT_LOCAL_FILE
|
|
184
|
+
end
|
|
185
|
+
|
|
123
186
|
def refresh_app_token
|
|
124
187
|
result = acquire_fresh_token
|
|
125
188
|
return nil unless result
|
|
@@ -17,7 +17,7 @@ module Legion
|
|
|
17
17
|
class Extractor
|
|
18
18
|
Message = Struct.new(
|
|
19
19
|
:content, # HTML message body
|
|
20
|
-
:sender, # display name (e.g. "
|
|
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
|
|