pushover 3.0.2 → 4.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +28 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +11 -24
  5. data/.ruby-version +1 -0
  6. data/CONVENTIONS.md +19 -0
  7. data/Gemfile +1 -2
  8. data/Guardfile +3 -3
  9. data/README.md +236 -44
  10. data/Rakefile +9 -59
  11. data/exe/pushover +6 -5
  12. data/lib/pushover/client.rb +53 -0
  13. data/lib/pushover/glances.rb +86 -0
  14. data/lib/pushover/groups.rb +137 -0
  15. data/lib/pushover/licenses.rb +67 -0
  16. data/lib/pushover/limits.rb +18 -0
  17. data/lib/pushover/message.rb +29 -32
  18. data/lib/pushover/message_encryption.rb +48 -0
  19. data/lib/pushover/message_validator.rb +167 -0
  20. data/lib/pushover/messages.rb +20 -0
  21. data/lib/pushover/receipt.rb +23 -10
  22. data/lib/pushover/receipts.rb +60 -0
  23. data/lib/pushover/response.rb +8 -3
  24. data/lib/pushover/sounds.rb +18 -0
  25. data/lib/pushover/subscriptions.rb +48 -0
  26. data/lib/pushover/teams.rb +93 -0
  27. data/lib/pushover/users.rb +37 -0
  28. data/lib/pushover/version.rb +1 -1
  29. data/lib/pushover.rb +14 -1
  30. data/pushover.gemspec +9 -7
  31. data/spec/gem/specification_spec.rb +19 -0
  32. data/spec/pushover/client_spec.rb +149 -0
  33. data/spec/pushover/glances_spec.rb +206 -0
  34. data/spec/pushover/groups_spec.rb +486 -0
  35. data/spec/pushover/licenses_spec.rb +238 -0
  36. data/spec/pushover/limits_spec.rb +92 -0
  37. data/spec/pushover/message_spec.rb +146 -26
  38. data/spec/pushover/messages_spec.rb +199 -0
  39. data/spec/pushover/receipt_spec.rb +119 -11
  40. data/spec/pushover/receipts_spec.rb +232 -0
  41. data/spec/pushover/response_spec.rb +21 -16
  42. data/spec/pushover/sounds_spec.rb +92 -0
  43. data/spec/pushover/subscriptions_spec.rb +185 -0
  44. data/spec/pushover/teams_spec.rb +325 -0
  45. data/spec/pushover/users_spec.rb +115 -0
  46. data/spec/spec_helper.rb +2 -3
  47. metadata +54 -18
  48. data/.travis.yml +0 -8
@@ -0,0 +1,60 @@
1
+ require 'uri'
2
+
3
+ module Pushover
4
+ # Receipts provides operations for the Receipt API.
5
+ class Receipts
6
+ RECEIPT_PATTERN = /\A[A-Za-z0-9]{30}\z/
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ # Retrieve the current status of an emergency message receipt.
13
+ # @return [Response] the Pushover API response
14
+ def get(receipt:)
15
+ validate_receipt!(receipt)
16
+
17
+ response = @client.connection.get(
18
+ path: "/1/receipts/#{receipt}.json",
19
+ query: { token: @client.token }
20
+ )
21
+ Response.create_from_excon_response(response)
22
+ end
23
+
24
+ # Cancel future retries for an emergency message receipt.
25
+ # @return [Response] the Pushover API response
26
+ def cancel(receipt:)
27
+ validate_receipt!(receipt)
28
+
29
+ response = @client.connection.post(
30
+ path: "/1/receipts/#{receipt}/cancel.json",
31
+ body: Oj.dump('token' => @client.token)
32
+ )
33
+ Response.create_from_excon_response(response)
34
+ end
35
+
36
+ # Cancel future retries for all emergency messages with a tag.
37
+ # @return [Response] the Pushover API response
38
+ def cancel_by_tag(tag:)
39
+ validate_tag!(tag)
40
+ encoded_tag = URI.encode_uri_component(tag)
41
+
42
+ response = @client.connection.post(
43
+ path: "/1/receipts/cancel_by_tag/#{encoded_tag}.json",
44
+ body: Oj.dump('token' => @client.token)
45
+ )
46
+ Response.create_from_excon_response(response)
47
+ end
48
+
49
+ private
50
+
51
+ def validate_receipt!(receipt)
52
+ raise ArgumentError, 'receipt must be supplied' unless receipt.is_a?(String) && !receipt.empty?
53
+ raise ArgumentError, 'receipt must be 30 alphanumeric characters' unless receipt.match?(RECEIPT_PATTERN)
54
+ end
55
+
56
+ def validate_tag!(tag)
57
+ raise ArgumentError, 'tag must be supplied' unless tag.is_a?(String) && !tag.empty?
58
+ end
59
+ end
60
+ end
@@ -6,7 +6,7 @@ module Pushover
6
6
  # @attribute request
