runearc 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 +21 -0
- data/README.md +369 -0
- data/lib/runearc/client.rb +265 -0
- data/lib/runearc/errors.rb +163 -0
- data/lib/runearc/resources.rb +396 -0
- data/lib/runearc/support.rb +18 -0
- data/lib/runearc/version.rb +5 -0
- data/lib/runearc/webhook.rb +123 -0
- data/lib/runearc.rb +15 -0
- data/sig/runearc.rbs +171 -0
- metadata +60 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Runearc
|
|
4
|
+
# Every failure the SDK reports. API errors carry the API's +code+, +message+ and +fix+;
|
|
5
|
+
# failures before a response arrives use +status+ 0 and the codes +network_error+,
|
|
6
|
+
# +timeout+, +missing_api_key+ or, for webhooks, +invalid_signature+ and friends.
|
|
7
|
+
#
|
|
8
|
+
# Rescue +Runearc::Error+ for everything, or one of its subclasses:
|
|
9
|
+
#
|
|
10
|
+
# Error
|
|
11
|
+
# +-- ConfigurationError no API key (status 0)
|
|
12
|
+
# +-- ConnectionError no response arrived (status 0)
|
|
13
|
+
# | +-- TimeoutError
|
|
14
|
+
# +-- WebhookVerificationError not a genuine, fresh Runearc webhook (status 0)
|
|
15
|
+
# +-- ApiError the API answered with an error
|
|
16
|
+
# +-- InvalidRequestError 400
|
|
17
|
+
# +-- AuthenticationError 401
|
|
18
|
+
# +-- LimitReachedError 402
|
|
19
|
+
# +-- PermissionError 403
|
|
20
|
+
# +-- NotFoundError 404
|
|
21
|
+
# +-- ConflictError 409
|
|
22
|
+
# +-- RateLimitError 429
|
|
23
|
+
# +-- ServerError 5xx
|
|
24
|
+
class Error < StandardError
|
|
25
|
+
# @return [Integer] the HTTP status, or 0 when no response arrived
|
|
26
|
+
attr_reader :status
|
|
27
|
+
|
|
28
|
+
# @return [String] a stable machine readable code, such as +"invalid_from"+ or +"rate_limited"+
|
|
29
|
+
attr_reader :code
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] the error kind: +"invalid"+, +"unauthorized"+, +"limit_reached"+, +"forbidden"+,
|
|
32
|
+
# +"not_found"+, +"conflict"+, +"rate_limited"+, +"unavailable"+ or +"internal"+ (derived from the
|
|
33
|
+
# status when the API sends none), or +"connection"+ when no response arrived
|
|
34
|
+
attr_reader :kind
|
|
35
|
+
|
|
36
|
+
# @return [String, nil] what to do about it, when there is something you can do
|
|
37
|
+
attr_reader :fix
|
|
38
|
+
|
|
39
|
+
# @return [Array<Hash>, nil] +invalid_batch+ only: every refused item (+:index+, +:code+, +:message+, +:fix+)
|
|
40
|
+
attr_reader :errors
|
|
41
|
+
|
|
42
|
+
# @return [Integer, nil] seconds the API asked us to wait (+Retry-After+), when it sent one
|
|
43
|
+
attr_reader :retry_after
|
|
44
|
+
|
|
45
|
+
# @param message [String, nil]
|
|
46
|
+
# @param status [Integer]
|
|
47
|
+
# @param code [String]
|
|
48
|
+
# @param kind [String, nil]
|
|
49
|
+
# @param fix [String, nil]
|
|
50
|
+
# @param errors [Array<Hash>, nil]
|
|
51
|
+
# @param retry_after [Integer, nil]
|
|
52
|
+
def initialize(message = nil, status: 0, code: "error", kind: nil, fix: nil, errors: nil, retry_after: nil)
|
|
53
|
+
super(message || code)
|
|
54
|
+
@status = status
|
|
55
|
+
@code = code
|
|
56
|
+
@kind = kind
|
|
57
|
+
@fix = fix
|
|
58
|
+
@errors = errors
|
|
59
|
+
@retry_after = retry_after
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Hash] the error as plain data, for logging or a JSON response
|
|
63
|
+
def to_h
|
|
64
|
+
{ name: self.class.name, status: status, code: code, kind: kind, message: message, fix: fix,
|
|
65
|
+
errors: errors }.compact
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# No API key was given and +RUNEARC_API_KEY+ is not set. Code +missing_api_key+.
|
|
70
|
+
class ConfigurationError < Error
|
|
71
|
+
def initialize(message = nil, **fields)
|
|
72
|
+
super(message, code: "missing_api_key", kind: "invalid", **fields)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# No response arrived: the network failed. Code +network_error+.
|
|
77
|
+
class ConnectionError < Error
|
|
78
|
+
def initialize(message = nil, **fields)
|
|
79
|
+
super(message, code: "network_error", kind: "connection", **fields)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# No response arrived within the per attempt timeout. Code +timeout+.
|
|
84
|
+
class TimeoutError < ConnectionError
|
|
85
|
+
def initialize(message = nil, **fields)
|
|
86
|
+
super(message, code: "timeout", **fields)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Raised by {Webhook.verify} when a payload is not a genuine, fresh Runearc webhook.
|
|
91
|
+
class WebhookVerificationError < Error
|
|
92
|
+
def initialize(message = nil, **fields)
|
|
93
|
+
super(message, kind: "invalid", **fields)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The API answered with an error status.
|
|
98
|
+
class ApiError < Error
|
|
99
|
+
# Builds the error class for a failed response, chosen by its status.
|
|
100
|
+
#
|
|
101
|
+
# @param message [String]
|
|
102
|
+
# @param status [Integer]
|
|
103
|
+
# @return [ApiError]
|
|
104
|
+
def self.build(message, status:, **fields)
|
|
105
|
+
kind = fields.delete(:kind) || default_kind(status)
|
|
106
|
+
class_for(status).new(message, status: status, kind: kind, **fields)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.class_for(status)
|
|
110
|
+
case status
|
|
111
|
+
when 400 then InvalidRequestError
|
|
112
|
+
when 401 then AuthenticationError
|
|
113
|
+
when 402 then LimitReachedError
|
|
114
|
+
when 403 then PermissionError
|
|
115
|
+
when 404 then NotFoundError
|
|
116
|
+
when 409 then ConflictError
|
|
117
|
+
when 429 then RateLimitError
|
|
118
|
+
when 500.. then ServerError
|
|
119
|
+
else ApiError
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.default_kind(status)
|
|
124
|
+
case status
|
|
125
|
+
when 401 then "unauthorized"
|
|
126
|
+
when 402 then "limit_reached"
|
|
127
|
+
when 403 then "forbidden"
|
|
128
|
+
when 404 then "not_found"
|
|
129
|
+
when 409 then "conflict"
|
|
130
|
+
when 429 then "rate_limited"
|
|
131
|
+
when 503 then "unavailable"
|
|
132
|
+
when 500.. then "internal"
|
|
133
|
+
else "invalid"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private_class_method :class_for, :default_kind
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 400: the request was refused as invalid.
|
|
141
|
+
class InvalidRequestError < ApiError; end
|
|
142
|
+
|
|
143
|
+
# 401: the API key is missing, wrong or revoked.
|
|
144
|
+
class AuthenticationError < ApiError; end
|
|
145
|
+
|
|
146
|
+
# 402: a plan limit was reached.
|
|
147
|
+
class LimitReachedError < ApiError; end
|
|
148
|
+
|
|
149
|
+
# 403: the key may not do this.
|
|
150
|
+
class PermissionError < ApiError; end
|
|
151
|
+
|
|
152
|
+
# 404: no such resource.
|
|
153
|
+
class NotFoundError < ApiError; end
|
|
154
|
+
|
|
155
|
+
# 409: the request conflicts with the current state, for example a reused idempotency key.
|
|
156
|
+
class ConflictError < ApiError; end
|
|
157
|
+
|
|
158
|
+
# 429: rate limited. {Error#retry_after} has the wait the API asked for.
|
|
159
|
+
class RateLimitError < ApiError; end
|
|
160
|
+
|
|
161
|
+
# 5xx: the API failed.
|
|
162
|
+
class ServerError < ApiError; end
|
|
163
|
+
end
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Runearc
|
|
4
|
+
# @api private
|
|
5
|
+
class Resource
|
|
6
|
+
def initialize(client)
|
|
7
|
+
@client = client
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def call(method, path, **options)
|
|
13
|
+
@client.request(method, path, **options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def seg(value)
|
|
17
|
+
Support.segment(value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# `scheduled_at` may be a Time, an RFC 3339 string or the relative form (`in 1 hour`).
|
|
21
|
+
def email_body(email)
|
|
22
|
+
return email unless email.key?(:scheduled_at)
|
|
23
|
+
|
|
24
|
+
email.merge(scheduled_at: Support.time(email[:scheduled_at]))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sending, reading, scheduling and canceling email.
|
|
29
|
+
#
|
|
30
|
+
# Every method also takes +request_timeout:+ (seconds); the ones that change something take
|
|
31
|
+
# +idempotency_key:+ where noted. +deliver+ and +batch+ get a random key when you give none.
|
|
32
|
+
class Emails < Resource
|
|
33
|
+
# Sends one email (or schedules it with +scheduled_at+). Named +deliver+ because +send+ is taken in Ruby.
|
|
34
|
+
#
|
|
35
|
+
# @param email [Hash] the email as the API takes it: +:from+, +:to+, +:subject+, and optionally +:cc+,
|
|
36
|
+
# +:bcc+, +:reply_to+, +:html+, +:text+, +:headers+, +:attachments+, +:tags+ and +:scheduled_at+
|
|
37
|
+
# (a +Time+, an RFC 3339 string with an offset, or +"in 1 hour"+; at most 30 days ahead)
|
|
38
|
+
# @param idempotency_key [String, nil] makes the request safe to repeat: the API returns the first
|
|
39
|
+
# result for the same key for 24 hours
|
|
40
|
+
# @param request_timeout [Numeric, nil]
|
|
41
|
+
# @return [Hash] +{ id: "eml_..." }+
|
|
42
|
+
def deliver(idempotency_key: nil, request_timeout: nil, **email)
|
|
43
|
+
call(:post, "/v1/emails", body: email_body(email), sends_mail: true,
|
|
44
|
+
idempotency_key: idempotency_key, request_timeout: request_timeout)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sends up to 100 emails in one request.
|
|
48
|
+
#
|
|
49
|
+
# @param emails [Array<Hash>] each item has the shape of {#deliver}
|
|
50
|
+
# @param batch_validation [String, Symbol, nil] +:strict+ (default): one invalid item refuses the whole
|
|
51
|
+
# batch with +invalid_batch+. +:permissive+: valid items send, invalid ones come back in +:errors+.
|
|
52
|
+
# @param idempotency_key [String, nil]
|
|
53
|
+
# @param request_timeout [Numeric, nil]
|
|
54
|
+
# @return [Hash] +{ data: [{ id: }], errors: [...] }+ (+errors+ only when permissive and some were refused)
|
|
55
|
+
def batch(emails, batch_validation: nil, idempotency_key: nil, request_timeout: nil)
|
|
56
|
+
headers = batch_validation ? { "x-batch-validation" => batch_validation.to_s } : nil
|
|
57
|
+
call(:post, "/v1/emails/batch", body: emails.map { |e| email_body(e) }, headers: headers, sends_mail: true,
|
|
58
|
+
idempotency_key: idempotency_key, request_timeout: request_timeout)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param id [String] an email id (+eml_...+)
|
|
62
|
+
# @return [Hash] the email with +:status+, +:last_event+, +:recipients+, +:html+, +:text+
|
|
63
|
+
def get(id, request_timeout: nil)
|
|
64
|
+
call(:get, "/v1/emails/#{seg(id)}", request_timeout: request_timeout)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param limit [Integer, nil] 1 to 100, default 50
|
|
68
|
+
# @param cursor [String, nil] the +:next_cursor+ of the previous page
|
|
69
|
+
# @return [Hash] +{ data: [...], next_cursor: }+; +next_cursor+ is nil on the last page
|
|
70
|
+
def list(limit: nil, cursor: nil, request_timeout: nil)
|
|
71
|
+
call(:get, "/v1/emails", query: { limit: limit, cursor: cursor }, request_timeout: request_timeout)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Changes when a scheduled email sends.
|
|
75
|
+
#
|
|
76
|
+
# @param id [String]
|
|
77
|
+
# @param scheduled_at [Time, String] a +Time+, an RFC 3339 string with an offset, or +"in 1 hour"+
|
|
78
|
+
# @return [Hash] +{ object: "email", id: }+
|
|
79
|
+
def update(id, scheduled_at:, request_timeout: nil)
|
|
80
|
+
call(:patch, "/v1/emails/#{seg(id)}", body: { scheduled_at: Support.time(scheduled_at) },
|
|
81
|
+
request_timeout: request_timeout)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Shorthand for +update(id, scheduled_at: ...)+.
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash]
|
|
87
|
+
def reschedule(id, scheduled_at, request_timeout: nil)
|
|
88
|
+
update(id, scheduled_at: scheduled_at, request_timeout: request_timeout)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Cancels a scheduled email.
|
|
92
|
+
#
|
|
93
|
+
# @return [Hash] +{ object: "email", id: }+
|
|
94
|
+
def cancel(id, request_timeout: nil)
|
|
95
|
+
call(:post, "/v1/emails/#{seg(id)}/cancel", idempotent: true, request_timeout: request_timeout)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The addresses you can send from.
|
|
100
|
+
class Senders < Resource
|
|
101
|
+
# @return [Hash] +{ data: [{ address:, domain:, verified:, ... }] }+
|
|
102
|
+
def list(request_timeout: nil)
|
|
103
|
+
call(:get, "/v1/senders", request_timeout: request_timeout)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Inboxes receive mail. Give one to an agent.
|
|
108
|
+
class Inboxes < Resource
|
|
109
|
+
# @param local_part [String, nil] random when nil
|
|
110
|
+
# @param display_name [String, nil]
|
|
111
|
+
# @param domain [String, nil] a custom domain of the workspace; the shared domain when nil
|
|
112
|
+
# @return [Hash] the inbox
|
|
113
|
+
def create(local_part: nil, display_name: nil, domain: nil, request_timeout: nil)
|
|
114
|
+
body = { local_part: local_part, display_name: display_name, domain: domain }.compact
|
|
115
|
+
call(:post, "/v1/inboxes", body: body, request_timeout: request_timeout)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @return [Hash] +{ data: [...] }+
|
|
119
|
+
def list(request_timeout: nil)
|
|
120
|
+
call(:get, "/v1/inboxes", request_timeout: request_timeout)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @param id [String] an inbox id (+ibx_...+)
|
|
124
|
+
# @return [Hash]
|
|
125
|
+
def get(id, request_timeout: nil)
|
|
126
|
+
call(:get, "/v1/inboxes/#{seg(id)}", request_timeout: request_timeout)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [nil]
|
|
130
|
+
def delete(id, request_timeout: nil)
|
|
131
|
+
call(:delete, "/v1/inboxes/#{seg(id)}", request_timeout: request_timeout)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Messages of an inbox: read, wait for, send and reply.
|
|
136
|
+
class Messages < Resource
|
|
137
|
+
# Messages of an inbox, newest first.
|
|
138
|
+
#
|
|
139
|
+
# @param inbox_id [String]
|
|
140
|
+
# @param limit [Integer, nil] 1 to 100, default 50
|
|
141
|
+
# @param cursor [String, nil]
|
|
142
|
+
# @return [Hash] +{ data: [...], next_cursor: }+
|
|
143
|
+
def list(inbox_id, limit: nil, cursor: nil, request_timeout: nil)
|
|
144
|
+
call(:get, "/v1/inboxes/#{seg(inbox_id)}/messages", query: { limit: limit, cursor: cursor },
|
|
145
|
+
request_timeout: request_timeout)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @param id [String] a message id (+msg_...+)
|
|
149
|
+
# @return [Hash]
|
|
150
|
+
def get(id, request_timeout: nil)
|
|
151
|
+
call(:get, "/v1/messages/#{seg(id)}", request_timeout: request_timeout)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The message as received (RFC 822).
|
|
155
|
+
#
|
|
156
|
+
# @return [String]
|
|
157
|
+
def raw(id, request_timeout: nil)
|
|
158
|
+
call(:get, "/v1/messages/#{seg(id)}/raw", text: true, request_timeout: request_timeout)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Blocks until a matching message arrives (up to +timeout+ seconds, max 120) and returns it
|
|
162
|
+
# with the codes and links found in it. Returns nil when nothing arrived in time; calling
|
|
163
|
+
# again is safe.
|
|
164
|
+
#
|
|
165
|
+
# @param inbox_id [String]
|
|
166
|
+
# @param from [String, nil] case insensitive substring of the sender address, such as +"github.com"+
|
|
167
|
+
# @param subject [String, nil] case insensitive substring of the subject
|
|
168
|
+
# @param to [String, nil] a recipient address, matched exactly
|
|
169
|
+
# @param timeout [Integer] seconds to wait, 1 to 120 (default 30)
|
|
170
|
+
# @param after [Time, String, nil] only messages received after this time; defaults to when the wait starts
|
|
171
|
+
# @param request_timeout [Numeric, nil] defaults to +timeout+ plus 15 seconds
|
|
172
|
+
# @return [Hash, nil] +{ message:, codes:, links: }+
|
|
173
|
+
def wait(inbox_id, from: nil, subject: nil, to: nil, timeout: 30, after: nil, request_timeout: nil)
|
|
174
|
+
call(:get, "/v1/inboxes/#{seg(inbox_id)}/wait",
|
|
175
|
+
query: { from: from, subject: subject, to: to, timeout: timeout, after: after },
|
|
176
|
+
request_timeout: request_timeout || (timeout + 15))
|
|
177
|
+
rescue ApiError => e
|
|
178
|
+
raise unless e.code == "wait_timeout"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Sends a new message from an inbox. Named +deliver+ because +send+ is taken in Ruby.
|
|
182
|
+
#
|
|
183
|
+
# @param inbox_id [String]
|
|
184
|
+
# @param message [Hash] +:to+, +:subject+, and optionally +:cc+, +:bcc+, +:text+, +:html+, +:reply_to+,
|
|
185
|
+
# +:headers+, +:attachments+
|
|
186
|
+
# @param idempotency_key [String, nil]
|
|
187
|
+
# @return [Hash] the message
|
|
188
|
+
def deliver(inbox_id, idempotency_key: nil, request_timeout: nil, **message)
|
|
189
|
+
call(:post, "/v1/inboxes/#{seg(inbox_id)}/messages", body: message, sends_mail: true,
|
|
190
|
+
idempotency_key: idempotency_key,
|
|
191
|
+
request_timeout: request_timeout)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Replies in the thread of a message.
|
|
195
|
+
#
|
|
196
|
+
# @param message_id [String]
|
|
197
|
+
# @param reply [Hash] +:text+, +:html+, +:reply_all+, +:headers+, +:attachments+
|
|
198
|
+
# @param idempotency_key [String, nil]
|
|
199
|
+
# @return [Hash] the reply message
|
|
200
|
+
def reply(message_id, idempotency_key: nil, request_timeout: nil, **reply)
|
|
201
|
+
call(:post, "/v1/messages/#{seg(message_id)}/reply", body: reply, sends_mail: true,
|
|
202
|
+
idempotency_key: idempotency_key,
|
|
203
|
+
request_timeout: request_timeout)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Conversation threads.
|
|
208
|
+
class Threads < Resource
|
|
209
|
+
# @param inbox_id [String]
|
|
210
|
+
# @return [Hash] +{ data: [...] }+
|
|
211
|
+
def list(inbox_id, request_timeout: nil)
|
|
212
|
+
call(:get, "/v1/inboxes/#{seg(inbox_id)}/threads", request_timeout: request_timeout)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @param id [String] a thread id
|
|
216
|
+
# @return [Hash] the thread with +:messages+
|
|
217
|
+
def get(id, request_timeout: nil)
|
|
218
|
+
call(:get, "/v1/threads/#{seg(id)}", request_timeout: request_timeout)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The shared support inbox view across every inbox you can read.
|
|
223
|
+
class Conversations < Resource
|
|
224
|
+
# @param status [String, nil] +"open"+, +"closed"+, +"snoozed"+ or +"all"+
|
|
225
|
+
# @param assignee [String, nil] +"me"+, +"none"+ or a user id
|
|
226
|
+
# @param inbox_id [String, nil]
|
|
227
|
+
# @param q [String, nil] search text
|
|
228
|
+
# @param cursor [String, nil]
|
|
229
|
+
# @param limit [Integer, nil]
|
|
230
|
+
# @return [Hash] +{ data: [...], next_cursor: }+
|
|
231
|
+
def list(status: nil, assignee: nil, inbox_id: nil, q: nil, cursor: nil, limit: nil, request_timeout: nil)
|
|
232
|
+
query = { status: status, assignee: assignee, inbox_id: inbox_id, q: q, cursor: cursor, limit: limit }
|
|
233
|
+
call(:get, "/v1/conversations", query: query, request_timeout: request_timeout)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @return [Hash] +{ open:, assigned_to_me:, unassigned:, by_inbox: }+
|
|
237
|
+
def counts(request_timeout: nil)
|
|
238
|
+
call(:get, "/v1/conversations/counts", request_timeout: request_timeout)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Changes a conversation. Only the fields you pass are sent, and an explicit nil is sent as null.
|
|
242
|
+
#
|
|
243
|
+
# @param id [String] the thread id
|
|
244
|
+
# @param fields [Hash] +status:+ (+"open"+ or +"closed"+), +assignee_user_id:+ (nil unassigns),
|
|
245
|
+
# +snoozed_until:+ (a +Time+ or RFC 3339 string; nil wakes the conversation)
|
|
246
|
+
# @return [Hash] the conversation
|
|
247
|
+
def update(id, request_timeout: nil, **fields)
|
|
248
|
+
body = fields.key?(:snoozed_until) ? fields.merge(snoozed_until: Support.time(fields[:snoozed_until])) : fields
|
|
249
|
+
call(:patch, "/v1/conversations/#{seg(id)}", body: body, request_timeout: request_timeout)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Marks the conversation read.
|
|
253
|
+
#
|
|
254
|
+
# @return [nil]
|
|
255
|
+
def read(id, request_timeout: nil)
|
|
256
|
+
call(:post, "/v1/conversations/#{seg(id)}/read", idempotent: true, request_timeout: request_timeout)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Marks the conversation unread.
|
|
260
|
+
#
|
|
261
|
+
# @return [nil]
|
|
262
|
+
def unread(id, request_timeout: nil)
|
|
263
|
+
call(:delete, "/v1/conversations/#{seg(id)}/read", request_timeout: request_timeout)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Sending and receiving domains.
|
|
268
|
+
class Domains < Resource
|
|
269
|
+
# @param name [String] such as +"acme.com"+
|
|
270
|
+
# @return [Hash] the domain with the DNS +:records+ to publish and +:next_step+
|
|
271
|
+
def create(name:, request_timeout: nil)
|
|
272
|
+
call(:post, "/v1/domains", body: { name: name }, request_timeout: request_timeout)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# @return [Hash] +{ data: [...] }+
|
|
276
|
+
def list(request_timeout: nil)
|
|
277
|
+
call(:get, "/v1/domains", request_timeout: request_timeout)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# @param id [String] a domain id (+dom_...+) or the domain name
|
|
281
|
+
# @return [Hash]
|
|
282
|
+
def get(id, request_timeout: nil)
|
|
283
|
+
call(:get, "/v1/domains/#{seg(id)}", request_timeout: request_timeout)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# @return [nil]
|
|
287
|
+
def delete(id, request_timeout: nil)
|
|
288
|
+
call(:delete, "/v1/domains/#{seg(id)}", request_timeout: request_timeout)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Checks the DNS records now and returns the domain with each record's status.
|
|
292
|
+
#
|
|
293
|
+
# @return [Hash]
|
|
294
|
+
def verify(id, request_timeout: nil)
|
|
295
|
+
call(:post, "/v1/domains/#{seg(id)}/verify", idempotent: true, request_timeout: request_timeout)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Webhook endpoints and their deliveries.
|
|
300
|
+
class Webhooks < Resource
|
|
301
|
+
# @param url [String]
|
|
302
|
+
# @param event_types [Array<String>, nil] empty or nil means every event type
|
|
303
|
+
# @param inbox_id [String, nil] nil means every inbox in the workspace
|
|
304
|
+
# @return [Hash] the webhook with its signing +:secret+ (+whsec_...+), shown once
|
|
305
|
+
def create(url:, event_types: nil, inbox_id: nil, request_timeout: nil)
|
|
306
|
+
body = { url: url, event_types: event_types, inbox_id: inbox_id }.compact
|
|
307
|
+
call(:post, "/v1/webhooks", body: body, request_timeout: request_timeout)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# @return [Hash] +{ data: [...] }+
|
|
311
|
+
def list(request_timeout: nil)
|
|
312
|
+
call(:get, "/v1/webhooks", request_timeout: request_timeout)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# @return [nil]
|
|
316
|
+
def delete(id, request_timeout: nil)
|
|
317
|
+
call(:delete, "/v1/webhooks/#{seg(id)}", request_timeout: request_timeout)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# @param id [String] the webhook id
|
|
321
|
+
# @param limit [Integer, nil]
|
|
322
|
+
# @return [Hash] +{ data: [...] }+
|
|
323
|
+
def deliveries(id, limit: nil, request_timeout: nil)
|
|
324
|
+
call(:get, "/v1/webhooks/#{seg(id)}/deliveries", query: { limit: limit }, request_timeout: request_timeout)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Delivers the event of a past delivery again.
|
|
328
|
+
#
|
|
329
|
+
# @param id [String] the webhook id
|
|
330
|
+
# @param delivery_id [String]
|
|
331
|
+
# @return [Hash] the new delivery
|
|
332
|
+
def replay(id, delivery_id, request_timeout: nil)
|
|
333
|
+
call(:post, "/v1/webhooks/#{seg(id)}/deliveries/#{seg(delivery_id)}/replay", request_timeout: request_timeout)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Same as {Webhook.verify}.
|
|
337
|
+
#
|
|
338
|
+
# @return [Hash] the event
|
|
339
|
+
def verify(**kwargs)
|
|
340
|
+
Webhook.verify(**kwargs)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# API keys.
|
|
345
|
+
class Keys < Resource
|
|
346
|
+
# @param name [String]
|
|
347
|
+
# @param permissions [String, nil] +"full"+ (default), +"sending"+ or +"inbox"+
|
|
348
|
+
# @param inbox_id [String, nil] inbox keys: the one inbox
|
|
349
|
+
# @param domain_id [String, nil] sending keys: limit to one domain (id or name)
|
|
350
|
+
# @return [Hash] +{ key:, secret: }+; the +secret+ is shown once
|
|
351
|
+
def create(name:, permissions: nil, inbox_id: nil, domain_id: nil, request_timeout: nil)
|
|
352
|
+
body = { name: name, permissions: permissions, inbox_id: inbox_id, domain_id: domain_id }.compact
|
|
353
|
+
call(:post, "/v1/keys", body: body, request_timeout: request_timeout)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# @return [Hash] +{ data: [...] }+
|
|
357
|
+
def list(request_timeout: nil)
|
|
358
|
+
call(:get, "/v1/keys", request_timeout: request_timeout)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Revokes the key at once.
|
|
362
|
+
#
|
|
363
|
+
# @return [nil]
|
|
364
|
+
def delete(id, request_timeout: nil)
|
|
365
|
+
call(:delete, "/v1/keys/#{seg(id)}", request_timeout: request_timeout)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Addresses mail is not sent to.
|
|
370
|
+
class Suppressions < Resource
|
|
371
|
+
# @return [Hash] +{ data: [{ address:, reason:, created_at: }] }+
|
|
372
|
+
def list(request_timeout: nil)
|
|
373
|
+
call(:get, "/v1/suppressions", request_timeout: request_timeout)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Lets mail go to the address again.
|
|
377
|
+
#
|
|
378
|
+
# @param address [String]
|
|
379
|
+
# @return [nil]
|
|
380
|
+
def delete(address, request_timeout: nil)
|
|
381
|
+
call(:delete, "/v1/suppressions/#{seg(address)}", request_timeout: request_timeout)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The event log.
|
|
386
|
+
class Events < Resource
|
|
387
|
+
# Events, oldest first. Pass the returned +:next_after+ as +after:+ to continue.
|
|
388
|
+
#
|
|
389
|
+
# @param after [String, nil] an event id
|
|
390
|
+
# @param limit [Integer, nil]
|
|
391
|
+
# @return [Hash] +{ data: [...], next_after: }+
|
|
392
|
+
def list(after: nil, limit: nil, request_timeout: nil)
|
|
393
|
+
call(:get, "/v1/events", query: { after: after, limit: limit }, request_timeout: request_timeout)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Runearc
|
|
6
|
+
# @api private
|
|
7
|
+
module Support
|
|
8
|
+
# A +Time+ becomes the RFC 3339 string the API wants; anything else is returned as given.
|
|
9
|
+
def self.time(value)
|
|
10
|
+
value.is_a?(Time) ? value.getutc.iso8601(3) : value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Percent-encodes one path segment.
|
|
14
|
+
def self.segment(value)
|
|
15
|
+
value.to_s.b.gsub(/[^A-Za-z0-9\-_.~]/n) { |byte| format("%%%02X", byte.ord) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|