userlist 0.9.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 569696fa5acbb7f0c5520aff129c6418666e4006039e128dbbe97590a74d10ae
4
- data.tar.gz: 73a0880766c7e8a17f9a0cae70eb07afdef8a202202f4ab17d4888d8e3ea459f
3
+ metadata.gz: 9ed2e089f640ba0d4865236a53b5744e9edfbbe4400337b789c6e09c80474870
4
+ data.tar.gz: ce7ad3b24cc47645ee91d69aa27f312628f840c427c52f49ae38823abf9ad1d8
5
5
  SHA512:
6
- metadata.gz: 9f63af4821d7d61af74c0c593ce97a597a6aa1c647b3d61b81fd69e9973daa4d9aedb6cc2a5744bb6fbae2170814e7955e3ff97b59a216146ecf1254be84cc35
7
- data.tar.gz: 8ec2008d7bf08e60b0d4dc0f43586e9dc62a17dea20f8c704efd8d186236683c4b2a4fd624b99f839247ef22d255f23bf258282d7ea52e7cfe31bcf97171b556
6
+ metadata.gz: 6c0b7d45d3edcb1a07e10e759b6d3911ff594f6feceadc7fb17eef9961ea693853e570e9223ce837cc11cc04b025fd227ed42ba4c5e35501406f9a636d39f22b
7
+ data.tar.gz: 64146b1ba4fda2069e74e230e81c7d1447166b03775fe048ba821d7ec04702ab69ea85a116ffb0243f4f3a4e01b83e7e53c28fef7813f9351851428bd6343743
@@ -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/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.4
2
+ TargetRubyVersion: 3.0
3
3
  NewCops: enable
4
4
  Exclude:
5
5
  - 'bin/*'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Userlist for Ruby Changelog
2
2
 
3
+ ## Unreleased (main)
4
+
5
+ ## v1.1.0 (2025-10-17)
6
+
7
+ - Allow specifying companies in messages
8
+ - Add reply_to support to DeliveryMethod for ActionMailer integration
9
+ - Fixes issue with deleting resources when they are not pushed by default
10
+
11
+ ## v1.0.0 (2025-04-08)
12
+
13
+ - Updates ActiveJob Worker to retry on errors with polynomially longer wait times, up to 10 attempts
14
+ - Improve internal error handling to rely on exceptions
15
+ - Adds support for messages
16
+
3
17
  ## v0.9.0 (2024-03-19)
4
18
 
5
19
  - Allows deleteing resources by using other identifiers (like email)
data/Gemfile CHANGED
@@ -4,6 +4,11 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'jwt', '~> 2.2'
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rspec', '~> 3.0'
10
+ gem 'webmock', '~> 3.18'
11
+
7
12
  gem 'guard-rspec', '~> 4.7'
8
13
  gem 'guard-rubocop', '~> 1.3'
9
14
  gem 'rubocop', '~> 1.45'
@@ -11,3 +16,5 @@ gem 'rubocop', '~> 1.45'
11
16
  gem 'sidekiq'
12
17
  gem 'activejob'
13
18
  gem 'uri'
19
+ gem 'irb'
20
+ 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.
@@ -75,12 +75,12 @@ module Userlist
75
75
  key?(name.to_sym) || super
76
76
  end
77
77
 
78
- def method_missing(name, *args, &block)
78
+ def method_missing(name, ...)
79
79
  if respond_to_missing?(name)
80
80
  name = name.to_s
81
81
  method = name =~ /=$/ ? :[]= : :[]
82
82
  name = name.sub(/=$/, '').to_sym
83
- send(method, name, *args, &block)
83
+ send(method, name, ...)
84
84
  else
85
85
  super
86
86
  end
@@ -0,0 +1,55 @@
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
+ reply_to: serialize_address(mail.reply_to),
23
+ subject: mail.subject,
24
+ body: serialize_body(mail.body)
25
+ }.compact
26
+ end
27
+
28
+ def serialize_address(address)
29
+ return if address.nil? || Array(address).empty?
30
+
31
+ Array(address).map(&:to_s)
32
+ end
33
+
34
+ def serialize_body(body)
35
+ return if body.nil?
36
+
37
+ if body.multipart?
38
+ parts = body.parts.filter_map { |part| serialize_part(part) }
39
+
40
+ return parts.first if parts.size == 1
41
+
42
+ { type: :multipart, content: parts }
43
+ else
44
+ { type: :text, content: body.decoded }
45
+ end
46
+ end
47
+
48
+ def serialize_part(part)
49
+ if part.content_type.start_with?('text/html')
50
+ { type: :html, content: part.decoded }
51
+ elsif part.content_type.start_with?('text/plain')
52
+ { type: :text, content: part.decoded }
53
+ end
54
+ end
55
+ 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
@@ -1,7 +1,7 @@
1
1
  module Userlist
