email_list_api 0.1.0.rc.1 → 0.1.0.rc.2
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/LICENSE.txt +21 -0
- data/README.md +114 -4
- data/lib/email_list_api/client.rb +157 -58
- data/lib/email_list_api/configuration.rb +14 -0
- data/lib/email_list_api/syncs_email_list_contact.rb +144 -0
- data/lib/email_list_api/version.rb +1 -1
- data/lib/email_list_api.rb +25 -0
- metadata +19 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1ed6ace184bb9f8af7eaffe002158c0b2dcd67bd5d11fcc4fad7a93bd33bca0
|
|
4
|
+
data.tar.gz: b14740c3dc18fca184637df8402f92957201e637c817ca193cde6298dcd1df8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e3aadd9e8de672857ee08a3d27bd657dde1ae6e9af9d75fe07a6abd0e6b8a128d1503804f824e6880aaafe6def2459dbfb05eb0207bc26e3b4e630014409c33
|
|
7
|
+
data.tar.gz: 03b298cb089969cb48e429b069cc07accb36cac2eac32d5d1c019790376ffd01a64ec87b497bb62400197689fecb586364e15254dfeb04b8d526329816f5ac10
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Email List Dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -18,21 +18,131 @@ Or install it yourself as:
|
|
|
18
18
|
|
|
19
19
|
$ gem install email_list_api
|
|
20
20
|
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
You can configure the gem globally using an initializer (recommended for Rails apps):
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# config/initializers/email_list_api.rb
|
|
27
|
+
EmailListApi.configure do |config|
|
|
28
|
+
config.api_key = ENV['EMAILLIST_API_KEY'] || Rails.application.credentials.dig(:emaillist, :api_key)
|
|
29
|
+
# api_version is optional (defaults to latest based on gem version)
|
|
30
|
+
# config.api_version = 1
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This sets a global default that will be used by all clients unless overridden. The configuration priority is:
|
|
35
|
+
1. Explicit parameter (highest priority)
|
|
36
|
+
2. Global configuration (`EmailListApi.configure`)
|
|
37
|
+
3. Environment variable (`EMAILLIST_API_KEY`)
|
|
38
|
+
|
|
21
39
|
## Usage
|
|
22
40
|
|
|
23
41
|
```ruby
|
|
24
42
|
require 'email_list_api'
|
|
25
43
|
|
|
44
|
+
# If configured globally, you can create a client without specifying the API key
|
|
45
|
+
client = EmailListApi::Client.new
|
|
46
|
+
|
|
47
|
+
# Or explicitly pass the API key (overrides global config)
|
|
26
48
|
client = EmailListApi::Client.new(api_key: 'your_api_key')
|
|
27
49
|
|
|
50
|
+
# Optionally specify API version (defaults to latest)
|
|
51
|
+
client = EmailListApi::Client.new(api_key: 'your_api_key', api_version: 1)
|
|
52
|
+
|
|
28
53
|
# Projects
|
|
29
54
|
projects = client.projects
|
|
30
55
|
project = client.create_project(name: 'My Project')
|
|
31
56
|
|
|
32
|
-
# Lists
|
|
33
|
-
lists = client.lists(
|
|
34
|
-
list = client.create_list(
|
|
57
|
+
# Lists (project can be slug or ID)
|
|
58
|
+
lists = client.lists(project: project['data']['slug'])
|
|
59
|
+
list = client.create_list(project: project['data']['slug'], name: 'Newsletter')
|
|
35
60
|
|
|
36
61
|
# Contacts
|
|
37
|
-
client.create_contact(
|
|
62
|
+
client.create_contact(project: project['data']['slug'], email: 'user@example.com')
|
|
63
|
+
client.update_contact(project: project['data']['slug'], id: contact_id, first_name: 'John')
|
|
64
|
+
# Upsert: creates if new, updates if exists (by email)
|
|
65
|
+
client.upsert_contact(project: project['data']['slug'], email: 'user@example.com', first_name: 'Jane')
|
|
66
|
+
|
|
67
|
+
# Unsubscribe URLs
|
|
68
|
+
# Contact responses include unsubscribe_urls hash (list_id => unsubscribe_url)
|
|
69
|
+
contact = client.contact(project: project['data']['slug'], id: contact_id)
|
|
70
|
+
contact['data']['unsubscribe_urls'] # => { 1 => "https://emaillist.dev/unsubscribe/...", 2 => "..." }
|
|
71
|
+
|
|
72
|
+
# List contact responses include unsubscribe_url for that specific list
|
|
73
|
+
list_contacts = client.list_contacts(project: project['data']['slug'], list_id: list_id)
|
|
74
|
+
list_contacts['data'].each do |contact|
|
|
75
|
+
unsubscribe_url = contact['unsubscribe_url'] # => "https://emaillist.dev/unsubscribe/..."
|
|
76
|
+
# Include this URL in your email templates
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Automatic Contact Sync (Rails Models)
|
|
81
|
+
|
|
82
|
+
The gem includes a concern that automatically syncs Rails models to EmailList contacts. This is useful for syncing User models or other models that represent contacts.
|
|
83
|
+
|
|
84
|
+
### Basic Usage
|
|
85
|
+
|
|
86
|
+
Include the concern in your model and configure it:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class User < ApplicationRecord
|
|
90
|
+
include EmailListApi::SyncsEmailListContact
|
|
91
|
+
|
|
92
|
+
syncs_email_list_contact project_slug: 'newsletter'
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Now, whenever a User is created or updated, it will automatically create or update the corresponding contact in EmailList.
|
|
97
|
+
|
|
98
|
+
### Custom Field Mapping
|
|
99
|
+
|
|
100
|
+
If your model uses different field names, you can configure them:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
class User < ApplicationRecord
|
|
104
|
+
include EmailListApi::SyncsEmailListContact
|
|
105
|
+
|
|
106
|
+
syncs_email_list_contact project_slug: 'newsletter',
|
|
107
|
+
email_field: :email_address,
|
|
108
|
+
first_name_field: :given_name,
|
|
109
|
+
last_name_field: :family_name
|
|
110
|
+
end
|
|
38
111
|
```
|
|
112
|
+
|
|
113
|
+
### Custom API Version
|
|
114
|
+
|
|
115
|
+
You can specify a custom API version per model (overrides global config):
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
class User < ApplicationRecord
|
|
119
|
+
include EmailListApi::SyncsEmailListContact
|
|
120
|
+
|
|
121
|
+
syncs_email_list_contact project_slug: 'newsletter',
|
|
122
|
+
api_version: 1
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The API key is always taken from the global configuration (`EmailListApi.configure`) or the `EMAILLIST_API_KEY` environment variable. The API version defaults to 1 if not specified, and the base URL is automatically determined (production: `https://emaillist.dev/api/v1`, development: `http://localhost:3000/api/v1`)
|
|
127
|
+
|
|
128
|
+
### Optional: Store Contact ID
|
|
129
|
+
|
|
130
|
+
You can optionally add a column to store the EmailList contact ID for faster lookups:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Migration
|
|
134
|
+
add_column :users, :emaillist_contact_id, :integer
|
|
135
|
+
add_index :users, :emaillist_contact_id
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The concern will automatically populate this field when contacts are created or updated.
|
|
139
|
+
|
|
140
|
+
## Environments
|
|
141
|
+
|
|
142
|
+
Each API key is associated with an environment. When you make API requests, all operations are automatically scoped to the environment associated with your API key. This means:
|
|
143
|
+
|
|
144
|
+
- Projects, lists, and contacts are automatically filtered to your environment
|
|
145
|
+
- You don't need to specify an environment in API calls - it's determined by your API key
|
|
146
|
+
- To work with multiple environments, use different API keys (one per environment)
|
|
147
|
+
|
|
148
|
+
You can manage environments and create API keys in the EmailList web dashboard.
|
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require 'faraday/retry'
|
|
3
|
-
require 'faraday/follow_redirects'
|
|
1
|
+
require 'httpx'
|
|
4
2
|
require 'json'
|
|
3
|
+
require 'uri'
|
|
5
4
|
|
|
6
5
|
module EmailListApi
|
|
7
6
|
class Client
|
|
8
|
-
|
|
7
|
+
PRODUCTION_BASE_URL = "https://emaillist.dev/api"
|
|
8
|
+
DEVELOPMENT_BASE_URL = "http://localhost:3000/api"
|
|
9
9
|
|
|
10
|
-
def initialize(api_key:
|
|
11
|
-
|
|
12
|
-
@
|
|
10
|
+
def initialize(api_key: nil, api_version: nil, base_url: nil)
|
|
11
|
+
# Priority: explicit parameter > global config
|
|
12
|
+
@api_key = api_key || EmailListApi.configuration.api_key
|
|
13
|
+
@version = api_version || EmailListApi.configuration.api_version || 1
|
|
14
|
+
@origin, @base_path = build_url_parts(base_url)
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def build_url_parts(base_url_override = nil)
|
|
20
|
+
# Priority: explicit parameter > config
|
|
21
|
+
base_url = base_url_override || EmailListApi.configuration.base_url
|
|
22
|
+
|
|
23
|
+
raise ArgumentError, "base_url must be configured" unless base_url
|
|
24
|
+
|
|
25
|
+
uri = URI.parse(base_url)
|
|
26
|
+
origin = "#{uri.scheme}://#{uri.host}#{uri.port && uri.port != (uri.scheme == 'https' ? 443 : 80) ? ":#{uri.port}" : ''}"
|
|
27
|
+
base_path = "#{uri.path}/v#{@version}".gsub(%r{/+}, '/')
|
|
28
|
+
[ origin, base_path ]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_path(path)
|
|
32
|
+
# Ensure path starts with / and combine with base_path
|
|
33
|
+
path = "/#{path}" unless path.start_with?('/')
|
|
34
|
+
"#{@base_path}#{path}".gsub(%r{/+}, '/')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
public
|
|
38
|
+
|
|
15
39
|
# Projects
|
|
16
40
|
def projects
|
|
17
41
|
get("projects")
|
|
@@ -25,6 +49,10 @@ module EmailListApi
|
|
|
25
49
|
post("projects", { project: { name: name, description: description } })
|
|
26
50
|
end
|
|
27
51
|
|
|
52
|
+
def upsert_project(name:, description: nil)
|
|
53
|
+
post("projects/upsert", { project: { name: name, description: description } })
|
|
54
|
+
end
|
|
55
|
+
|
|
28
56
|
def update_project(id, name: nil, description: nil)
|
|
29
57
|
params = { project: {} }
|
|
30
58
|
params[:project][:name] = name if name
|
|
@@ -37,116 +65,187 @@ module EmailListApi
|
|
|
37
65
|
end
|
|
38
66
|
|
|
39
67
|
# Lists
|
|
40
|
-
def lists(
|
|
41
|
-
get("projects/#{
|
|
68
|
+
def lists(project:)
|
|
69
|
+
get("projects/#{project}/lists")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def list(project:, slug:)
|
|
73
|
+
get("projects/#{project}/lists/#{slug}")
|
|
42
74
|
end
|
|
43
75
|
|
|
44
|
-
def
|
|
45
|
-
|
|
76
|
+
def create_list(project:, name:, description: nil)
|
|
77
|
+
post("projects/#{project}/lists", { list: { name: name, description: description } })
|
|
46
78
|
end
|
|
47
79
|
|
|
48
|
-
def
|
|
49
|
-
post("projects/#{
|
|
80
|
+
def upsert_list(project:, name:, description: nil)
|
|
81
|
+
post("projects/#{project}/lists/upsert", { list: { name: name, description: description } })
|
|
50
82
|
end
|
|
51
83
|
|
|
52
|
-
def update_list(
|
|
84
|
+
def update_list(project:, slug:, name: nil, description: nil)
|
|
53
85
|
params = { list: {} }
|
|
54
86
|
params[:list][:name] = name if name
|
|
55
87
|
params[:list][:description] = description if description
|
|
56
|
-
patch("projects/#{
|
|
88
|
+
patch("projects/#{project}/lists/#{slug}", params)
|
|
57
89
|
end
|
|
58
90
|
|
|
59
|
-
def delete_list(
|
|
60
|
-
delete("projects/#{
|
|
91
|
+
def delete_list(project:, slug:)
|
|
92
|
+
delete("projects/#{project}/lists/#{slug}")
|
|
61
93
|
end
|
|
62
94
|
|
|
63
95
|
# Contacts
|
|
64
|
-
def contacts(
|
|
65
|
-
get("projects/#{
|
|
96
|
+
def contacts(project:, page: 1)
|
|
97
|
+
get("projects/#{project}/contacts", { page: page })
|
|
66
98
|
end
|
|
67
99
|
|
|
68
|
-
def contact(
|
|
69
|
-
get("projects/#{
|
|
100
|
+
def contact(project:, id:)
|
|
101
|
+
get("projects/#{project}/contacts/#{id}")
|
|
70
102
|
end
|
|
71
103
|
|
|
72
|
-
def create_contact(
|
|
104
|
+
def create_contact(project:, email:, first_name: nil, last_name: nil)
|
|
73
105
|
params = { contact: { email: email } }
|
|
74
106
|
params[:contact][:first_name] = first_name if first_name
|
|
75
107
|
params[:contact][:last_name] = last_name if last_name
|
|
76
|
-
post("projects/#{
|
|
108
|
+
post("projects/#{project}/contacts", params)
|
|
77
109
|
end
|
|
78
110
|
|
|
79
|
-
def update_contact(
|
|
111
|
+
def update_contact(project:, id:, email: nil, first_name: nil, last_name: nil)
|
|
80
112
|
params = { contact: {} }
|
|
81
113
|
params[:contact][:email] = email if email
|
|
82
114
|
params[:contact][:first_name] = first_name if first_name
|
|
83
115
|
params[:contact][:last_name] = last_name if last_name
|
|
84
|
-
patch("projects/#{
|
|
116
|
+
patch("projects/#{project}/contacts/#{id}", params)
|
|
85
117
|
end
|
|
86
118
|
|
|
87
|
-
def
|
|
88
|
-
|
|
119
|
+
def upsert_contact(project:, email:, first_name: nil, last_name: nil)
|
|
120
|
+
params = { contact: { email: email } }
|
|
121
|
+
params[:contact][:first_name] = first_name if first_name
|
|
122
|
+
params[:contact][:last_name] = last_name if last_name
|
|
123
|
+
post("projects/#{project}/contacts/upsert", params)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def delete_contact(project:, id:)
|
|
127
|
+
delete("projects/#{project}/contacts/#{id}")
|
|
89
128
|
end
|
|
90
129
|
|
|
91
130
|
# List Memberships
|
|
92
|
-
def list_contacts(
|
|
93
|
-
get("projects/#{
|
|
131
|
+
def list_contacts(project:, list_slug:)
|
|
132
|
+
get("projects/#{project}/lists/#{list_slug}/contacts")
|
|
94
133
|
end
|
|
95
134
|
|
|
96
|
-
def add_contacts_to_list(
|
|
135
|
+
def add_contacts_to_list(project:, list_slug:, contacts:)
|
|
97
136
|
# contacts can be an array of hashes or a single hash, but API expects array
|
|
98
|
-
contacts = [contacts] unless contacts.is_a?(Array)
|
|
99
|
-
post("projects/#{
|
|
137
|
+
contacts = [ contacts ] unless contacts.is_a?(Array)
|
|
138
|
+
post("projects/#{project}/lists/#{list_slug}/contacts", { contacts: contacts })
|
|
100
139
|
end
|
|
101
140
|
|
|
102
|
-
def add_contact_to_list(
|
|
141
|
+
def add_contact_to_list(project:, list_slug:, contact_id:)
|
|
103
142
|
# Helper for adding single existing contact by ID
|
|
104
|
-
add_contacts_to_list(
|
|
143
|
+
add_contacts_to_list(project: project, list_slug: list_slug, contacts: [ { id: contact_id } ])
|
|
105
144
|
end
|
|
106
145
|
|
|
107
|
-
def remove_contact_from_list(
|
|
108
|
-
delete("projects/#{
|
|
146
|
+
def remove_contact_from_list(project:, list_slug:, contact_id:)
|
|
147
|
+
delete("projects/#{project}/lists/#{list_slug}/contacts/#{contact_id}")
|
|
109
148
|
end
|
|
110
149
|
|
|
111
150
|
# Bulk Operations
|
|
112
|
-
def bulk_create_contacts(
|
|
113
|
-
post("projects/#{
|
|
151
|
+
def bulk_create_contacts(project:, contacts:)
|
|
152
|
+
post("projects/#{project}/contacts/bulk", { contacts: contacts })
|
|
114
153
|
end
|
|
115
154
|
|
|
116
155
|
private
|
|
117
156
|
|
|
118
|
-
def
|
|
119
|
-
@
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
conn.headers['User-Agent'] = "EmailListApi Ruby Client/#{EmailListApi::VERSION}"
|
|
128
|
-
conn.adapter Faraday.default_adapter
|
|
129
|
-
end
|
|
157
|
+
def http_client
|
|
158
|
+
@http_client ||= HTTPX.plugin(:follow_redirects).plugin(:retries).with(
|
|
159
|
+
origin: @origin,
|
|
160
|
+
headers: {
|
|
161
|
+
'Authorization' => "Bearer #{@api_key}",
|
|
162
|
+
'Content-Type' => 'application/json',
|
|
163
|
+
'User-Agent' => "EmailListApi Ruby Client/#{EmailListApi::VERSION}"
|
|
164
|
+
}
|
|
165
|
+
)
|
|
130
166
|
end
|
|
131
167
|
|
|
132
168
|
def get(path, params = {})
|
|
133
|
-
response =
|
|
134
|
-
response
|
|
169
|
+
response = http_client.get(build_path(path), params: params)
|
|
170
|
+
raise_error_if_needed(response)
|
|
171
|
+
parse_json_response(response)
|
|
135
172
|
end
|
|
136
173
|
|
|
137
174
|
def post(path, body = {})
|
|
138
|
-
response =
|
|
139
|
-
response
|
|
175
|
+
response = http_client.post(build_path(path), json: body)
|
|
176
|
+
raise_error_if_needed(response)
|
|
177
|
+
parse_json_response(response)
|
|
140
178
|
end
|
|
141
179
|
|
|
142
180
|
def patch(path, body = {})
|
|
143
|
-
response =
|
|
144
|
-
response
|
|
181
|
+
response = http_client.patch(build_path(path), json: body)
|
|
182
|
+
raise_error_if_needed(response)
|
|
183
|
+
parse_json_response(response)
|
|
145
184
|
end
|
|
146
185
|
|
|
147
186
|
def delete(path)
|
|
148
|
-
response =
|
|
149
|
-
response
|
|
187
|
+
response = http_client.delete(build_path(path))
|
|
188
|
+
raise_error_if_needed(response)
|
|
189
|
+
parse_json_response(response)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def parse_json_response(response)
|
|
193
|
+
# Handle 204 No Content and empty responses
|
|
194
|
+
return nil if response.status == 204 || response.body.to_s.empty?
|
|
195
|
+
|
|
196
|
+
response.json
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def raise_error_if_needed(response)
|
|
200
|
+
# HTTPX returns status as Integer (200-299 are success)
|
|
201
|
+
return if response.status >= 200 && response.status < 300
|
|
202
|
+
|
|
203
|
+
# Try to parse JSON error response
|
|
204
|
+
error_message = extract_error_message(response)
|
|
205
|
+
raise HTTPX::Error, error_message
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def extract_error_message(response)
|
|
209
|
+
body = response.body.to_s
|
|
210
|
+
|
|
211
|
+
# Try to parse as JSON first
|
|
212
|
+
begin
|
|
213
|
+
json = JSON.parse(body)
|
|
214
|
+
if json.is_a?(Hash)
|
|
215
|
+
# Extract error message from common JSON error formats
|
|
216
|
+
error = json['error'] || json['errors']
|
|
217
|
+
if error.is_a?(Hash)
|
|
218
|
+
message = error['message'] || error['code'] || error.to_s
|
|
219
|
+
elsif error.is_a?(Array) && error.any?
|
|
220
|
+
message = error.first.is_a?(Hash) ? (error.first['message'] || error.first.to_s) : error.first.to_s
|
|
221
|
+
elsif error.is_a?(String)
|
|
222
|
+
message = error
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
return "HTTP #{response.status}: #{message || json.to_s}" if message
|
|
226
|
+
end
|
|
227
|
+
rescue JSON::ParserError
|
|
228
|
+
# Not JSON, continue to HTML handling
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# If it's HTML (likely an error page), extract a meaningful message
|
|
232
|
+
if body.include?('<!DOCTYPE') || body.include?('<html')
|
|
233
|
+
# Extract title or h1 if available
|
|
234
|
+
if body =~ /<title>(.*?)<\/title>/i
|
|
235
|
+
title = $1.strip
|
|
236
|
+
return "HTTP #{response.status}: #{title}" unless title.empty?
|
|
237
|
+
end
|
|
238
|
+
if body =~ /<h1[^>]*>(.*?)<\/h1>/i
|
|
239
|
+
heading = $1.strip
|
|
240
|
+
return "HTTP #{response.status}: #{heading}" unless heading.empty?
|
|
241
|
+
end
|
|
242
|
+
# Fallback to a generic message for HTML errors
|
|
243
|
+
return "HTTP #{response.status}: Server returned an error page"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Default: return status and truncated body
|
|
247
|
+
truncated_body = body.length > 200 ? "#{body[0..200]}..." : body
|
|
248
|
+
"HTTP #{response.status}: #{truncated_body}"
|
|
150
249
|
end
|
|
151
250
|
end
|
|
152
251
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module EmailListApi
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :api_key, :api_version, :base_url, :raise_on_sync_error
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@api_key = nil
|
|
7
|
+
@api_version = nil
|
|
8
|
+
@base_url = nil
|
|
9
|
+
# Default: raise exceptions on sync errors (fail loudly)
|
|
10
|
+
# Set to false to fail silently if needed (e.g., during gem bugs)
|
|
11
|
+
@raise_on_sync_error = true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require 'active_support/concern'
|
|
2
|
+
|
|
3
|
+
module EmailListApi
|
|
4
|
+
module SyncsEmailListContact
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Optional: Add a column to store the contact_id for caching
|
|
9
|
+
# You can add this migration: add_column :users, :emaillist_contact_id, :integer
|
|
10
|
+
|
|
11
|
+
after_create :sync_to_email_list_after_create
|
|
12
|
+
after_update :sync_to_email_list_after_update
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
# Configure the sync behavior
|
|
17
|
+
# Example:
|
|
18
|
+
# syncs_email_list_contact project_slug: 'my-project',
|
|
19
|
+
# email_field: :email_address,
|
|
20
|
+
# first_name_field: :given_name,
|
|
21
|
+
# last_name_field: :family_name,
|
|
22
|
+
# api_version: 1
|
|
23
|
+
#
|
|
24
|
+
# @param project_slug [String] The project slug to sync contacts to
|
|
25
|
+
# @param email_field [Symbol] The attribute name for email (default: :email)
|
|
26
|
+
# @param first_name_field [Symbol] The attribute name for first name (default: :first_name)
|
|
27
|
+
# @param last_name_field [Symbol] The attribute name for last name (default: :last_name)
|
|
28
|
+
# @param api_version [Integer, nil] API version override (default: nil, uses global config)
|
|
29
|
+
# @param always_sync [Boolean] Sync on every save, not just when fields change (default: false)
|
|
30
|
+
def syncs_email_list_contact(project_slug:,
|
|
31
|
+
email_field: :email,
|
|
32
|
+
first_name_field: :first_name,
|
|
33
|
+
last_name_field: :last_name,
|
|
34
|
+
api_version: nil,
|
|
35
|
+
always_sync: false)
|
|
36
|
+
@emaillist_project = project_slug
|
|
37
|
+
@emaillist_email_field = email_field
|
|
38
|
+
@emaillist_first_name_field = first_name_field
|
|
39
|
+
@emaillist_last_name_field = last_name_field
|
|
40
|
+
@emaillist_api_version = api_version
|
|
41
|
+
@emaillist_always_sync = always_sync
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def emaillist_project
|
|
45
|
+
@emaillist_project
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emaillist_email_field
|
|
49
|
+
@emaillist_email_field || :email
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def emaillist_first_name_field
|
|
53
|
+
@emaillist_first_name_field || :first_name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emaillist_last_name_field
|
|
57
|
+
@emaillist_last_name_field || :last_name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def emaillist_api_version
|
|
61
|
+
@emaillist_api_version
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def emaillist_always_sync
|
|
65
|
+
@emaillist_always_sync || false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def emaillist_client
|
|
72
|
+
# Use shared client if no model-specific api_version override is configured
|
|
73
|
+
if self.class.emaillist_api_version.nil?
|
|
74
|
+
EmailListApi.client
|
|
75
|
+
else
|
|
76
|
+
# Create a custom client only if model has specific api_version override
|
|
77
|
+
@emaillist_client ||= EmailListApi::Client.new(api_version: self.class.emaillist_api_version)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def sync_to_email_list_after_create
|
|
82
|
+
sync_to_email_list
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def sync_to_email_list_after_update
|
|
86
|
+
# If always_sync is enabled, sync on every update
|
|
87
|
+
if self.class.emaillist_always_sync
|
|
88
|
+
sync_to_email_list
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Otherwise, only sync if relevant fields changed
|
|
93
|
+
email_field = self.class.emaillist_email_field
|
|
94
|
+
first_name_field = self.class.emaillist_first_name_field
|
|
95
|
+
last_name_field = self.class.emaillist_last_name_field
|
|
96
|
+
|
|
97
|
+
if saved_change_to_attribute?(email_field) ||
|
|
98
|
+
saved_change_to_attribute?(first_name_field) ||
|
|
99
|
+
saved_change_to_attribute?(last_name_field)
|
|
100
|
+
sync_to_email_list
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sync_to_email_list
|
|
105
|
+
return unless self.class.emaillist_project
|
|
106
|
+
|
|
107
|
+
email = read_attribute(self.class.emaillist_email_field)
|
|
108
|
+
return unless email.present?
|
|
109
|
+
|
|
110
|
+
first_name = read_attribute(self.class.emaillist_first_name_field)
|
|
111
|
+
last_name = read_attribute(self.class.emaillist_last_name_field)
|
|
112
|
+
|
|
113
|
+
project_slug = self.class.emaillist_project
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
# Ensure project exists (idempotent - creates if doesn't exist)
|
|
117
|
+
# Convert slug to name for upsert (e.g., "my-project" -> "My Project")
|
|
118
|
+
project_name = project_slug.to_s.split('-').map(&:capitalize).join(' ')
|
|
119
|
+
emaillist_client.upsert_project(name: project_name)
|
|
120
|
+
|
|
121
|
+
# Upsert contact (creates if new, updates if exists)
|
|
122
|
+
contact = emaillist_client.upsert_contact(
|
|
123
|
+
project: project_slug,
|
|
124
|
+
email: email,
|
|
125
|
+
first_name: first_name,
|
|
126
|
+
last_name: last_name
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Optionally store the contact_id if column exists
|
|
130
|
+
if respond_to?(:emaillist_contact_id=) && contact && contact[:data]
|
|
131
|
+
contact_id = contact[:data][:id]
|
|
132
|
+
update_column(:emaillist_contact_id, contact_id) if persisted? && contact_id
|
|
133
|
+
end
|
|
134
|
+
rescue => e
|
|
135
|
+
# Log error
|
|
136
|
+
Rails.logger.error("Failed to sync #{self.class.name}##{id} to EmailList: #{e.message}") if defined?(Rails)
|
|
137
|
+
Rails.logger.error(e.backtrace.join("\n")) if defined?(Rails) && Rails.logger.respond_to?(:level) && Rails.logger.level <= 0
|
|
138
|
+
|
|
139
|
+
# Raise exception based on global configuration (defaults to true)
|
|
140
|
+
raise e if EmailListApi.configuration.raise_on_sync_error
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/email_list_api.rb
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
require "email_list_api/version"
|
|
2
|
+
require "email_list_api/configuration"
|
|
2
3
|
require "email_list_api/client"
|
|
4
|
+
require "email_list_api/syncs_email_list_contact"
|
|
3
5
|
|
|
4
6
|
module EmailListApi
|
|
5
7
|
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_writer :configuration
|
|
11
|
+
|
|
12
|
+
def configuration
|
|
13
|
+
@configuration ||= Configuration.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
yield(configuration)
|
|
18
|
+
# Reset client when configuration changes
|
|
19
|
+
@client = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset
|
|
23
|
+
@configuration = Configuration.new
|
|
24
|
+
@client = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def client
|
|
28
|
+
@client ||= Client.new
|
|
29
|
+
end
|
|
30
|
+
end
|
|
6
31
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: email_list_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.rc.
|
|
4
|
+
version: 0.1.0.rc.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Email List Dev
|
|
@@ -10,47 +10,33 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: httpx
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '1.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: faraday-retry
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
25
|
+
version: '1.0'
|
|
40
26
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
27
|
+
name: activesupport
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
43
29
|
requirements:
|
|
44
|
-
- - "
|
|
30
|
+
- - "~>"
|
|
45
31
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0'
|
|
32
|
+
version: '5.0'
|
|
47
33
|
type: :runtime
|
|
48
34
|
prerelease: false
|
|
49
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
36
|
requirements:
|
|
51
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
52
38
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0'
|
|
39
|
+
version: '5.0'
|
|
54
40
|
- !ruby/object:Gem::Dependency
|
|
55
41
|
name: bundler
|
|
56
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -80,19 +66,19 @@ dependencies:
|
|
|
80
66
|
- !ruby/object:Gem::Version
|
|
81
67
|
version: '13.0'
|
|
82
68
|
- !ruby/object:Gem::Dependency
|
|
83
|
-
name:
|
|
69
|
+
name: minitest
|
|
84
70
|
requirement: !ruby/object:Gem::Requirement
|
|
85
71
|
requirements:
|
|
86
72
|
- - "~>"
|
|
87
73
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '
|
|
74
|
+
version: '5.0'
|
|
89
75
|
type: :development
|
|
90
76
|
prerelease: false
|
|
91
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
78
|
requirements:
|
|
93
79
|
- - "~>"
|
|
94
80
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '
|
|
81
|
+
version: '5.0'
|
|
96
82
|
description: A simple Ruby client for managing projects, lists, and contacts via the
|
|
97
83
|
Email List API.
|
|
98
84
|
email:
|
|
@@ -101,14 +87,18 @@ executables: []
|
|
|
101
87
|
extensions: []
|
|
102
88
|
extra_rdoc_files: []
|
|
103
89
|
files:
|
|
90
|
+
- LICENSE.txt
|
|
104
91
|
- README.md
|
|
105
92
|
- lib/email_list_api.rb
|
|
106
93
|
- lib/email_list_api/client.rb
|
|
94
|
+
- lib/email_list_api/configuration.rb
|
|
95
|
+
- lib/email_list_api/syncs_email_list_contact.rb
|
|
107
96
|
- lib/email_list_api/version.rb
|
|
108
|
-
homepage: https://
|
|
97
|
+
homepage: https://emaillist.dev/docs
|
|
109
98
|
licenses:
|
|
110
99
|
- MIT
|
|
111
|
-
metadata:
|
|
100
|
+
metadata:
|
|
101
|
+
allowed_push_host: https://rubygems.org
|
|
112
102
|
rdoc_options: []
|
|
113
103
|
require_paths:
|
|
114
104
|
- lib
|
|
@@ -116,7 +106,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
116
106
|
requirements:
|
|
117
107
|
- - ">="
|
|
118
108
|
- !ruby/object:Gem::Version
|
|
119
|
-
version: '0'
|
|
109
|
+
version: '3.0'
|
|
120
110
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
111
|
requirements:
|
|
122
112
|
- - ">="
|