teems 0.3.0 → 0.3.2

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.
@@ -116,12 +116,28 @@ module Teems
116
116
 
117
117
  # Polls Safari's URL bar via AppleScript and manages Safari tab navigation
118
118
  module SafariOAuthPolling
119
+ # Sign-in can require an account picker, MFA or a PIV/CAC certificate prompt, so the
120
+ # redirect is polled in chunks for several minutes rather than a single short burst.
121
+ POLL_TICK_SECONDS = 0.1
122
+ POLL_CHUNK_SECONDS = 15
123
+ POLL_CHUNKS = 12
124
+
119
125
  private
120
126
 
121
127
  def poll_safari_query_redirect
122
- raw = run_applescript(poll_query_redirect_script).to_s
123
- if raw.empty? || raw == 'timeout'
124
- log('Safari OAuth: fast capture missed, falling back')
128
+ POLL_CHUNKS.times do |chunk|
129
+ raw = run_applescript(poll_query_redirect_script).to_s
130
+ return handle_poll_result(raw) unless raw == 'timeout'
131
+
132
+ announce_sign_in_wait(chunk)
133
+ end
134
+ log('Safari OAuth: timed out waiting for the sign-in redirect')
135
+ nil
136
+ end
137
+
138
+ def handle_poll_result(raw)
139
+ if raw.empty?
140
+ log('Safari OAuth: unable to read Safari tab URLs')
125
141
  return nil
126
142
  end
127
143
 
@@ -129,19 +145,30 @@ module Teems
129
145
  parse_oauth_params(raw)
130
146
  end
131
147
 
132
- # Poll for ?code= in the URL — Safari's URL property includes query strings
148
+ def announce_sign_in_wait(chunk)
149
+ return notify('Waiting for you to finish signing in to Safari...') if chunk.zero?
150
+
151
+ log("Safari OAuth: still waiting for sign-in (#{(chunk + 1) * POLL_CHUNK_SECONDS}s)")
152
+ end
153
+
154
+ # Poll every tab for ?code= — Safari's URL property includes query strings, and the
155
+ # redirect can land in a tab other than the one we opened.
133
156
  def poll_query_redirect_script
134
157
  <<~APPLESCRIPT
135
158
  tell application "Safari"
136
- repeat 600 times
137
- try
138
- set pageURL to URL of current tab of front window
139
- if pageURL contains "teams.microsoft.com/go?" then
140
- set codeStart to offset of "?" in pageURL
141
- return text (codeStart + 1) thru -1 of pageURL
142
- end if
143
- end try
144
- delay 0.02
159
+ repeat #{(POLL_CHUNK_SECONDS / POLL_TICK_SECONDS).to_i} times
160
+ repeat with w in windows
161
+ repeat with t in tabs of w
162
+ try
163
+ set pageURL to (URL of t) as text
164
+ if pageURL contains "teams.microsoft.com/go?" then
165
+ set codeStart to offset of "?" in pageURL
166
+ return text (codeStart + 1) thru -1 of pageURL
167
+ end if
168
+ end try
169
+ end repeat
170
+ end repeat
171
+ delay #{POLL_TICK_SECONDS}
145
172
  end repeat
146
173
  return "timeout"
147
174
  end tell
@@ -192,20 +219,20 @@ module Teems
192
219
  include SafariOAuthPolling
193
220
 
194
221
  SKYPE_RESOURCE = 'https://api.spaces.skype.com'
222
+ # Used when no previous login is on disk; Entra ID resolves the tenant from the account.
223
+ DEFAULT_TENANT = 'common'
195
224
 
196
225
  private
197
226
 
198
227
  def try_safari_oauth
199
228
  hint, tenant = stored_login_hint
200
- return nil unless tenant
201
-
202
- log('Trying fast Safari OAuth flow...')
203
- safari_code_flow({ tenant: tenant, hint: hint })
229
+ log('Trying Safari OAuth flow...')
230
+ safari_code_flow({ tenant: tenant || DEFAULT_TENANT, hint: hint })
204
231
  rescue StandardError => e
205
232
  log("Safari OAuth error: #{e.class}: #{e.message}")
206
233
  nil
207
234
  ensure
208
- close_teams_tab if tenant
235
+ close_teams_tab
209
236
  end
210
237
 
211
238
  def safari_code_flow(context)
@@ -273,12 +300,19 @@ module Teems
273
300
  # :reek:FeatureEnvy
274
301
  def assemble_safari_result(context, graph, skype)
275
302
  spaces_token = skype[:spaces_token]
276
- { auth_token: graph['access_token'],
303
+ auth_token = graph['access_token']
304
+ { auth_token: auth_token,
277
305
  skype_token: exchange_skype_via_http(spaces_token),
278
306
  skype_spaces_token: spaces_token, chatsvc_token: nil,
279
307
  refresh_token: skype[:refresh_token],
280
308
  client_id: OAuthUrlBuilder::TEAMS_APP_ID,
281
- tenant_id: context[:tenant] }
309
+ tenant_id: resolved_tenant(auth_token, context[:tenant]) }
310
+ end
311
+
312
+ # Prefer the tenant the token was actually issued for over the placeholder used to
313
+ # start the flow, so refreshes hit the tenant-specific endpoint.
314
+ def resolved_tenant(auth_token, requested)
315
+ decode_jwt(auth_token.to_s)&.fetch('tid', nil) || requested
282
316
  end
