Skip to content

Commit

Permalink
Merge pull request #15 from cheddar-me/use-fcm-gem
Browse files Browse the repository at this point in the history
Add support for send_all to messaging client.
  • Loading branch information
skatkov authored Oct 19, 2023
2 parents d9736a5 + 5bba560 commit 2064dad
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 83 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ Gemfile.lock

# rspec failure tracking
.rspec_status

# ruby version
.ruby-version
3 changes: 2 additions & 1 deletion firebase-admin-sdk.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency "googleauth", "> 0.16", "< 2.0"
spec.add_runtime_dependency "faraday", "> 1", "< 3"
spec.add_runtime_dependency "jwt", ">= 1.5", "< 3.0"
spec.add_runtime_dependency "google-apis-fcm_v1", ">= 0.19.0", "< 1.0"

spec.add_development_dependency "rake"
spec.add_development_dependency "rspec"
spec.add_development_dependency "webmock"
spec.add_development_dependency "fakefs"
spec.add_development_dependency "climate_control"
spec.add_development_dependency "standard"
spec.add_development_dependency "activesupport"
spec.add_development_dependency "activesupport", "~> 7.0.8"
end
2 changes: 2 additions & 0 deletions lib/firebase-admin-sdk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@
require_relative "firebase/admin/messaging/multicast_message"
require_relative "firebase/admin/messaging/error_info"
require_relative "firebase/admin/messaging/topic_management_response"
require_relative "firebase/admin/messaging/send_response"
require_relative "firebase/admin/messaging/batch_response"
require_relative "firebase/admin/messaging/client"
8 changes: 4 additions & 4 deletions lib/firebase/admin/messaging/aps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Admin
module Messaging
# Aps dictionary to be included in an APNS payload.
class APS
# @return [APSAlert, String, nil]
# @return [Firebase::Admin::Messaging::APSAlert, String, nil]
# Alert to be included in the message.
attr_accessor :alert

Expand All @@ -12,7 +12,7 @@ class APS
# remain unchanged.
attr_accessor :badge

# @return [String, CriticalSound, nil]
# @return [Firebase::Admin::Messaging::CriticalSound, String, nil]
# Sound to be played with the message.
attr_accessor :sound

Expand All @@ -39,12 +39,12 @@ class APS

# Initializes an {APS}.
#
# @param [APSAlert, String, nil] alert
# @param [Firebase::Admin::Messaging::APSAlert, String, nil] alert
# Alert to be included in the message (optional).
# @param [Integer, nil] badge
# Badge to be displayed with the message (optional).
# Set to 0 to remove the badge. When not specified, the badge will remain unchanged.
# @param [String, CriticalSound, nil] sound
# @param [Firebase::Admin::Messaging::CriticalSound, String, nil] sound
# Sound to be played with the message (optional).
# @param [Boolean, nil] content_available
# Specifies whether to configure a background update notification (optional).
Expand Down
29 changes: 29 additions & 0 deletions lib/firebase/admin/messaging/batch_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Firebase
module Admin
module Messaging
# The response received from a batch request
class BatchResponse
# The list of responses (possibly empty).
# @return [Array<SendResponse>]
attr_reader :responses

# The number of successful messages.
# @return [Integer]
attr_reader :success_count

# The number of failed messages.
# @return [Integer]
attr_reader :failure_count

# A response received from a batch request.
#
# @param [Array<SendResponse>] responses
def initialize(responses:)
@responses = responses
@success_count = responses.count(:success?)
@failure_count = responses.count - @success_count
end
end
end
end
end
49 changes: 37 additions & 12 deletions lib/firebase/admin/messaging/client.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
require "google-apis-fcm_v1"

module Firebase
module Admin
module Messaging
# A client for communicating with the Firebase Cloud Messaging service.
class Client
def initialize(app)
@project_id = app.project_id
@http_client = Firebase::Admin::Internal::HTTPClient.new(credentials: app.credentials)
@project_path = "projects/#{app.project_id}"
@message_encoder = MessageEncoder.new
@http_client = Firebase::Admin::Internal::HTTPClient.new(credentials: app.credentials)
@service = Google::Apis::FcmV1::FirebaseCloudMessagingService.new
@service.authorization = app.credentials
end