7
7
  # @return [String] request is a UUID representing the specific call
8
8
  # @attribute errors
9
- # @return [Array] errors includes any errors made
9
+ # @return [Array, Hash] errors includes any errors made
10
10
  # @attribute receipt
11
11
  # @return [String] receipt returns a receipt if requested
12
12
  # @attribute headers
@@ -27,12 +27,16 @@ module Pushover
27
27
 
28
28
  # purty.
29
29
  def to_s
30
- "#{errors ? 'errors: ' + errors.join("\n") : 'status: ok'}, #{limits}"
30
+ "#{errors ? "errors: #{formatted_errors}" : 'status: ok'}, #{limits}"
31
31
  end
32
32
 
33
33
  private
34
34
 
35
- # :nocov:
35
+ def formatted_errors
36
+ errors.respond_to?(:join) ? errors.join("\n") : errors
37
+ end
38
+
39
+ # simplecov:disable
36
40
  # Application limits
37
41
  # @return [Array] 0: Remaining Calls, 1: Total Limit, 2: Limit Reset
38
42
  def limits
@@ -42,5 +46,6 @@ module Pushover
42
46
  output.define_singleton_method(:to_s) { "requests #{self[0]} of #{self[1]}, reset on #{Time.at(self[2].to_f)}" }
43
47
  output
44
48
  end
49
+ # simplecov:enable
45
50
  end
46
51
  end
