mailcycle 0.1.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 +7 -0
- data/LICENSE +202 -0
- data/README.md +208 -0
- data/lib/mailcycle/client.rb +803 -0
- data/lib/mailcycle/crypto/asymmetric.rb +126 -0
- data/lib/mailcycle/crypto/auth_proof.rb +58 -0
- data/lib/mailcycle/crypto/cipher.rb +136 -0
- data/lib/mailcycle/crypto/identity.rb +170 -0
- data/lib/mailcycle/crypto/mnemonic.rb +126 -0
- data/lib/mailcycle/crypto/primitives.rb +116 -0
- data/lib/mailcycle/crypto/sealed_box.rb +93 -0
- data/lib/mailcycle/crypto/wordlist.rb +171 -0
- data/lib/mailcycle/encoding.rb +93 -0
- data/lib/mailcycle/errors.rb +82 -0
- data/lib/mailcycle/events.rb +310 -0
- data/lib/mailcycle/http.rb +113 -0
- data/lib/mailcycle/keys.rb +81 -0
- data/lib/mailcycle/mail.rb +143 -0
- data/lib/mailcycle/manage.rb +202 -0
- data/lib/mailcycle/models.rb +243 -0
- data/lib/mailcycle/pairing.rb +338 -0
- data/lib/mailcycle/trackers.rb +312 -0
- data/lib/mailcycle/version.rb +5 -0
- data/lib/mailcycle/webhook_signature.rb +60 -0
- data/lib/mailcycle/websocket.rb +320 -0
- data/lib/mailcycle.rb +109 -0
- metadata +83 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailcycle
|
|
4
|
+
# API keys, webhooks and sessions.
|
|
5
|
+
#
|
|
6
|
+
# The names on keys and webhooks are sealed here under the vault key, bound
|
|
7
|
+
# to the record's kind and id, exactly as the app seals them, so the server
|
|
8
|
+
# stores none. The id is chosen here for that reason: the name has to be
|
|
9
|
+
# bound to it before the record exists.
|
|
10
|
+
#
|
|
11
|
+
# Minting and revoking keys, and adding, changing and removing webhooks, need
|
|
12
|
+
# a signed-in session. The API answers `session_required` to an API key.
|
|
13
|
+
module Names
|
|
14
|
+
API_KEY_RECORD = "api-key"
|
|
15
|
+
WEBHOOK_RECORD = "webhook"
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# A record's name, sealed as `{"name": ...}` under the vault key.
|
|
20
|
+
def seal_name(client, kind, id, name)
|
|
21
|
+
Vault.seal(client.require_keys("seal a name"), kind, id, { "name" => name })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The name, or nil if there is no phrase or it will not open.
|
|
25
|
+
#
|
|
26
|
+
# A name that will not open is shown as unreadable rather than failing the
|
|
27
|
+
# list, the same as an address label.
|
|
28
|
+
def open_name(client, kind, id, meta)
|
|
29
|
+
Wire.maybe_text(Vault.open_or_empty(client.keys, kind, id, meta)["name"])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# API keys: list, create and revoke. Session only.
|
|
34
|
+
class ApiKeys
|
|
35
|
+
def initialize(client)
|
|
36
|
+
@client = client
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The account's keys, newest first, with their names opened.
|
|
40
|
+
def list
|
|
41
|
+
Wire.array(Wire.object(@client.http.request("GET", "/api-keys"))["apiKeys"]).map { |item| to_api_key(item) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Makes a key. Operator plan and up.
|
|
45
|
+
#
|
|
46
|
+
# The token comes back once, here. Keep it somewhere private; the API
|
|
47
|
+
# holds only its hash and cannot show it again.
|
|
48
|
+
def create(name)
|
|
49
|
+
id = Vault.new_id("key", 16)
|
|
50
|
+
meta = Names.seal_name(@client, Names::API_KEY_RECORD, id, name)
|
|
51
|
+
result = Wire.object(@client.http.request("POST", "/api-keys", body: { "id" => id, "meta" => meta }))
|
|
52
|
+
CreatedApiKey.new(api_key: to_api_key(result["apiKey"]), token: Wire.text(result["token"]))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Revokes a key. It stops working at once.
|
|
56
|
+
def revoke(key_id)
|
|
57
|
+
@client.http.request("DELETE", "/api-keys/#{Mailcycle.path_segment(key_id)}")
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def to_api_key(wire)
|
|
64
|
+
wire = Wire.object(wire)
|
|
65
|
+
id = Wire.text(wire["id"])
|
|
66
|
+
ApiKey.new(
|
|
67
|
+
id: id,
|
|
68
|
+
name: Names.open_name(@client, Names::API_KEY_RECORD, id, wire["meta"]),
|
|
69
|
+
hint: Wire.text(wire["hint"]),
|
|
70
|
+
created_at: Wire.text(wire["createdAt"]),
|
|
71
|
+
last_used_at: Wire.maybe_text(wire["lastUsedAt"])
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Webhooks, Operator and up.
|
|
77
|
+
#
|
|
78
|
+
# A session can do everything. An API key can list, read deliveries, send a
|
|
79
|
+
# test and redeliver.
|
|
80
|
+
class Webhooks
|
|
81
|
+
def initialize(client)
|
|
82
|
+
@client = client
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The account's webhooks, with their names opened, and every event type
|
|
86
|
+
# one can subscribe to.
|
|
87
|
+
def list
|
|
88
|
+
result = Wire.object(@client.http.request("GET", "/webhook-endpoints"))
|
|
89
|
+
WebhookList.new(webhooks: Wire.array(result["webhooks"]).map { |item| to_webhook(item) },
|
|
90
|
+
event_types: Wire.strings(result["eventTypes"]))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Adds a webhook. `events` are event types, or `["*"]` for all of them.
|
|
94
|
+
#
|
|
95
|
+
# The signing secret comes back once, here. Keep it to check deliveries
|
|
96
|
+
# with `Mailcycle.verify_webhook_signature`.
|
|
97
|
+
def create(url, events, name)
|
|
98
|
+
id = Vault.new_id("whk", 16)
|
|
99
|
+
meta = Names.seal_name(@client, Names::WEBHOOK_RECORD, id, name)
|
|
100
|
+
body = { "id" => id, "url" => url, "events" => events, "meta" => meta }
|
|
101
|
+
result = Wire.object(@client.http.request("POST", "/webhook-endpoints", body: body))
|
|
102
|
+
CreatedWebhook.new(webhook: to_webhook(result["webhook"]), secret: Wire.text(result["secret"]))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Changes what is given and leaves the rest. `enabled` switches it on or
|
|
106
|
+
# off.
|
|
107
|
+
def update(webhook_id, url: nil, events: nil, name: nil, enabled: nil)
|
|
108
|
+
body = {}
|
|
109
|
+
body["url"] = url unless url.nil?
|
|
110
|
+
body["events"] = events unless events.nil?
|
|
111
|
+
body["meta"] = Names.seal_name(@client, Names::WEBHOOK_RECORD, webhook_id, name) unless name.nil?
|
|
112
|
+
body["status"] = enabled ? "active" : "disabled" unless enabled.nil?
|
|
113
|
+
to_webhook(Wire.object(@client.http.request("PATCH", path(webhook_id), body: body))["webhook"])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Removes the webhook and its delivery log.
|
|
117
|
+
def delete(webhook_id)
|
|
118
|
+
@client.http.request("DELETE", path(webhook_id))
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A new signing secret, returned once. The old one stops at once.
|
|
123
|
+
def rotate_secret(webhook_id)
|
|
124
|
+
Wire.text(Wire.object(@client.http.request("POST", "#{path(webhook_id)}/rotate-secret", body: {}))["secret"])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Sends a `webhook.test` event to it.
|
|
128
|
+
def test(webhook_id)
|
|
129
|
+
queued(@client.http.request("POST", "#{path(webhook_id)}/test", body: {}))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The delivery log, newest first.
|
|
133
|
+
#
|
|
134
|
+
# `cursor` is the `next_cursor` of the page before, passed back as it is.
|
|
135
|
+
# `limit` is up to 200, 50 when it is not given.
|
|
136
|
+
def deliveries(webhook_id, cursor: nil, limit: nil)
|
|
137
|
+
query = []
|
|
138
|
+
query << ["cursor", cursor] if cursor
|
|
139
|
+
query << ["limit", limit.to_s] if limit
|
|
140
|
+
result = Wire.object(@client.http.request("GET", "#{path(webhook_id)}/deliveries", query: query))
|
|
141
|
+
DeliveryPage.new(deliveries: Wire.array(result["deliveries"]).map { |item| WebhookDelivery.from_wire(item) },
|
|
142
|
+
next_cursor: Wire.maybe_text(result["nextCursor"]))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Sends a delivery's event again, with the same event id.
|
|
146
|
+
def redeliver(webhook_id, delivery_id)
|
|
147
|
+
route = "#{path(webhook_id)}/deliveries/#{Mailcycle.path_segment(delivery_id)}/redeliver"
|
|
148
|
+
queued(@client.http.request("POST", route, body: {}))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def path(id)
|
|
154
|
+
"/webhook-endpoints/#{Mailcycle.path_segment(id)}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def queued(result)
|
|
158
|
+
result = Wire.object(result)
|
|
159
|
+
QueuedDelivery.new(delivery_id: Wire.text(result["deliveryId"]), event_id: Wire.maybe_text(result["eventId"]))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def to_webhook(wire)
|
|
163
|
+
wire = Wire.object(wire)
|
|
164
|
+
id = Wire.text(wire["id"])
|
|
165
|
+
Webhook.new(
|
|
166
|
+
id: id,
|
|
167
|
+
name: Names.open_name(@client, Names::WEBHOOK_RECORD, id, wire["meta"]),
|
|
168
|
+
url: Wire.text(wire["url"]),
|
|
169
|
+
events: Wire.strings(wire["events"]),
|
|
170
|
+
status: Wire.text(wire["status"]),
|
|
171
|
+
disabled_reason: Wire.maybe_text(wire["disabledReason"]),
|
|
172
|
+
failing_since: Wire.maybe_text(wire["failingSince"]),
|
|
173
|
+
last_success_at: Wire.maybe_text(wire["lastSuccessAt"]),
|
|
174
|
+
created_at: Wire.text(wire["createdAt"])
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Signed-in sessions. Session only.
|
|
180
|
+
class Sessions
|
|
181
|
+
def initialize(client)
|
|
182
|
+
@client = client
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The account's live sessions, newest first. `current` marks this one.
|
|
186
|
+
def list
|
|
187
|
+
Wire.array(Wire.object(@client.http.request("GET", "/accounts/sessions"))["sessions"])
|
|
188
|
+
.map { |item| Session.from_wire(item) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Ends one session.
|
|
192
|
+
def revoke(session_id)
|
|
193
|
+
@client.http.request("DELETE", "/accounts/sessions/#{Mailcycle.path_segment(session_id)}")
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Ends every session but this one. Returns how many ended.
|
|
198
|
+
def revoke_others
|
|
199
|
+
Wire.count(Wire.object(@client.http.request("DELETE", "/accounts/sessions"))["revoked"])
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailcycle
|
|
4
|
+
# The shapes a caller sees.
|
|
5
|
+
#
|
|
6
|
+
# Each one carries the fields the API documents. Where the API's record has
|
|
7
|
+
# room for more (a plan, a domain), anything it adds later is kept in `extra`
|
|
8
|
+
# rather than dropped, so a new server field does not need a new release to
|
|
9
|
+
# reach a caller.
|
|
10
|
+
module Wire
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Reading a server's JSON, which may be any shape at all: the value as the
|
|
14
|
+
# type asked for, or the fallback.
|
|
15
|
+
def object(value)
|
|
16
|
+
value.is_a?(Hash) ? value : {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def array(value)
|
|
20
|
+
value.is_a?(Array) ? value : []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def text(value, fallback = "")
|
|
24
|
+
value.is_a?(String) ? value : fallback
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def maybe_text(value)
|
|
28
|
+
value.is_a?(String) ? value : nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def strings(value)
|
|
32
|
+
array(value).grep(String)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def count(value)
|
|
36
|
+
value.is_a?(Integer) && value >= 0 ? value : 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def flag(value, fallback)
|
|
40
|
+
[true, false].include?(value) ? value : fallback
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Plan = Struct.new(:id, :name, :extra, keyword_init: true) do
|
|
45
|
+
def self.from_wire(wire)
|
|
46
|
+
wire = Wire.object(wire)
|
|
47
|
+
new(id: Wire.text(wire["id"]), name: Wire.text(wire["name"]), extra: wire.except("id", "name"))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# `plan_id` is the account's own plan id, and `plan` is that plan's record.
|
|
52
|
+
Account = Struct.new(:id, :plan_id, :created_at, :plan, keyword_init: true)
|
|
53
|
+
|
|
54
|
+
# An email address on the account.
|
|
55
|
+
#
|
|
56
|
+
# `status` is `active` when assigned to a device, `unassigned` otherwise.
|
|
57
|
+
# `label` is the label set in the app, nil without the phrase or if it will
|
|
58
|
+
# not open. `sender_name` is the name mail from the address goes out under,
|
|
59
|
+
# nil without the phrase or if none is set. `receives` says whether the
|
|
60
|
+
# address can receive mail: it was created with its public key.
|
|
61
|
+
Address = Struct.new(:id, :email_address, :status, :created_at, :retention_days, :assigned_worker_id,
|
|
62
|
+
:paused_at, :label, :sender_name, :receives, keyword_init: true)
|
|
63
|
+
|
|
64
|
+
# `reserved` is true for a domain that belongs to this account alone.
|
|
65
|
+
Domain = Struct.new(:domain, :reserved, :extra, keyword_init: true) do
|
|
66
|
+
def self.from_wire(wire)
|
|
67
|
+
wire = Wire.object(wire)
|
|
68
|
+
new(domain: Wire.text(wire["domain"]), reserved: Wire.flag(wire["reserved"], false),
|
|
69
|
+
extra: wire.except("domain", "reserved"))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# One file on a received message.
|
|
74
|
+
#
|
|
75
|
+
# `content_id` is set on a part the HTML shows inline, where it says `cid:`
|
|
76
|
+
# and this id. `stored` is false when the part had no body; fetching it would
|
|
77
|
+
# answer 404.
|
|
78
|
+
Attachment = Struct.new(:index, :filename, :mime_type, :size, :content_id, :stored, keyword_init: true)
|
|
79
|
+
|
|
80
|
+
# A message, opened on this machine.
|
|
81
|
+
#
|
|
82
|
+
# - `sent_via`: for sent mail, `app`, `api`, `device`, `agent` or `member`
|
|
83
|
+
# (a team member).
|
|
84
|
+
# - `key_epoch`: the address key epoch it was sealed under. 0 unless the
|
|
85
|
+
# address's keys were rotated.
|
|
86
|
+
# - `opened`: false when the content could not be opened: no phrase, or the
|
|
87
|
+
# wrong one.
|
|
88
|
+
# - `reply_to`: where a reply goes: Reply-To if the sender set one,
|
|
89
|
+
# otherwise From.
|
|
90
|
+
# - `html`: the HTML part as sent, remote images and all. Do not render this
|
|
91
|
+
# one.
|
|
92
|
+
# - `safe_html`: the HTML with everything remote removed, as the app shows
|
|
93
|
+
# it. `blocked` is what it had taken out of it, and who it would have told.
|
|
94
|
+
# - `message_id`: the sender's Message-ID, for threading a reply.
|
|
95
|
+
Message = Struct.new(:id, :address_id, :received_at, :read, :starred, :folder, :size_bytes, :sent_via,
|
|
96
|
+
:key_epoch, :opened, :from_address, :from_name, :reply_to, :to, :subject, :text, :html,
|
|
97
|
+
:safe_html, :blocked, :message_id, :attachments, keyword_init: true)
|
|
98
|
+
|
|
99
|
+
# `next_cursor` is opaque. Pass it back as it is to get the next page; nil
|
|
100
|
+
# on the last.
|
|
101
|
+
MessagePage = Struct.new(:messages, :next_cursor, keyword_init: true)
|
|
102
|
+
|
|
103
|
+
# What a send returns. `message_id` is the Message-ID the sending service
|
|
104
|
+
# gave it. `sent_copy_id` is the id of the copy kept in Sent, which the
|
|
105
|
+
# message calls take; nil when no copy was kept, as for an address without a
|
|
106
|
+
# public key.
|
|
107
|
+
SendResult = Struct.new(:message_id, :sent_copy_id, keyword_init: true)
|
|
108
|
+
|
|
109
|
+
# A paired device or agent. `name` and `tags` are what was set in the app;
|
|
110
|
+
# nil and empty without the phrase.
|
|
111
|
+
Device = Struct.new(:id, :platform, :kind, :send_mode, :status, :created_at, :last_active_at, :paused_at,
|
|
112
|
+
:name, :tags, keyword_init: true)
|
|
113
|
+
|
|
114
|
+
# One account event from the log: ids and times, never content.
|
|
115
|
+
ActivityEntry = Struct.new(:id, :kind, :created_at, :address_id, :worker_id, keyword_init: true) do
|
|
116
|
+
def self.from_wire(wire)
|
|
117
|
+
wire = Wire.object(wire)
|
|
118
|
+
new(id: Wire.text(wire["id"]), kind: Wire.text(wire["kind"]), created_at: Wire.text(wire["createdAt"]),
|
|
119
|
+
address_id: Wire.maybe_text(wire["inboxId"]), worker_id: Wire.maybe_text(wire["workerId"]))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# One page of the event log, newest first. `next_cursor` is opaque: pass it
|
|
124
|
+
# back as it is to get the page before; nil on the last.
|
|
125
|
+
ActivityPage = Struct.new(:activity, :next_cursor, keyword_init: true)
|
|
126
|
+
|
|
127
|
+
# One event from the live stream. Events carry ids, never content.
|
|
128
|
+
AccountEvent = Struct.new(:id, :event_type, :created_at, :payload, keyword_init: true) do
|
|
129
|
+
# The event a wire value describes, or nil if it is not one.
|
|
130
|
+
def self.from_wire(wire)
|
|
131
|
+
return nil unless wire.is_a?(Hash)
|
|
132
|
+
|
|
133
|
+
id, type, created = wire.values_at("id", "type", "createdAt")
|
|
134
|
+
return nil unless [id, type, created].all? { |field| field.nil? || field.is_a?(String) }
|
|
135
|
+
|
|
136
|
+
payload = wire["payload"]
|
|
137
|
+
return nil unless payload.nil? || payload.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
new(id: id || "", event_type: type || "", created_at: created || "", payload: payload || {})
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# An API key. The token itself is shown once, when it is created.
|
|
144
|
+
#
|
|
145
|
+
# `name` is the name set when it was made, opened with the phrase; nil if it
|
|
146
|
+
# will not open. `hint` is the token's last four characters. `last_used_at`
|
|
147
|
+
# is rounded down to five minutes, and nil until the key is used.
|
|
148
|
+
ApiKey = Struct.new(:id, :name, :hint, :created_at, :last_used_at, keyword_init: true)
|
|
149
|
+
|
|
150
|
+
# A new API key and its token. The token is not shown again.
|
|
151
|
+
CreatedApiKey = Struct.new(:api_key, :token, keyword_init: true)
|
|
152
|
+
|
|
153
|
+
# A webhook. `name` is opened with the phrase, nil if it will not open.
|
|
154
|
+
# `events` are event types, or `["*"]` for every event. `status` is `active`
|
|
155
|
+
# or `disabled`. `failing_since` is set while every attempt since the last
|
|
156
|
+
# success has failed.
|
|
157
|
+
Webhook = Struct.new(:id, :name, :url, :events, :status, :disabled_reason, :failing_since, :last_success_at,
|
|
158
|
+
:created_at, keyword_init: true)
|
|
159
|
+
|
|
160
|
+
# The account's webhooks, and every event type one can ask for.
|
|
161
|
+
WebhookList = Struct.new(:webhooks, :event_types, keyword_init: true)
|
|
162
|
+
|
|
163
|
+
# A new webhook and its signing secret. The secret is not shown again.
|
|
164
|
+
CreatedWebhook = Struct.new(:webhook, :secret, keyword_init: true)
|
|
165
|
+
|
|
166
|
+
# One attempt to deliver an event to a webhook.
|
|
167
|
+
#
|
|
168
|
+
# `event` is the body as sent: type, ids and times, never content. `status`
|
|
169
|
+
# is `pending`, `succeeded` or `failed`.
|
|
170
|
+
WebhookDelivery = Struct.new(:id, :event_id, :event_type, :event, :status, :attempts, :response_status, :error,
|
|
171
|
+
:created_at, :last_attempt_at, :next_attempt_at, keyword_init: true) do
|
|
172
|
+
def self.from_wire(wire)
|
|
173
|
+
wire = Wire.object(wire)
|
|
174
|
+
status = wire["responseStatus"]
|
|
175
|
+
new(
|
|
176
|
+
id: Wire.text(wire["id"]),
|
|
177
|
+
event_id: Wire.text(wire["eventId"]),
|
|
178
|
+
event_type: Wire.text(wire["eventType"]),
|
|
179
|
+
event: AccountEvent.from_wire(wire["event"]) || AccountEvent.new(id: "", event_type: "", created_at: "",
|
|
180
|
+
payload: {}),
|
|
181
|
+
status: Wire.text(wire["status"]),
|
|
182
|
+
attempts: Wire.count(wire["attempts"]),
|
|
183
|
+
response_status: status.is_a?(Integer) ? status : nil,
|
|
184
|
+
error: Wire.maybe_text(wire["error"]),
|
|
185
|
+
created_at: Wire.text(wire["createdAt"]),
|
|
186
|
+
last_attempt_at: Wire.maybe_text(wire["lastAttemptAt"]),
|
|
187
|
+
next_attempt_at: Wire.maybe_text(wire["nextAttemptAt"])
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# `next_cursor` is opaque: pass it back as it is to get the next page; nil on
|
|
193
|
+
# the last.
|
|
194
|
+
DeliveryPage = Struct.new(:deliveries, :next_cursor, keyword_init: true)
|
|
195
|
+
|
|
196
|
+
# The delivery a test or a redelivery queued. `event_id` is set for a test,
|
|
197
|
+
# which makes a new event. A redelivery sends the original event again, id
|
|
198
|
+
# and all.
|
|
199
|
+
QueuedDelivery = Struct.new(:delivery_id, :event_id, keyword_init: true)
|
|
200
|
+
|
|
201
|
+
# A signed-in session on the account. `current` is true for the session this
|
|
202
|
+
# client is using.
|
|
203
|
+
Session = Struct.new(:id, :created_at, :expires_at, :current, keyword_init: true) do
|
|
204
|
+
def self.from_wire(wire)
|
|
205
|
+
wire = Wire.object(wire)
|
|
206
|
+
new(id: Wire.text(wire["id"]), created_at: Wire.text(wire["createdAt"]),
|
|
207
|
+
expires_at: Wire.text(wire["expiresAt"]), current: Wire.flag(wire["current"], false))
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The account's subscription, balance, and any pause after a missed payment.
|
|
212
|
+
#
|
|
213
|
+
# `pending_plan_id` is the plan the account moves to when the paid month runs
|
|
214
|
+
# out. `balance_minor` is the account balance, in minor units. `pause` is
|
|
215
|
+
# what a missed payment has paused, and when it is deleted; nil when nothing
|
|
216
|
+
# is paused.
|
|
217
|
+
Subscription = Struct.new(:id, :plan_id, :status, :renewal_date, :pending_plan_id, :balance_minor, :pause,
|
|
218
|
+
keyword_init: true)
|
|
219
|
+
|
|
220
|
+
# The kind of name the server rolls for a new address.
|
|
221
|
+
module NameStyle
|
|
222
|
+
# Like `maya.holt`.
|
|
223
|
+
PERSON = "person"
|
|
224
|
+
# Like `harbor-supply42`.
|
|
225
|
+
BUSINESS = "business"
|
|
226
|
+
# Like `support_desk`.
|
|
227
|
+
TEAM = "team"
|
|
228
|
+
# Like `quiet_falcon`. What the server rolls when no style is given.
|
|
229
|
+
NEUTRAL = "neutral"
|
|
230
|
+
ALL = [PERSON, BUSINESS, TEAM, NEUTRAL].freeze
|
|
231
|
+
|
|
232
|
+
# The name the API uses, from a symbol or a string.
|
|
233
|
+
def self.wire(style)
|
|
234
|
+
name = style.to_s
|
|
235
|
+
unless ALL.include?(name)
|
|
236
|
+
raise ArgumentError,
|
|
237
|
+
"Unknown name style #{style.inspect}: use one of #{ALL.join(', ')}."
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
name
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|