samagotchi 0.111.0 → 0.112.0
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 +44 -1
- data/docs/architecture.md +2 -2
- data/docs/configuration.md +24 -14
- data/docs/internals/remote-sessions.md +5 -1
- data/docs/internals/web-push.md +122 -46
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +11 -10
- data/lib/samagotchi/reply_wait.rb +30 -3
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/session_access/remote.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -0
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/public/chains.js +9 -6
- data/lib/samagotchi/web/public/format.js +20 -5
- data/lib/samagotchi/web/public/index.html +16 -5
- data/lib/samagotchi/web/public/lists_view.js +3 -2
- data/lib/samagotchi/web/public/notifications.js +115 -13
- data/lib/samagotchi/web/public/notify.js +3 -3
- data/lib/samagotchi/web/public/participant.html +4 -0
- data/lib/samagotchi/web/public/push.js +16 -17
- data/lib/samagotchi/web/public/session_state.js +4 -3
- data/lib/samagotchi/web/public/sw.js +6 -5
- data/lib/samagotchi/web/push/attention.rb +80 -35
- data/lib/samagotchi/web/push/client.rb +23 -6
- data/lib/samagotchi/web/push/notifier.rb +152 -20
- data/lib/samagotchi/web/push/presence.rb +60 -0
- data/lib/samagotchi/web/push/store.rb +84 -6
- data/lib/samagotchi/web/push.rb +8 -4
- data/lib/samagotchi/web/push_routes.rb +49 -8
- data/lib/samagotchi/web/server.rb +2 -1
- data/lib/samagotchi/web/session_access_routes.rb +19 -8
- metadata +2 -1
|
@@ -8,6 +8,7 @@ require "time"
|
|
|
8
8
|
require "uri"
|
|
9
9
|
require_relative "crypto"
|
|
10
10
|
require_relative "../private_files"
|
|
11
|
+
require_relative "../../log"
|
|
11
12
|
require_relative "../../atomic_file"
|
|
12
13
|
require_relative "../../session"
|
|
13
14
|
|
|
@@ -16,8 +17,10 @@ module Samagotchi
|
|
|
16
17
|
module Push
|
|
17
18
|
# This machine's Web Push state, in $XDG_STATE_HOME/samagotchi/web-push/
|
|
18
19
|
# (next to web-token and web-grants/; a 0700 folder, 0600 files, never
|
|
19
|
-
# in config.yml): vapid.pem, chi web's one VAPID key,
|
|
20
|
-
# subscriptions.json, the devices that turned the bell on
|
|
20
|
+
# in config.yml): vapid.pem, chi web's one VAPID key,
|
|
21
|
+
# subscriptions.json, the devices that turned the bell on, and
|
|
22
|
+
# sent.json, the attention keys already handled (a restart pushes
|
|
23
|
+
# none of them again). A
|
|
21
24
|
# subscription holds no personal data (an opaque endpoint and two
|
|
22
25
|
# keys); the endpoint and keys never leave this store but for the push
|
|
23
26
|
# itself: what the page and the log show is Sub#public_h (the short
|
|
@@ -29,7 +32,11 @@ module Samagotchi
|
|
|
29
32
|
DIR = "web-push"
|
|
30
33
|
KEY_FILE = "vapid.pem"
|
|
31
34
|
SUBS_FILE = "subscriptions.json"
|
|
35
|
+
SENT_FILE = "sent.json"
|
|
32
36
|
LOCK = "store.lock"
|
|
37
|
+
# A handled key is kept this long (a restart catches up questions
|
|
38
|
+
# under an hour old).
|
|
39
|
+
SENT_KEEP = 24 * 3600
|
|
33
40
|
|
|
34
41
|
# The push services a subscription's endpoint may name: the
|
|
35
42
|
# browsers' (Apple, Google, Mozilla, Microsoft). chi web POSTs to
|
|
@@ -87,9 +94,12 @@ module Samagotchi
|
|
|
87
94
|
# Never the endpoint or the keys: what the page, the CLI and the log show.
|
|
88
95
|
def public_h
|
|
89
96
|
{ id: id, label: label, origin: origin, created_at: created_at, last_ok_at: last_ok_at,
|
|
90
|
-
last_error: last_error }
|
|
97
|
+
last_error: last_error, prefs: { done: done? } }
|
|
91
98
|
end
|
|
92
99
|
|
|
100
|
+
# The device wants a push for a long turn done (off by default).
|
|
101
|
+
def done? = prefs["done"] == true
|
|
102
|
+
|
|
93
103
|
def inspect = "#<Sub #{id} #{label.inspect}>"
|
|
94
104
|
alias_method :to_s, :inspect
|
|
95
105
|
end
|
|
@@ -101,6 +111,7 @@ module Samagotchi
|
|
|
101
111
|
end
|
|
102
112
|
|
|
103
113
|
def self.short_id(endpoint) = Digest::SHA256.hexdigest(endpoint.to_s)[0, 8]
|
|
114
|
+
def self.short_id?(text) = text.is_a?(String) && text.match?(/\A[0-9a-f]{8}\z/)
|
|
104
115
|
|
|
105
116
|
attr_reader :dir
|
|
106
117
|
|
|
@@ -113,6 +124,7 @@ module Samagotchi
|
|
|
113
124
|
@clock = clock
|
|
114
125
|
@key_mutex = Mutex.new
|
|
115
126
|
@key = nil
|
|
127
|
+
@sent_corrupt_logged = false
|
|
116
128
|
end
|
|
117
129
|
|
|
118
130
|
# Never the key.
|
|
@@ -121,11 +133,12 @@ module Samagotchi
|
|
|
121
133
|
|
|
122
134
|
def key_path = File.join(@dir, KEY_FILE)
|
|
123
135
|
def subs_path = File.join(@dir, SUBS_FILE)
|
|
136
|
+
def sent_path = File.join(@dir, SENT_FILE)
|
|
124
137
|
|
|
125
138
|
# Refuses a store others could read (PrivateFiles).
|
|
126
139
|
# @raise [Insecure]
|
|
127
140
|
def check_permissions!
|
|
128
|
-
PrivateFiles.check!(@dir, [key_path, subs_path], holds: "chi's push key and devices", error: Insecure)
|
|
141
|
+
PrivateFiles.check!(@dir, [key_path, subs_path, sent_path], holds: "chi's push key and devices", error: Insecure)
|
|
129
142
|
end
|
|
130
143
|
|
|
131
144
|
# chi web's VAPID key: vapid.pem, made (0600) the first time.
|
|
@@ -158,20 +171,30 @@ module Samagotchi
|
|
|
158
171
|
# Adds the subscription a page's pushManager.subscribe gave (its
|
|
159
172
|
# JSON: endpoint, keys {p256dh, auth}); the same endpoint again
|
|
160
173
|
# replaces the old entry (a device subscribing again).
|
|
174
|
+
# +replaces+ is the short id this device had before (the page keeps
|
|
175
|
+
# it): that entry on +origin+ goes too, before the cap is counted,
|
|
176
|
+
# since the device has a new endpoint and the old one is dead (iOS
|
|
177
|
+
# revokes silently: its push service keeps answering 201). Yields
|
|
178
|
+
# the entry dropped that way, if any; an unknown id is ignored.
|
|
161
179
|
# @return [Sub]
|
|
162
180
|
# @raise [Invalid] one message per refusal
|
|
163
|
-
def add(subscription, origin:, label:)
|
|
181
|
+
def add(subscription, origin:, label:, replaces: nil)
|
|
164
182
|
endpoint, p256dh, auth = validate(subscription)
|
|
165
183
|
at = stamp(now)
|
|
166
184
|
sub = Sub.new(id: Store.short_id(endpoint), endpoint: endpoint, p256dh: p256dh, auth: auth, origin: origin,
|
|
167
185
|
label: label.to_s[0, LABEL_MAX], created_at: at, last_ok_at: nil, last_error: nil,
|
|
168
186
|
prefs: { "done" => false })
|
|
169
|
-
|
|
187
|
+
replaced = nil
|
|
188
|
+
added = update do |subs|
|
|
170
189
|
kept = subs.reject { |old| old.endpoint == endpoint }
|
|
190
|
+
replaced = kept.find { |old| old.id == replaces && old.origin == origin } if replaces.is_a?(String)
|
|
191
|
+
kept -= [replaced] if replaced
|
|
171
192
|
raise Invalid, "chi web already pushes to #{MAX_SUBS} devices: turn the bell off on one first" if kept.size >= MAX_SUBS
|
|
172
193
|
|
|
173
194
|
[kept + [sub], sub]
|
|
174
195
|
end
|
|
196
|
+
yield replaced if replaced && block_given?
|
|
197
|
+
added
|
|
175
198
|
end
|
|
176
199
|
|
|
177
200
|
# Removes the subscription with this short id or endpoint.
|
|
@@ -183,6 +206,21 @@ module Samagotchi
|
|
|
183
206
|
end
|
|
184
207
|
end
|
|
185
208
|
|
|
209
|
+
# Sets the device's prefs (+done+: a push for a long turn done).
|
|
210
|
+
# @param origin [String, nil] only a device subscribed there
|
|
211
|
+
# @return [Sub, nil] the device as it is now, nil when there is none
|
|
212
|
+
def set_prefs(id, done:, origin: nil)
|
|
213
|
+
update do |subs|
|
|
214
|
+
found = nil
|
|
215
|
+
subs = subs.map do |sub|
|
|
216
|
+
next sub unless sub.id == id && (origin.nil? || sub.origin == origin)
|
|
217
|
+
|
|
218
|
+
found = sub.with(prefs: sub.prefs.merge("done" => done == true))
|
|
219
|
+
end
|
|
220
|
+
[subs, found]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
186
224
|
# Records a delivery: +ok+ true stamps last_ok_at (and clears the
|
|
187
225
|
# error), else +error+ (a short word: "410", "timeout") is kept.
|
|
188
226
|
def mark(id, ok: false, error: nil)
|
|
@@ -196,10 +234,50 @@ module Samagotchi
|
|
|
196
234
|
end
|
|
197
235
|
end
|
|
198
236
|
|
|
237
|
+
# The attention keys already handled (pushed to a device, or
|
|
238
|
+
# skipped while the owner looked, or over the cap): key → iso8601
|
|
239
|
+
# time. None when sent.json is missing; one that doesn't read is
|
|
240
|
+
# logged once and read as none (the next #mark_sent rewrites it:
|
|
241
|
+
# it only spares a restart's repeat).
|
|
242
|
+
# @return [Hash{String => String}]
|
|
243
|
+
def sent_keys = read_sent
|
|
244
|
+
|
|
245
|
+
# Records +key+ as handled now; keys older than SENT_KEEP go.
|
|
246
|
+
def mark_sent(key)
|
|
247
|
+
locked do
|
|
248
|
+
at = now
|
|
249
|
+
keys = read_sent.reject { |_, time| stale_sent?(time, at) }
|
|
250
|
+
keys[key.to_s] = stamp(at)
|
|
251
|
+
body = JSON.pretty_generate({ "version" => 1, "keys" => keys })
|
|
252
|
+
AtomicFile.write(sent_path, "#{body}\n", perm: 0o600)
|
|
253
|
+
end
|
|
254
|
+
nil
|
|
255
|
+
end
|
|
256
|
+
|
|
199
257
|
private
|
|
200
258
|
|
|
201
259
|
def now = @clock.call
|
|
202
260
|
|
|
261
|
+
def read_sent
|
|
262
|
+
data = JSON.parse(File.read(sent_path))
|
|
263
|
+
keys = data["keys"] if data.is_a?(Hash)
|
|
264
|
+
raise JSON::ParserError, "no keys" unless keys.is_a?(Hash) && keys.all? { |k, v| k.is_a?(String) && v.is_a?(String) }
|
|
265
|
+
|
|
266
|
+
keys
|
|
267
|
+
rescue Errno::ENOENT
|
|
268
|
+
{}
|
|
269
|
+
rescue JSON::ParserError
|
|
270
|
+
Log.warn(:web, "push_sent_corrupt", path: sent_path) unless @sent_corrupt_logged
|
|
271
|
+
@sent_corrupt_logged = true
|
|
272
|
+
{}
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def stale_sent?(time, at)
|
|
276
|
+
at - Time.iso8601(time) > SENT_KEEP
|
|
277
|
+
rescue ArgumentError
|
|
278
|
+
true
|
|
279
|
+
end
|
|
280
|
+
|
|
203
281
|
def stamp(time) = time.utc.iso8601
|
|
204
282
|
|
|
205
283
|
# @return [Array(String, String, String)] endpoint, p256dh, auth
|
data/lib/samagotchi/web/push.rb
CHANGED
|
@@ -4,13 +4,14 @@ require_relative "push/crypto"
|
|
|
4
4
|
require_relative "push/store"
|
|
5
5
|
require_relative "push/client"
|
|
6
6
|
require_relative "push/attention"
|
|
7
|
+
require_relative "push/presence"
|
|
7
8
|
require_relative "push/notifier"
|
|
8
9
|
require_relative "../config"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
module Web
|
|
12
13
|
# Web Push: chi web reaches the owner's phone (a Home Screen app that
|
|
13
|
-
# turned the bell on) when a session
|
|
14
|
+
# turned the bell on) when a session needs the user or a turn failed.
|
|
14
15
|
# It needs an https public origin (web.public_url or chi web --tunnel
|
|
15
16
|
# tailscale) and web.push.enabled (on by default); without a subscribed
|
|
16
17
|
# device it does nothing. See docs/configuration.md (Phone
|
|
@@ -21,9 +22,12 @@ module Samagotchi
|
|
|
21
22
|
class Refused < StandardError; end
|
|
22
23
|
|
|
23
24
|
# What chi web's push routes and its Notifier share for one run: the
|
|
24
|
-
# store, the client that signs with its key,
|
|
25
|
-
#
|
|
26
|
-
|
|
25
|
+
# store, the client that signs with its key, the public origin the
|
|
26
|
+
# devices subscribe on, and the pages' presence (the route writes it,
|
|
27
|
+
# the Notifier reads it).
|
|
28
|
+
Runtime = Data.define(:store, :client, :origin, :presence) do
|
|
29
|
+
def initialize(presence: Presence.new, **) = super
|
|
30
|
+
end
|
|
27
31
|
|
|
28
32
|
module_function
|
|
29
33
|
|
|
@@ -7,7 +7,8 @@ module Samagotchi
|
|
|
7
7
|
module Web
|
|
8
8
|
# The owner's Web Push routes (App::ROUTES, all :owner rows: a
|
|
9
9
|
# participant gets no pushes): what the page's bell asks (GET /api/push),
|
|
10
|
-
# subscribing this device and unsubscribing it,
|
|
10
|
+
# subscribing this device and unsubscribing it, a test push, and the
|
|
11
|
+
# pages' presence (no push while the owner looks at one).
|
|
11
12
|
# Included in App: its helpers (json_response, error_response,
|
|
12
13
|
# parse_json, request_body) and @push (a Push::Runtime, nil when push is
|
|
13
14
|
# off: no https public_url, or web.push.enabled false) answer. See
|
|
@@ -17,32 +18,61 @@ module Samagotchi
|
|
|
17
18
|
|
|
18
19
|
private
|
|
19
20
|
|
|
20
|
-
# {enabled, public_key, origin, this}: +this+ is the
|
|
21
|
-
# endpoint the page sends as ?endpoint= (its
|
|
22
|
-
# this store doesn't have it for this
|
|
21
|
+
# {enabled, public_key, origin, this, devices, stale}: +this+ is the
|
|
22
|
+
# device whose endpoint the page sends as ?endpoint= (its
|
|
23
|
+
# pushManager's), nil when this store doesn't have it for this
|
|
24
|
+
# origin; +devices+ this origin's (Sub#public_h, +this+ marked),
|
|
25
|
+
# +stale+ how many were made on another origin (never sent to).
|
|
23
26
|
def handle_push_info(req)
|
|
24
27
|
return json_response(200, { enabled: false, public_key: nil, origin: nil, this: nil }) unless @push
|
|
25
28
|
|
|
26
29
|
endpoint = req.params["endpoint"].to_s
|
|
27
|
-
|
|
30
|
+
subs = @push.store.subscriptions
|
|
31
|
+
mine = subs.select { |sub| sub.origin == @push.origin }
|
|
32
|
+
this = endpoint.empty? ? nil : mine.find { |sub| sub.endpoint == endpoint }
|
|
28
33
|
json_response(200, { enabled: true, public_key: Push::Crypto.public_key_b64(@push.store.vapid_key),
|
|
29
|
-
origin: @push.origin, this: this && { id: this.id, label: this.label }
|
|
34
|
+
origin: @push.origin, this: this && { id: this.id, label: this.label },
|
|
35
|
+
devices: mine.map { |sub| sub.public_h.merge(this: sub.equal?(this)) },
|
|
36
|
+
stale: subs.size - mine.size })
|
|
30
37
|
end
|
|
31
38
|
|
|
32
|
-
# {subscription: {endpoint, keys: {p256dh, auth}}
|
|
39
|
+
# {subscription: {endpoint, keys: {p256dh, auth}}, replaces?: id} →
|
|
40
|
+
# 201 {id, label}. +replaces+ is the id this device was given before
|
|
41
|
+
# (the page keeps it): that entry goes (Store#add).
|
|
33
42
|
def handle_push_subscribe(req)
|
|
34
43
|
return push_off unless @push
|
|
35
44
|
|
|
36
45
|
body = push_body(req)
|
|
37
46
|
return error_response(400, "invalid_json", "invalid JSON body") unless body.is_a?(Hash)
|
|
38
47
|
|
|
39
|
-
sub = @push.store.add(body["subscription"], origin: @push.origin, label: device_label(req)
|
|
48
|
+
sub = @push.store.add(body["subscription"], origin: @push.origin, label: device_label(req),
|
|
49
|
+
replaces: body["replaces"]) do |old|
|
|
50
|
+
Log.info(:web, "push_replaced", device: old.id)
|
|
51
|
+
end
|
|
40
52
|
Log.info(:web, "push_subscribed", device: sub.id)
|
|
41
53
|
json_response(201, { id: sub.id, label: sub.label })
|
|
42
54
|
rescue Push::Store::Invalid => e
|
|
43
55
|
error_response(400, "invalid_subscription", e.message)
|
|
44
56
|
end
|
|
45
57
|
|
|
58
|
+
# PATCH …/subscriptions/ID {prefs: {done: true|false}} → 200 the
|
|
59
|
+
# device (Sub#public_h): whether it gets a push for a long turn done.
|
|
60
|
+
def handle_push_prefs(req, id)
|
|
61
|
+
return push_off unless @push
|
|
62
|
+
|
|
63
|
+
body = push_body(req)
|
|
64
|
+
prefs = body["prefs"] if body.is_a?(Hash)
|
|
65
|
+
unless prefs.is_a?(Hash) && prefs.keys == ["done"] && [true, false].include?(prefs["done"])
|
|
66
|
+
return error_response(400, "invalid_prefs", "give {prefs: {done: true|false}}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sub = Push::Store.short_id?(id) && @push.store.set_prefs(id, done: prefs["done"], origin: @push.origin)
|
|
70
|
+
return error_response(404, "unknown_device", "no device #{id[0, 16]} pushes from this origin") unless sub
|
|
71
|
+
|
|
72
|
+
Log.info(:web, "push_prefs", device: sub.id, done: sub.done?)
|
|
73
|
+
json_response(200, sub.public_h)
|
|
74
|
+
end
|
|
75
|
+
|
|
46
76
|
# {endpoint} → 204: this device's bell turned off.
|
|
47
77
|
def handle_push_unsubscribe(req)
|
|
48
78
|
return push_off unless @push
|
|
@@ -75,6 +105,17 @@ module Samagotchi
|
|
|
75
105
|
json_response(200, { devices: results })
|
|
76
106
|
end
|
|
77
107
|
|
|
108
|
+
# {page, front} → 204: whether the user is at this page (visible,
|
|
109
|
+
# focused, touched lately; notifications.js reports it). Also with
|
|
110
|
+
# push off: a desk tab has nothing to be told.
|
|
111
|
+
def handle_push_presence(req)
|
|
112
|
+
page, front = Push::Presence.parse(push_body(req))
|
|
113
|
+
return error_response(400, "invalid_presence", "give {page: id, front: true|false}") unless page
|
|
114
|
+
|
|
115
|
+
@push&.presence&.report(page, front)
|
|
116
|
+
[204, { "Cache-Control" => "no-store" }, []]
|
|
117
|
+
end
|
|
118
|
+
|
|
78
119
|
def push_off
|
|
79
120
|
error_response(409, "push_off", "push needs an https public URL (chi web --tunnel tailscale, or web.public_url) " \
|
|
80
121
|
"and web.push.enabled")
|
|
@@ -219,7 +219,8 @@ module Samagotchi
|
|
|
219
219
|
# The Notifier seeds from the hub's snapshot, which is empty before
|
|
220
220
|
# a scan: one now, so what is already open never pushes.
|
|
221
221
|
hub.scan
|
|
222
|
-
notifier = Push::Notifier.new(store: push.store, client: push.client, origin: push.origin
|
|
222
|
+
notifier = Push::Notifier.new(store: push.store, client: push.client, origin: push.origin,
|
|
223
|
+
presence: push.presence).start(hub)
|
|
223
224
|
Samagotchi::Log.info(:web, "push", devices: push.store.for_origin(push.origin).size)
|
|
224
225
|
end
|
|
225
226
|
hub.start
|
|
@@ -171,13 +171,16 @@ module Samagotchi
|
|
|
171
171
|
# (SessionAccess::Local#wait_reply). Body: cursor, baseline
|
|
172
172
|
# ({messages, question_id, last_turn}, as the prepare route gave it),
|
|
173
173
|
# turn, queued, ended_before, owner_grace (seconds, null: wait for
|
|
174
|
-
# whatever wakes a worker), timeout (seconds, capped)
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
# question, outcome, …; never the
|
|
178
|
-
# a parent may pick on the
|
|
179
|
-
# (ParentReport.strategy_for). A timeout
|
|
180
|
-
#
|
|
174
|
+
# whatever wakes a worker), timeout (seconds, capped), running_look
|
|
175
|
+
# (the last timeout's, as it came). 200: the ReplyWait::Result's
|
|
176
|
+
# fields (status: done, waiting_for_answer, no_reply, error, stopped,
|
|
177
|
+
# timeout or worker_gone; text, file, question, outcome, …; never the
|
|
178
|
+
# Session) and parent_strategy, what a parent may pick on the
|
|
179
|
+
# session's questions here (ParentReport.strategy_for). A timeout
|
|
180
|
+
# status: ask again, with its running_look ({last_end, claimed}, or
|
|
181
|
+
# null: the running turn this slice saw, so a settle between two
|
|
182
|
+
# slices still ends the wait). 503 server_stopping when chi web stops
|
|
183
|
+
# mid-wait.
|
|
181
184
|
def handle_wait(req, id)
|
|
182
185
|
body = body_hash(req) or return error_response(400, "invalid_json", "invalid JSON body")
|
|
183
186
|
options, error = wait_options(body)
|
|
@@ -209,8 +212,15 @@ module Samagotchi
|
|
|
209
212
|
end
|
|
210
213
|
|
|
211
214
|
timeout = [numbers["timeout"] || WAIT_MAX_SECONDS, WAIT_MAX_SECONDS].min
|
|
215
|
+
look = body["running_look"]
|
|
216
|
+
running_look = look.nil? ? nil : ReplyWait::RunningLook.from_h(look)
|
|
217
|
+
if !look.nil? && running_look.nil?
|
|
218
|
+
return [nil, error_response(400, "invalid_wait", "running_look is {last_end, claimed: [file names]}")]
|
|
219
|
+
end
|
|
220
|
+
|
|
212
221
|
[{ cursor: strings["cursor"], turn: strings["turn"], baseline: baseline, timeout: timeout,
|
|
213
|
-
owner_grace: numbers["owner_grace"], queued: body["queued"] == true, ended_before: body["ended_before"] == true
|
|
222
|
+
owner_grace: numbers["owner_grace"], queued: body["queued"] == true, ended_before: body["ended_before"] == true,
|
|
223
|
+
running_look: running_look }, nil]
|
|
214
224
|
end
|
|
215
225
|
|
|
216
226
|
def wait_baseline(raw)
|
|
@@ -231,6 +241,7 @@ module Samagotchi
|
|
|
231
241
|
RESULT_FIELDS.each { |key| json[key] = result[key] }
|
|
232
242
|
json[:text] = json[:text]&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
233
243
|
json[:question] = result.question&.to_file
|
|
244
|
+
json[:running_look] = result.running_look&.to_h
|
|
234
245
|
json[:parent_strategy] = strategy_json(strategy)
|
|
235
246
|
json
|
|
236
247
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.112.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -843,6 +843,7 @@ files:
|
|
|
843
843
|
- lib/samagotchi/web/push/client.rb
|
|
844
844
|
- lib/samagotchi/web/push/crypto.rb
|
|
845
845
|
- lib/samagotchi/web/push/notifier.rb
|
|
846
|
+
- lib/samagotchi/web/push/presence.rb
|
|
846
847
|
- lib/samagotchi/web/push/store.rb
|
|
847
848
|
- lib/samagotchi/web/push_routes.rb
|
|
848
849
|
- lib/samagotchi/web/qr.rb
|