@@ -0,0 +1,18 @@
1
+ module Pushover
2
+ # Sounds provides operations for the Sounds API.
3
+ class Sounds
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ # Retrieve built-in and application-account custom sounds.
9
+ # @return [Response] the Pushover API response
10
+ def get
11
+ response = @client.connection.get(
12
+ path: '/1/sounds.json',
13
+ query: { token: @client.token }
14
+ )
15
+ Response.create_from_excon_response(response)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,48 @@
1
+ require 'pushover/users'
2
+
3
+ module Pushover
4
+ # Subscriptions provides operations for the Subscription API.
5
+ class Subscriptions
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # Migrate an existing user key to an application subscription.
11
+ # @return [Response] the Pushover API response
12
+ def migrate(subscription:, user:, device_name: nil, sound: nil)
13
+ validate_subscription!(subscription)
14
+ validate_user!(user)
15
+ validate_device_name!(device_name) unless device_name.nil?
16
+ validate_sound!(sound) unless sound.nil?
17
+
18
+ body = { 'token' => @client.token, 'subscription' => subscription, 'user' => user }
19
+ body['device_name'] = device_name unless device_name.nil?
20
+ body['sound'] = sound unless sound.nil?
21
+
22
+ response = @client.connection.post(path: '/1/subscriptions/migrate.json', body: Oj.dump(body))
23
+ Response.create_from_excon_response(response)
24
+ end
25
+
26
+ private
27
+
28
+ def validate_subscription!(subscription)
29
+ raise ArgumentError, 'subscription must be supplied' if subscription.nil? || subscription == ''
30
+ raise ArgumentError, 'subscription must be a String' unless subscription.is_a?(String)
31
+ end
32
+
33
+ def validate_user!(user)
34
+ raise ArgumentError, 'user must be supplied' if user.nil? || user == ''
35
+ raise ArgumentError, 'user must be 30 alphanumeric characters' unless user.is_a?(String) && user.match?(Users::USER_PATTERN)
36
+ end
37
+
38
+ def validate_device_name!(device_name)
39
+ return if device_name.is_a?(String) && device_name.match?(Users::DEVICE_PATTERN)
40
+
41
+ raise ArgumentError, 'device_name must be 1 to 25 letters, numbers, underscores, or hyphens'
42
+ end
43
+
44
+ def validate_sound!(sound)
45
+ raise ArgumentError, 'sound must be a String' unless sound.is_a?(String)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,93 @@
1
+ module Pushover
2
+ # Teams provides operations for the Pushover for Teams API.
3
+ class Teams
4
+ TEXT_FIELDS = %i[name password group].freeze
5
+ FLAGS = %i[instant admin].freeze
6
+
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # Retrieve the team and its users and devices.
12
+ # @return [Response] the Pushover API response
13
+ def get
14
+ response = @client.connection.get(
15
+ path: '/1/teams.json',
16
+ query: { token: @client.token }
17
+ )
18
+ Response.create_from_excon_response(response)
19
+ end
20
+
21
+ # Add a user or send an invitation to the team.
22
+ # @return [Response] the Pushover API response
23
+ def add_user(email:, **fields)
24
+ validate_email!(email)
25
+ validate_add_user_fields!(fields)
26
+
27
+ body = { 'email' => email }.merge(fields.slice(*TEXT_FIELDS).compact.transform_keys(&:to_s))
28
+ add_flags!(body, **fields.slice(*FLAGS))
29
+ post_action('add_user', body)
30
+ end
31
+
32
+ # Revoke a pending team invitation.
33
+ # @return [Response] the Pushover API response
34
+ def revoke_invitation(email:)
35
+ post_email_action('revoke_invitation', email)
36
+ end
37
+
38
+ # Remove a user from the team and its delivery groups.
39
+ # @return [Response] the Pushover API response
40
+ def remove_user(email:)
41
+ post_email_action('remove_user', email)
42
+ end
43
+
44
+ private
45
+
46
+ def post_email_action(action, email)
47
+ validate_email!(email)
48
+ post_action(action, 'email' => email)
49
+ end
50
+
51
+ def post_action(action, fields)
52
+ body = { 'token' => @client.token }.merge(fields)
53
+ response = @client.connection.post(
54
+ path: "/1/teams/#{action}.json",
55
+ body: Oj.dump(body)
56
+ )
57
+ Response.create_from_excon_response(response)
58
+ end
59
+
60
+ def add_flags!(body, **flags)
61
+ flags.each do |field, value|
62
+ next if value.nil?
63
+
64
+ validate_flag!(field, value)
65
+ body[field.to_s] = 'true' if value
66
+ end
67
+ end
68
+
69
+ def validate_email!(email)
70
+ return if email.is_a?(String) && !email.strip.empty?
71
+
72
+ raise ArgumentError, 'email must be a nonblank String'
73
+ end
74
+
75
+ def validate_text_fields!(fields)
76
+ invalid = TEXT_FIELDS.find { |field| fields.key?(field) && !fields[field].is_a?(String) }
77
+ raise ArgumentError, "#{invalid} must be a String" if invalid
78
+ end
79
+
80
+ def validate_add_user_fields!(fields)
81
+ unknown = fields.keys - (TEXT_FIELDS + FLAGS)
82
+ raise ArgumentError, "unsupported team user parameter: #{unknown.first}" unless unknown.empty?
83
+
84
+ validate_text_fields!(fields.compact)
85
+ end
86
+
87
+ def validate_flag!(field, value)
88
+ return if [true, false].include?(value)
89
+
90
+ raise ArgumentError, "#{field} must be true or false"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,37 @@
1
+ module Pushover
2
+ # Users provides operations for the User/Group Validation API.
3
+ class Users
4
+ USER_PATTERN = /\A[A-Za-z0-9]{30}\z/
5
+ DEVICE_PATTERN = /\A[A-Za-z0-9_-]{1,25}\z/
6
+
7
+ def initialize(client)
8
+ @client = client
9
+ end
10
+
11
+ # Validate a user or group identifier and optional device.
12
+ # @return [Response] the Pushover API response
13
+ def validate(user:, device: nil)
14
+ validate_user!(user)
15
+ validate_device!(device) unless device.nil?
16
+
17
+ body = { 'token' => @client.token, 'user' => user }
18
+ body['device'] = device if device
19
+
20
+ response = @client.connection.post(path: '/1/users/validate.json', body: Oj.dump(body))
21
+ Response.create_from_excon_response(response)
22
+ end
23
+
24
+ private
25
+
26
+ def validate_user!(user)
27
+ raise ArgumentError, 'user must be supplied' if user.nil? || user == ''
28
+ raise ArgumentError, 'user must be 30 alphanumeric characters' unless user.is_a?(String) && user.match?(USER_PATTERN)
29
+ end
30
+
31
+ def validate_device!(device)
32
+ return if device.is_a?(String) && device.match?(DEVICE_PATTERN)
33
+
34
+ raise ArgumentError, 'device must be 1 to 25 letters, numbers, underscores, or hyphens'
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module Pushover
2
- VERSION = "3.0.2".freeze # The current version of Pushover.
2
+ VERSION = "4.0.0".freeze # The current version of Pushover.
3
3
  end
