userlist 0.9.0 → 1.0.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/.github/workflows/test.yml +3 -1
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -0
- data/README.md +18 -1
- data/lib/userlist/delivery_method.rb +52 -0
- data/lib/userlist/push/client.rb +21 -4
- data/lib/userlist/push/message.rb +19 -0
- data/lib/userlist/push/strategies/active_job/worker.rb +2 -0
- data/lib/userlist/push/strategies/direct.rb +1 -5
- data/lib/userlist/push/strategies/threaded/worker.rb +1 -5
- data/lib/userlist/push.rb +10 -1
- data/lib/userlist/retryable.rb +23 -9
- data/lib/userlist/version.rb +1 -1
- data/lib/userlist.rb +18 -1
- metadata +5 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b77ff6c1af95fbe8c6e90914fc03840ca25c53b5fa1cafd0e5f50c670f41117
|
4
|
+
data.tar.gz: 51f8e21e0641f04f1e33f52f97d86ae11935bb8d6596dec3acc3b8b9708a15ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dcd7b3e74eb02dad0b26682a8f35ca34c90511059d43c867e6b0c7ef6c051d1c8f45d9e57e9be88a40a24af3980f33a326239f541254806cf4da9fd7fb6db4f3
|
7
|
+
data.tar.gz: 62d43c452750bfca92a29c18ee1ff4f20fd924b18bc26afbe535567bdd159fde9bfbd8c33f25a53b6b6c3b0ca5ae5617f11077dadfeafafde0c352debdad3af8
|
data/.github/workflows/test.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Userlist for Ruby Changelog
|
2
2
|
|
3
|
+
## Unreleased (main)
|
4
|
+
|
5
|
+
## v1.0.0 (2025-04-08)
|
6
|
+
|
7
|
+
- Updates ActiveJob Worker to retry on errors with polynomially longer wait times, up to 10 attempts
|
8
|
+
- Improve internal error handling to rely on exceptions
|
9
|
+
- Adds support for messages
|
10
|
+
|
3
11
|
## v0.9.0 (2024-03-19)
|
4
12
|
|
5
13
|
- Allows deleteing resources by using other identifiers (like email)
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Userlist for Ruby
|
1
|
+
# Userlist for Ruby [](https://github.com/userlist/userlist-ruby)
|
2
2
|
|
3
3
|
This gem helps with integrating [Userlist](https://userlist.com) into Ruby applications.
|
4
4
|
|
@@ -186,6 +186,23 @@ Userlist::Push.events.push(
|
|
186
186
|
)
|
187
187
|
```
|
188
188
|
|
189
|
+
### Sending Transactional Messages
|
190
|
+
|
191
|
+
To send transactional messages, use the `Userlist::Push.messages.push` method.
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
message = {
|
195
|
+
user: 'user-1',
|
196
|
+
template: 'welcome-email',
|
197
|
+
properties: {
|
198
|
+
account_name: 'Example, Inc.',
|
199
|
+
billing_plan: 'Pro'
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
Userlist::Push.messages.push(message)
|
204
|
+
```
|
205
|
+
|
189
206
|
### Tokens for in-app messages
|
190
207
|
|
191
208
|
In order to use in-app messages, you must create a JWT token for the currently signed in user on the server side. To do this, please configure both the `push_key` and the `push_id` configuration variables. Afterwards, you can use the `Userlist::Token.generate` method to get a signed token for the given user identifier.
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Userlist::DeliveryMethod
|
2
|
+
attr_reader :userlist, :settings
|
3
|
+
|
4
|
+
def initialize(settings = {})
|
5
|
+
@settings = settings
|
6
|
+
|
7
|
+
@userlist = Userlist::Push.new(settings)
|
8
|
+
end
|
9
|
+
|
10
|
+
def deliver!(mail)
|
11
|
+
message = serialize(mail)
|
12
|
+
|
13
|
+
userlist.messages.push(message.merge(theme: nil))
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def serialize(mail)
|
19
|
+
{
|
20
|
+
to: serialize_address(mail.to),
|
21
|
+
from: serialize_address(mail.from),
|
22
|
+
subject: mail.subject,
|
23
|
+
body: serialize_body(mail.body)
|
24
|
+
}.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
def serialize_address(address)
|
28
|
+
Array(address).map(&:to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize_body(body)
|
32
|
+
return if body.nil?
|
33
|
+
|
34
|
+
if body.multipart?
|
35
|
+
parts = body.parts.filter_map { |part| serialize_part(part) }
|
36
|
+
|
37
|
+
return parts.first if parts.size == 1
|
38
|
+
|
39
|
+
{ type: :multipart, content: parts }
|
40
|
+
else
|
41
|
+
{ type: :text, content: body.decoded }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def serialize_part(part)
|
46
|
+
if part.content_type.start_with?('text/html')
|
47
|
+
{ type: :html, content: part.decoded }
|
48
|
+
elsif part.content_type.start_with?('text/plain')
|
49
|
+
{ type: :text, content: part.decoded }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/userlist/push/client.rb
CHANGED
@@ -49,18 +49,35 @@ module Userlist
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def request(method, path, payload = nil)
|
52
|
+
request = build_request(method, path, payload)
|
53
|
+
|
54
|
+
logger.debug "Sending #{request.method} to #{URI.join(endpoint, request.path)} with body #{request.body}"
|
55
|
+
|
56
|
+
response = process_request(request)
|
57
|
+
|
58
|
+
logger.debug "Recieved #{response.code} #{response.message} with body #{response.body}"
|
59
|
+
|
60
|
+
handle_response(response)
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_request(method, path, payload)
|
52
64
|
request = method.new(path)
|
53
65
|
request['Accept'] = 'application/json'
|
54
66
|
request['Authorization'] = "Push #{token}"
|
55
67
|
request['Content-Type'] = 'application/json; charset=UTF-8'
|
56
68
|
request.body = JSON.generate(payload) if payload
|
69
|
+
request
|
70
|
+
end
|
57
71
|
|
58
|
-
|
59
|
-
|
72
|
+
def process_request(request)
|
60
73
|
http.start unless http.started?
|
61
|
-
|
74
|
+
http.request(request)
|
75
|
+
rescue Timeout::Error => e
|
76
|
+
raise Userlist::TimeoutError, e.message
|
77
|
+
end
|
62
78
|
|
63
|
-
|
79
|
+
def handle_response(response)
|
80
|
+
raise(Userlist::RequestError, response) if response.code.to_i >= 400
|
64
81
|
|
65
82
|
response
|
66
83
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Userlist
|
2
|
+
class Push
|
3
|
+
class Message < Resource
|
4
|
+
include Operations::Create
|
5
|
+
|
6
|
+
has_one :user, type: 'Userlist::Push::User'
|
7
|
+
|
8
|
+
def initialize(payload = {}, config = Userlist.config)
|
9
|
+
raise Userlist::ArgumentError, 'Missing required payload' unless payload
|
10
|
+
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def push?
|
15
|
+
super && (user.nil? || user.push?)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -5,6 +5,8 @@ module Userlist
|
|
5
5
|
module Strategies
|
6
6
|
class ActiveJob
|
7
7
|
class Worker < ::ActiveJob::Base
|
8
|
+
retry_on Userlist::TimeoutError, Userlist::RequestError, wait: :polynomially_longer, attempts: 10
|
9
|
+
|
8
10
|
def perform(method, *args)
|
9
11
|
client = Userlist::Push::Client.new
|
10
12
|
client.public_send(method, *args)
|
data/lib/userlist/push.rb
CHANGED
@@ -12,13 +12,14 @@ require 'userlist/push/user'
|
|
12
12
|
require 'userlist/push/company'
|
13
13
|
require 'userlist/push/relationship'
|
14
14
|
require 'userlist/push/event'
|
15
|
+
require 'userlist/push/message'
|
15
16
|
|
16
17
|
require 'userlist/push/serializer'
|
17
18
|
|
18
19
|
module Userlist
|
19
20
|
class Push
|
20
21
|
class << self
|
21
|
-
[:event, :track, :user, :identify, :company, :users, :events, :companies, :relationships].each do |method|
|
22
|
+
[:event, :track, :user, :identify, :company, :message, :users, :events, :companies, :relationships, :messages].each do |method|
|
22
23
|
define_method(method) { |*args| default_push_instance.send(method, *args) }
|
23
24
|
end
|
24
25
|
|
@@ -52,6 +53,10 @@ module Userlist
|
|
52
53
|
@relationships ||= Relation.new(self, Relationship, [Operations::Create, Operations::Delete])
|
53
54
|
end
|
54
55
|
|
56
|
+
def messages
|
57
|
+
@messages ||= Relation.new(self, Message, [Operations::Create])
|
58
|
+
end
|
59
|
+
|
55
60
|
def event(payload = {})
|
56
61
|
events.create(payload)
|
57
62
|
end
|
@@ -64,6 +69,10 @@ module Userlist
|
|
64
69
|
companies.create(payload)
|
65
70
|
end
|
66
71
|
|
72
|
+
def message(payload = {})
|
73
|
+
messages.create(payload)
|
74
|
+
end
|
75
|
+
|
67
76
|
alias track event
|
68
77
|
alias identify user
|
69
78
|
end
|
data/lib/userlist/retryable.rb
CHANGED
@@ -7,12 +7,24 @@ module Userlist
|
|
7
7
|
MULTIPLIER = 2
|
8
8
|
MAX_DELAY = 10_000
|
9
9
|
|
10
|
+
DEFAULT_RETRY_CHECK = lambda do |error|
|
11
|
+
case error
|
12
|
+
when Userlist::RequestError
|
13
|
+
status = error.status
|
14
|
+
status >= 500 || status == 429
|
15
|
+
when Userlist::TimeoutError
|
16
|
+
true
|
17
|
+
else
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
10
22
|
def initialize(retries: RETRIES, delay: DELAY, max_delay: MAX_DELAY, multiplier: MULTIPLIER, &retry_check)
|
11
23
|
@retries = retries
|
12
24
|
@delay = delay
|
13
25
|
@max_delay = max_delay
|
14
26
|
@multiplier = multiplier
|
15
|
-
@retry_check = retry_check
|
27
|
+
@retry_check = retry_check || DEFAULT_RETRY_CHECK
|
16
28
|
end
|
17
29
|
|
18
30
|
def retry?(value)
|
@@ -21,15 +33,17 @@ module Userlist
|
|
21
33
|
|
22
34
|
def attempt
|
23
35
|
(0..@retries).each do |attempt|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
36
|
+
begin
|
37
|
+
if attempt.positive?
|
38
|
+
milliseconds = delay(attempt)
|
39
|
+
logger.debug "Retrying in #{milliseconds}ms, #{@retries - attempt} retries left"
|
40
|
+
sleep(milliseconds / 1000.0)
|
41
|
+
end
|
42
|
+
|
43
|
+
return yield
|
44
|
+
rescue Userlist::Error => e
|
45
|
+
raise e unless retry?(e)
|
28
46
|
end
|
29
|
-
|
30
|
-
result = yield
|
31
|
-
|
32
|
-
return result unless retry?(result)
|
33
47
|
end
|
34
48
|
|
35
49
|
logger.debug 'Retries exhausted, giving up'
|
data/lib/userlist/version.rb
CHANGED
data/lib/userlist.rb
CHANGED
@@ -6,6 +6,7 @@ require 'userlist/logging'
|
|
6
6
|
require 'userlist/retryable'
|
7
7
|
require 'userlist/push'
|
8
8
|
require 'userlist/token'
|
9
|
+
require 'userlist/delivery_method'
|
9
10
|
|
10
11
|
module Userlist
|
11
12
|
class Error < StandardError; end
|
@@ -18,7 +19,7 @@ module Userlist
|
|
18
19
|
def initialize(key)
|
19
20
|
@key = key.to_sym
|
20
21
|
|
21
|
-
super
|
22
|
+
super(<<~MESSAGE)
|
22
23
|
Missing required configuration value for `#{key}`
|
23
24
|
|
24
25
|
Please set a value for `#{key}` using an environment variable:
|
@@ -34,6 +35,22 @@ module Userlist
|
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
38
|
+
class TimeoutError < Error; end
|
39
|
+
|
40
|
+
class RequestError < Error
|
41
|
+
attr_reader :response
|
42
|
+
|
43
|
+
def initialize(response)
|
44
|
+
super("Request failed with status #{response.code}: #{response.body}")
|
45
|
+
|
46
|
+
@response = response
|
47
|
+
end
|
48
|
+
|
49
|
+
def status
|
50
|
+
@response.code.to_i
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
37
54
|
class << self
|
38
55
|
def config
|
39
56
|
@config ||= Userlist::Config.new
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: userlist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benedikt Deicke
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-04-08 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: bundler
|
@@ -80,7 +79,6 @@ dependencies:
|
|
80
79
|
- - "~>"
|
81
80
|
- !ruby/object:Gem::Version
|
82
81
|
version: '3.18'
|
83
|
-
description:
|
84
82
|
email:
|
85
83
|
- benedikt@userlist.com
|
86
84
|
executables: []
|
@@ -102,11 +100,13 @@ files:
|
|
102
100
|
- bin/setup
|
103
101
|
- lib/userlist.rb
|
104
102
|
- lib/userlist/config.rb
|
103
|
+
- lib/userlist/delivery_method.rb
|
105
104
|
- lib/userlist/logging.rb
|
106
105
|
- lib/userlist/push.rb
|
107
106
|
- lib/userlist/push/client.rb
|
108
107
|
- lib/userlist/push/company.rb
|
109
108
|
- lib/userlist/push/event.rb
|
109
|
+
- lib/userlist/push/message.rb
|
110
110
|
- lib/userlist/push/operations/create.rb
|
111
111
|
- lib/userlist/push/operations/delete.rb
|
112
112
|
- lib/userlist/push/relation.rb
|
@@ -133,7 +133,6 @@ licenses:
|
|
133
133
|
- MIT
|
134
134
|
metadata:
|
135
135
|
rubygems_mfa_required: 'true'
|
136
|
-
post_install_message:
|
137
136
|
rdoc_options: []
|
138
137
|
require_paths:
|
139
138
|
- lib
|
@@ -148,8 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
147
|
- !ruby/object:Gem::Version
|
149
148
|
version: '0'
|
150
149
|
requirements: []
|
151
|
-
rubygems_version: 3.
|
152
|
-
signing_key:
|
150
|
+
rubygems_version: 3.6.2
|
153
151
|
specification_version: 4
|
154
152
|
summary: Ruby wrapper for the Userlist API
|
155
153
|
test_files: []
|