laneful-ruby 1.1.1 → 1.2.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/lib/laneful/client.rb +138 -17
- data/lib/laneful/models.rb +89 -15
- data/lib/laneful/org_models.rb +452 -0
- data/lib/laneful/version.rb +1 -1
- data/lib/laneful/webhooks.rb +1 -1
- data/lib/laneful.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed5ff94086f558d6ea4bad6c914ec22cbc7b8d012cc27fc3d385d3cbc05c5c49
|
|
4
|
+
data.tar.gz: 02a6ba4d5c4dbee58809d4e15214623fc273770b3779d73fe6ae25b61de75252
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc30d8a4112104938f451247b7c24654365f4bb6f8efb5dbeacff4947b00841115563bc7e93d4e36b2eb17e3f115a3616d91a99ebff081d0f322fc9ebdca2060
|
|
7
|
+
data.tar.gz: 560628e6bd7d5cefa3b9e3229c4be1806d8703d6a7c61e919b94d386b9b3e4a27f1ea95602096e5783236489f4c234233676ebeadf42591eb3db767ec4cf6875
|
data/lib/laneful/client.rb
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
3
6
|
module Laneful
|
|
4
|
-
# Main client for communicating with the Laneful
|
|
7
|
+
# Main client for communicating with the Laneful API.
|
|
8
|
+
# Email sending uses a send host (https://your-endpoint.send.laneful.net).
|
|
9
|
+
# Domain, unsubscribe-group, and analytics endpoints use the organization
|
|
10
|
+
# API host (https://api.laneful.net).
|
|
5
11
|
class Client
|
|
6
12
|
include HTTParty
|
|
7
13
|
|
|
8
|
-
# Default timeout in seconds
|
|
9
14
|
DEFAULT_TIMEOUT = 30
|
|
10
15
|
|
|
16
|
+
# Repeat array keys as workspace_ids=1&workspace_ids=2 (not workspace_ids[]=).
|
|
17
|
+
query_string_normalizer HTTParty::Request::NON_RAILS_QUERY_STRING_NORMALIZER
|
|
18
|
+
|
|
11
19
|
attr_reader :base_url, :auth_token, :timeout
|
|
12
20
|
|
|
13
21
|
def initialize(base_url, auth_token, timeout: DEFAULT_TIMEOUT)
|
|
@@ -16,26 +24,112 @@ module Laneful
|
|
|
16
24
|
@timeout = timeout
|
|
17
25
|
|
|
18
26
|
validate_configuration!
|
|
19
|
-
|
|
20
|
-
# Configure HTTParty
|
|
21
|
-
self.class.base_uri @base_url
|
|
22
|
-
self.class.default_timeout @timeout
|
|
23
|
-
self.class.headers default_headers
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Sends a single email
|
|
27
|
-
def send_email(email)
|
|
28
|
-
send_emails([email])
|
|
30
|
+
def send_email(email, mail_settings = nil)
|
|
31
|
+
send_emails([email], mail_settings)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
# Sends multiple emails
|
|
32
|
-
def send_emails(emails)
|
|
35
|
+
def send_emails(emails, mail_settings = nil)
|
|
33
36
|
validate_emails!(emails)
|
|
34
37
|
|
|
35
38
|
request_data = { 'emails' => emails.map(&:to_hash) }
|
|
36
|
-
|
|
39
|
+
request_data['mail_settings'] = mail_settings.to_hash if mail_settings
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
request('POST', '/email/send', request_data)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# List unsubscribe groups for a workspace.
|
|
45
|
+
# Uses the organization API host (https://api.laneful.net).
|
|
46
|
+
def list_unsubscribe_groups(workspace_id, params = nil)
|
|
47
|
+
ListUnsubscribeGroupsResponse.from_hash(
|
|
48
|
+
request('GET', "/workspaces/#{workspace_id}/unsubscribe-groups", nil, params&.to_query)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create an unsubscribe group in a workspace.
|
|
53
|
+
def create_unsubscribe_group(workspace_id, name)
|
|
54
|
+
data = request('POST', "/workspaces/#{workspace_id}/unsubscribe-groups", { 'name' => name })
|
|
55
|
+
UnsubscribeGroup.from_hash(data['unsubscribe_group'] || data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Update an unsubscribe group.
|
|
59
|
+
def update_unsubscribe_group(workspace_id, unsubscribe_group_id, name)
|
|
60
|
+
data = request(
|
|
61
|
+
'PATCH',
|
|
62
|
+
"/workspaces/#{workspace_id}/unsubscribe-groups/#{unsubscribe_group_id}",
|
|
63
|
+
{ 'name' => name }
|
|
64
|
+
)
|
|
65
|
+
UnsubscribeGroup.from_hash(data['unsubscribe_group'] || data)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# List sending domains for a workspace.
|
|
69
|
+
def list_domains(workspace_id, params = nil)
|
|
70
|
+
ListDomainsResponse.from_hash(
|
|
71
|
+
request('GET', "/workspaces/#{workspace_id}/domains", nil, params&.to_query)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get a single sending domain by name.
|
|
76
|
+
def get_domain(workspace_id, domain)
|
|
77
|
+
Domain.from_hash(
|
|
78
|
+
request('GET', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}")
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Create a sending domain in a workspace.
|
|
83
|
+
def create_domain(workspace_id, create_request)
|
|
84
|
+
Domain.from_hash(
|
|
85
|
+
request('POST', "/workspaces/#{workspace_id}/domains", create_request.to_hash)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Update a domain's mutable settings (currently the email track).
|
|
90
|
+
def update_domain(workspace_id, domain, update_request)
|
|
91
|
+
Domain.from_hash(
|
|
92
|
+
request(
|
|
93
|
+
'PATCH',
|
|
94
|
+
"/workspaces/#{workspace_id}/domains/#{encode_path(domain)}",
|
|
95
|
+
update_request.to_hash
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Trigger DNS verification for a domain.
|
|
101
|
+
def verify_domain(workspace_id, domain)
|
|
102
|
+
Domain.from_hash(
|
|
103
|
+
request('POST', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}/verify")
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Delete a sending domain from a workspace.
|
|
108
|
+
def delete_domain(workspace_id, domain)
|
|
109
|
+
SuccessResponse.from_hash(
|
|
110
|
+
request('DELETE', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}")
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# List domains whose spam complaint ratio reached a critical level.
|
|
115
|
+
def list_domain_spam_ratio_radar(params = nil)
|
|
116
|
+
ListDomainSpamRatioRadarResponse.from_hash(
|
|
117
|
+
request('GET', '/analytics/radar/domain-spam-ratio', nil, params&.to_query)
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# List daily Google Postmaster Tools spam-rate reports.
|
|
122
|
+
def list_google_postmaster_spam_reports(params = nil)
|
|
123
|
+
ListGooglePostmasterSpamReportsResponse.from_hash(
|
|
124
|
+
request('GET', '/analytics/google-postmaster/spam-reports', nil, params&.to_query)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# List daily Microsoft SNDS reports for the organization's sending IPs.
|
|
129
|
+
def list_snds_reports(params = nil)
|
|
130
|
+
ListSndsReportsResponse.from_hash(
|
|
131
|
+
request('GET', '/analytics/microsoft-snds/reports', nil, params&.to_query)
|
|
132
|
+
)
|
|
39
133
|
end
|
|
40
134
|
|
|
41
135
|
private
|
|
@@ -45,7 +139,6 @@ module Laneful
|
|
|
45
139
|
|
|
46
140
|
raise ValidationException, 'Auth token cannot be empty' if auth_token.nil? || auth_token.empty?
|
|
47
141
|
|
|
48
|
-
# Basic URL validation
|
|
49
142
|
return if base_url.match?(%r{^https?://})
|
|
50
143
|
|
|
51
144
|
raise ValidationException, 'Base URL must be a valid HTTP/HTTPS URL'
|
|
@@ -59,18 +152,46 @@ module Laneful
|
|
|
59
152
|
end
|
|
60
153
|
end
|
|
61
154
|
|
|
62
|
-
def default_headers
|
|
63
|
-
{
|
|
155
|
+
def default_headers(with_json: true)
|
|
156
|
+
headers = {
|
|
64
157
|
'Authorization' => "Bearer #{auth_token}",
|
|
65
|
-
'Content-Type' => 'application/json',
|
|
66
158
|
'Accept' => 'application/json',
|
|
67
159
|
'User-Agent' => USER_AGENT
|
|
68
160
|
}
|
|
161
|
+
headers['Content-Type'] = 'application/json' if with_json
|
|
162
|
+
headers
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def build_url(path)
|
|
166
|
+
"#{base_url.chomp('/')}/#{API_VERSION}/#{path.sub(%r{\A/}, '')}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def encode_path(value)
|
|
170
|
+
CGI.escape(value.to_s).gsub('+', '%20')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def request(method, path, json = nil, query = nil)
|
|
174
|
+
url = build_url(path)
|
|
175
|
+
|
|
176
|
+
options = {
|
|
177
|
+
headers: default_headers(with_json: !json.nil?),
|
|
178
|
+
timeout: timeout
|
|
179
|
+
}
|
|
180
|
+
options[:query] = query unless query.nil? || query.empty?
|
|
181
|
+
options[:body] = json.to_json unless json.nil?
|
|
182
|
+
|
|
183
|
+
begin
|
|
184
|
+
response = self.class.send(method.downcase, url, options)
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
raise HttpException.new("HTTP request failed: #{e.message}", 0, e)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
handle_response(response)
|
|
69
190
|
end
|
|
70
191
|
|
|
71
192
|
def handle_response(response)
|
|
72
193
|
case response.code
|
|
73
|
-
when 200, 201, 202
|
|
194
|
+
when 200, 201, 202, 204
|
|
74
195
|
parse_success_response(response)
|
|
75
196
|
when 404
|
|
76
197
|
raise HttpException.new(
|
data/lib/laneful/models.rb
CHANGED
|
@@ -59,12 +59,15 @@ module Laneful
|
|
|
59
59
|
|
|
60
60
|
# Configuration for email tracking settings
|
|
61
61
|
class TrackingSettings
|
|
62
|
-
attr_reader :opens, :clicks, :unsubscribes
|
|
62
|
+
attr_reader :opens, :clicks, :unsubscribes, :unsubscribe_group_id, :unsubscribe_group_name
|
|
63
63
|
|
|
64
|
-
def initialize(opens: false, clicks: false, unsubscribes: false
|
|
64
|
+
def initialize(opens: false, clicks: false, unsubscribes: false,
|
|
65
|
+
unsubscribe_group_id: nil, unsubscribe_group_name: nil)
|
|
65
66
|
@opens = opens
|
|
66
67
|
@clicks = clicks
|
|
67
68
|
@unsubscribes = unsubscribes
|
|
69
|
+
@unsubscribe_group_id = unsubscribe_group_id
|
|
70
|
+
@unsubscribe_group_name = unsubscribe_group_name
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
# Creates tracking settings from a hash representation
|
|
@@ -72,22 +75,31 @@ module Laneful
|
|
|
72
75
|
new(
|
|
73
76
|
opens: data['opens'] || false,
|
|
74
77
|
clicks: data['clicks'] || false,
|
|
75
|
-
unsubscribes: data['unsubscribes'] || false
|
|
78
|
+
unsubscribes: data['unsubscribes'] || false,
|
|
79
|
+
unsubscribe_group_id: data['unsubscribe_group_id'],
|
|
80
|
+
unsubscribe_group_name: data['unsubscribe_group_name']
|
|
76
81
|
)
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def to_hash
|
|
80
|
-
{
|
|
85
|
+
hash = {
|
|
81
86
|
'opens' => opens,
|
|
82
87
|
'clicks' => clicks,
|
|
83
88
|
'unsubscribes' => unsubscribes
|
|
84
89
|
}
|
|
90
|
+
hash['unsubscribe_group_id'] = unsubscribe_group_id unless unsubscribe_group_id.nil?
|
|
91
|
+
hash['unsubscribe_group_name'] = unsubscribe_group_name unless unsubscribe_group_name.nil?
|
|
92
|
+
hash
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
def ==(other)
|
|
88
96
|
return false unless other.is_a?(TrackingSettings)
|
|
89
97
|
|
|
90
|
-
opens == other.opens &&
|
|
98
|
+
opens == other.opens &&
|
|
99
|
+
clicks == other.clicks &&
|
|
100
|
+
unsubscribes == other.unsubscribes &&
|
|
101
|
+
unsubscribe_group_id == other.unsubscribe_group_id &&
|
|
102
|
+
unsubscribe_group_name == other.unsubscribe_group_name
|
|
91
103
|
end
|
|
92
104
|
|
|
93
105
|
def eql?(other)
|
|
@@ -95,7 +107,7 @@ module Laneful
|
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def hash
|
|
98
|
-
[opens, clicks, unsubscribes].hash
|
|
110
|
+
[opens, clicks, unsubscribes, unsubscribe_group_id, unsubscribe_group_name].hash
|
|
99
111
|
end
|
|
100
112
|
|
|
101
113
|
def to_s
|
|
@@ -103,14 +115,53 @@ module Laneful
|
|
|
103
115
|
end
|
|
104
116
|
end
|
|
105
117
|
|
|
118
|
+
# Request-level mail settings (sandbox mode, return message IDs)
|
|
119
|
+
class MailSettings
|
|
120
|
+
attr_reader :sandbox_mode, :return_message_ids
|
|
121
|
+
|
|
122
|
+
def initialize(sandbox_mode: nil, return_message_ids: nil)
|
|
123
|
+
@sandbox_mode = sandbox_mode
|
|
124
|
+
@return_message_ids = return_message_ids
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.from_hash(data)
|
|
128
|
+
new(
|
|
129
|
+
sandbox_mode: data['sandbox_mode'],
|
|
130
|
+
return_message_ids: data['return_message_ids']
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def to_hash
|
|
135
|
+
hash = {}
|
|
136
|
+
hash['sandbox_mode'] = sandbox_mode unless sandbox_mode.nil?
|
|
137
|
+
hash['return_message_ids'] = return_message_ids unless return_message_ids.nil?
|
|
138
|
+
hash
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def ==(other)
|
|
142
|
+
return false unless other.is_a?(MailSettings)
|
|
143
|
+
|
|
144
|
+
sandbox_mode == other.sandbox_mode && return_message_ids == other.return_message_ids
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def eql?(other)
|
|
148
|
+
self == other
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def hash
|
|
152
|
+
[sandbox_mode, return_message_ids].hash
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
106
156
|
# Represents a file attachment for an email
|
|
107
157
|
class Attachment
|
|
108
|
-
attr_reader :filename, :content_type, :content
|
|
158
|
+
attr_reader :filename, :content_type, :content, :inline_id
|
|
109
159
|
|
|
110
|
-
def initialize(filename, content_type, content)
|
|
160
|
+
def initialize(filename, content_type, content, inline_id = nil)
|
|
111
161
|
@filename = filename
|
|
112
162
|
@content_type = content_type
|
|
113
163
|
@content = content
|
|
164
|
+
@inline_id = inline_id
|
|
114
165
|
validate!
|
|
115
166
|
end
|
|
116
167
|
|
|
@@ -124,22 +175,27 @@ module Laneful
|
|
|
124
175
|
|
|
125
176
|
# Creates an attachment from a hash representation
|
|
126
177
|
def self.from_hash(data)
|
|
127
|
-
filename = data['file_name'] || data['filename']
|
|
128
|
-
new(filename, data['content_type'], data['content'])
|
|
178
|
+
filename = data['file_name'] || data['filename']
|
|
179
|
+
new(filename, data['content_type'], data['content'], data['inline_id'])
|
|
129
180
|
end
|
|
130
181
|
|
|
131
182
|
def to_hash
|
|
132
|
-
{
|
|
183
|
+
hash = {
|
|
133
184
|
'file_name' => filename,
|
|
134
185
|
'content_type' => content_type,
|
|
135
186
|
'content' => content
|
|
136
187
|
}
|
|
188
|
+
hash['inline_id'] = inline_id if inline_id && !inline_id.empty?
|
|
189
|
+
hash
|
|
137
190
|
end
|
|
138
191
|
|
|
139
192
|
def ==(other)
|
|
140
193
|
return false unless other.is_a?(Attachment)
|
|
141
194
|
|
|
142
|
-
filename == other.filename &&
|
|
195
|
+
filename == other.filename &&
|
|
196
|
+
content_type == other.content_type &&
|
|
197
|
+
content == other.content &&
|
|
198
|
+
inline_id == other.inline_id
|
|
143
199
|
end
|
|
144
200
|
|
|
145
201
|
def eql?(other)
|
|
@@ -147,7 +203,7 @@ module Laneful
|
|
|
147
203
|
end
|
|
148
204
|
|
|
149
205
|
def hash
|
|
150
|
-
[filename, content_type, content].hash
|
|
206
|
+
[filename, content_type, content, inline_id].hash
|
|
151
207
|
end
|
|
152
208
|
|
|
153
209
|
def to_s
|
|
@@ -193,12 +249,13 @@ module Laneful
|
|
|
193
249
|
|
|
194
250
|
# Represents a single email to be sent
|
|
195
251
|
class Email
|
|
196
|
-
attr_reader :from, :to, :cc, :bcc, :subject, :text_content, :html_content,
|
|
252
|
+
attr_reader :from, :from_header, :to, :cc, :bcc, :subject, :text_content, :html_content,
|
|
197
253
|
:template_id, :template_data, :attachments, :headers, :reply_to,
|
|
198
254
|
:send_time, :webhook_data, :tag, :tracking
|
|
199
255
|
|
|
200
256
|
def initialize(builder)
|
|
201
257
|
@from = builder.instance_variable_get(:@from)
|
|
258
|
+
@from_header = builder.instance_variable_get(:@from_header)
|
|
202
259
|
@to = builder.instance_variable_get(:@to).dup.freeze
|
|
203
260
|
@cc = builder.instance_variable_get(:@cc).dup.freeze
|
|
204
261
|
@bcc = builder.instance_variable_get(:@bcc).dup.freeze
|
|
@@ -222,6 +279,7 @@ module Laneful
|
|
|
222
279
|
builder = Builder.new
|
|
223
280
|
|
|
224
281
|
builder.from(Address.from_hash(data['from'])) if data && data['from']
|
|
282
|
+
builder.from_header(Address.from_hash(data['from_header'])) if data && data['from_header']
|
|
225
283
|
|
|
226
284
|
data['to'].each { |to_data| builder.to(Address.from_hash(to_data)) } if data && data['to']
|
|
227
285
|
|
|
@@ -253,6 +311,7 @@ module Laneful
|
|
|
253
311
|
|
|
254
312
|
# Required fields
|
|
255
313
|
hash['from'] = from.to_hash
|
|
314
|
+
hash['from_header'] = from_header.to_hash if from_header
|
|
256
315
|
hash['to'] = to.map(&:to_hash)
|
|
257
316
|
hash['subject'] = subject if subject
|
|
258
317
|
|
|
@@ -278,6 +337,7 @@ module Laneful
|
|
|
278
337
|
return false unless other.is_a?(Email)
|
|
279
338
|
|
|
280
339
|
from == other.from &&
|
|
340
|
+
from_header == other.from_header &&
|
|
281
341
|
to == other.to &&
|
|
282
342
|
cc == other.cc &&
|
|
283
343
|
bcc == other.bcc &&
|
|
@@ -300,7 +360,7 @@ module Laneful
|
|
|
300
360
|
end
|
|
301
361
|
|
|
302
362
|
def hash
|
|
303
|
-
[from, to, cc, bcc, subject, text_content, html_content, template_id,
|
|
363
|
+
[from, from_header, to, cc, bcc, subject, text_content, html_content, template_id,
|
|
304
364
|
template_data, attachments, headers, reply_to, send_time, webhook_data, tag, tracking].hash
|
|
305
365
|
end
|
|
306
366
|
|
|
@@ -340,6 +400,15 @@ module Laneful
|
|
|
340
400
|
raise ValidationException, 'Email must have either content (text/HTML) or a template ID'
|
|
341
401
|
end
|
|
342
402
|
|
|
403
|
+
if webhook_data
|
|
404
|
+
raise ValidationException, 'Webhook data cannot have more than 20 keys' if webhook_data.size > 20
|
|
405
|
+
|
|
406
|
+
webhook_data.each do |key, value|
|
|
407
|
+
raise ValidationException, 'Webhook data keys cannot exceed 50 characters' if key.to_s.length > 50
|
|
408
|
+
raise ValidationException, 'Webhook data values cannot exceed 100 characters' if value.to_s.length > 100
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
343
412
|
# Validate send time
|
|
344
413
|
return unless send_time && send_time <= Time.now.to_i
|
|
345
414
|
|
|
@@ -360,6 +429,11 @@ module Laneful
|
|
|
360
429
|
self
|
|
361
430
|
end
|
|
362
431
|
|
|
432
|
+
def from_header(address)
|
|
433
|
+
@from_header = address
|
|
434
|
+
self
|
|
435
|
+
end
|
|
436
|
+
|
|
363
437
|
def to(address)
|
|
364
438
|
@to << address
|
|
365
439
|
self
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laneful
|
|
4
|
+
# An unsubscribe group in a workspace
|
|
5
|
+
class UnsubscribeGroup
|
|
6
|
+
attr_reader :unsubscribe_group_id, :name, :created_at
|
|
7
|
+
|
|
8
|
+
def initialize(unsubscribe_group_id:, name:, created_at: 0)
|
|
9
|
+
@unsubscribe_group_id = unsubscribe_group_id
|
|
10
|
+
@name = name
|
|
11
|
+
@created_at = created_at
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_hash(data)
|
|
15
|
+
new(
|
|
16
|
+
unsubscribe_group_id: (data['unsubscribe_group_id'] || 0).to_i,
|
|
17
|
+
name: data['name'] || '',
|
|
18
|
+
created_at: (data['created_at'] || 0).to_i
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_hash
|
|
23
|
+
{
|
|
24
|
+
'unsubscribe_group_id' => unsubscribe_group_id,
|
|
25
|
+
'name' => name,
|
|
26
|
+
'created_at' => created_at
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Paginated list of unsubscribe groups
|
|
32
|
+
class ListUnsubscribeGroupsResponse
|
|
33
|
+
attr_reader :unsubscribe_groups, :next_cursor
|
|
34
|
+
|
|
35
|
+
def initialize(unsubscribe_groups:, next_cursor: nil)
|
|
36
|
+
@unsubscribe_groups = unsubscribe_groups || []
|
|
37
|
+
@next_cursor = next_cursor
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.from_hash(data)
|
|
41
|
+
groups = (data['unsubscribe_groups'] || []).map { |group| UnsubscribeGroup.from_hash(group) }
|
|
42
|
+
new(unsubscribe_groups: groups, next_cursor: data['next_cursor'])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Query parameters for listing unsubscribe groups
|
|
47
|
+
class ListUnsubscribeGroupsParams
|
|
48
|
+
attr_reader :cursor, :limit, :search
|
|
49
|
+
|
|
50
|
+
def initialize(cursor: nil, limit: nil, search: nil)
|
|
51
|
+
@cursor = cursor
|
|
52
|
+
@limit = limit
|
|
53
|
+
@search = search
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_query
|
|
57
|
+
query = {}
|
|
58
|
+
query['cursor'] = cursor if present?(cursor)
|
|
59
|
+
query['limit'] = limit if limit&.positive?
|
|
60
|
+
query['search'] = search if present?(search)
|
|
61
|
+
query
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def present?(value)
|
|
67
|
+
!value.nil? && !value.to_s.empty?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A sending domain and its verification state
|
|
72
|
+
class Domain
|
|
73
|
+
attr_reader :domain, :tracking, :return_path, :verified, :tracking_verified,
|
|
74
|
+
:return_path_verified, :dkim1_verified, :dkim2_verified,
|
|
75
|
+
:dmarc_verified, :require_tls, :email_track_id
|
|
76
|
+
|
|
77
|
+
def initialize(domain:, tracking: '', return_path: '', verified: false, tracking_verified: false,
|
|
78
|
+
return_path_verified: false, dkim1_verified: false, dkim2_verified: false,
|
|
79
|
+
dmarc_verified: false, require_tls: false, email_track_id: '')
|
|
80
|
+
@domain = domain
|
|
81
|
+
@tracking = tracking
|
|
82
|
+
@return_path = return_path
|
|
83
|
+
@verified = verified
|
|
84
|
+
@tracking_verified = tracking_verified
|
|
85
|
+
@return_path_verified = return_path_verified
|
|
86
|
+
@dkim1_verified = dkim1_verified
|
|
87
|
+
@dkim2_verified = dkim2_verified
|
|
88
|
+
@dmarc_verified = dmarc_verified
|
|
89
|
+
@require_tls = require_tls
|
|
90
|
+
@email_track_id = email_track_id
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.from_hash(data)
|
|
94
|
+
new(
|
|
95
|
+
domain: data['domain'] || '',
|
|
96
|
+
tracking: data['tracking'] || '',
|
|
97
|
+
return_path: data['return_path'] || '',
|
|
98
|
+
verified: truthy?(data['verified']),
|
|
99
|
+
tracking_verified: truthy?(data['tracking_verified']),
|
|
100
|
+
return_path_verified: truthy?(data['return_path_verified']),
|
|
101
|
+
dkim1_verified: truthy?(data['dkim1_verified']),
|
|
102
|
+
dkim2_verified: truthy?(data['dkim2_verified']),
|
|
103
|
+
dmarc_verified: truthy?(data['dmarc_verified']),
|
|
104
|
+
require_tls: truthy?(data['require_tls']),
|
|
105
|
+
email_track_id: data['email_track_id'] || ''
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def to_hash
|
|
110
|
+
{
|
|
111
|
+
'domain' => domain,
|
|
112
|
+
'tracking' => tracking,
|
|
113
|
+
'return_path' => return_path,
|
|
114
|
+
'verified' => verified,
|
|
115
|
+
'tracking_verified' => tracking_verified,
|
|
116
|
+
'return_path_verified' => return_path_verified,
|
|
117
|
+
'dkim1_verified' => dkim1_verified,
|
|
118
|
+
'dkim2_verified' => dkim2_verified,
|
|
119
|
+
'dmarc_verified' => dmarc_verified,
|
|
120
|
+
'require_tls' => require_tls,
|
|
121
|
+
'email_track_id' => email_track_id
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.truthy?(value)
|
|
126
|
+
value == true
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Paginated list of sending domains
|
|
131
|
+
class ListDomainsResponse
|
|
132
|
+
attr_reader :domains, :next_cursor
|
|
133
|
+
|
|
134
|
+
def initialize(domains:, next_cursor: nil)
|
|
135
|
+
@domains = domains || []
|
|
136
|
+
@next_cursor = next_cursor
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.from_hash(data)
|
|
140
|
+
pagination = data['pagination'] || {}
|
|
141
|
+
new(
|
|
142
|
+
domains: (data['domains'] || []).map { |domain| Domain.from_hash(domain) },
|
|
143
|
+
next_cursor: pagination['next_cursor']
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Query parameters for listing domains
|
|
149
|
+
class ListDomainsParams
|
|
150
|
+
attr_reader :cursor, :limit, :filter_domain
|
|
151
|
+
|
|
152
|
+
def initialize(cursor: nil, limit: nil, filter_domain: nil)
|
|
153
|
+
@cursor = cursor
|
|
154
|
+
@limit = limit
|
|
155
|
+
@filter_domain = filter_domain
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def to_query
|
|
159
|
+
query = {}
|
|
160
|
+
query['cursor'] = cursor if present?(cursor)
|
|
161
|
+
query['limit'] = limit if limit&.positive?
|
|
162
|
+
query['filter[domain]'] = filter_domain if present?(filter_domain)
|
|
163
|
+
query
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def present?(value)
|
|
169
|
+
!value.nil? && !value.to_s.empty?
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Request body for creating a sending domain
|
|
174
|
+
class CreateDomainRequest
|
|
175
|
+
attr_reader :domain, :tracking, :return_path, :require_tls, :email_track_id
|
|
176
|
+
|
|
177
|
+
def initialize(domain:, tracking:, return_path:, require_tls: nil, email_track_id: nil)
|
|
178
|
+
@domain = domain
|
|
179
|
+
@tracking = tracking
|
|
180
|
+
@return_path = return_path
|
|
181
|
+
@require_tls = require_tls
|
|
182
|
+
@email_track_id = email_track_id
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def to_hash
|
|
186
|
+
hash = {
|
|
187
|
+
'domain' => domain,
|
|
188
|
+
'tracking' => tracking,
|
|
189
|
+
'return_path' => return_path
|
|
190
|
+
}
|
|
191
|
+
hash['require_tls'] = require_tls unless require_tls.nil?
|
|
192
|
+
hash['email_track_id'] = email_track_id if email_track_id && !email_track_id.empty?
|
|
193
|
+
hash
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Request body for updating a domain's mutable settings.
|
|
198
|
+
# Pass a track ID to set the email track, an empty string to clear it,
|
|
199
|
+
# or nil to leave it unchanged.
|
|
200
|
+
class UpdateDomainRequest
|
|
201
|
+
attr_reader :email_track_id
|
|
202
|
+
|
|
203
|
+
def initialize(email_track_id = nil)
|
|
204
|
+
@email_track_id = email_track_id
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def to_hash
|
|
208
|
+
return {} if email_track_id.nil?
|
|
209
|
+
|
|
210
|
+
{ 'email_track_id' => email_track_id }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Generic success message from mutating endpoints
|
|
215
|
+
class SuccessResponse
|
|
216
|
+
attr_reader :message
|
|
217
|
+
|
|
218
|
+
def initialize(message:)
|
|
219
|
+
@message = message
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.from_hash(data)
|
|
223
|
+
new(message: data['message'] || '')
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def to_hash
|
|
227
|
+
{ 'message' => message }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# A sending domain whose spam complaint ratio reached a critical level
|
|
232
|
+
class DomainSpamRatioRadar
|
|
233
|
+
attr_reader :workspace_id, :domain, :esp, :spam_ratio, :date
|
|
234
|
+
|
|
235
|
+
def initialize(workspace_id:, domain:, esp:, spam_ratio:, date:)
|
|
236
|
+
@workspace_id = workspace_id
|
|
237
|
+
@domain = domain
|
|
238
|
+
@esp = esp
|
|
239
|
+
@spam_ratio = spam_ratio
|
|
240
|
+
@date = date
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def self.from_hash(data)
|
|
244
|
+
new(
|
|
245
|
+
workspace_id: (data['workspace_id'] || 0).to_i,
|
|
246
|
+
domain: data['domain'] || '',
|
|
247
|
+
esp: data['esp'] || '',
|
|
248
|
+
spam_ratio: (data['spam_ratio'] || 0).to_f,
|
|
249
|
+
date: (data['date'] || '').to_s
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Paginated list of domain spam-ratio radar entries
|
|
255
|
+
class ListDomainSpamRatioRadarResponse
|
|
256
|
+
attr_reader :radar, :next_cursor
|
|
257
|
+
|
|
258
|
+
def initialize(radar:, next_cursor: nil)
|
|
259
|
+
@radar = radar || []
|
|
260
|
+
@next_cursor = next_cursor
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.from_hash(data)
|
|
264
|
+
new(
|
|
265
|
+
radar: (data['radar'] || []).map { |entry| DomainSpamRatioRadar.from_hash(entry) },
|
|
266
|
+
next_cursor: data['next_cursor']
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Query parameters for listing domain spam-ratio radar entries
|
|
272
|
+
class ListDomainSpamRatioRadarParams
|
|
273
|
+
attr_reader :workspace_ids, :domain, :start_date, :end_date, :cursor, :limit
|
|
274
|
+
|
|
275
|
+
def initialize(workspace_ids: [], domain: nil, start_date: nil, end_date: nil, cursor: nil, limit: nil)
|
|
276
|
+
@workspace_ids = workspace_ids || []
|
|
277
|
+
@domain = domain
|
|
278
|
+
@start_date = start_date
|
|
279
|
+
@end_date = end_date
|
|
280
|
+
@cursor = cursor
|
|
281
|
+
@limit = limit
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def to_query
|
|
285
|
+
query = {}
|
|
286
|
+
query['workspace_ids'] = workspace_ids unless workspace_ids.empty?
|
|
287
|
+
query['domain'] = domain if present?(domain)
|
|
288
|
+
query['start_date'] = start_date if present?(start_date)
|
|
289
|
+
query['end_date'] = end_date if present?(end_date)
|
|
290
|
+
query['cursor'] = cursor if present?(cursor)
|
|
291
|
+
query['limit'] = limit if limit&.positive?
|
|
292
|
+
query
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def present?(value)
|
|
298
|
+
!value.nil? && !value.to_s.empty?
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# A daily Gmail spam-rate report from Google Postmaster Tools
|
|
303
|
+
class GooglePostmasterSpamReport
|
|
304
|
+
attr_reader :workspace_id, :domain, :date, :spam_ratio
|
|
305
|
+
|
|
306
|
+
def initialize(workspace_id:, domain:, date:, spam_ratio:)
|
|
307
|
+
@workspace_id = workspace_id
|
|
308
|
+
@domain = domain
|
|
309
|
+
@date = date
|
|
310
|
+
@spam_ratio = spam_ratio
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def self.from_hash(data)
|
|
314
|
+
new(
|
|
315
|
+
workspace_id: (data['workspace_id'] || 0).to_i,
|
|
316
|
+
domain: data['domain'] || '',
|
|
317
|
+
date: (data['date'] || '').to_s,
|
|
318
|
+
spam_ratio: (data['spam_ratio'] || 0).to_f
|
|
319
|
+
)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Paginated list of Google Postmaster spam reports
|
|
324
|
+
class ListGooglePostmasterSpamReportsResponse
|
|
325
|
+
attr_reader :spam_reports, :next_cursor
|
|
326
|
+
|
|
327
|
+
def initialize(spam_reports:, next_cursor: nil)
|
|
328
|
+
@spam_reports = spam_reports || []
|
|
329
|
+
@next_cursor = next_cursor
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def self.from_hash(data)
|
|
333
|
+
new(
|
|
334
|
+
spam_reports: (data['spam_reports'] || []).map { |report| GooglePostmasterSpamReport.from_hash(report) },
|
|
335
|
+
next_cursor: data['next_cursor']
|
|
336
|
+
)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Query parameters for listing Google Postmaster spam reports
|
|
341
|
+
class ListGooglePostmasterSpamReportsParams
|
|
342
|
+
attr_reader :workspace_ids, :domain, :start_date, :end_date, :cursor, :limit
|
|
343
|
+
|
|
344
|
+
def initialize(workspace_ids: [], domain: nil, start_date: nil, end_date: nil, cursor: nil, limit: nil)
|
|
345
|
+
@workspace_ids = workspace_ids || []
|
|
346
|
+
@domain = domain
|
|
347
|
+
@start_date = start_date
|
|
348
|
+
@end_date = end_date
|
|
349
|
+
@cursor = cursor
|
|
350
|
+
@limit = limit
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def to_query
|
|
354
|
+
query = {}
|
|
355
|
+
query['workspace_ids'] = workspace_ids unless workspace_ids.empty?
|
|
356
|
+
query['domain'] = domain if present?(domain)
|
|
357
|
+
query['start_date'] = start_date if present?(start_date)
|
|
358
|
+
query['end_date'] = end_date if present?(end_date)
|
|
359
|
+
query['cursor'] = cursor if present?(cursor)
|
|
360
|
+
query['limit'] = limit if limit&.positive?
|
|
361
|
+
query
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
private
|
|
365
|
+
|
|
366
|
+
def present?(value)
|
|
367
|
+
!value.nil? && !value.to_s.empty?
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# A daily Microsoft SNDS report for a sending IP
|
|
372
|
+
class SndsReport
|
|
373
|
+
FILTER_UNKNOWN = ''
|
|
374
|
+
FILTER_GREEN = 'GREEN'
|
|
375
|
+
FILTER_YELLOW = 'YELLOW'
|
|
376
|
+
FILTER_RED = 'RED'
|
|
377
|
+
|
|
378
|
+
attr_reader :ip, :date, :rcpt_commands, :data_commands, :message_recipients,
|
|
379
|
+
:filter_result, :complaint_rate, :trap_hits
|
|
380
|
+
|
|
381
|
+
def initialize(ip:, date:, rcpt_commands:, data_commands:, message_recipients:,
|
|
382
|
+
filter_result:, complaint_rate:, trap_hits:)
|
|
383
|
+
@ip = ip
|
|
384
|
+
@date = date
|
|
385
|
+
@rcpt_commands = rcpt_commands
|
|
386
|
+
@data_commands = data_commands
|
|
387
|
+
@message_recipients = message_recipients
|
|
388
|
+
@filter_result = filter_result
|
|
389
|
+
@complaint_rate = complaint_rate
|
|
390
|
+
@trap_hits = trap_hits
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def self.from_hash(data)
|
|
394
|
+
new(
|
|
395
|
+
ip: data['ip'] || '',
|
|
396
|
+
date: (data['date'] || '').to_s,
|
|
397
|
+
rcpt_commands: (data['rcpt_commands'] || 0).to_i,
|
|
398
|
+
data_commands: (data['data_commands'] || 0).to_i,
|
|
399
|
+
message_recipients: (data['message_recipients'] || 0).to_i,
|
|
400
|
+
filter_result: data['filter_result'] || FILTER_UNKNOWN,
|
|
401
|
+
complaint_rate: (data['complaint_rate'] || 0).to_f,
|
|
402
|
+
trap_hits: (data['trap_hits'] || 0).to_i
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Paginated list of Microsoft SNDS reports
|
|
408
|
+
class ListSndsReportsResponse
|
|
409
|
+
attr_reader :snds_reports, :next_cursor
|
|
410
|
+
|
|
411
|
+
def initialize(snds_reports:, next_cursor: nil)
|
|
412
|
+
@snds_reports = snds_reports || []
|
|
413
|
+
@next_cursor = next_cursor
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def self.from_hash(data)
|
|
417
|
+
new(
|
|
418
|
+
snds_reports: (data['snds_reports'] || []).map { |report| SndsReport.from_hash(report) },
|
|
419
|
+
next_cursor: data['next_cursor']
|
|
420
|
+
)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Query parameters for listing Microsoft SNDS reports
|
|
425
|
+
class ListSndsReportsParams
|
|
426
|
+
attr_reader :ip, :start_date, :end_date, :cursor, :limit
|
|
427
|
+
|
|
428
|
+
def initialize(ip: nil, start_date: nil, end_date: nil, cursor: nil, limit: nil)
|
|
429
|
+
@ip = ip
|
|
430
|
+
@start_date = start_date
|
|
431
|
+
@end_date = end_date
|
|
432
|
+
@cursor = cursor
|
|
433
|
+
@limit = limit
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def to_query
|
|
437
|
+
query = {}
|
|
438
|
+
query['ip'] = ip if present?(ip)
|
|
439
|
+
query['start_date'] = start_date if present?(start_date)
|
|
440
|
+
query['end_date'] = end_date if present?(end_date)
|
|
441
|
+
query['cursor'] = cursor if present?(cursor)
|
|
442
|
+
query['limit'] = limit if limit&.positive?
|
|
443
|
+
query
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
private
|
|
447
|
+
|
|
448
|
+
def present?(value)
|
|
449
|
+
!value.nil? && !value.to_s.empty?
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
data/lib/laneful/version.rb
CHANGED
data/lib/laneful/webhooks.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Laneful
|
|
|
12
12
|
|
|
13
13
|
# Valid event types as documented
|
|
14
14
|
VALID_EVENT_TYPES = %w[
|
|
15
|
-
delivery open click drop spam_complaint unsubscribe bounce
|
|
15
|
+
request delivery open click drop spam_complaint unsubscribe bounce
|
|
16
16
|
].freeze
|
|
17
17
|
|
|
18
18
|
# UUID pattern for lane_id validation
|
data/lib/laneful.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'base64'
|
|
|
8
8
|
require_relative 'laneful/version'
|
|
9
9
|
require_relative 'laneful/exceptions'
|
|
10
10
|
require_relative 'laneful/models'
|
|
11
|
+
require_relative 'laneful/org_models'
|
|
11
12
|
require_relative 'laneful/client'
|
|
12
13
|
require_relative 'laneful/webhooks'
|
|
13
14
|
|
|
@@ -23,5 +24,5 @@ module Laneful
|
|
|
23
24
|
DEFAULT_TIMEOUT = 30
|
|
24
25
|
|
|
25
26
|
# User agent string
|
|
26
|
-
USER_AGENT =
|
|
27
|
+
USER_AGENT = "laneful-ruby/#{VERSION}".freeze
|
|
27
28
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: laneful-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Laneful Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: httparty
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- lib/laneful/client.rb
|
|
51
51
|
- lib/laneful/exceptions.rb
|
|
52
52
|
- lib/laneful/models.rb
|
|
53
|
+
- lib/laneful/org_models.rb
|
|
53
54
|
- lib/laneful/version.rb
|
|
54
55
|
- lib/laneful/webhooks.rb
|
|
55
56
|
homepage: https://github.com/laneful/laneful-ruby
|