data/lib/pushover.rb CHANGED
@@ -1,9 +1,22 @@
1
1
  require 'oj'
2
2
  require 'excon'
3
3
 
4
+ require 'pushover/client'
5
+ require 'pushover/glances'
6
+ require 'pushover/groups'
7
+ require 'pushover/licenses'
8
+ require 'pushover/limits'
9
+ require 'pushover/message_encryption'
10
+ require 'pushover/message_validator'
11
+ require 'pushover/messages'
4
12
  require 'pushover/message'
5
13
  require 'pushover/response'
14
+ require 'pushover/receipts'
6
15
  require 'pushover/receipt'
16
+ require 'pushover/sounds'
17
+ require 'pushover/subscriptions'
18
+ require 'pushover/teams'
19
+ require 'pushover/users'
7
20
  require 'pushover/version'
8
21
 
9
22
  # pushover interface for ruby
@@ -11,5 +24,5 @@ module Pushover
11
24
  # headers to include in every request.
12
25
  HEADERS = { 'Content-Type' => 'application/json', 'User-Agent' => "pushover (ruby gem) v#{VERSION}" }.freeze
13
26
  # excon connection to use for every request.
14
- Excon = Excon.new('https://api.pushover.net', headers: HEADERS).freeze
27
+ Excon = Excon.new('https://api.pushover.net', headers: HEADERS)
15
28
  end
data/pushover.gemspec CHANGED
@@ -3,9 +3,9 @@ Gem::Specification.new do |spec|
3
3
  spec.name = 'pushover'
4
4
  spec.authors = ['Ernie Brodeur']
5
5
  spec.email = ['ebrodeur@ujami.net']
6
- spec.date = Time.now.strftime('%Y-%m-%d')
7
6
  spec.version = Pushover::VERSION
8
7
  spec.platform = Gem::Platform::RUBY
8
+ spec.required_ruby_version = '>= 3.3.0'
9
9
  spec.license = 'MIT'
10
10
 
11
11
  # descriptions
@@ -14,13 +14,15 @@ Gem::Specification.new do |spec|
14
14
  spec.homepage = 'https://github.com/erniebrodeur/pushover'
15
15
 
16
16
  # files
17
- spec.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
18
- spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
19
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.files = `git ls-files -z --cached --others --exclude-standard`.split("\0").select { |file| File.file?(file) }
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |file| File.basename(file) }
20
20
  spec.require_paths = ['lib']
21
21
 
22
22
  # dependencies.
23
- spec.add_runtime_dependency 'excon'
24
- spec.add_runtime_dependency 'gli'
25
- spec.add_runtime_dependency 'oj'
23
+ spec.add_dependency 'base64'
24
+ spec.add_dependency 'excon'
25
+ spec.add_dependency 'gli'
26
+ spec.add_dependency 'oj'
27
+ spec.metadata['rubygems_mfa_required'] = 'true'
26
28
  end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Gem::Specification do