2
2
  class Push
3
3
  class Company < Resource
4
- include Operations::Create
4
+ include Operations::Push
5
5
  include Operations::Delete
6
6
 
7
7
  def self.endpoint
@@ -1,7 +1,7 @@
1
1
  module Userlist
2
2
  class Push
3
3
  class Event < Resource
4
- include Operations::Create
4
+ include Operations::Push
5
5
 
6
6
  has_one :user, type: 'Userlist::Push::User'
7
7
  has_one :company, type: 'Userlist::Push::Company'
@@ -0,0 +1,20 @@
1
+ module Userlist
2
+ class Push
3
+ class Message < Resource
4
+ include Operations::Push
5
+
6
+ has_one :user, type: 'Userlist::Push::User'
7
+ has_one :company, type: 'Userlist::Push::Company'
8
+
9
+ def initialize(payload = {}, config = Userlist.config)
10
+ raise Userlist::ArgumentError, 'Missing required payload' unless payload
11
+
12
+ super
13
+ end
14
+
15
+ def push?
16
+ super && (user.nil? || user.push?)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -5,8 +5,9 @@ module Userlist
5
5
  module ClassMethods
6
6
  def delete(payload = {}, config = self.config)
7
7
  return false unless resource = from_payload(payload, config)
8
+ return false unless resource.delete?
8
9
 
9
- strategy.call(:delete, endpoint, resource) if resource.delete?
10
+ strategy.call(:delete, endpoint, resource.for_context(:delete))
10
11
  end
11
12
  end
12
13
 
@@ -1,24 +1,22 @@
1
1
  module Userlist
2
2
  class Push
3
3
  module Operations
4
- module Create
4
+ module Push
5
5
  module ClassMethods
6
- def create(payload = {}, config = self.config)
6
+ def push(payload = {}, config = self.config)
7
7
  return false unless resource = from_payload(payload, config)
8
+ return false unless resource.push?
8
9
 
9
- strategy.call(:post, endpoint, resource) if resource.create?
10
+ strategy.call(:post, endpoint, resource.for_context(:push))
10
11
  end
11
12
 
12
- alias push create
13
+ alias create push
14
+ alias update push
13
15
  end
14
16
 
15
17
  def self.included(base)
16
18
  base.extend(ClassMethods)
17
19
  end
18
-
19
- def create?
20
- push?
21
- end
22
20
  end
23
21
  end
24
22
  end
@@ -1,7 +1,7 @@
1
1
  module Userlist
2
2
  class Push
3
3
  class Relationship < Resource
4
- include Operations::Create
4
+ include Operations::Push
5
5
  include Operations::Delete
6
6
 
7
7
  has_one :user, type: 'Userlist::Push::User'
@@ -19,18 +19,18 @@ module Userlist
19
19
  new(payload, config)
20
20
  end
21
21
 
22
- def relationship_names
23
- @relationship_names ||= relationships.keys
22
+ def association_names
23
+ @association_names ||= associations.keys
24
24
  end
25
25
 
26
- def relationships
27
- @relationships ||= {}
26
+ def associations
27
+ @associations ||= {}
28
28
  end
29
29
 
30
30
  protected
31
31
 
32
32
  def has_one(name, type:) # rubocop:disable Naming/PredicateName
33
- relationships[name.to_sym] = { type: type }
33
+ associations[name.to_sym] = { type: type }
34
34
 
35
35
  generated_methods.class_eval <<-RUBY, __FILE__, __LINE__ + 1
36
36
  def #{name} # def company
@@ -40,14 +40,14 @@ module Userlist
40
40
  end
41
41
 
42
42
  def has_many(name, **options) # rubocop:disable Naming/PredicateName
43
- relationships[name.to_sym] = options
43
+ associations[name.to_sym] = options
44
44
 
45
45
  generated_methods.class_eval <<-RUBY, __FILE__, __LINE__ + 1
