Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,18 @@ jobs:
- name: Run tests
run: |
bundle exec rspec

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler: default
bundler-cache: true

- name: Run RuboCop
run: bundle exec rubocop --parallel
2 changes: 2 additions & 0 deletions .hound.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rubocop:
enabled: false
67 changes: 67 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
plugins:
- rubocop-rspec

AllCops:
TargetRubyVersion: 2.7
NewCops: enable
SuggestExtensions: false
Exclude:
- "vendor/**/*"
- "tmp/**/*"

# This is a small gem; some style cops add noise without value.
Style/Documentation:
Enabled: false
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes

# Renaming `is_not_registered?` / `has_canonical_id?` would change the gem's
# (admittedly internal) Ruby surface area. Out of scope for a lint pass.
Naming/PredicatePrefix:
Enabled: false

Layout/LineLength:
Max: 120
Exclude:
- "fcm.gemspec"

# Out of scope for a lint pass: bumping required_ruby_version is a semver-relevant
# change for gem consumers and should be considered separately.
Gemspec/RequiredRubyVersion:
Enabled: false

# Enabling MFA is a publishing-policy decision for the gem maintainer, not
# something a lint pass should silently introduce.
Gemspec/RequireMFA:
Enabled: false

# Code metrics are coarse signals that fight a lot of legacy code.
Metrics/AbcSize:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false
Metrics/BlockLength:
Exclude:
- "spec/**/*"
- "fcm.gemspec"

# Specs are intentionally long, descriptive, and use `.should` syntax in
# places — none of these add value here.
RSpec/ExampleLength:
Enabled: false
RSpec/MultipleExpectations:
Enabled: false
RSpec/MultipleMemoizedHelpers:
Enabled: false
RSpec/NestedGroups:
Enabled: false
RSpec/NoExpectationExample:
Enabled: false
14 changes: 9 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# frozen_string_literal: true

source "https://rubygems.org"
gemspec

gem 'rake'
gem 'rspec'
gem 'webmock'
gem 'ci_reporter_rspec'
gem 'googleauth'
gem "ci_reporter_rspec"
gem "googleauth"
gem "rake"
gem "rspec"
gem "rubocop", require: false
gem "rubocop-rspec", require: false
gem "webmock"
16 changes: 9 additions & 7 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
require 'rspec/core/rake_task'
# frozen_string_literal: true

require "rspec/core/rake_task"
require "bundler/gem_tasks"
require "rake/tasklib"
require 'ci/reporter/rake/rspec'
require "ci/reporter/rake/rspec"

RSpec::Core::RakeTask.new(:spec => ["ci:setup:rspec"]) do |t|
t.pattern = 'spec/**/*_spec.rb'
RSpec::Core::RakeTask.new(spec: ["ci:setup:rspec"]) do |t|
t.pattern = "spec/**/*_spec.rb"
end

RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
spec.rspec_opts = ['--format documentation']
spec.pattern = "spec/**/*_spec.rb"
spec.rspec_opts = ["--format documentation"]
end

task :default => :spec
task default: :spec
14 changes: 7 additions & 7 deletions fcm.gemspec
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
# frozen_string_literal: true

$LOAD_PATH.push File.expand_path("lib", __dir__)

Gem::Specification.new do |s|
s.name = "fcm"
Expand All @@ -8,17 +9,16 @@ Gem::Specification.new do |s|
s.authors = ["Kashif Rasul", "Shoaib Burq"]
s.email = ["kashif@decision-labs.com", "shoaib@decision-labs.com"]
s.homepage = "https://github.com/decision-labs/fcm"
s.summary = %q{Reliably deliver messages and notifications via FCM}
s.description = %q{fcm provides ruby bindings to Firebase Cloud Messaging (FCM) a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost to Android, iOS or Web browsers.}
s.summary = "Reliably deliver messages and notifications via FCM"
s.description = "fcm provides ruby bindings to Firebase Cloud Messaging (FCM) a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost to Android, iOS or Web browsers."
s.license = "MIT"

s.required_ruby_version = ">= 2.4.0"

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
s.require_paths = ["lib"]

s.add_runtime_dependency("faraday", ">= 1.0.0", "< 3.0")
s.add_runtime_dependency("googleauth", "~> 1")
s.add_dependency("faraday", ">= 1.0.0", "< 3.0")
s.add_dependency("googleauth", "~> 1")
end
95 changes: 49 additions & 46 deletions lib/fcm.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require "faraday"
require "cgi"
require "json"
Expand All @@ -12,7 +14,7 @@ class InvalidCredentialError < StandardError; end

GROUP_NOTIFICATION_BASE_URI = "https://android.googleapis.com"
INSTANCE_ID_API = "https://iid.googleapis.com"
TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/
TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/.freeze

def initialize(json_key_path = "", project_name = "", http_options = {})
@json_key_path = json_key_path
Expand Down Expand Up @@ -51,7 +53,7 @@ def initialize(json_key_path = "", project_name = "", http_options = {})
def send_notification_v1(message)
return if @project_name.empty?

post_body = { 'message': message }
post_body = { message: message }
for_uri(BASE_URI_V1) do |connection|
response = connection.post(
"#{@project_name}/messages:send", post_body.to_json
Expand All @@ -67,7 +69,7 @@ def create_notification_key(key_name, project_id, registration_ids = [])
notification_key_name: key_name)

extra_headers = {
"project_id" => project_id,
"project_id" => project_id
}

for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
Expand All @@ -84,7 +86,7 @@ def add_registration_ids(key_name, project_id, notification_key, registration_id
notification_key: notification_key)