# Sends a message via Firebase Cloud Messaging (FCM).
Expand All @@ -19,13 +24,10 @@ def initialize(app)
#
# @return [String] A message id that uniquely identifies the message.
def send_one(message, dry_run: false)
body = {
validate_only: dry_run,
message: @message_encoder.encode(message)
}
res = @http_client.post(send_url, body, FCM_HEADERS)
res.body["name"]
rescue Faraday::Error => e
body = encode_message(message, dry_run: dry_run)
res = @service.send_message(@project_path, body, options: {skip_serialization: true})
res.name
rescue Google::Apis::Error => e
raise parse_fcm_error(e)
end

Expand All @@ -39,7 +41,21 @@ def send_one(message, dry_run: false)
#
# @return [BatchResponse] A batch response.
def send_all(messages, dry_run: false)
raise NotImplementedError
raise "messages must be an Array" unless messages.is_a?(Array)
raise "messages must not contain more than 500 elements" unless messages.length < 500

responses = []
@service.batch do |service|
options = {skip_serialization: true}
messages.each do |message|
body = encode_message(message, dry_run: dry_run)
service.send_message(@project_path, body, options: options) do |res, err|
wrapped_err = parse_fcm_error(err) unless err.nil?
responses << SendResponse.new(message_id: res&.name, error: wrapped_err)
end
end
end
BatchResponse.new(responses: responses)
end

# Sends the given multicast message to all tokens via Firebase Cloud Messaging (FCM).
Expand Down Expand Up @@ -124,9 +140,9 @@ def make_topic_mgmt_request(tokens, topic, operation)
TopicManagementResponse.new(res)
end

# @param [Faraday::Error] err
# @param [Google::Apis::Error] err
def parse_fcm_error(err)
msg, info = parse_platform_error(err.response_status, err.response_body)
msg, info = parse_platform_error(err.status_code, err.body)
return err if info.empty?

details = info["details"] || []
Expand All @@ -153,10 +169,19 @@ def parse_platform_error(status_code, body)
[msg, details]
end

# Encodes a send message request.
def encode_message(message, dry_run:)
body = {
message: @message_encoder.encode(message),
validateOnly: dry_run
}
JSON.generate(body)
end

FCM_HOST = "https://fcm.googleapis.com"
FCM_HEADERS = {"X-GOOG-API-FORMAT-VERSION": "2"}
IID_HOST = "https://iid.googleapis.com"
IID_HEADERS = {"access_token_auth" => "true"}
IID_HEADERS = {access_token_auth: "true"}

