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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +28 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +11 -24
- data/.ruby-version +1 -0
- data/CONVENTIONS.md +19 -0
- data/Gemfile +1 -2
- data/Guardfile +3 -3
- data/README.md +236 -44
- data/Rakefile +9 -59
- data/exe/pushover +6 -5
- data/lib/pushover/client.rb +53 -0
- data/lib/pushover/glances.rb +86 -0
- data/lib/pushover/groups.rb +137 -0
- data/lib/pushover/licenses.rb +67 -0
- data/lib/pushover/limits.rb +18 -0
- data/lib/pushover/message.rb +29 -32
- data/lib/pushover/message_encryption.rb +48 -0
- data/lib/pushover/message_validator.rb +167 -0
- data/lib/pushover/messages.rb +20 -0
- data/lib/pushover/receipt.rb +23 -10
- data/lib/pushover/receipts.rb +60 -0
- data/lib/pushover/response.rb +8 -3
- data/lib/pushover/sounds.rb +18 -0
- data/lib/pushover/subscriptions.rb +48 -0
- data/lib/pushover/teams.rb +93 -0
- data/lib/pushover/users.rb +37 -0
- data/lib/pushover/version.rb +1 -1
- data/lib/pushover.rb +14 -1
- data/pushover.gemspec +9 -7
- data/spec/gem/specification_spec.rb +19 -0
- data/spec/pushover/client_spec.rb +149 -0
- data/spec/pushover/glances_spec.rb +206 -0
- data/spec/pushover/groups_spec.rb +486 -0
- data/spec/pushover/licenses_spec.rb +238 -0
- data/spec/pushover/limits_spec.rb +92 -0
- data/spec/pushover/message_spec.rb +146 -26
- data/spec/pushover/messages_spec.rb +199 -0
- data/spec/pushover/receipt_spec.rb +119 -11
- data/spec/pushover/receipts_spec.rb +232 -0
- data/spec/pushover/response_spec.rb +21 -16
- data/spec/pushover/sounds_spec.rb +92 -0
- data/spec/pushover/subscriptions_spec.rb +185 -0
- data/spec/pushover/teams_spec.rb +325 -0
- data/spec/pushover/users_spec.rb +115 -0
- data/spec/spec_helper.rb +2 -3
- metadata +54 -18
- data/.travis.yml +0 -8
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'pushover/users'
|
|
2
|
+
|
|
3
|
+
module Pushover
|
|
4
|
+
# Glances provides operations for the Glances API.
|
|
5
|
+
class Glances
|
|
6
|
+
DATA_FIELDS = %i[title text subtext count percent].freeze
|
|
7
|
+
SUPPORTED_FIELDS = ([:device] + DATA_FIELDS).freeze
|
|
8
|
+
TEXT_FIELDS = %i[title text subtext].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Update one or more fields shown on a user's registered widgets.
|
|
15
|
+
# @return [Response] the Pushover API response
|
|
16
|
+
def update(user:, **params)
|
|
17
|
+
params = params.compact
|
|
18
|
+
validate_supported_fields!(params)
|
|
19
|
+
validate_user!(user)
|
|
20
|
+
validate_data_fields!(params)
|
|
21
|
+
validate_device!(params)
|
|
22
|
+
validate_text_fields!(params)
|
|
23
|
+
validate_count!(params)
|
|
24
|
+
validate_percent!(params)
|
|
25
|
+
|
|
26
|
+
body = { 'token' => @client.token, 'user' => user }.merge(params.transform_keys(&:to_s))
|
|
27
|
+
response = @client.connection.post(path: '/1/glances.json', body: Oj.dump(body))
|
|
28
|
+
Response.create_from_excon_response(response)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate_supported_fields!(params)
|
|
34
|
+
unknown = params.keys - SUPPORTED_FIELDS
|
|
35
|
+
raise ArgumentError, "unsupported glance parameter: #{unknown.first}" unless unknown.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_user!(user)
|
|
39
|
+
raise ArgumentError, 'user must be supplied' if user.nil? || user == ''
|
|
40
|
+
raise ArgumentError, 'user must be 30 alphanumeric characters' unless user.is_a?(String) && user.match?(Users::USER_PATTERN)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate_data_fields!(params)
|
|
44
|
+
return if DATA_FIELDS.any? { |field| params.key?(field) }
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, 'at least one glance data field must be supplied'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_device!(params)
|
|
50
|
+
return unless params.key?(:device)
|
|
51
|
+
|
|
52
|
+
device = params[:device]
|
|
53
|
+
return if device == '' || (device.is_a?(String) && device.match?(Users::DEVICE_PATTERN))
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, 'device must be blank or 1 to 25 letters, numbers, underscores, or hyphens'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_text_fields!(params)
|
|
59
|
+
TEXT_FIELDS.each do |field|
|
|
60
|
+
next unless params.key?(field)
|
|
61
|
+
|
|
62
|
+
value = params[field]
|
|
63
|
+
raise ArgumentError, "#{field} must be a String" unless value.is_a?(String)
|
|
64
|
+
raise ArgumentError, "#{field} must be at most 100 characters" if value.length > 100
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_count!(params)
|
|
69
|
+
return unless params.key?(:count)
|
|
70
|
+
|
|
71
|
+
count = params[:count]
|
|
72
|
+
return if count == '' || count.is_a?(Integer)
|
|
73
|
+
|
|
74
|
+
raise ArgumentError, 'count must be an Integer or blank'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_percent!(params)
|
|
78
|
+
return unless params.key?(:percent)
|
|
79
|
+
|
|
80
|
+
percent = params[:percent]
|
|
81
|
+
return if percent == '' || (percent.is_a?(Integer) && percent.between?(0, 100))
|
|
82
|
+
|
|
83
|
+
raise ArgumentError, 'percent must be an Integer from 0 to 100 or blank'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'pushover/users'
|
|
2
|
+
|
|
3
|
+
module Pushover
|
|
4
|
+
# Groups provides operations for the Delivery Groups API.
|
|
5
|
+
class Groups
|
|
6
|
+
GROUP_PATTERN = /\A[A-Za-z0-9]{30}\z/
|
|
7
|
+
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Create an empty delivery group.
|
|
13
|
+
# @return [Response] the Pushover API response
|
|
14
|
+
def create(name:)
|
|
15
|
+
validate_name!(name)
|
|
16
|
+
|
|
17
|
+
response = @client.connection.post(
|
|
18
|
+
path: '/1/groups.json',
|
|
19
|
+
body: Oj.dump('token' => @client.token, 'name' => name)
|
|
20
|
+
)
|
|
21
|
+
Response.create_from_excon_response(response)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# List delivery groups owned by the client's account or team.
|
|
25
|
+
# @return [Response] the Pushover API response
|
|
26
|
+
def list
|
|
27
|
+
response = @client.connection.get(
|
|
28
|
+
path: '/1/groups.json',
|
|
29
|
+
query: { token: @client.token }
|
|
30
|
+
)
|
|
31
|
+
Response.create_from_excon_response(response)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Retrieve a delivery group's name and users.
|
|
35
|
+
# @return [Response] the Pushover API response
|
|
36
|
+
def get(group:)
|
|
37
|
+
validate_group!(group)
|
|
38
|
+
|
|
39
|
+
response = @client.connection.get(
|
|
40
|
+
path: "/1/groups/#{group}.json",
|
|
41
|
+
query: { token: @client.token }
|
|
42
|
+
)
|
|
43
|
+
Response.create_from_excon_response(response)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Add a user membership to a delivery group.
|
|
47
|
+
# @return [Response] the Pushover API response
|
|
48
|
+
def add_user(group:, user:, device: nil, memo: nil)
|
|
49
|
+
validate_membership!(group, user, device)
|
|
50
|
+
validate_memo!(memo) unless memo.nil?
|
|
51
|
+
|
|
52
|
+
fields = membership_fields(user, device)
|
|
53
|
+
fields['memo'] = memo unless memo.nil?
|
|
54
|
+
post_group_action(group, 'add_user', fields)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Remove a user's matching memberships from a delivery group.
|
|
58
|
+
# @return [Response] the Pushover API response
|
|
59
|
+
def remove_user(group:, user:, device: nil)
|
|
60
|
+
post_user_action('remove_user', group:, user:, device:)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Temporarily disable a user's matching group memberships.
|
|
64
|
+
# @return [Response] the Pushover API response
|
|
65
|
+
def disable_user(group:, user:, device: nil)
|
|
66
|
+
post_user_action('disable_user', group:, user:, device:)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Re-enable a user's matching group memberships.
|
|
70
|
+
# @return [Response] the Pushover API response
|
|
71
|
+
def enable_user(group:, user:, device: nil)
|
|
72
|
+
post_user_action('enable_user', group:, user:, device:)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Rename a delivery group.
|
|
76
|
+
# @return [Response] the Pushover API response
|
|
77
|
+
def rename(group:, name:)
|
|
78
|
+
validate_group!(group)
|
|
79
|
+
validate_name!(name)
|
|
80
|
+
post_group_action(group, 'rename', 'name' => name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def post_user_action(action, group:, user:, device:)
|
|
86
|
+
validate_membership!(group, user, device)
|
|
87
|
+
post_group_action(group, action, membership_fields(user, device))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def post_group_action(group, action, fields)
|
|
91
|
+
body = { 'token' => @client.token }.merge(fields)
|
|
92
|
+
response = @client.connection.post(
|
|
93
|
+
path: "/1/groups/#{group}/#{action}.json",
|
|
94
|
+
body: Oj.dump(body)
|
|
95
|
+
)
|
|
96
|
+
Response.create_from_excon_response(response)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def membership_fields(user, device)
|
|
100
|
+
fields = { 'user' => user }
|
|
101
|
+
fields['device'] = device unless device.nil?
|
|
102
|
+
fields
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_membership!(group, user, device)
|
|
106
|
+
validate_group!(group)
|
|
107
|
+
validate_user!(user)
|
|
108
|
+
validate_device!(device) unless device.nil?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_group!(group)
|
|
112
|
+
raise ArgumentError, 'group must be supplied' if group.nil? || group == ''
|
|
113
|
+
raise ArgumentError, 'group must be 30 alphanumeric characters' unless group.is_a?(String) && group.match?(GROUP_PATTERN)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_user!(user)
|
|
117
|
+
raise ArgumentError, 'user must be supplied' if user.nil? || user == ''
|
|
118
|
+
raise ArgumentError, 'user must be 30 alphanumeric characters' unless user.is_a?(String) && user.match?(Users::USER_PATTERN)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def validate_device!(device)
|
|
122
|
+
return if device == '' || (device.is_a?(String) && device.match?(Users::DEVICE_PATTERN))
|
|
123
|
+
|
|
124
|
+
raise ArgumentError, 'device must be blank or 1 to 25 letters, numbers, underscores, or hyphens'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate_memo!(memo)
|
|
128
|
+
raise ArgumentError, 'memo must be a String' unless memo.is_a?(String)
|
|
129
|
+
raise ArgumentError, 'memo must be at most 200 characters' if memo.length > 200
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_name!(name)
|
|
133
|
+
raise ArgumentError, 'name must be supplied' if name.nil? || name == ''
|
|
134
|
+
raise ArgumentError, 'name must be a String' unless name.is_a?(String)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'pushover/users'
|
|
2
|
+
|
|
3
|
+
module Pushover
|
|
4
|
+
# Licenses provides operations for the Licensing API.
|
|
5
|
+
class Licenses
|
|
6
|
+
OPERATING_SYSTEMS = ['Android', 'iOS', 'Desktop'].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Retrieve the application's available prepaid license credits.
|
|
13
|
+
# @return [Response] the Pushover API response
|
|
14
|
+
def get
|
|
15
|
+
response = @client.connection.get(
|
|
16
|
+
path: '/1/licenses.json',
|
|
17
|
+
query: { token: @client.token }
|
|
18
|
+
)
|
|
19
|
+
Response.create_from_excon_response(response)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Permanently assign one prepaid license credit to a user or e-mail address.
|
|
23
|
+
# @return [Response] the Pushover API response
|
|
24
|
+
def assign(user: nil, email: nil, os: nil)
|
|
25
|
+
validate_recipient!(user, email)
|
|
26
|
+
validate_os!(os) unless os.nil?
|
|
27
|
+
|
|
28
|
+
body = { 'token' => @client.token }
|
|
29
|
+
body['user'] = user unless user.nil?
|
|
30
|
+
body['email'] = email unless email.nil?
|
|
31
|
+
body['os'] = os unless os.nil?
|
|
32
|
+
|
|
33
|
+
response = @client.connection.post(
|
|
34
|
+
path: '/1/licenses/assign.json',
|
|
35
|
+
body: Oj.dump(body),
|
|
36
|
+
idempotent: false
|
|
37
|
+
)
|
|
38
|
+
Response.create_from_excon_response(response)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def validate_recipient!(user, email)
|
|
44
|
+
raise ArgumentError, 'user or email must be supplied' if user.nil? && email.nil?
|
|
45
|
+
|
|
46
|
+
validate_user!(user) unless user.nil?
|
|
47
|
+
validate_email!(email) unless email.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_user!(user)
|
|
51
|
+
raise ArgumentError, 'user must be supplied' if user == ''
|
|
52
|
+
raise ArgumentError, 'user must be 30 alphanumeric characters' unless user.is_a?(String) && user.match?(Users::USER_PATTERN)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_email!(email)
|
|
56
|
+
return if email.is_a?(String) && !email.strip.empty?
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, 'email must be a nonblank String'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_os!(os)
|
|
62
|
+
return if os == '' || OPERATING_SYSTEMS.include?(os)
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, 'os must be blank, Android, iOS, or Desktop'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Pushover
|
|
2
|
+
# Limits provides operations for the account and team usage limits API.
|
|
3
|
+
class Limits
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Retrieve the current monthly message usage limits.
|
|
9
|
+
# @return [Response] the Pushover API response
|
|
10
|
+
def get
|
|
11
|
+
response = @client.connection.get(
|
|
12
|
+
path: '/1/apps/limits.json',
|
|
13
|
+
query: { token: @client.token }
|
|
14
|
+
)
|
|
15
|
+
Response.create_from_excon_response(response)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/pushover/message.rb
CHANGED
|
@@ -1,38 +1,35 @@
|
|
|
1
1
|
module Pushover
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# @!attribute [rw] expire
|
|
24
|
-
# @return [Numeric] expire time until message expires
|
|
25
|
-
# @!attribute [rw] retry
|
|
26
|
-
# @return [Numeric] how long to retry for, in seconds
|
|
27
|
-
# @!attribute [rw] callback
|
|
28
|
-
# @return [String] callback url
|
|
29
|
-
Message = Struct.new(:token, :user, :message, :attachment, :device, :title, :url, :url_title, :priority, :sound, :timestamp, :expire, :retry, :callback, keyword_init: true) do
|
|
30
|
-
# push the configured message to pushover.
|
|
31
|
-
# @return [Response] response for the receipt request
|
|
2
|
+
# Deprecated compatibility interface for sending messages.
|
|
3
|
+
class Message
|
|
4
|
+
ATTRIBUTES = %i[
|
|
5
|
+
token user message attachment device title url url_title priority sound timestamp expire retry callback
|
|
6
|
+
].freeze
|
|
7
|
+
private_constant :ATTRIBUTES
|
|
8
|
+
|
|
9
|
+
attr_accessor(*ATTRIBUTES)
|
|
10
|
+
|
|
11
|
+
def initialize(attributes = {}, **keywords)
|
|
12
|
+
raise ArgumentError, 'attributes must be supplied as a Hash' unless attributes.is_a?(Hash)
|
|
13
|
+
|
|
14
|
+
values = attributes.merge(keywords).transform_keys(&:to_sym)
|
|
15
|
+
unknown = values.keys - ATTRIBUTES
|
|
16
|
+
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
17
|
+
|
|
18
|
+
values.each { |attribute, value| public_send("#{attribute}=", value) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Send the configured message through the shared client.
|
|
22
|
+
# @return [Response] the Pushover API response
|
|
32
23
|
def push
|
|
33
|
-
|
|
24
|
+
require 'pushover' unless defined?(Pushover::Excon)
|
|
25
|
+
warn 'Pushover::Message#push is deprecated; use Pushover::Client.new(token: ...).messages.create(...)', uplevel: 1
|
|
26
|
+
%i[token user message].each { |param| raise "#{param} must be supplied" unless public_send(param) }
|
|
27
|
+
raise ArgumentError, 'raw attachment is unsupported; use attachment_base64 and attachment_type with Pushover::Client#messages.create' unless attachment.nil?
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
params = ATTRIBUTES.to_h { |attribute| [attribute, public_send(attribute)] }
|
|
30
|
+
.except(:token, :attachment)
|
|
31
|
+
.compact
|
|
32
|
+
Client.new(token: token).messages.create(**params)
|
|
36
33
|
end
|
|
37
34
|
end
|
|
38
35
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'openssl'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
|
|
7
|
+
module Pushover
|
|
8
|
+
# MessageEncryption implements Pushover's end-to-end encryption wire format.
|
|
9
|
+
class MessageEncryption
|
|
10
|
+
FIELDS = %i[message title url url_title].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(hexadecimal_key)
|
|
13
|
+
@key = [hexadecimal_key].pack('H*')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def encrypt(params)
|
|
17
|
+
params.merge(encrypted: 1).tap do |encrypted|
|
|
18
|
+
FIELDS.each { |field| encrypted[field] = encrypt_field(params[field]) if params.key?(field) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def encrypt_field(plaintext)
|
|
25
|
+
iv = SecureRandom.random_bytes(16)
|
|
26
|
+
encryptor = cipher(iv)
|
|
27
|
+
ciphertext = encryptor.update(compress(plaintext)) + encryptor.final
|
|
28
|
+
hmac = OpenSSL::HMAC.digest('SHA256', @key, iv + ciphertext)
|
|
29
|
+
Base64.strict_encode64(iv + ciphertext + hmac)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cipher(initialization_vector)
|
|
33
|
+
OpenSSL::Cipher.new('aes-256-cbc').encrypt.tap do |cipher|
|
|
34
|
+
cipher.key = @key
|
|
35
|
+
cipher.iv = initialization_vector
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def compress(plaintext)
|
|
40
|
+
output = StringIO.new(''.b)
|
|
41
|
+
writer = Zlib::GzipWriter.new(output, Zlib::BEST_COMPRESSION)
|
|
42
|
+
writer.mtime = 0
|
|
43
|
+
writer.write(plaintext)
|
|
44
|
+
writer.close
|
|
45
|
+
output.string
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
module Pushover
|
|
5
|
+
# MessageValidator enforces deterministic Message API request rules.
|
|
6
|
+
class MessageValidator
|
|
7
|
+
ATTACHMENT_LIMIT = 5_242_880
|
|
8
|
+
CHARACTER_LIMITS = { message: 1024, title: 250, url: 512, url_title: 100 }.freeze
|
|
9
|
+
EMERGENCY_FIELDS = %i[retry expire callback tags].freeze
|
|
10
|
+
FLAGS = %i[html monospace].freeze
|
|
11
|
+
PRIORITIES = [-2, -1, 0, 1, 2].freeze
|
|
12
|
+
STRING_FIELDS = %i[
|
|
13
|
+
user message attachment_base64 attachment_type device sound title url url_title callback tags
|
|
14
|
+
].freeze
|
|
15
|
+
SUPPORTED_FIELDS = (STRING_FIELDS + FLAGS + %i[priority timestamp ttl retry expire encryption_key]).freeze
|
|
16
|
+
|
|
17
|
+
def initialize(**params)
|
|
18
|
+
@params = params.compact
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate
|
|
22
|
+
validate_supported_fields
|
|
23
|
+
validate_required_fields
|
|
24
|
+
validate_string_fields
|
|
25
|
+
validate_character_limits
|
|
26
|
+
validate_users
|
|
27
|
+
validate_devices
|
|
28
|
+
validate_flags
|
|
29
|
+
validate_numbers
|
|
30
|
+
validate_emergency_fields
|
|
31
|
+
validate_callback
|
|
32
|
+
validate_attachment
|
|
33
|
+
validate_encryption_key
|
|
34
|
+
@params
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def validate_supported_fields
|
|
40
|
+
unknown = @params.keys - SUPPORTED_FIELDS
|
|
41
|
+
raise ArgumentError, "unsupported message parameter: #{unknown.first}" unless unknown.empty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_required_fields
|
|
45
|
+
%i[user message].each do |field|
|
|
46
|
+
value = @params[field]
|
|
47
|
+
raise ArgumentError, "#{field} must be supplied" unless value.is_a?(String) && !value.empty?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_string_fields
|
|
52
|
+
STRING_FIELDS.each do |field|
|
|
53
|
+
next unless @params.key?(field)
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, "#{field} must be a String" unless @params[field].is_a?(String)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_character_limits
|
|
60
|
+
CHARACTER_LIMITS.each do |field, maximum|
|
|
61
|
+
next unless @params.key?(field)
|
|
62
|
+
next if @params[field].length <= maximum
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, "#{field} must be at most #{maximum} characters"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_users
|
|
69
|
+
raise ArgumentError, 'user may contain at most 50 users' if @params[:user].split(',', -1).length > 50
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_devices
|
|
73
|
+
return unless @params.key?(:device)
|
|
74
|
+
|
|
75
|
+
valid = @params[:device].split(',', -1).all? { |device| device.match?(/\A[A-Za-z0-9_-]{1,25}\z/) }
|
|
76
|
+
raise ArgumentError, 'device contains an invalid name' unless valid
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate_flags
|
|
80
|
+
FLAGS.each do |field|
|
|
81
|
+
next unless @params.key?(field)
|
|
82
|
+
|
|
83
|
+
value = @params[field]
|
|
84
|
+
raise ArgumentError, "#{field} must be true, false, 1, or 0" unless [true, false, 1, 0].include?(value)
|
|
85
|
+
|
|
86
|
+
@params[field] = [true, 1].include?(value) ? 1 : 0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return unless @params[:html] == 1 && @params[:monospace] == 1
|
|
90
|
+
|
|
91
|
+
raise ArgumentError, 'html and monospace cannot both be enabled'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_numbers
|
|
95
|
+
validate_allowed_integer(:priority, PRIORITIES)
|
|
96
|
+
validate_minimum_integer(:timestamp, 0)
|
|
97
|
+
validate_minimum_integer(:ttl, 1, 'ttl must be a positive integer')
|
|
98
|
+
validate_minimum_integer(:retry, 30, 'retry must be at least 30 seconds')
|
|
99
|
+
validate_minimum_integer(:expire, 1)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_allowed_integer(field, allowed)
|
|
103
|
+
return unless @params.key?(field)
|
|
104
|
+
|
|
105
|
+
value = @params[field]
|
|
106
|
+
return if value.is_a?(Integer) && allowed.include?(value)
|
|
107
|
+
|
|
108
|
+
raise ArgumentError, "#{field} must be one of #{allowed.join(', ')}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_minimum_integer(field, minimum, message = nil)
|
|
112
|
+
return unless @params.key?(field)
|
|
113
|
+
return if @params[field].is_a?(Integer) && @params[field] >= minimum
|
|
114
|
+
|
|
115
|
+
raise ArgumentError, message || "#{field} must be at least #{minimum}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_emergency_fields
|
|
119
|
+
if @params[:priority] == 2
|
|
120
|
+
raise ArgumentError, 'retry and expire must be supplied with priority 2' unless @params.key?(:retry) && @params.key?(:expire)
|
|
121
|
+
raise ArgumentError, 'expire must be at most 10800 seconds' if @params[:expire] > 10_800
|
|
122
|
+
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
field = EMERGENCY_FIELDS.find { |name| @params.key?(name) }
|
|
127
|
+
raise ArgumentError, "#{field} is only valid with priority 2" if field
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_attachment
|
|
131
|
+
data = @params[:attachment_base64]
|
|
132
|
+
type = @params[:attachment_type]
|
|
133
|
+
return unless data || type
|
|
134
|
+
|
|
135
|
+
raise ArgumentError, 'attachment_base64 must be supplied with attachment_type' unless data
|
|
136
|
+
raise ArgumentError, 'attachment_type must be supplied with attachment_base64' unless type
|
|
137
|
+
raise ArgumentError, 'attachment_type must be an image MIME type' unless type.match?(/\Aimage\/[A-Za-z0-9.+-]+\z/)
|
|
138
|
+
|
|
139
|
+
decoded = decode_attachment(data)
|
|
140
|
+
raise ArgumentError, 'attachment must not exceed 5242880 bytes' if decoded.bytesize > ATTACHMENT_LIMIT
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_callback
|
|
144
|
+
return unless @params.key?(:callback)
|
|
145
|
+
|
|
146
|
+
callback = URI.parse(@params[:callback])
|
|
147
|
+
return if callback.is_a?(URI::HTTP) && callback.host
|
|
148
|
+
|
|
149
|
+
raise ArgumentError, 'callback must be an HTTP or HTTPS URL'
|
|
150
|
+
rescue URI::InvalidURIError
|
|
151
|
+
raise ArgumentError, 'callback must be an HTTP or HTTPS URL'
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def decode_attachment(data)
|
|
155
|
+
Base64.strict_decode64(data)
|
|
156
|
+
rescue ArgumentError
|
|
157
|
+
raise ArgumentError, 'attachment_base64 must be valid Base64'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def validate_encryption_key
|
|
161
|
+
return unless @params.key?(:encryption_key)
|
|
162
|
+
return if @params[:encryption_key].is_a?(String) && @params[:encryption_key].match?(/\A[0-9a-fA-F]{64}\z/)
|
|
163
|
+
|
|
164
|
+
raise ArgumentError, 'encryption_key must be 64 hexadecimal characters'
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Pushover
|
|
2
|
+
# Messages provides operations for the Message API.
|
|
3
|
+
class Messages
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Create and send a message.
|
|
9
|
+
# @return [Response] the Pushover API response
|
|
10
|
+
def create(user:, message:, **)
|
|
11
|
+
params = MessageValidator.new(user: user, message: message, **).validate
|
|
12
|
+
encryption_key = params.delete(:encryption_key)
|
|
13
|
+
params = MessageEncryption.new(encryption_key).encrypt(params) if encryption_key
|
|
14
|
+
body = { 'token' => @client.token }.merge(params.transform_keys(&:to_s))
|
|
15
|
+
|
|
16
|
+
response = @client.connection.post(path: '/1/messages.json', body: Oj.dump(body))
|
|
17
|
+
Response.create_from_excon_response(response)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/pushover/receipt.rb
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
module Pushover
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
# Deprecated compatibility interface for retrieving receipts.
|
|
3
|
+
class Receipt
|
|
4
|
+
ATTRIBUTES = %i[receipt token].freeze
|
|
5
|
+
private_constant :ATTRIBUTES
|
|
6
|
+
|
|
7
|
+
attr_accessor(*ATTRIBUTES)
|
|
8
|
+
|
|
9
|
+
def initialize(attributes = {}, **keywords)
|
|
10
|
+
raise ArgumentError, 'attributes must be supplied as a Hash' unless attributes.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
values = attributes.merge(keywords).transform_keys(&:to_sym)
|
|
13
|
+
unknown = values.keys - ATTRIBUTES
|
|
14
|
+
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
15
|
+
|
|
16
|
+
values.each { |attribute, value| public_send("#{attribute}=", value) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Retrieve the configured receipt through the shared client.
|
|
20
|
+
# @return [Response] the Pushover API response
|
|
10
21
|
def get
|
|
11
|
-
|
|
22
|
+
require 'pushover' unless defined?(Pushover::Excon)
|
|
23
|
+
warn 'Pushover::Receipt#get is deprecated; use Pushover::Client.new(token: ...).receipts.get(receipt: ...)', uplevel: 1
|
|
24
|
+
%i[receipt token].each { |param| raise "#{param} must be supplied" unless public_send(param) }
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
Client.new(token: token).receipts.get(receipt: receipt)
|
|
14
27
|
end
|
|
15
28
|
end
|
|
16
29
|
end
|