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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 569696fa5acbb7f0c5520aff129c6418666e4006039e128dbbe97590a74d10ae
4
- data.tar.gz: 73a0880766c7e8a17f9a0cae70eb07afdef8a202202f4ab17d4888d8e3ea459f
3
+ metadata.gz: 4b77ff6c1af95fbe8c6e90914fc03840ca25c53b5fa1cafd0e5f50c670f41117
4
+ data.tar.gz: 51f8e21e0641f04f1e33f52f97d86ae11935bb8d6596dec3acc3b8b9708a15ea
5
5
  SHA512:
6
- metadata.gz: 9f63af4821d7d61af74c0c593ce97a597a6aa1c647b3d61b81fd69e9973daa4d9aedb6cc2a5744bb6fbae2170814e7955e3ff97b59a216146ecf1254be84cc35
7
- data.tar.gz: 8ec2008d7bf08e60b0d4dc0f43586e9dc62a17dea20f8c704efd8d186236683c4b2a4fd624b99f839247ef22d255f23bf258282d7ea52e7cfe31bcf97171b556
6
+ metadata.gz: dcd7b3e74eb02dad0b26682a8f35ca34c90511059d43c867e6b0c7ef6c051d1c8f45d9e57e9be88a40a24af3980f33a326239f541254806cf4da9fd7fb6db4f3
7
+ data.tar.gz: 62d43c452750bfca92a29c18ee1ff4f20fd924b18bc26afbe535567bdd159fde9bfbd8c33f25a53b6b6c3b0ca5ae5617f11077dadfeafafde0c352debdad3af8
@@ -8,9 +8,11 @@ jobs:
8
8
  - 3.0
9
9
  - 3.1
10
10
  - 3.2
11
+ - 3.3
12
+ - 3.4
11
13
  runs-on: ubuntu-latest
12
14
  steps:
13
- - uses: actions/checkout@v3
15
+ - uses: actions/checkout@v4
14
16
  - uses: ruby/setup-ruby@v1
15
17
  with:
16
18
  ruby-version: ${{ matrix.ruby }}
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
@@ -11,3 +11,5 @@ gem 'rubocop', '~> 1.45'
11
11
  gem 'sidekiq'
12
12
  gem 'activejob'
13
13
  gem 'uri'
14
+ gem 'irb'
15
+ gem 'mail'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Userlist for Ruby [![Build Status](https://github.com/userlist/userlist-ruby/workflows/Tests/badge.svg)](https://github.com/userlist/userlist-ruby)
1
+ # Userlist for Ruby [![Build Status](https://github.com/userlist/userlist-ruby/workflows/Tests/badge.svg)](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
@@ -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
- logger.debug "Sending #{request.method} to #{URI.join(endpoint, request.path)} with body #{request.body}"
59
-
72
+ def process_request(request)
60
73
  http.start unless http.started?
61
- response = http.request(request)
74
+ http.request(request)
75
+ rescue Timeout::Error => e
76
+ raise Userlist::TimeoutError, e.message
77
+ end
62
78
 
63
- logger.debug "Recieved #{response.code} #{response.message} with body #{response.body}"
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)
@@ -19,11 +19,7 @@ module Userlist
19
19
  end
20
20
 
21
21
  def retryable
22
- @retryable ||= Userlist::Retryable.new do |response|
23
- status = response.code.to_i
24
-
25
- status >= 500 || status == 429
26
- end
22
+ @retryable ||= Userlist::Retryable.new
27
23
  end
28
24
  end
29
25
  end
@@ -46,11 +46,7 @@ module Userlist
46
46
  end
47
47
 
48
48
  def retryable
49
- @retryable ||= Userlist::Retryable.new do |response|
50
- status = response.code.to_i
51
-
52
- status >= 500 || status == 429
53
- end
49
+ @retryable ||= Userlist::Retryable.new
54
50
  end
55
51
  end
56
52
  end
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
@@ -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
- if attempt.positive?
25
- milliseconds = delay(attempt)
26
- logger.debug "Retrying in #{milliseconds}ms, #{@retries - attempt} retries left"
27
- sleep(milliseconds / 1000.0)
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'
@@ -1,3 +1,3 @@
1
1
  module Userlist
2
- VERSION = '0.9.0'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
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 <<~MESSAGE
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.9.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: 2024-03-19 00:00:00.000000000 Z
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.5.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: []