tempest-rb 0.1.0 → 0.1.1
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/lib/tempest/avatar_store.rb +9 -0
- data/lib/tempest/cli.rb +12 -0
- data/lib/tempest/repl/dispatcher.rb +1 -1
- data/lib/tempest/repl/formatter.rb +1 -1
- data/lib/tempest/repl/runner.rb +24 -1
- data/lib/tempest/repl/screen.rb +2 -0
- data/lib/tempest/session.rb +35 -3
- data/lib/tempest/version.rb +1 -1
- data/lib/tempest/xrpc_client.rb +12 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 425f1a89085c0d2d762d9ef2697d0f34b752e7bfb479ff4dec077678987fa378
|
|
4
|
+
data.tar.gz: e61021b6f27a4e038f8b1b4d51697c169f5c90b99f7dc4ea0cdcfffacde4ba65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3058fd2a2457246b63225ad6f4d46ac85e90e791dc0f364da1bc37ee1ab6b4ecb185aca488cd2f83c59e79c168d21e65f115a0b08c2cf49865ccf2f2c4e76f1
|
|
7
|
+
data.tar.gz: 6c092f9332873d3cea39f85fa33b8447f6fc4b8685c8b25b18490de845ac0eda408de15494a29e18a7f465d3e005248bbacb497f2365a4b7854512e10fcc86bf
|
data/lib/tempest/avatar_store.rb
CHANGED
|
@@ -123,6 +123,11 @@ module Tempest
|
|
|
123
123
|
cached = @mutex.synchronize { @cache[did] }
|
|
124
124
|
return cached_value(cached) unless cached.nil?
|
|
125
125
|
|
|
126
|
+
if @async && (path = cached_file_for(did))
|
|
127
|
+
@mutex.synchronize { @cache[did] = path }
|
|
128
|
+
return path
|
|
129
|
+
end
|
|
130
|
+
|
|
126
131
|
if @async
|
|
127
132
|
enqueue_resolve(did)
|
|
128
133
|
nil
|
|
@@ -141,6 +146,10 @@ module Tempest
|
|
|
141
146
|
value.equal?(NOT_FOUND) ? nil : value
|
|
142
147
|
end
|
|
143
148
|
|
|
149
|
+
def cached_file_for(did)
|
|
150
|
+
Dir.glob(File.join(@cache_dir, "#{sanitize(did)}__*.png")).max_by { |path| File.mtime(path) }
|
|
151
|
+
end
|
|
152
|
+
|
|
144
153
|
def resolve_and_cache(did)
|
|
145
154
|
path = resolve_sync(did)
|
|
146
155
|
@mutex.synchronize { @cache[did] = path.nil? ? NOT_FOUND : path }
|
data/lib/tempest/cli.rb
CHANGED
|
@@ -100,6 +100,7 @@ module Tempest
|
|
|
100
100
|
avatar_store: avatar_store,
|
|
101
101
|
timeline_store: timeline_store(env),
|
|
102
102
|
opener: opener_for(env: env),
|
|
103
|
+
reauth: build_reauth(env, stdout, stdin, session_factory),
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
begin
|
|
@@ -155,6 +156,17 @@ module Tempest
|
|
|
155
156
|
value.nil? || value.empty? ? nil : value
|
|
156
157
|
end
|
|
157
158
|
|
|
159
|
+
# Builds the proc REPL::Runner uses to honour `:relogin`. The lambda
|
|
160
|
+
# re-reads credentials from `env` on each call (so a user can update env
|
|
161
|
+
# in-process if needed) and goes through the same 2FA prompt path as
|
|
162
|
+
# initial sign-in.
|
|
163
|
+
def build_reauth(env, stdout, stdin, session_factory)
|
|
164
|
+
lambda do
|
|
165
|
+
config = Tempest::Config.from_env(env)
|
|
166
|
+
create_with_2fa(config, env, stdout, stdin, session_factory)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
158
170
|
def create_with_2fa(config, env, stdout, stdin, session_factory)
|
|
159
171
|
token = env["TEMPEST_AUTH_FACTOR_TOKEN"]
|
|
160
172
|
session_factory.call(config, auth_factor_token: token)
|
|
@@ -5,7 +5,7 @@ module Tempest
|
|
|
5
5
|
Command = Data.define(:name, :args)
|
|
6
6
|
|
|
7
7
|
class Dispatcher
|
|
8
|
-
KNOWN_COMMANDS = %i[timeline quit help stream open].freeze
|
|
8
|
+
KNOWN_COMMANDS = %i[timeline quit help stream open relogin].freeze
|
|
9
9
|
DOLLAR_ID = /\A\$[A-Z]{2}\z/.freeze
|
|
10
10
|
|
|
11
11
|
def dispatch(input)
|
|
@@ -190,7 +190,7 @@ module Tempest
|
|
|
190
190
|
prefix += id_label(var) if var
|
|
191
191
|
prefix += bracket(time) if time
|
|
192
192
|
identity = handle ? handle_label(handle) : did_label(did)
|
|
193
|
-
identity = "#{icon}
|
|
193
|
+
identity = "#{icon} #{identity}" if icon
|
|
194
194
|
"#{prefix}#{identity}: #{text}"
|
|
195
195
|
end
|
|
196
196
|
|
data/lib/tempest/repl/runner.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Tempest
|
|
|
18
18
|
:timeline Fetch and print the home timeline
|
|
19
19
|
:stream on|off Toggle the Jetstream live feed
|
|
20
20
|
:open $LX Open the URL with id $LX in the browser
|
|
21
|
+
:relogin Re-authenticate when the cached session is dead
|
|
21
22
|
:help Show this help
|
|
22
23
|
:quit Exit tempest (or Ctrl-D)
|
|
23
24
|
|
|
@@ -28,10 +29,12 @@ module Tempest
|
|
|
28
29
|
|
|
29
30
|
DEFAULT_OPENER = ->(url) { system("open", url) }
|
|
30
31
|
|
|
32
|
+
RELOGIN_HINT = "type :relogin to re-authenticate".freeze
|
|
33
|
+
|
|
31
34
|
def initialize(session:, client:, input:, output:, dispatcher: Dispatcher.new,
|
|
32
35
|
stream_manager: nil, handle_resolver: nil, stream_output: nil,
|
|
33
36
|
timeline_store: nil, registry: Registry.new, opener: DEFAULT_OPENER,
|
|
34
|
-
avatar_store: nil)
|
|
37
|
+
avatar_store: nil, reauth: nil)
|
|
35
38
|
@session = session
|
|
36
39
|
@client = client
|
|
37
40
|
@input = input
|
|
@@ -44,6 +47,7 @@ module Tempest
|
|
|
44
47
|
@registry = registry
|
|
45
48
|
@opener = opener
|
|
46
49
|
@avatar_store = avatar_store
|
|
50
|
+
@reauth = reauth
|
|
47
51
|
# URIs already printed via bootstrap_timeline or backfill_timeline.
|
|
48
52
|
# Jetstream's cursor-replay can re-emit those same posts on startup
|
|
49
53
|
# (the persisted cursor is older than the getTimeline window), so the
|
|
@@ -104,6 +108,8 @@ module Tempest
|
|
|
104
108
|
handle_reply(command.args[0], command.args[1])
|
|
105
109
|
when :open
|
|
106
110
|
handle_open(command.args.first)
|
|
111
|
+
when :relogin
|
|
112
|
+
handle_relogin
|
|
107
113
|
when :unknown
|
|
108
114
|
@output.puts "unknown command: :#{command.args.first}"
|
|
109
115
|
end
|
|
@@ -133,10 +139,25 @@ module Tempest
|
|
|
133
139
|
def handle_post(text)
|
|
134
140
|
response = Post.create(@client, did: @session.did, text: text)
|
|
135
141
|
@output.puts "posted: #{response["uri"]}"
|
|
142
|
+
rescue Tempest::AuthenticationError => e
|
|
143
|
+
@output.puts "error: #{e.message} (#{RELOGIN_HINT})"
|
|
136
144
|
rescue Tempest::Error => e
|
|
137
145
|
@output.puts "error: #{e.message}"
|
|
138
146
|
end
|
|
139
147
|
|
|
148
|
+
def handle_relogin
|
|
149
|
+
if @reauth.nil?
|
|
150
|
+
@output.puts "relogin is not available in this session"
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
new_session = @reauth.call
|
|
155
|
+
@session.replace_with!(new_session)
|
|
156
|
+
@output.puts "signed in as @#{@session.handle}"
|
|
157
|
+
rescue Tempest::Error => e
|
|
158
|
+
@output.puts "relogin failed: #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
140
161
|
def handle_reply(var, body)
|
|
141
162
|
target = @registry.find_post(var)
|
|
142
163
|
if target.nil?
|
|
@@ -155,6 +176,8 @@ module Tempest
|
|
|
155
176
|
reply: { uri: reply_uri_for(target), cid: target.cid },
|
|
156
177
|
)
|
|
157
178
|
@output.puts "posted: #{response["uri"]}"
|
|
179
|
+
rescue Tempest::AuthenticationError => e
|
|
180
|
+
@output.puts "error: #{e.message} (#{RELOGIN_HINT})"
|
|
158
181
|
rescue Tempest::Error => e
|
|
159
182
|
@output.puts "error: #{e.message}"
|
|
160
183
|
end
|
data/lib/tempest/repl/screen.rb
CHANGED
|
@@ -37,6 +37,7 @@ module Tempest
|
|
|
37
37
|
|
|
38
38
|
def disable
|
|
39
39
|
return unless @enabled
|
|
40
|
+
@io.print "\e_Ga=d,q=2\e\\"
|
|
40
41
|
@io.print "\e[r"
|
|
41
42
|
@io.flush if @io.respond_to?(:flush)
|
|
42
43
|
@enabled = false
|
|
@@ -130,6 +131,7 @@ module Tempest
|
|
|
130
131
|
|
|
131
132
|
def wrap_to_cols(line)
|
|
132
133
|
return [line] unless @cols && @cols.positive?
|
|
134
|
+
return [line] if line.include?("\e_G")
|
|
133
135
|
return [line] if Reline::Unicode.calculate_width(line, true) <= @cols
|
|
134
136
|
|
|
135
137
|
chunks, _ = Reline::Unicode.split_by_width(line, @cols)
|
data/lib/tempest/session.rb
CHANGED
|
@@ -46,6 +46,7 @@ module Tempest
|
|
|
46
46
|
@handle = handle
|
|
47
47
|
@pds_host = pds_host
|
|
48
48
|
@identifier = identifier
|
|
49
|
+
@refresh_mutex = Mutex.new
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def access_expired?
|
|
@@ -54,7 +55,40 @@ module Tempest
|
|
|
54
55
|
Time.now.to_i + EXPIRY_LEEWAY_SECONDS >= exp
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
# Adopts another Session's credentials in place. Used by :relogin so the
|
|
59
|
+
# XRPCClient that already holds a reference to this Session keeps working
|
|
60
|
+
# without having to be reconstructed.
|
|
61
|
+
def replace_with!(other)
|
|
62
|
+
@refresh_mutex.synchronize do
|
|
63
|
+
@access_jwt = other.access_jwt
|
|
64
|
+
@refresh_jwt = other.refresh_jwt
|
|
65
|
+
@did = other.did
|
|
66
|
+
@handle = other.handle
|
|
67
|
+
@pds_host = other.pds_host
|
|
68
|
+
end
|
|
69
|
+
@on_change&.call(self)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Refreshes the session using the current refresh_jwt.
|
|
74
|
+
#
|
|
75
|
+
# When `if_unchanged_from:` is supplied, the refresh is skipped if the
|
|
76
|
+
# session's access_jwt has already moved past that value. Combined with the
|
|
77
|
+
# internal mutex, this lets concurrent callers coalesce a single
|
|
78
|
+
# refreshSession round-trip: the first caller refreshes while the rest wait
|
|
79
|
+
# for the lock and then observe the new token, no-op'ing instead of issuing
|
|
80
|
+
# duplicate refresh requests.
|
|
81
|
+
def refresh!(if_unchanged_from: nil)
|
|
82
|
+
@refresh_mutex.synchronize do
|
|
83
|
+
return self if if_unchanged_from && @access_jwt != if_unchanged_from
|
|
84
|
+
|
|
85
|
+
perform_refresh
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def perform_refresh
|
|
58
92
|
url = "#{@pds_host}/xrpc/com.atproto.server.refreshSession"
|
|
59
93
|
response = Tempest::HTTP.post_json(
|
|
60
94
|
url,
|
|
@@ -77,8 +111,6 @@ module Tempest
|
|
|
77
111
|
self
|
|
78
112
|
end
|
|
79
113
|
|
|
80
|
-
private
|
|
81
|
-
|
|
82
114
|
def jwt_exp(token)
|
|
83
115
|
_, payload, _ = token.split(".")
|
|
84
116
|
return nil if payload.nil?
|
data/lib/tempest/version.rb
CHANGED
data/lib/tempest/xrpc_client.rb
CHANGED
|
@@ -38,10 +38,11 @@ module Tempest
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def perform
|
|
41
|
-
|
|
41
|
+
attempted_jwt = @session.access_jwt
|
|
42
|
+
response = yield(attempted_jwt)
|
|
42
43
|
|
|
43
|
-
if response
|
|
44
|
-
@session.refresh!
|
|
44
|
+
if auth_expired_response?(response)
|
|
45
|
+
@session.refresh!(if_unchanged_from: attempted_jwt)
|
|
45
46
|
response = yield(@session.access_jwt)
|
|
46
47
|
end
|
|
47
48
|
|
|
@@ -49,5 +50,13 @@ module Tempest
|
|
|
49
50
|
|
|
50
51
|
response.body
|
|
51
52
|
end
|
|
53
|
+
|
|
54
|
+
def auth_expired_response?(response)
|
|
55
|
+
return true if response.unauthorized?
|
|
56
|
+
return false unless response.status == 400
|
|
57
|
+
return false unless response.body.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
response.body["error"] == "ExpiredToken"
|
|
60
|
+
end
|
|
52
61
|
end
|
|
53
62
|
end
|