283
317
  end
284
318
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'open3'
4
3
  require_relative 'token_extractor_scripts'
5
4
  require_relative 'token_exchange_scripts'
6
5
  require_relative 'headless_extract'
@@ -58,7 +57,7 @@ module Teems
58
57
  end
59
58
 
60
59
  def run_applescript(script)
61
- output, _stderr, status = Open3.capture3('osascript', '-e', script)
60
+ output, _stderr, status = Support::Subprocess.capture3('osascript', '-e', script)
62
61
  return applescript_failure(status) unless status.success?
63
62
 
64
63
  output.strip
@@ -396,6 +395,7 @@ module Teems
396
395
  end
397
396
 
398
397
  def log(message) = @output&.debug(message)
398
+ def notify(message) = @output&.info(message)
399
399
  end
400
400
  end
401
401
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Teems
6
+ module Support
7
+ # Wrapper around Open3.capture3 for long-running child processes.
8
+ #
9
+ # Open3 reads stdout and stderr on background threads. When the process is signalled
10
+ # while those reads are in flight, the pipes close underneath them and each thread
11
+ # raises IOError, which Ruby prints as a backtrace before the main thread can exit
12
+ # cleanly. Waiting on Safari sign-in or the headless helper keeps us inside capture3
13
+ # for minutes at a time, so Ctrl-C there is routine rather than exceptional.
14
+ #
15
+ # Reader threads inherit Thread.report_on_exception when they are created, so muting
16
+ # it for the duration of the call silences exactly those threads and nothing else.
17
+ module Subprocess
18
+ module_function
19
+
20
+ def capture3(*command)
21
+ previous = Thread.report_on_exception
22
+ Thread.report_on_exception = false
23
+ Open3.capture3(*command)
24
+ ensure
25
+ Thread.report_on_exception = previous
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/teems/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teems
4
- VERSION = '0.3.0'
4
+ VERSION = '0.3.2'
5
5
  end
data/lib/teems.rb CHANGED
@@ -78,6 +78,7 @@ module Teems
78
78
  autoload :MessageFormatter, 'teems/formatters/message_formatter'
79
79
  autoload :MarkdownFormatter, 'teems/formatters/markdown_formatter'
80
80
  autoload :CalendarFormatter, 'teems/formatters/calendar_formatter'
81
+ autoload :TranscriptMarkdown, 'teems/formatters/transcript_markdown'
81
82
  end
82
83
 
83
84
  # CLI commands implementing user-facing functionality
@@ -95,6 +96,7 @@ module Teems
95
96
  autoload :Ooo, 'teems/commands/ooo'
96
97
  autoload :Org, 'teems/commands/org'
97
98
  autoload :Meeting, 'teems/commands/meeting'
99
+ autoload :Transcripts, 'teems/commands/transcripts'
98
100
  autoload :Status, 'teems/commands/status'
99
101
  end
100
102
 
@@ -117,5 +119,6 @@ module Teems
117
119
  autoload :ErrorLogger, 'teems/support/error_logger'
118
120
  autoload :Timezone, 'teems/support/timezone'
119
121
  autoload :TimeParsing, 'teems/support/time_parsing'
122
+ autoload :Subprocess, 'teems/support/subprocess'
120
123
  end
121
124
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teems
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Boehs
@@ -49,12 +49,14 @@ files:
49
49
  - lib/teems/commands/org.rb
50
50
  - lib/teems/commands/status.rb
51
51
  - lib/teems/commands/sync.rb
52
+ - lib/teems/commands/transcripts.rb
52
53
  - lib/teems/commands/who.rb
53
54
  - lib/teems/formatters/calendar_formatter.rb
54
55
  - lib/teems/formatters/format_utils.rb
55
56
  - lib/teems/formatters/markdown_formatter.rb
56
57
  - lib/teems/formatters/message_formatter.rb
57
58
  - lib/teems/formatters/output.rb
59
+ - lib/teems/formatters/transcript_markdown.rb
58
60
  - lib/teems/models/account.rb
59
61
  - lib/teems/models/channel.rb
60
62
  - lib/teems/models/chat.rb
@@ -83,6 +85,7 @@ files:
83
85
  - lib/teems/services/token_store.rb
84
86
  - lib/teems/support/error_logger.rb
85
87
  - lib/teems/support/help_formatter.rb
88
+ - lib/teems/support/subprocess.rb
86
89
  - lib/teems/support/time_parsing.rb
87
90
  - lib/teems/support/timezone.rb
88
91
  - lib/teems/support/xdg_paths.rb