extra_headers = {
"project_id" => project_id,
"project_id" => project_id
}

for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
Expand All @@ -101,7 +103,7 @@ def remove_registration_ids(key_name, project_id, notification_key, registration
notification_key: notification_key)

extra_headers = {
"project_id" => project_id,
"project_id" => project_id
}

for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
Expand All @@ -116,7 +118,7 @@ def recover_notification_key(key_name, project_id)
params = { notification_key_name: key_name }

extra_headers = {
"project_id" => project_id,
"project_id" => project_id
}

for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
Expand All @@ -139,11 +141,11 @@ def topic_unsubscription(topic, registration_token)
end

def batch_topic_subscription(topic, registration_tokens)
manage_topics_relationship(topic, registration_tokens, 'Add')
manage_topics_relationship(topic, registration_tokens, "Add")
end

def batch_topic_unsubscription(topic, registration_tokens)
manage_topics_relationship(topic, registration_tokens, 'Remove')
manage_topics_relationship(topic, registration_tokens, "Remove")
end

def manage_topics_relationship(topic, registration_tokens, action)
Expand All @@ -165,28 +167,28 @@ def get_instance_id_info(iid_token, options = {})
end

def send_to_topic(topic, options = {})
if topic.gsub(TOPIC_REGEX, '').length.zero?
body = { 'message': { 'topic': topic }.merge(options) }

for_uri(BASE_URI_V1) do |connection|
response = connection.post(
"#{@project_name}/messages:send", body.to_json
)
build_response(response)
end
return unless topic.gsub(TOPIC_REGEX, "").empty?

body = { message: { topic: topic }.merge(options) }

for_uri(BASE_URI_V1) do |connection|
response = connection.post(
"#{@project_name}/messages:send", body.to_json
)
build_response(response)
end
end

def send_to_topic_condition(condition, options = {})
if validate_condition?(condition)
body = { 'message': { 'condition': condition }.merge(options) }

for_uri(BASE_URI_V1) do |connection|
response = connection.post(
"#{@project_name}/messages:send", body.to_json
)
build_response(response)
end
return unless validate_condition?(condition)

body = { message: { condition: condition }.merge(options) }

for_uri(BASE_URI_V1) do |connection|
response = connection.post(
"#{@project_name}/messages:send", body.to_json
)
build_response(response)
end
end

Expand All @@ -200,7 +202,7 @@ def for_uri(uri, extra_headers = {})
faraday.adapter Faraday.default_adapter
faraday.headers["Content-Type"] = "application/json"
faraday.headers["Authorization"] = "Bearer #{jwt_token}"
faraday.headers["access_token_auth"]= "true"
faraday.headers["access_token_auth"] = "true"
extra_headers.each do |key, value|
faraday.headers[key] = value
end
Expand All @@ -221,9 +223,14 @@ def build_response(response, registration_ids = [])
response_hash[:response] = "success"
body = JSON.parse(body) unless body.empty?
response_hash[:canonical_ids] = build_canonical_ids(body, registration_ids) unless registration_ids.empty?
response_hash[:not_registered_ids] = build_not_registered_ids(body, registration_ids) unless registration_ids.empty?
unless registration_ids.empty?
response_hash[:not_registered_ids] =
build_not_registered_ids(body, registration_ids)
end
when 400
response_hash[:response] = "Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields."
response_hash[:response] =
"Only applies for JSON requests. Indicates that the request could not be parsed as JSON, " \
"or it contained invalid fields."
when 401
response_hash[:response] = "There was an error authenticating the sender account."
when 503
Expand All @@ -236,23 +243,19 @@ def build_response(response, registration_ids = [])

def build_canonical_ids(body, registration_ids)
canonical_ids = []
unless body.empty?
if body["canonical_ids"] > 0
body["results"].each_with_index do |result, index|
canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result)
end
if !body.empty? && body["canonical_ids"].positive?
body["results"].each_with_index do |result, index|
canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result)
end
end
canonical_ids
end

def build_not_registered_ids(body, registration_id)
not_registered_ids = []
unless body.empty?
if body["failure"] > 0
body["results"].each_with_index do |result, index|
not_registered_ids << registration_id[index] if is_not_registered?(result)
end
if !body.empty? && body["failure"].positive?
body["results"].each_with_index do |result, index|
not_registered_ids << registration_id[index] if is_not_registered?(result)
end
end
not_registered_ids
Expand All @@ -272,32 +275,32 @@ def validate_condition?(condition)

def validate_condition_format?(condition)
bad_characters = condition.gsub(
/(topics|in|\s|\(|\)|(&&)|[!]|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/,
/(topics|in|\s|\(|\)|(&&)|!|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/,
""
)
bad_characters.length == 0
bad_characters.empty?
end

def validate_condition_topics?(condition)
topics = condition.scan(/(?:^|\S|\s)'([^']*?)'(?:$|\S|\s)/).flatten
topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").length == 0 }
topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").empty? }
end

def jwt_token
scope = "https://www.googleapis.com/auth/firebase.messaging"
@authorizer ||= Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: json_key,
scope: scope,
scope: scope
)
token = @authorizer.fetch_access_token!
token["access_token"]
end

def raise_credentials_error(param)
error_msg = 'credentials must be an IO-like ' \
'object or path. You passed'
error_msg = "credentials must be an IO-like " \
"object or path. You passed"

param_klass = param.nil? ? 'nil' : "a #{param.class.name}"
param_klass = param.nil? ? "nil" : "a #{param.class.name}"
error_msg += " #{param_klass}."
raise InvalidCredentialError, error_msg
end
Expand Down
Loading