46
46
  def #{name} # def companies
47
- relationship = self.class.relationships[:#{name}] # relationship = self.class.relationships[:companies]
47
+ associations = self.class.associations[:#{name}] # associations = self.class.associations[:companies]
48
48
  #
49
- ResourceCollection.new(payload[:#{name}], relationship, self, config) # ResourceCollection.new(payload[:companies], relationship, self, config)
50
- end #
49
+ ResourceCollection.new(payload[:#{name}], associations, self, config) # ResourceCollection.new(payload[:companies], associations, self, config)
50
+ end # end
51
51
  RUBY
52
52
  end
53
53
 
@@ -58,13 +58,14 @@ module Userlist
58
58
  end
59
59
  end
60
60
 
61
- attr_reader :payload, :config
61
+ attr_reader :payload, :config, :context
62
62
 
63
63
  def initialize(payload = {}, config = Userlist.config)
64
64
  raise Userlist::ArgumentError, 'Missing required payload' unless payload
65
65
 
66
66
  @payload = payload
67
67
  @config = config
68
+ @context = :push
68
69
  end
69
70
 
70
71
  def respond_to_missing?(method, include_private = false)
@@ -73,7 +74,7 @@ module Userlist
73
74
  end
74
75
 
75
76
  def to_hash
76
- Serializer.serialize(self)
77
+ Serializer.serialize(self, context: context)
77
78
  end
78
79
  alias to_h to_hash
79
80
 
@@ -95,17 +96,23 @@ module Userlist
95
96
  alias == eql?
96
97
 
97
98
  def attribute_names
98
- payload.keys.map(&:to_sym) - relationship_names
99
+ payload.keys.map(&:to_sym) - association_names
99
100
  end
100
101
 
101
- def relationship_names
102
- self.class.relationship_names.to_a
102
+ def association_names
103
+ self.class.association_names.to_a
103
104
  end
104
105
 
105
106
  def push?
106
107
  true
107
108
  end
108
109
 
110
+ def for_context(context)
111
+ dup.tap do |instance|
112
+ instance.instance_variable_set(:@context, context)
113
+ end
114
+ end
115
+
109
116
  private
110
117
 
111
118
  def method_missing(method, *args, &block)
@@ -3,8 +3,14 @@ require 'set'
3
3
  module Userlist
4
4
  class Push
5
5
  class Serializer
6
- def self.serialize(resource)
7
- new.serialize(resource)
6
+ def self.serialize(resource, **options)
7
+ new(**options).serialize(resource)
8
+ end
9
+
10
+ attr_reader :context
11
+
12
+ def initialize(context:)
13
+ @context = context
8
14
  end
9
15
 
10
16
  def serialize(resource)
@@ -12,6 +18,13 @@ module Userlist
12
18
  resource
13
19
  end
14
20
 
21
+ def serialize?(resource)
22
+ method_name = "#{context}?"
23
+
24
+ resource.respond_to?(method_name) &&
25
+ resource.public_send(method_name)
26
+ end
27
+
15
28
  private
16
29
 
17
30
  def serialize_resource(resource)
@@ -19,7 +32,7 @@ module Userlist
19
32
 
20
33
  serialized_resources << resource
21
34
 
22
- return unless resource.push?
35
+ return unless serialize?(resource)
23
36
 
24
37
  serialized = {}
25
38
 
@@ -27,8 +40,8 @@ module Userlist
27
40
  serialized[name] = resource.send(name)
28
41
  end
29
42
 
30
- resource.relationship_names.each do |name|
31
- next unless result = serialize_relationship(resource.send(name))
43
+ resource.association_names.each do |name|
44
+ next unless result = serialize_association(resource.send(name))
32
45
 
33
46
  serialized[name] = result
34
47
  end
@@ -36,22 +49,22 @@ module Userlist
36
49
  serialized
37
50
  end
38
51
 
39
- def serialize_relationship(relationship)
40
- return unless relationship
52
+ def serialize_association(association)
53
+ return unless association
41
54
 
42
- case relationship
55
+ case association
43
56
  when Userlist::Push::ResourceCollection
44
- serialize_collection(relationship)
57
+ serialize_collection(association)
45
58
  when Userlist::Push::Resource
46
- serialize_resource(relationship)
59
+ serialize_resource(association)
47
60
  else
48
- raise "Cannot serialize relationship type: #{relationship.class}"
61
+ raise "Cannot serialize association type: #{association.class}"
49
62
  end
50
63
  end
51
64
 
52
65
  def serialize_collection(collection)
53
66
  serialized = collection
54
- .map(&method(:serialize_relationship))
67
+ .map(&method(:serialize_association))
55
68
  .compact
56
69
  .reject(&:empty?)
57
70
 
@@ -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)
@@ -27,7 +27,7 @@ module Userlist
27
27
  def options
28
28
  @options ||= begin
29
29
  options = config.push_strategy_options || {}
30
- options.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
30
+ options.transform_keys(&:to_sym)
31
31
  end
32
32
  end
33
33
 
@@ -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
@@ -25,7 +25,7 @@ module Userlist
25
25
  def options
26
26
  @options ||= begin
27
27
  options = config.push_strategy_options || {}
28
- options.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
28
+ options.transform_keys(&:to_s)
29
29
  end
30
30
  end
31
31
 
@@ -18,14 +18,12 @@ module Userlist
18
18
  logger.info 'Starting worker thread...'
19
19
 
20
20
  loop do
21
- begin
22
- method, *args = *queue.pop
23
- break if method == :stop
24
-
25
- retryable.attempt { client.public_send(method, *args) }
26
- rescue StandardError => e
27
- logger.error "Failed to deliver payload: [#{e.class.name}] #{e.message}"
28
- end
21
+ method, *args = *queue.pop
22
+ break if method == :stop
23
+
24
+ retryable.attempt { client.public_send(method, *args) }
25
+ rescue StandardError => e
26
+ logger.error "Failed to deliver payload: [#{e.class.name}] #{e.message}"
29
27
  end
30
28
 
31
29
  logger.info "Worker thread exited with #{queue.size} tasks still in the queue..."
@@ -46,11 +44,7 @@ module Userlist
46
44
  end
47
45
 
48
46
  def retryable
49
- @retryable ||= Userlist::Retryable.new do |response|
50
- status = response.code.to_i
51
-
52
- status >= 500 || status == 429
53
- end
47
+ @retryable ||= Userlist::Retryable.new
54
48
  end
55
49
  end
56
50
  end
@@ -1,7 +1,7 @@
1
1
  module Userlist
2
2
  class Push
3
3
  class User < Resource
4
- include Operations::Create
4
+ include Operations::Push
5
5
  include Operations::Delete
6
6
 
7
7
  has_many :relationships, type: 'Userlist::Push::Relationship', inverse: :user
data/lib/userlist/push.rb CHANGED
@@ -5,20 +5,21 @@ require 'userlist/push/resource'
5
5
  require 'userlist/push/resource_collection'
6
6
  require 'userlist/push/relation'
7
7
 
8
- require 'userlist/push/operations/create'
8
+ require 'userlist/push/operations/push'
9
9
  require 'userlist/push/operations/delete'
10
10
 
11
11
  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
 
@@ -37,31 +38,39 @@ module Userlist
37
38
  attr_reader :config, :strategy
38
39
 
39
40
  def events
40
- @events ||= Relation.new(self, Event, [Operations::Create])
41
+ @events ||= Relation.new(self, Event, [Operations::Push])
41
42
  end
42
43
 
43
44
  def users
44
- @users ||= Relation.new(self, User, [Operations::Create, Operations::Delete])
45
+ @users ||= Relation.new(self, User, [Operations::Push, Operations::Delete])
45
46
  end
46
47
 
47
48
  def companies
48
- @companies ||= Relation.new(self, Company, [Operations::Create, Operations::Delete])
49
+ @companies ||= Relation.new(self, Company, [Operations::Push, Operations::Delete])
49
50
  end
50
51
 
51
52
  def relationships
52
- @relationships ||= Relation.new(self, Relationship, [Operations::Create, Operations::Delete])
53
+ @relationships ||= Relation.new(self, Relationship, [Operations::Push, Operations::Delete])
54
+ end
55
+
56
+ def messages
57
+ @messages ||= Relation.new(self, Message, [Operations::Push])
53
58
  end
54
59
 
55
60
  def event(payload = {})
56
- events.create(payload)
61
+ events.push(payload)
57
62
  end
58
63
 
59
64
  def user(payload = {})
60
- users.create(payload)
65
+ users.push(payload)
61
66
  end
62
67
 
63
68
  def company(payload = {})
64
- companies.create(payload)
69
+ companies.push(payload)
70
+ end
71
+
72
+ def message(payload = {})
73
+ messages.push(payload)
65
74
  end
66
75
 
67
76
  alias track event
@@ -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)
@@ -27,9 +39,9 @@ module Userlist
27
39
  sleep(milliseconds / 1000.0)
28
40
  end
29
41
 
30
- result = yield
31
-
32
- return result unless retry?(result)
42
+ return yield
43
+ rescue Userlist::Error => e
44
+ raise e unless retry?(e)
33
45
  end
34
46
 
35
47
  logger.debug 'Retries exhausted, giving up'
@@ -1,3 +1,3 @@
1
1
  module Userlist
2
- VERSION = '0.9.0'.freeze
2
+ VERSION = '1.1.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
data/userlist.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['benedikt@userlist.com']
10
10
 
11
11
  spec.summary = 'Ruby wrapper for the Userlist API'
12
- spec.homepage = 'http://github.com/userlistio/userlist-ruby'
12
+ spec.homepage = 'http://github.com/userlist/userlist-ruby'
13
13
  spec.license = 'MIT'
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
@@ -19,13 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.required_ruby_version = '>= 2.4'
23
-
24
- spec.add_development_dependency 'bundler', '>= 1.15'
25
- spec.add_development_dependency 'jwt', '~> 2.2'
26
- spec.add_development_dependency 'rake', '~> 13.0'
27
- spec.add_development_dependency 'rspec', '~> 3.0'
28
- spec.add_development_dependency 'webmock', '~> 3.18'
22
+ spec.required_ruby_version = '>= 3.0'
29
23
 
30
24
  spec.metadata = { 'rubygems_mfa_required' => 'true' }
31
25
  end
metadata CHANGED
@@ -1,86 +1,14 @@
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.1.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
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '1.15'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '1.15'
27
- - !ruby/object:Gem::Dependency
28
- name: jwt
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '2.2'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.2'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '13.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '13.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.0'
69
- - !ruby/object:Gem::Dependency
70
- name: webmock
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '3.18'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '3.18'
83
- description:
10
+ date: 2025-10-17 00:00:00.000000000 Z
11
+ dependencies: []
84
12
  email:
85
13
  - benedikt@userlist.com
86
14
  executables: []
@@ -102,13 +30,15 @@ files:
102
30
  - bin/setup
103
31
  - lib/userlist.rb
104
32
  - lib/userlist/config.rb
33
+ - lib/userlist/delivery_method.rb
105
34
  - lib/userlist/logging.rb
106
35
  - lib/userlist/push.rb
107
36
  - lib/userlist/push/client.rb
108
37
  - lib/userlist/push/company.rb
109
38
  - lib/userlist/push/event.rb
110
- - lib/userlist/push/operations/create.rb
39
+ - lib/userlist/push/message.rb
111
40
  - lib/userlist/push/operations/delete.rb
41
+ - lib/userlist/push/operations/push.rb
112
42
  - lib/userlist/push/relation.rb
113
43
  - lib/userlist/push/relationship.rb
114
44
  - lib/userlist/push/resource.rb
@@ -128,12 +58,11 @@ files:
128
58
  - lib/userlist/token.rb
129
59
  - lib/userlist/version.rb
130
60
  - userlist.gemspec
131
- homepage: http://github.com/userlistio/userlist-ruby
61
+ homepage: http://github.com/userlist/userlist-ruby
132
62
  licenses:
133
63
  - MIT
134
64
  metadata:
135
65
  rubygems_mfa_required: 'true'
136
- post_install_message:
137
66
  rdoc_options: []
138
67
  require_paths:
139
68
  - lib
@@ -141,15 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
70
  requirements:
142
71
  - - ">="
143
72
  - !ruby/object:Gem::Version
144
- version: '2.4'
73
+ version: '3.0'
145
74
  required_rubygems_version: !ruby/object:Gem::Requirement
146
75
  requirements:
147
76
  - - ">="
148
77
  - !ruby/object:Gem::Version
149
78
  version: '0'
150
79
  requirements: []
151
- rubygems_version: 3.5.3
152
- signing_key:
80
+ rubygems_version: 3.6.2
153
81
  specification_version: 4
154
82
  summary: Ruby wrapper for the Userlist API
155
83
  test_files: []