FCM_ERROR_TYPES = {
"APNS_AUTH_ERROR" => ThirdPartyAuthError,
Expand Down
76 changes: 39 additions & 37 deletions lib/firebase/admin/messaging/message_encoder.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "json"

module Firebase
module Admin
module Messaging
Expand All @@ -6,7 +8,7 @@ class MessageEncoder
#
# @param [Message] message
# The message to encode.
# @return [Hash]
# @return [String] A json encoded string.
def encode(message)
raise ArgumentError, "message must be a Message" unless message.is_a?(Message)
result = {
Expand All @@ -17,7 +19,7 @@ def encode(message)
notification: encode_notification(message.notification),
token: check_string("Message.token", message.token, non_empty: true),
topic: check_string("Message.topic", message.topic, non_empty: true),
fcm_options: encode_fcm_options(message.fcm_options)
fcmOptions: encode_fcm_options(message.fcm_options)
}
result[:topic] = sanitize_topic_name(result[:topic])
result = remove_nil_values(result)
Expand Down Expand Up @@ -47,13 +49,13 @@ def encode_android(v)
return nil unless v
raise ArgumentError, "Message.android must be an AndroidConfig." unless v.is_a?(AndroidConfig)
result = {
collapse_key: check_string("AndroidConfig.collapse_key", v.collapse_key),
collapseKey: check_string("AndroidConfig.collapse_key", v.collapse_key),
data: check_string_hash("AndroidConfig.data", v.data),
notification: encode_android_notification(v.notification),
priority: check_string("AndroidConfig.priority", v.priority, non_empty: true),
restricted_package_name: check_string("AndroidConfig.restricted_package_name", v.restricted_package_name),
restrictedPackageName: check_string("AndroidConfig.restricted_package_name", v.restricted_package_name),
ttl: encode_duration("AndroidConfig.ttl", v.ttl),
fcm_options: encode_android_fcm_options(v.fcm_options)
fcmOptions: encode_android_fcm_options(v.fcm_options)
}
result = remove_nil_values(result)
if result.key?(:priority) && !%w[normal high].include?(result[:priority])
Expand All @@ -71,49 +73,49 @@ def encode_android_notification(v)

result = {
body: check_string("AndroidNotification.body", v.body),
body_loc_key: check_string("AndroidNotification.body_loc_key", v.body_loc_key),
body_loc_args: check_string_array("AndroidNotification.body_loc_args", v.body_loc_args),
click_action: check_string("AndroidNotification.click_action", v.click_action),
bodyLocKey: check_string("AndroidNotification.body_loc_key", v.body_loc_key),
bodyLocArgs: check_string_array("AndroidNotification.body_loc_args", v.body_loc_args),
clickAction: check_string("AndroidNotification.click_action", v.click_action),
color: check_color("AndroidNotification.color", v.color, allow_alpha: true, required: false),
icon: check_string("AndroidNotification.icon", v.icon),
sound: check_string("AndroidNotification.sound", v.sound),
tag: check_string("AndroidNotification.tag", v.tag),
title: check_string("AndroidNotification.title", v.title),
title_loc_key: check_string("AndroidNotification.title_loc_key", v.title_loc_key),
title_loc_args: check_string_array("AndroidNotification.title_loc_args", v.title_loc_args),
channel_id: check_string("AndroidNotification.channel_id", v.channel_id),
titleLocKey: check_string("AndroidNotification.title_loc_key", v.title_loc_key),
titleLocArgs: check_string_array("AndroidNotification.title_loc_args", v.title_loc_args),
channelId: check_string("AndroidNotification.channel_id", v.channel_id),
image: check_string("AndroidNotification.image", v.image),
ticker: check_string("AndroidNotification.ticker", v.ticker),
sticky: v.sticky,
event_time: check_time("AndroidNotification.event_time", v.event_time),
local_only: v.local_only,
notification_priority: check_string("AndroidNotification.priority", v.priority, non_empty: true),
vibrate_timings: check_numeric_array("AndroidNotification.vibrate_timings", v.vibrate_timings),
default_vibrate_timings: v.default_vibrate_timings,
default_sound: v.default_sound,
default_light_settings: v.default_light_settings,
light_settings: encode_light_settings(v.light_settings),
eventTime: check_time("AndroidNotification.event_time", v.event_time),
localOnly: v.local_only,
notificationPriority: check_string("AndroidNotification.priority", v.priority, non_empty: true),
vibrateTimings: check_numeric_array("AndroidNotification.vibrate_timings", v.vibrate_timings),
defaultVibrateTimings: v.default_vibrate_timings,
defaultSound: v.default_sound,
defaultLightSettings: v.default_light_settings,
lightSettings: encode_light_settings(v.light_settings),
visibility: check_string("AndroidNotification.visibility", v.visibility, non_empty: true),
notification_count: check_numeric("AndroidNotification.notification_count", v.notification_count)
notificationCount: check_numeric("AndroidNotification.notification_count", v.notification_count)
}
result = remove_nil_values(result)

if result.key?(:body_loc_args) && !result.key?(:body_loc_key)
if result.key?(:bodyLocArgs) && !result.key?(:bodyLocKey)
raise ArgumentError, "AndroidNotification.body_loc_key is required when specifying body_loc_args"
elsif result.key?(:title_loc_args) && !result.key?(:title_loc_key)
elsif result.key?(:titleLocArgs) && !result.key?(:titleLocKey)
raise ArgumentError, "AndroidNotification.title_loc_key is required when specifying title_loc_args"
end

if (event_time = result[:event_time])
event_time = event_time.dup.utc unless event_time.utc?
result[:event_time] = event_time.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
if (event_time = result[:eventTime])
event_time = event_time.getutc unless event_time.utc?
result[:eventTime] = event_time.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
end

if (priority = result[:notification_priority])
if (priority = result[:notificationPriority])
unless %w[min low default high max].include?(priority)
raise ArgumentError, "AndroidNotification.priority must be 'default', 'min', 'low', 'high' or 'max'."
end
result[:notification_priority] = "PRIORITY_#{priority.upcase}"
result[:notificationPriority] = "PRIORITY_#{priority.upcase}"
end

if (visibility = result[:visibility])
Expand All @@ -123,11 +125,11 @@ def encode_android_notification(v)
result[:visibility] = visibility.upcase
end

if (vibrate_timings = result[:vibrate_timings])
if (vibrate_timings = result[:vibrateTimings])
vibrate_timing_strings = vibrate_timings.map do |t|
encode_duration("AndroidNotification.vibrate_timings", t)
end
result[:vibrate_timings] = vibrate_timing_strings
result[:vibrateTimings] = vibrate_timing_strings
end

result
Expand All @@ -140,7 +142,7 @@ def encode_android_fcm_options(v)
raise ArgumentError, "AndroidConfig.fcm_options must be an AndroidFCMOptions"
end
result = {
analytics_label: check_analytics_label("AndroidFCMOptions.analytics_label", v.analytics_label)
analyticsLabel: check_analytics_label("AndroidFCMOptions.analytics_label", v.analytics_label)
}
remove_nil_values(result)
end
Expand All @@ -159,14 +161,14 @@ def encode_light_settings(v)
raise ArgumentError, "AndroidNotification.light_settings must be a LightSettings." unless v.is_a?(LightSettings)
result = {
color: encode_color("LightSettings.color", v.color, allow_alpha: true),
light_on_duration: encode_duration("LightSettings.light_on_duration", v.light_on_duration),
light_off_duration: encode_duration("LightSettings.light_off_duration", v.light_off_duration)
lightOnDuration: encode_duration("LightSettings.light_on_duration", v.light_on_duration),
lightOffDuration: encode_duration("LightSettings.light_off_duration", v.light_off_duration)
}
result = remove_nil_values(result)
unless result.key?(:light_on_duration)
unless result.key?(:lightOnDuration)
raise ArgumentError, "LightSettings.light_on_duration is required"
end
unless result.key?(:light_off_duration)
unless result.key?(:lightOffDuration)
raise ArgumentError, "LightSettings.light_off_duration is required"
end
result
Expand All @@ -190,7 +192,7 @@ def encode_apns(apns)
result = {
headers: check_string_hash("APNSConfig.headers", apns.headers),
payload: encode_apns_payload(apns.payload),
fcm_options: encode_apns_fcm_options(apns.fcm_options)
fcmOptions: encode_apns_fcm_options(apns.fcm_options)
}
remove_nil_values(result)
end
Expand All @@ -213,7 +215,7 @@ def encode_apns_fcm_options(options)
return nil unless options
raise ArgumentError, "APNSConfig.fcm_options must be an APNSFCMOptions" unless options.is_a?(APNSFCMOptions)
result = {
analytics_label: check_analytics_label("APNSFCMOptions.analytics_label", options.analytics_label),
analyticsLabel: check_analytics_label("APNSFCMOptions.analytics_label", options.analytics_label),
image: check_string("APNSFCMOptions.image", options.image)
}
remove_nil_values(result)
Expand Down Expand Up @@ -331,7 +333,7 @@ def encode_fcm_options(options)
return nil unless options
raise ArgumentError, "Message.fcm_options must be a FCMOptions." unless options.is_a?(FCMOptions)
result = {
analytics_label: check_analytics_label("Message.fcm_options", options.analytics_label)
analyticsLabel: check_analytics_label("Message.fcm_options", options.analytics_label)
}
remove_nil_values(result)
end
Expand Down
31 changes: 31 additions & 0 deletions lib/firebase/admin/messaging/send_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Firebase
module Admin
module Messaging
# The response received from an individual batched request.
class SendResponse
# A message id string that uniquely identifies the message.
# @return [String]
attr_reader :message_id

# The error if one occurred while sending the message.
# @return [Error]
attr_reader :error

# A boolean indicating if the request was successful.
# @return [Boolean]
def success?
!!@message_id
end

# Initializes the object.
#
# @param [String, nil] message_id the id of the sent message
# @param [Error, nil] error the error that occurred
def initialize(message_id:, error:)
@message_id = message_id
@error = error
end
end
end
end
end
Loading

0 comments on commit 2064dad

Please sign in to comment.