4
+ subject(:specification) do
5
+ described_class.load(File.expand_path('../../pushover.gemspec', __dir__))
6
+ end
7
+
8
+ it 'uses exe as the executable directory' do
9
+ expect(specification.bindir).to eq('exe')
10
+ end
11
+
12
+ it 'installs only the public pushover CLI' do
13
+ expect(specification.executables).to eq(['pushover'])
14
+ end
15
+
16
+ it 'packages the public CLI' do
17
+ expect(specification.files).to include('exe/pushover')
18
+ end
19
+ end
@@ -0,0 +1,149 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pushover::Client do
4
+ describe '#initialize' do
5
+ it 'requires an application token' do
6
+ expect { described_class.new(token: '') }.to raise_error ArgumentError, /token must be supplied/
7
+ end
8
+ end
9
+
10
+ describe '#messages' do
11
+ subject(:client) { described_class.new(token: 'token') }
12
+
13
+ it 'returns a message resource' do
14
+ expect(client.messages).to be_a Pushover::Messages
15
+ end
16
+
17
+ it 'reuses the message resource' do
18
+ messages = client.messages
19
+
20
+ expect(client.messages).to equal messages
21
+ end
22
+ end
23
+
24
+ describe '#receipts' do
25
+ subject(:client) { described_class.new(token: 'token') }
26
+
27
+ it 'returns a receipt resource' do
28
+ expect(client.receipts).to be_a Pushover::Receipts
29
+ end
30
+
31
+ it 'reuses the receipt resource' do
32
+ receipts = client.receipts
33
+
34
+ expect(client.receipts).to equal receipts
35
+ end
36
+ end
37
+
38
+ describe '#users' do
39
+ subject(:client) { described_class.new(token: 'token') }
40
+
41
+ it 'returns a user resource' do
42
+ expect(client.users).to be_a Pushover::Users
43
+ end
44
+
45
+ it 'reuses the user resource' do
46
+ users = client.users
47
+
48
+ expect(client.users).to equal users
49
+ end
50
+ end
51
+
52
+ describe '#sounds' do
53
+ subject(:client) { described_class.new(token: 'token') }
54
+
55
+ it 'returns a sounds resource' do
56
+ expect(client.sounds).to be_a Pushover::Sounds
57
+ end
58
+
59
+ it 'reuses the sounds resource' do
60
+ sounds = client.sounds
61
+
62
+ expect(client.sounds).to equal sounds
63
+ end
64
+ end
65
+
66
+ describe '#limits' do
67
+ subject(:client) { described_class.new(token: 'token') }
68
+
69
+ it 'returns a limits resource' do
70
+ expect(client.limits).to be_a Pushover::Limits
71
+ end
72
+
73
+ it 'reuses the limits resource' do
74
+ limits = client.limits
75
+
76
+ expect(client.limits).to equal limits
77
+ end
78
+ end
79
+
80
+ describe '#glances' do
81
+ subject(:client) { described_class.new(token: 'token') }
82
+
83
+ it 'returns a glances resource' do
84
+ expect(client.glances).to be_a Pushover::Glances
85
+ end
86
+
87
+ it 'reuses the glances resource' do
88
+ glances = client.glances
89
+
90
+ expect(client.glances).to equal glances
91
+ end
92
+ end
93
+
94
+ describe '#groups' do
95
+ subject(:client) { described_class.new(token: 'token') }
96
+
97
+ it 'returns a groups resource' do
98
+ expect(client.groups).to be_a Pushover::Groups
99
+ end
100
+
101
+ it 'reuses the groups resource' do
102
+ groups = client.groups
103
+
104
+ expect(client.groups).to equal groups
105
+ end
106
+ end
107
+
108
+ describe '#subscriptions' do
109
+ subject(:client) { described_class.new(token: 'token') }
110
+
111
+ it 'returns a subscriptions resource' do
112
+ expect(client.subscriptions).to be_a Pushover::Subscriptions
113
+ end
114
+
115
+ it 'reuses the subscriptions resource' do
116
+ subscriptions = client.subscriptions
117
+
118
+ expect(client.subscriptions).to equal subscriptions
119
+ end
120
+ end
121
+
122
+ describe '#licenses' do
123
+ subject(:client) { described_class.new(token: 'token') }
124
+
125
+ it 'returns a licenses resource' do
126
+ expect(client.licenses).to be_a Pushover::Licenses
127
+ end
128
+
129
+ it 'reuses the licenses resource' do
130
+ licenses = client.licenses
131
+
132
+ expect(client.licenses).to equal licenses
133
+ end
134
+ end
135
+
136
+ describe '#teams' do
137
+ subject(:client) { described_class.new(token: 'team-token') }
138
+
139
+ it 'returns a teams resource' do
140
+ expect(client.teams).to be_a Pushover::Teams
141
+ end
142
+
143
+ it 'reuses the teams resource' do
144
+ teams = client.teams
145
+
146
+ expect(client.teams).to equal teams
147
+ end
148
+ end
149
+ end