├── .envrc ├── .ruby-version ├── CODEOWNERS ├── .rspec ├── shell.nix ├── lib ├── onesignal │ ├── version.rb │ ├── commands.rb │ ├── responses.rb │ ├── commands │ │ ├── fetch_players.rb │ │ ├── autoloader.rb │ │ ├── csv_export.rb │ │ ├── fetch_player.rb │ │ ├── delete_player.rb │ │ ├── create_notification.rb │ │ ├── fetch_notification.rb │ │ ├── fetch_notifications.rb │ │ └── base_command.rb │ ├── autoloader.rb │ ├── auto_map.rb │ ├── responses │ │ ├── autoloader.rb │ │ ├── csv_export.rb │ │ ├── base_response.rb │ │ ├── player.rb │ │ └── notification.rb │ ├── buttons.rb │ ├── notification │ │ ├── contents.rb │ │ └── headings.rb │ ├── segment.rb │ ├── configuration.rb │ ├── attachments.rb │ ├── sounds.rb │ ├── icons.rb │ ├── notification.rb │ ├── included_targets.rb │ ├── client.rb │ └── filter.rb └── onesignal.rb ├── bin ├── setup └── console ├── Rakefile ├── spec ├── factories │ ├── segments.rb │ ├── buttons.rb │ ├── clients.rb │ ├── sounds.rb │ ├── attachments.rb │ ├── icons.rb │ └── notifications.rb ├── support │ └── factory_bot.rb ├── onesignal │ ├── responses │ │ ├── csv_export_spec.rb │ │ └── notification_spec.rb │ ├── segment_spec.rb │ ├── buttons_spec.rb │ ├── sounds_spec.rb │ ├── attachments_spec.rb │ ├── configuration_spec.rb │ ├── notification │ │ ├── contents_spec.rb │ │ └── headings_spec.rb │ ├── icons_spec.rb │ ├── included_targets_spec.rb │ ├── client_spec.rb │ ├── notification_spec.rb │ └── filter_spec.rb ├── spec_helper.rb └── api_calls_spec.rb ├── .gitignore ├── .editorconfig ├── Gemfile ├── LICENSE.txt ├── .github └── pull_request_template.md ├── onesignal-ruby.gemspec ├── CHANGELOG.md ├── .circleci └── config.yml ├── CODE_OF_CONDUCT.md ├── fixtures └── vcr_cassettes │ ├── os-csv-export.yml │ ├── os-fetch-player.yml │ ├── os-fetch-players.yml │ ├── os-delete-player.yml │ ├── os-send-noti.yml │ └── os-fetch-noti.yml ├── .rubocop.yml └── README.md /.envrc: -------------------------------------------------------------------------------- 1 | use_nix 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.0 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @MatteoJoliveau @bernardini687 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | pkgs.mkShell { 4 | buildInputs = [ 5 | ruby_2_7 6 | ]; 7 | } 8 | -------------------------------------------------------------------------------- /lib/onesignal/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | VERSION = '0.6.0' 5 | API_VERSION = 'v1' 6 | end 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/onesignal/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | require 'onesignal/commands/autoloader' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/onesignal/responses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Responses 5 | require 'onesignal/responses/autoloader' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /spec/factories/segments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :segment do 5 | initialize_with { new(name: Faker::Movies::StarWars.character) } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | .idea/ 14 | *.iml 15 | .env 16 | .env.* 17 | *.gem 18 | 19 | Gemfile.lock 20 | -------------------------------------------------------------------------------- /lib/onesignal/commands/fetch_players.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | class FetchPlayers < BaseCommand 6 | def call 7 | client.fetch_players 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'factory_bot' 4 | 5 | RSpec.configure do |config| 6 | config.include FactoryBot::Syntax::Methods 7 | 8 | config.before(:suite) do 9 | FactoryBot.find_definitions 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /lib/onesignal/autoloader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{File.expand_path(__dir__)}/*.rb"].each do |file| 4 | filename = File.basename file 5 | classname = filename.split('.rb').first.camelize 6 | OneSignal.autoload classname, File.expand_path("../#{filename}", __FILE__) 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/buttons.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :buttons, class: OneSignal::Buttons do 5 | buttons [{id: 'option_a', text: 'Option A' }, {id: 'option_b', text: 'Option B' }] 6 | 7 | initialize_with { new(attributes) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/onesignal/auto_map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module AutoMap 5 | def create_readers **attributes 6 | attributes.keys.each do |k| 7 | self.class.define_method k do 8 | attributes[k] 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/onesignal/commands/autoloader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{File.expand_path(__dir__)}/*.rb"].each do |file| 4 | filename = File.basename file 5 | classname = filename.split('.rb').first.camelize 6 | OneSignal::Commands.autoload classname, File.expand_path("../#{filename}", __FILE__) 7 | end 8 | -------------------------------------------------------------------------------- /lib/onesignal/responses/autoloader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{File.expand_path(__dir__)}/*.rb"].each do |file| 4 | filename = File.basename file 5 | classname = filename.split('.rb').first.camelize 6 | OneSignal::Responses.autoload classname, File.expand_path("../#{filename}", __FILE__) 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/clients.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :client, class: OneSignal::Client do 5 | app_id { 'app_id' } 6 | api_key { 'api_key' } 7 | api_url { 'http://api_url' } 8 | 9 | initialize_with { new(app_id, api_key, api_url) } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/sounds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :sounds, class: OneSignal::Sounds do 5 | ios { 'test.wav' } 6 | windows { 'test.wav' } 7 | android { 'test' } 8 | amazon { 'test' } 9 | 10 | initialize_with { new(attributes) } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/onesignal/commands/csv_export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | class CsvExport < BaseCommand 6 | def initialize params 7 | @params = params || {} 8 | end 9 | 10 | def call 11 | client.csv_export @params 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in onesignal-ruby.gemspec 8 | gemspec 9 | 10 | group :test do 11 | gem 'faker', git: 'https://github.com/stympy/faker.git', branch: 'master' 12 | end 13 | -------------------------------------------------------------------------------- /lib/onesignal/commands/fetch_player.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | class FetchPlayer < BaseCommand 6 | def initialize player_id 7 | @player_id = player_id 8 | end 9 | 10 | def call 11 | client.fetch_player @player_id 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/onesignal/buttons.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Buttons 5 | attr_reader :buttons 6 | 7 | def initialize buttons: nil 8 | @buttons = buttons 9 | end 10 | 11 | def as_json options = nil 12 | { 13 | 'buttons' => @buttons.as_json(options), 14 | } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/onesignal/commands/delete_player.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | class DeletePlayer < BaseCommand 6 | def initialize player_id 7 | @player_id = player_id 8 | end 9 | 10 | def call 11 | client.delete_player @player_id 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/onesignal/commands/create_notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | class CreateNotification < BaseCommand 6 | def initialize notification 7 | @notification = notification 8 | end 9 | 10 | def call 11 | client.create_notification @notification 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/onesignal/commands/fetch_notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | class FetchNotification < BaseCommand 6 | def initialize notification_id 7 | @notification_id = notification_id 8 | end 9 | 10 | def call 11 | client.fetch_notification @notification_id 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'onesignal' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /lib/onesignal/notification/contents.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Notification 5 | class Contents 6 | include OneSignal::AutoMap 7 | extend Forwardable 8 | 9 | def_delegators :@content, :as_json, :to_json 10 | 11 | def initialize en:, **content 12 | @content = content.merge(en: en) 13 | create_readers **@content 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/onesignal/notification/headings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Notification 5 | class Headings 6 | include OneSignal::AutoMap 7 | extend Forwardable 8 | 9 | def_delegators :@headings, :as_json, :to_json 10 | 11 | def initialize en:, **headings 12 | @headings = headings.merge(en: en) 13 | create_readers **@headings 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/onesignal/responses/csv_export_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe OneSignal::Responses::CsvExport do 6 | let(:json) do 7 | <<~JSON 8 | { 9 | "csv_file_url": "https://some.csv.gz" 10 | } 11 | JSON 12 | end 13 | 14 | it 'has csv_file_url attribute' do 15 | expect(described_class.from_json(json).csv_file_url).to eq "https://some.csv.gz" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/factories/attachments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :attachments, class: OneSignal::Attachments do 5 | data { { test: 'test' } } 6 | url { Faker::Internet.url } 7 | ios_attachments { { robot: Faker::Avatar.image } } 8 | android_picture { Faker::Avatar.image } 9 | amazon_picture { Faker::Avatar.image } 10 | chrome_picture { Faker::Avatar.image } 11 | 12 | initialize_with { new(attributes) } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/onesignal/commands/fetch_notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Commands 5 | class FetchNotifications < BaseCommand 6 | def initialize page_limit, page_offset, kind 7 | @page_limit = page_limit 8 | @page_offset = page_offset 9 | @kind = kind 10 | end 11 | 12 | def call 13 | client.fetch_notifications page_limit: @page_limit, page_offset: @page_offset, kind: @kind 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/onesignal/segment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Segment 5 | ALL_USERS = 'All' 6 | ACTIVE_USERS = 'Active Users' 7 | ENGAGED_USERS = 'Engaged Users' 8 | INACTIVE_USERS = 'Inactive Users' 9 | SUBSCRIBED_USERS = 'Subscribed Users' 10 | 11 | def initialize name: 12 | @name = name 13 | end 14 | 15 | def as_json _options = nil 16 | @name.to_s 17 | end 18 | 19 | def to_s 20 | @name.to_s 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/onesignal/responses/csv_export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Responses 5 | # Example JSON 6 | # { 7 | # "csv_file_url": "https://onesignal.s3.amazonaws.com/csv_exports/test/users_abc123.csv.gz" 8 | # } 9 | class CsvExport < BaseResponse 10 | ATTRIBUTES_WHITELIST = %i[csv_file_url].freeze 11 | 12 | def self.from_json json 13 | body = json.is_a?(String) ? JSON.parse(json) : json 14 | new(body) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/onesignal/commands/base_command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simple_command' 4 | 5 | module OneSignal 6 | module Commands 7 | class BaseCommand 8 | prepend ::SimpleCommand 9 | 10 | def call 11 | raise NotImplementedError, 'this is an abstract class' 12 | end 13 | 14 | def client 15 | @client ||= OneSignal::Client.new(config.app_id, config.api_key, config.api_url) 16 | end 17 | 18 | def config 19 | OneSignal.config 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/onesignal/segment_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | include OneSignal 5 | 6 | describe Segment do 7 | it 'requires a name' do 8 | expect { described_class.new }.to raise_error ArgumentError, /name/ 9 | end 10 | 11 | context 'json' do 12 | subject { build :segment } 13 | 14 | it 'serializes as json' do 15 | expect(subject.as_json).to eq subject.to_s 16 | end 17 | 18 | it 'serializes to json' do 19 | expect(subject.to_json).to eq "\"#{subject}\"" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/onesignal/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | 5 | module OneSignal 6 | class Configuration 7 | attr_accessor :app_id, :api_key, :api_url, :active, :logger 8 | 9 | def initialize 10 | @app_id = ENV['ONESIGNAL_APP_ID'] 11 | @api_key = ENV['ONESIGNAL_API_KEY'] 12 | @api_url = "https://onesignal.com/api/#{OneSignal::API_VERSION}" 13 | @active = true 14 | @logger = Logger.new(STDOUT).tap do |logger| 15 | logger.level = Logger::INFO 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/onesignal/buttons_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe OneSignal::Buttons do 6 | let(:params) do 7 | { buttons: [{id: 'option_a', text: 'Option A' }, {id: 'option_b', text: 'Option B' }]} 8 | end 9 | 10 | subject { build :buttons } 11 | 12 | it 'creates buttons' do 13 | expect(described_class.new(params)).to be_instance_of OneSignal::Buttons 14 | end 15 | 16 | fit 'serializes as json' do 17 | expect(subject.as_json.deep_symbolize_keys).to include( 18 | buttons: subject.buttons, 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/onesignal/responses/base_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Responses 5 | class BaseResponse 6 | def initialize attributes = {} 7 | @attributes = attributes.deep_symbolize_keys 8 | .keep_if { |k, _v| self.class::ATTRIBUTES_WHITELIST.include?(k.to_sym) } 9 | 10 | self.class::ATTRIBUTES_WHITELIST.each do |attribute| 11 | self.class.send(:define_method, attribute) do 12 | @attributes[attribute] 13 | end 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/factories/icons.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :icons, class: OneSignal::Icons do 5 | small_icon { Faker::Avatar.image } 6 | huawei_small_icon { Faker::Avatar.image } 7 | large_icon { Faker::Avatar.image } 8 | huawei_large_icon { Faker::Avatar.image } 9 | adm_small_icon { Faker::Avatar.image } 10 | adm_large_icon { Faker::Avatar.image } 11 | chrome_web_icon { Faker::Avatar.image } 12 | firefox_icon { Faker::Avatar.image } 13 | chrome_icon { Faker::Avatar.image } 14 | 15 | initialize_with { new(attributes) } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/onesignal/responses/player.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Responses 5 | class Player < BaseResponse 6 | ATTRIBUTES_WHITELIST = %i[id identifier session_count language timezone 7 | game_version device_os device_type device_model tags 8 | ad_id last_active playtime amount_spent created_at 9 | invalid_identifier badge_count sdk test_type ip].freeze 10 | 11 | def invalid_identifier? 12 | invalid_identifier 13 | end 14 | 15 | def self.from_json json 16 | body = json.is_a?(String) ? JSON.parse(json) : json 17 | new(body) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/factories/notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :contents, class: OneSignal::Notification::Contents do 5 | en { Faker::Games::Fallout.quote } 6 | 7 | initialize_with { new(attributes) } 8 | end 9 | 10 | factory :headings, class: OneSignal::Notification::Headings do 11 | en { Faker::Games::Fallout.quote } 12 | 13 | initialize_with { new(attributes) } 14 | end 15 | 16 | factory :notification, class: OneSignal::Notification do 17 | contents 18 | headings 19 | attachments { build :attachments } 20 | icons { build :icons } 21 | included_segments { [build(:segment), build(:segment)] } 22 | excluded_segments { [build(:segment), build(:segment)] } 23 | send_after { Time.now } 24 | 25 | initialize_with do 26 | new(attributes) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'vcr' 5 | require 'onesignal' 6 | require 'support/factory_bot' 7 | require 'faker' 8 | require 'dotenv/load' 9 | 10 | SPEC_ROOT = __dir__ 11 | 12 | VCR.configure do |c| 13 | c.cassette_library_dir = File.join(SPEC_ROOT, '..', 'fixtures', 'vcr_cassettes') 14 | c.hook_into :webmock 15 | c.ignore_hosts 'codeclimate.com' 16 | c.configure_rspec_metadata! 17 | end 18 | 19 | RSpec.configure do |config| 20 | # Enable flags like --only-failures and --next-failure 21 | config.example_status_persistence_file_path = '.rspec_status' 22 | 23 | # Disable RSpec exposing methods globally on `Module` and `main` 24 | config.disable_monkey_patching! 25 | 26 | config.expose_dsl_globally = true 27 | 28 | config.expect_with :rspec do |c| 29 | c.syntax = :expect 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/onesignal/attachments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Attachments 5 | attr_reader :data, :url, :ios_attachments, :android_picture, :amazon_picture, :chrome_picture 6 | 7 | def initialize data: nil, url: nil, ios_attachments: nil, android_picture: nil, amazon_picture: nil, chrome_picture: nil 8 | @data = data 9 | @url = url 10 | @ios_attachments = ios_attachments 11 | @android_picture = android_picture 12 | @amazon_picture = amazon_picture 13 | @chrome_picture = chrome_picture 14 | end 15 | 16 | def as_json options = nil 17 | { 18 | 'data' => @data.as_json(options), 19 | 'url' => @url, 20 | 'ios_attachments' => @ios_attachments.as_json(options), 21 | 'big_picture' => @android_picture, 22 | 'adm_big_picture' => @amazon_picture, 23 | 'chrome_big_picture' => @chrome_picture 24 | } 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/onesignal/sounds_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe OneSignal::Sounds do 6 | let(:params) do 7 | { 8 | ios: 'test.wav', 9 | windows: 'test.wav', 10 | android: 'test', 11 | amazon: 'test' 12 | } 13 | end 14 | 15 | subject { build :sounds } 16 | 17 | it 'creates an attachment' do 18 | expect(described_class.new(params)).to be_instance_of OneSignal::Sounds 19 | end 20 | 21 | it 'refuses an invalid filename' do 22 | expect { described_class.new(ios: 'test') }.to raise_error OneSignal::Sounds::InvalidError 23 | expect { described_class.new(windows: 'test') }.to raise_error OneSignal::Sounds::InvalidError 24 | end 25 | 26 | it 'serializes as json' do 27 | expect(subject.as_json.deep_symbolize_keys).to include( 28 | ios_sound: subject.ios, 29 | android_sound: subject.android, 30 | adm_sound: subject.amazon, 31 | wp_wns_sound: subject.windows 32 | ) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/onesignal/attachments_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe OneSignal::Attachments do 6 | let(:params) do 7 | { data: { 'test' => 'test' }, 8 | url: Faker::Internet.url, 9 | ios_attachments: { 'robot' => Faker::Avatar.image }, 10 | android_picture: Faker::Avatar.image, 11 | amazon_picture: Faker::Avatar.image, 12 | chrome_picture: Faker::Avatar.image } 13 | end 14 | 15 | subject { build :attachments } 16 | 17 | it 'creates an attachment' do 18 | expect(described_class.new(params)).to be_instance_of OneSignal::Attachments 19 | end 20 | 21 | it 'serializes as json' do 22 | expect(subject.as_json.deep_symbolize_keys).to include( 23 | data: subject.data, 24 | url: subject.url, 25 | ios_attachments: subject.ios_attachments, 26 | big_picture: subject.android_picture, 27 | adm_big_picture: subject.amazon_picture, 28 | chrome_big_picture: subject.chrome_picture 29 | ) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/onesignal/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Config DSL' do 6 | let(:test_string) { 'test' } 7 | let(:env_string) { 'env' } 8 | 9 | it 'has sensible defaults' do 10 | config = OneSignal.config 11 | 12 | expect(config.app_id).to eq ENV['ONESIGNAL_APP_ID'] 13 | expect(config.api_key).to eq ENV['ONESIGNAL_API_KEY'] 14 | expect(config.api_url).to eq "https://onesignal.com/api/#{OneSignal::API_VERSION}" 15 | expect(config.active).to be_truthy 16 | end 17 | 18 | it 'configure the library via a DSL' do 19 | OneSignal.configure do |config| 20 | config.api_url = test_string 21 | config.app_id = test_string 22 | config.api_key = test_string 23 | config.active = false 24 | end 25 | 26 | config = OneSignal.config 27 | 28 | expect(config.app_id).to eq test_string 29 | expect(config.api_key).to eq test_string 30 | expect(config.api_url).to eq test_string 31 | expect(config.active).to be_falsey 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/onesignal/notification/contents_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'onesignal' 5 | include OneSignal 6 | 7 | describe Notification::Contents do 8 | it 'requires at least an english content' do 9 | expect { described_class.new }.to raise_error ArgumentError, /en/ 10 | end 11 | 12 | it 'creates a new Content with only english' do 13 | expect(described_class.new(en: 'Test').en).to eq 'Test' 14 | end 15 | 16 | it 'can have content in multiple languages' do 17 | content = described_class.new(en: 'Test', it: 'Prova', fr: 'Essai') 18 | expect(content.en).to eq 'Test' 19 | expect(content.it).to eq 'Prova' 20 | expect(content.fr).to eq 'Essai' 21 | expect { content.de }.to raise_error NoMethodError 22 | end 23 | 24 | context 'json' do 25 | subject { build :contents } 26 | 27 | it 'serializes as json' do 28 | expect(subject.as_json).to eq('en' => subject.en) 29 | end 30 | 31 | it 'serializes to json' do 32 | expect(subject.to_json).to eq "{\"en\":\"#{subject.en}\"}" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/onesignal/notification/headings_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'onesignal' 5 | include OneSignal 6 | 7 | describe Notification::Headings do 8 | it 'requires at least an english content' do 9 | expect { described_class.new }.to raise_error ArgumentError, /en/ 10 | end 11 | 12 | it 'creates a new Content with only english' do 13 | expect(described_class.new(en: 'Test').en).to eq 'Test' 14 | end 15 | 16 | it 'can have content in multiple languages' do 17 | content = described_class.new(en: 'Test', it: 'Prova', fr: 'Essai') 18 | expect(content.en).to eq 'Test' 19 | expect(content.it).to eq 'Prova' 20 | expect(content.fr).to eq 'Essai' 21 | expect { content.de }.to raise_error NoMethodError 22 | end 23 | 24 | context 'json' do 25 | subject { build :headings } 26 | 27 | it 'serializes as json' do 28 | expect(subject.as_json).to eq('en' => subject.en) 29 | end 30 | 31 | it 'serializes to json' do 32 | expect(subject.to_json).to eq "{\"en\":\"#{subject.en}\"}" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mikamai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/onesignal/sounds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Sounds 5 | attr_reader :ios, :android, :amazon, :windows 6 | 7 | def initialize ios: nil, android: nil, amazon: nil, windows: nil 8 | validate ios: ios, windows: windows 9 | 10 | @ios = ios 11 | @android = android 12 | @amazon = amazon 13 | @windows = windows 14 | end 15 | 16 | def as_json options = nil 17 | { 18 | 'ios_sound' => @ios.as_json(options), 19 | 'android_sound' => @android.as_json(options), 20 | 'adm_sound' => @amazon.as_json(options), 21 | 'wp_wns_sound' => @windows.as_json(options) 22 | } 23 | end 24 | 25 | private 26 | 27 | REGEX = /.*.\.\w*/.freeze 28 | 29 | def validate ios: nil, windows: nil 30 | ios_valid = !ios.nil? && (REGEX =~ ios).nil? 31 | windows_valid = !windows.nil? && (REGEX =~ windows).nil? 32 | raise InvalidError, "provide file extension for iOS: #{ios}" if ios_valid 33 | raise InvalidError, "provide file extension for windows: #{ios}" if windows_valid 34 | end 35 | end 36 | 37 | class InvalidError < StandardError 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Hello there! Welcome. Thank you for your contribution! 2 | 3 | Here you can describe the code you are submitting. You can also reference related issues or other 4 | pull requests that might add context. 5 | 6 | Remember to assign the pull request to a [CODEOWNER](../CODEOWNERS) for review. 7 | 8 | **ALWAYS** open PRs targeting [develop](https://github.com/mikamai/onesignal-ruby/tree/develop), as it is our mainline branch. Master is only used to cut stable releases. 9 | 10 | ## Type of change 11 | 12 | Please delete options that are not relevant. 13 | 14 | - [ ] Bug fix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | - [ ] This change requires a documentation update 18 | 19 | ## Checklist 20 | 21 | - [ ] I have added tests for each new piece of code I added, or updated existing tests if I changed some existing code 22 | - [ ] I have added the relevant entry in the `Unreleased` section of the CHANGELOG file 23 | - [ ] I have made corresponding changes to the documentation (namely the README file) 24 | -------------------------------------------------------------------------------- /lib/onesignal/icons.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Icons 5 | attr_reader :small_icon, :huawei_small_icon, :large_icon, :huawei_large_icon, :adm_small_icon, 6 | :adm_large_icon, :chrome_web_icon, :firefox_icon, :chrome_icon 7 | 8 | def initialize **params 9 | @small_icon = params[:small_icon] 10 | @huawei_small_icon = params[:huawei_small_icon] 11 | @large_icon = params[:large_icon] 12 | @huawei_large_icon = params[:huawei_large_icon] 13 | @adm_small_icon = params[:adm_small_icon] 14 | @adm_large_icon = params[:adm_large_icon] 15 | @chrome_web_icon = params[:chrome_web_icon] 16 | @firefox_icon = params[:firefox_icon] 17 | @chrome_icon = params[:chrome_icon] 18 | end 19 | 20 | def as_json options = nil 21 | { 22 | 'small_icon' => @small_icon, 23 | 'huawei_small_icon' => @huawei_small_icon, 24 | 'large_icon' => @large_icon, 25 | 'huawei_large_icon' => @huawei_large_icon, 26 | 'adm_small_icon' => @adm_small_icon, 27 | 'adm_large_icon' => @adm_large_icon, 28 | 'chrome_web_icon' => @chrome_web_icon, 29 | 'firefox_icon' => @firefox_icon, 30 | 'chrome_icon' => @chrome_icon 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/onesignal/icons_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe OneSignal::Icons do 6 | let(:params) do 7 | { 8 | small_icon: Faker::Avatar.image, 9 | huawei_small_icon: Faker::Avatar.image, 10 | large_icon: Faker::Avatar.image, 11 | huawei_large_icon: Faker::Avatar.image, 12 | adm_small_icon: Faker::Avatar.image, 13 | adm_large_icon: Faker::Avatar.image, 14 | chrome_web_icon: Faker::Avatar.image, 15 | firefox_icon: Faker::Avatar.image, 16 | chrome_icon: Faker::Avatar.image 17 | } 18 | end 19 | 20 | subject { build :icons } 21 | 22 | it 'creates an attachment' do 23 | expect(described_class.new(params)).to be_instance_of OneSignal::Icons 24 | end 25 | 26 | it 'serializes as json' do 27 | expect(subject.as_json.deep_symbolize_keys).to include( 28 | small_icon: subject.small_icon, 29 | huawei_small_icon: subject.huawei_small_icon, 30 | large_icon: subject.large_icon, 31 | huawei_large_icon: subject.huawei_large_icon, 32 | adm_small_icon: subject.adm_small_icon, 33 | adm_large_icon: subject.adm_large_icon, 34 | chrome_web_icon: subject.chrome_web_icon, 35 | firefox_icon: subject.firefox_icon, 36 | chrome_icon: subject.chrome_icon 37 | ) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/onesignal/responses/notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | module Responses 5 | # Example JSON 6 | # { 7 | # "id": '481a2734-6b7d-11e4-a6ea-4b53294fa671', 8 | # "successful": 15, 9 | # "failed": 1, 10 | # "converted": 3, 11 | # "remaining": 0, 12 | # "queued_at": 1_415_914_655, 13 | # "send_after": 1_415_914_655, 14 | # "completed_at": 1_415_914_656, 15 | # "url": 'https://yourWebsiteToOpen.com', 16 | # "data": { 17 | # "foo": 'bar', 18 | # "your": 'custom metadata' 19 | # }, 20 | # "canceled": false, 21 | # "headings": { 22 | # "en": 'English and default language heading', 23 | # "es": 'Spanish language heading' 24 | # }, 25 | # "contents": { 26 | # "en": 'English language content', 27 | # "es": 'Hola' 28 | # } 29 | # } 30 | class Notification < BaseResponse 31 | ATTRIBUTES_WHITELIST = %i[id successful failed converted remaining 32 | queued_at send_after completed_at url data 33 | canceled headings contents].freeze 34 | 35 | def canceled? 36 | canceled 37 | end 38 | 39 | def self.from_json json 40 | body = json.is_a?(String) ? JSON.parse(json) : json 41 | new(body) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/onesignal/included_targets_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'onesignal' 5 | include OneSignal 6 | 7 | describe IncludedTargets do 8 | let(:params) do 9 | { 10 | include_email_tokens: ['test'], 11 | include_external_user_ids: ['test'], 12 | include_ios_tokens: ['test'], 13 | include_wp_wns_uris: ['test'], 14 | include_amazon_reg_ids: 'test', 15 | include_chrome_reg_ids: ['test'], 16 | include_chrome_web_reg_ids: 'test', 17 | include_android_reg_ids: ['test'] 18 | } 19 | end 20 | 21 | subject { described_class.new(params) } 22 | 23 | it 'serializes as json' do 24 | expect(subject.as_json.deep_symbolize_keys).to include( 25 | include_email_tokens: subject.include_email_tokens, 26 | include_external_user_ids: subject.include_external_user_ids, 27 | include_ios_tokens: subject.include_ios_tokens, 28 | include_wp_wns_uris: subject.include_wp_wns_uris, 29 | include_amazon_reg_ids: subject.include_amazon_reg_ids, 30 | include_chrome_reg_ids: subject.include_chrome_reg_ids, 31 | include_chrome_web_reg_ids: subject.include_chrome_web_reg_ids, 32 | include_android_reg_ids: subject.include_android_reg_ids 33 | ) 34 | end 35 | 36 | it 'raises an ArgumentError if include_player_ids is used with other targets' do 37 | expect { 38 | described_class.new(params.merge(include_player_ids: 'test')) 39 | }.to raise_error ArgumentError 40 | end 41 | 42 | it 'logs a warning if an unrecommended param is passed' do 43 | expect(OneSignal.config.logger).to receive(:warn).exactly(6).times 44 | subject 45 | end 46 | 47 | it 'does not log a warning if no unrecommended param is passed' do 48 | expect(OneSignal.config.logger).not_to receive(:warn) 49 | described_class.new include_player_ids: 'test' 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/onesignal/notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'onesignal/notification/contents' 4 | require 'onesignal/notification/headings' 5 | 6 | module OneSignal 7 | class Notification 8 | attr_reader :contents, :headings, :template_id, :included_segments, :excluded_segments, 9 | :included_targets, :email_subject, :send_after, :attachments, :sounds, :buttons, 10 | :email_body, :icons, :external_id 11 | 12 | def initialize **params 13 | unless params.include?(:contents) || params.include?(:template_id) 14 | raise ArgumentError, 'missing contents or template_id' 15 | end 16 | 17 | @contents = params[:contents] 18 | @headings = params[:headings] 19 | @template_id = params[:template_id] 20 | @included_segments = params[:included_segments] 21 | @excluded_segments = params[:excluded_segments] 22 | @included_targets = params[:included_targets] 23 | @email_subject = params[:email_subject] 24 | @email_body = params[:email_body] 25 | @send_after = params[:send_after].to_s 26 | @priority = params[:priority] 27 | @attachments = params[:attachments] 28 | @filters = params[:filters] 29 | @sounds = params[:sounds] 30 | @buttons = params[:buttons] 31 | @icons = params[:icons] 32 | @external_id = params[:external_id] 33 | end 34 | 35 | def as_json options = {} 36 | super(options) 37 | .except('attachments', 'sounds', 'included_targets', 'icons') 38 | .merge(@attachments&.as_json(options) || {}) 39 | .merge(@sounds&.as_json(options) || {}) 40 | .merge(@buttons&.as_json(options) || {}) 41 | .merge(@included_targets&.as_json(options) || {}) 42 | .merge(@icons&.as_json(options) || {}) 43 | .select { |_k, v| v.present? } 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/onesignal/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | include OneSignal 5 | 6 | describe Client do 7 | subject { build :client } 8 | 9 | it 'creates a new client' do 10 | expect(subject).to be_instance_of Client 11 | end 12 | 13 | context 'error handling' do 14 | it 'does not raise an error if the response code is lesser than 400' do 15 | res = double :res, body: '{}', status: 200 16 | expect { 17 | subject.send :handle_errors, res 18 | }.not_to raise_error 19 | end 20 | 21 | it 'raises an error if the response code is greater than 399' do 22 | res = double :res, body: '{ "errors": ["Internal Server Error"] }', status: 500 23 | expect { 24 | subject.send :handle_errors, res 25 | }.to raise_error Client::ApiError, 'Internal Server Error' 26 | end 27 | 28 | it 'raises an error if the response code is greater than 399 with default error message' do 29 | res = double :res, body: '{}', status: 401 30 | expect { 31 | subject.send :handle_errors, res 32 | }.to raise_error Client::ApiError, 'Error code 401' 33 | end 34 | 35 | it 'raises an error if the body contains errors' do 36 | res = double :res, body: '{ "errors": ["Internal Server Error"] }', status: 200 37 | expect { 38 | subject.send :handle_errors, res 39 | }.to raise_error Client::ApiError, 'Internal Server Error' 40 | end 41 | end 42 | 43 | context 'fetch_notifications' do 44 | it 'appends kind if present' do 45 | expected_url = 'notifications?limit=50&offset=0&kind=1' 46 | expect(subject).to receive(:get).with(expected_url) 47 | subject.fetch_notifications(page_limit: 50, page_offset: 0, kind: 1) 48 | end 49 | 50 | it "doesn't append kind if absent" do 51 | expected_url = 'notifications?limit=50&offset=0' 52 | expect(subject).to receive(:get).with(expected_url) 53 | subject.fetch_notifications(page_limit: 50, page_offset: 0, kind: nil) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /onesignal-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'onesignal/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'onesignal-ruby' 9 | spec.version = OneSignal::VERSION 10 | spec.authors = ['Matteo Joliveau'] 11 | spec.email = ['matteo.joliveau@mikamai.com'] 12 | 13 | spec.summary = 'Ruby wrapper to OneSignal API' 14 | spec.description = 'Ruby wrapper to OneSignal API, mapping to Plain Old Ruby Objects' 15 | spec.homepage = 'https://github.com/mikamai/onesignal-ruby' 16 | spec.license = 'MIT' 17 | 18 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 19 | # to allow pushing to a single host or delete this section to allow pushing to any host. 20 | if spec.respond_to?(:metadata) 21 | spec.metadata['allowed_push_host'] = 'https://rubygems.org' 22 | else 23 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 24 | 'public gem pushes.' 25 | end 26 | 27 | # Specify which files should be added to the gem when it is released. 28 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 29 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 30 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 31 | end 32 | spec.bindir = 'exe' 33 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 34 | spec.require_paths = ['lib'] 35 | 36 | spec.add_development_dependency 'bundler', '~> 2.0' 37 | spec.add_development_dependency 'dotenv', '~> 2.5' 38 | spec.add_development_dependency 'factory_bot', '~> 4.11' 39 | spec.add_development_dependency 'rake', '~> 13.0' 40 | spec.add_development_dependency 'rspec', '~> 3.0' 41 | spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4' 42 | spec.add_development_dependency 'vcr', '~> 4.0', '>= 4.0.0' 43 | spec.add_development_dependency 'webmock', '~> 3.4' 44 | 45 | spec.add_runtime_dependency 'activesupport', '>= 5.0.0', '< 8' 46 | spec.add_runtime_dependency 'faraday', '>= 1', '< 3' 47 | spec.add_runtime_dependency 'simple_command', '~> 0', '>= 0.0.9' 48 | end 49 | -------------------------------------------------------------------------------- /lib/onesignal/included_targets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class IncludedTargets 5 | attr_reader :include_player_ids, :include_email_tokens, :include_external_user_ids, :include_ios_tokens, 6 | :include_wp_wns_uris, :include_amazon_reg_ids, :include_chrome_reg_ids, :include_chrome_web_reg_ids, 7 | :include_android_reg_ids 8 | 9 | def initialize params 10 | raise ArgumentError, 'include_player_ids cannot be used with other targets' if params.key?(:include_player_ids) && params.keys.count > 1 11 | 12 | @include_player_ids = params[:include_player_ids] 13 | @include_email_tokens = params[:include_email_tokens] 14 | @include_external_user_ids = params[:include_external_user_ids] 15 | 16 | @include_ios_tokens = print_warning params, :include_ios_tokens 17 | @include_wp_wns_uris = print_warning params, :include_wp_wns_uris 18 | @include_amazon_reg_ids = print_warning params, :include_amazon_reg_ids 19 | @include_chrome_reg_ids = print_warning params, :include_chrome_reg_ids 20 | @include_chrome_web_reg_ids = print_warning params, :include_chrome_web_reg_ids 21 | @include_android_reg_ids = print_warning params, :include_android_reg_ids 22 | end 23 | 24 | def print_warning params, name 25 | if params.key? name 26 | OneSignal.config.logger.warn "OneSignal WARNING - Use of #{name} is not recommended. " \ 27 | 'Use either include_player_ids, include_email_tokens or include_external_user_ids. ' \ 28 | 'See https://documentation.onesignal.com/reference#section-send-to-specific-devices' 29 | end 30 | params[name] 31 | end 32 | 33 | def as_json options = nil 34 | { 35 | 'include_player_ids' => @include_player_ids, 36 | 'include_email_tokens' => @include_email_tokens, 37 | 'include_external_user_ids' => @include_external_user_ids, 38 | 'include_ios_tokens' => @include_ios_tokens, 39 | 'include_wp_wns_uris' => @include_wp_wns_uris, 40 | 'include_amazon_reg_ids' => @include_amazon_reg_ids, 41 | 'include_chrome_reg_ids' => @include_chrome_reg_ids, 42 | 'include_chrome_web_reg_ids' => @include_chrome_web_reg_ids, 43 | 'include_android_reg_ids' => @include_android_reg_ids 44 | } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/onesignal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/version' 4 | require 'active_support/isolated_execution_state' if ActiveSupport::VERSION::MAJOR == 7 5 | require 'active_support/core_ext/string' 6 | require 'active_support/json' 7 | require 'onesignal/version' 8 | require 'onesignal/commands' 9 | 10 | ActiveSupport.escape_html_entities_in_json = false 11 | 12 | module OneSignal 13 | class << self 14 | def configure 15 | yield config 16 | end 17 | 18 | def send_notification notification 19 | return unless OneSignal.config.active 20 | 21 | created = Commands::CreateNotification.call notification 22 | fetch_notification(JSON.parse(created.body)['id']) 23 | end 24 | 25 | def fetch_notification notification_id 26 | return unless OneSignal.config.active 27 | 28 | fetched = Commands::FetchNotification.call notification_id 29 | Responses::Notification.from_json fetched.body 30 | end 31 | 32 | def fetch_notifications(page_limit: 50, page_offset: 0, kind: nil) 33 | return unless OneSignal.config.active 34 | 35 | Enumerator.new() do |yielder| 36 | limit = page_limit 37 | offset = page_offset 38 | 39 | fetched = Commands::FetchNotifications.call limit, offset, kind 40 | parsed = JSON.parse(fetched.body) 41 | 42 | total_count = parsed["total_count"] 43 | max_pages = (total_count / limit.to_f).ceil 44 | 45 | loop do 46 | parsed['notifications'].each do |notification| 47 | yielder << Responses::Notification.from_json(notification) 48 | end 49 | offset += 1 50 | break if offset >= max_pages 51 | fetched = Commands::FetchNotifications.call limit, offset*limit, kind 52 | parsed = JSON.parse(fetched.body) 53 | end 54 | end 55 | end 56 | 57 | def fetch_player player_id 58 | return unless OneSignal.config.active 59 | 60 | fetched = Commands::FetchPlayer.call player_id 61 | Responses::Player.from_json fetched.body 62 | end 63 | 64 | def delete_player player_id 65 | return unless OneSignal.config.active 66 | 67 | fetched = Commands::DeletePlayer.call player_id 68 | Responses::Player.from_json fetched.body 69 | end 70 | 71 | def fetch_players 72 | return unless OneSignal.config.active 73 | 74 | fetched = Commands::FetchPlayers.call 75 | JSON.parse(fetched.body)['players'].map { |player| Responses::Player.from_json player } 76 | end 77 | 78 | def csv_export params = {} 79 | return unless OneSignal.config.active 80 | 81 | fetched = Commands::CsvExport.call params 82 | Responses::CsvExport.from_json fetched.body 83 | end 84 | 85 | def config 86 | @config ||= Configuration.new 87 | end 88 | 89 | alias define configure 90 | end 91 | end 92 | 93 | require 'onesignal/autoloader' 94 | require 'onesignal/responses' 95 | -------------------------------------------------------------------------------- /lib/onesignal/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faraday' 4 | 5 | module OneSignal 6 | class Client 7 | class ApiError < RuntimeError; end 8 | 9 | def initialize app_id, api_key, api_url 10 | @app_id = app_id 11 | @api_key = api_key 12 | @api_url = api_url 13 | @conn = ::Faraday.new(url: api_url) do |faraday| 14 | # faraday.response :logger do |logger| 15 | # logger.filter(/(api_key=)(\w+)/, '\1[REMOVED]') 16 | # logger.filter(/(Basic )(\w+)/, '\1[REMOVED]') 17 | # end 18 | faraday.adapter Faraday.default_adapter 19 | end 20 | end 21 | 22 | def create_notification notification 23 | post 'notifications', notification 24 | end 25 | 26 | def fetch_notification notification_id 27 | get "notifications/#{notification_id}" 28 | end 29 | 30 | def fetch_notifications page_limit: 50, page_offset: 0, kind: nil 31 | url = "notifications?limit=#{page_limit}&offset=#{page_offset}" 32 | url = kind ? "#{url}&kind=#{kind}" : url 33 | get url 34 | end 35 | 36 | def fetch_players 37 | get 'players' 38 | end 39 | 40 | def fetch_player player_id 41 | get "players/#{player_id}" 42 | end 43 | 44 | def delete_player player_id 45 | delete "players/#{player_id}" 46 | end 47 | 48 | def csv_export extra_fields: nil, last_active_since: nil, segment_name: nil 49 | post "players/csv_export?app_id=#{@app_id}", 50 | extra_fields: extra_fields, 51 | last_active_since: last_active_since&.to_i&.to_s, 52 | segment_name: segment_name 53 | end 54 | 55 | private 56 | 57 | def create_body payload 58 | body = payload.as_json.delete_if { |_, v| v.nil? } 59 | body['app_id'] = @app_id 60 | body 61 | end 62 | 63 | def delete url 64 | res = @conn.delete do |req| 65 | req.url url, app_id: @app_id 66 | req.headers['Authorization'] = "Basic #{@api_key}" 67 | end 68 | 69 | handle_errors res 70 | end 71 | 72 | def post url, body 73 | res = @conn.post do |req| 74 | req.url url 75 | req.body = create_body(body).to_json 76 | req.headers['Content-Type'] = 'application/json' 77 | req.headers['Authorization'] = "Basic #{@api_key}" 78 | end 79 | 80 | handle_errors res 81 | end 82 | 83 | def get url 84 | res = @conn.get do |req| 85 | req.url url, app_id: @app_id 86 | req.headers['Content-Type'] = 'application/json' 87 | req.headers['Authorization'] = "Basic #{@api_key}" 88 | end 89 | 90 | handle_errors res 91 | end 92 | 93 | def handle_errors res 94 | errors = JSON.parse(res.body).fetch 'errors', [] 95 | raise ApiError, (errors.first || "Error code #{res.status}") if res.status > 399 || errors.any? 96 | 97 | res 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - Add priority as notification parameter ([#36](https://github.com/mikamai/onesignal-ruby/pull/36)) 10 | 11 | ## [0.6.0] - 2021-10-21 12 | ### Added 13 | - Support for external_player_id and for deleting a Player thanks to [@reachire-smendola] ([#35](https://github.com/mikamai/onesignal-ruby/pull/35)) 14 | - Support for icons thanks to [@mtayllan] ([#33](https://github.com/mikamai/onesignal-ruby/pull/33)) 15 | 16 | 17 | ## [0.5.0] - 2021-05-17 18 | ### Added 19 | - Support for the email_subject field thanks to [@regedarek] ([#26](https://github.com/mikamai/onesignal-ruby/pull/26)) 20 | with tests contributed by [@martinjaimem] ([#28](https://github.com/mikamai/onesignal-ruby/pull/28)) 21 | - Support for the email_body field thanks to [@martinjaimem] ([#29](https://github.com/mikamai/onesignal-ruby/pull/29)) 22 | 23 | ### Changed 24 | - Bump Faraday to ~> 1.0 thanks to [@jongirard] (#25) 25 | 26 | ## [0.4.0] - 2021-01-19 27 | ### Added 28 | - Support for OneSignal action buttons thanks to [@jongirard] ([#15](https://github.com/mikamai/onesignal-ruby/pull/15)) 29 | - Support for Ruby 2.7 thanks to [@hotatekaoru] ([#13](https://github.com/mikamai/onesignal-ruby/pull/13)) 30 | - Support for Rails 6 thanks to [@chrismaximin] ([#11](https://github.com/mikamai/onesignal-ruby/pull/11)) 31 | - API call for fetching all notifications thanks to [@rgould] ([#7](https://github.com/mikamai/onesignal-ruby/pull/7)) 32 | - CSV export thanks to [@joecorcoran] ([#6](https://github.com/mikamai/onesignal-ruby/pull/6)) 33 | 34 | ### Changed 35 | - Bump bundler to ~> 2.0 36 | 37 | ## [0.3.0] - 2019-01-08 38 | ## Added 39 | - Support for specific device targets for notifications, thanks to [@gabriel-dehan]. 40 | 41 | ## [0.2.0] - 2019-01-07 42 | First public release. This version is the first publicly available on [RubyGems](https://rubygems.org/gems/onesignal-ruby). 43 | 44 | [Unreleased]: https://github.com/mikamai/onesignal-ruby/compare/0.6.0...HEAD 45 | [0.6.0]: https://github.com/mikamai/onesignal-ruby/compare/0.5.0...0.6.0 46 | [0.5.0]: https://github.com/mikamai/onesignal-ruby/compare/0.4.0...0.5.0 47 | [0.4.0]: https://github.com/mikamai/onesignal-ruby/compare/0.3.0...0.4.0 48 | [0.3.0]: https://github.com/mikamai/onesignal-ruby/compare/0.2.0...0.3.0 49 | [0.2.0]: https://github.com/mikamai/onesignal-ruby/releases/tag/0.2.0 50 | 51 | [@chrismaximin]: https://github.com/chrismaximin 52 | [@gabriel-dehan]: https://github.com/gabriel-dehan 53 | [@hotatekaoru]: https://github.com/hotatekaoru 54 | [@joecorcoran]: https://github.com/joecorcoran 55 | [@jongirard]: https://github.com/jongirard 56 | [@martinjaimem]: https://github.com/martinjaimem 57 | [@regedarek]: https://github.com/regedarek 58 | [@rgould]: https://github.com/rgould 59 | [@reachire-smendola]: https://github.com/reachire-smendola -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/ruby:2.7.0 6 | working_directory: ~/repo 7 | 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | name: Restore Bundle cache 12 | keys: 13 | - bundle-{{ checksum "onesignal-ruby.gemspec" }} 14 | - bundle- 15 | - run: 16 | name: Install dependencies 17 | command: | 18 | bundle install --jobs=4 --retry=3 --path vendor/bundle 19 | - save_cache: 20 | name: Save Bundle cache 21 | paths: 22 | - ./vendor/bundle 23 | key: bundle-{{ checksum "onesignal-ruby.gemspec" }} 24 | 25 | - run: 26 | name: Run tests 27 | command: | 28 | mkdir /tmp/test-results 29 | TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" 30 | 31 | bundle exec rspec --format progress \ 32 | --format RspecJunitFormatter \ 33 | --out /tmp/test-results/rspec.xml \ 34 | --format progress \ 35 | $TEST_FILES 36 | 37 | - store_test_results: 38 | path: /tmp/test-results 39 | - store_artifacts: 40 | path: /tmp/test-results 41 | destination: test-results 42 | release: 43 | docker: 44 | - image: circleci/ruby:2.7.0 45 | working_directory: ~/repo 46 | steps: 47 | - checkout 48 | - restore_cache: 49 | name: Restore Bundle cache 50 | keys: 51 | - bundle-{{ checksum "onesignal-ruby.gemspec" }} 52 | - bundle- 53 | - run: 54 | name: Install dependencies 55 | command: | 56 | bundle install --jobs=4 --retry=3 --path vendor/bundle 57 | - save_cache: 58 | name: Save Bundle cache 59 | paths: 60 | - ./vendor/bundle 61 | key: bundle-{{ checksum "onesignal-ruby.gemspec" }} 62 | 63 | - run: 64 | name: Package gem 65 | command: gem build --output release.gem 66 | 67 | - run: 68 | name: Configure credentials 69 | command: | 70 | mkdir -p ~/.gem 71 | echo ":rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials 72 | chmod 0600 ~/.gem/credentials 73 | - run: 74 | name: Publish gem 75 | command: gem push release.gem --key rubygems 76 | 77 | workflows: 78 | version: 2 79 | test: 80 | jobs: 81 | - test 82 | test_release: 83 | jobs: 84 | - test: 85 | filters: &release-filters 86 | branches: 87 | ignore: /.*/ 88 | tags: 89 | only: /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ 90 | - release: 91 | requires: 92 | - test 93 | filters: 94 | <<: *release-filters 95 | -------------------------------------------------------------------------------- /spec/onesignal/responses/notification_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe OneSignal::Responses::Notification do 6 | let(:json) do 7 | <<~JSON 8 | { 9 | "adm_big_picture": null, 10 | "adm_group": null, 11 | "adm_group_message": null, 12 | "adm_large_icon": null, 13 | "adm_small_icon": null, 14 | "adm_sound": null, 15 | "spoken_text": null, 16 | "alexa_ssml": null, 17 | "alexa_display_title": null, 18 | "amazon_background_data": null, 19 | "android_accent_color": null, 20 | "android_group": null, 21 | "android_group_message": null, 22 | "android_led_color": null, 23 | "android_sound": null, 24 | "android_visibility": null, 25 | "app_id": "22bc6dec-5150-4d6d-8628-377259d2dd14", 26 | "big_picture": null, 27 | "buttons": null, 28 | "canceled": false, 29 | "chrome_big_picture": null, 30 | "chrome_icon": null, 31 | "chrome_web_icon": null, 32 | "chrome_web_image": null, 33 | "chrome_web_badge": null, 34 | "content_available": null, 35 | "contents": { 36 | "en": "LiveTest" 37 | }, 38 | "converted": 0, 39 | "data": null, 40 | "delayed_option": null, 41 | "delivery_time_of_day": null, 42 | "errored": 0, 43 | "excluded_segments": [], 44 | "failed": 0, 45 | "firefox_icon": null, 46 | "headings": { 47 | "en": "This is a live test for OneSignal" 48 | }, 49 | "id": "fe82c1ae-54c2-458b-8aad-7edc3e8a96c4", 50 | "include_player_ids": null, 51 | "included_segments": [ 52 | "Active Users" 53 | ], 54 | "ios_badgeCount": null, 55 | "ios_badgeType": null, 56 | "ios_category": null, 57 | "ios_sound": null, 58 | "apns_alert": null, 59 | "isAdm": false, 60 | "isAndroid": false, 61 | "isChrome": false, 62 | "isChromeWeb": false, 63 | "isAlexa": false, 64 | "isFirefox": false, 65 | "isIos": true, 66 | "isSafari": false, 67 | "isWP": false, 68 | "isWP_WNS": false, 69 | "isEdge": false, 70 | "large_icon": null, 71 | "priority": null, 72 | "queued_at": 1530178925, 73 | "remaining": 1, 74 | "send_after": 1530178925, 75 | "completed_at": null, 76 | "small_icon": null, 77 | "successful": 0, 78 | "tags": null, 79 | "filters": null, 80 | "template_id": null, 81 | "ttl": null, 82 | "url": null, 83 | "web_buttons": null, 84 | "web_push_topic": null, 85 | "wp_sound": null, 86 | "wp_wns_sound": null 87 | } 88 | JSON 89 | end 90 | 91 | it 'creates an object from a JSON string' do 92 | expect(described_class.from_json(json)).to be_instance_of described_class 93 | end 94 | 95 | it 'creates an object from an hash' do 96 | expect(described_class.from_json(JSON.parse(json))).to be_instance_of described_class 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/onesignal/notification_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | include OneSignal 5 | 6 | describe Notification do 7 | let(:contents) { build :contents } 8 | let(:headings) { build :headings } 9 | 10 | it 'requires at least some contents' do 11 | expect { described_class.new }.to raise_error ArgumentError, 'missing contents or template_id' 12 | end 13 | 14 | it 'creates a new notification' do 15 | expect(described_class.new(contents: contents, headings: headings, send_after: Time.now)).to be_instance_of Notification 16 | end 17 | 18 | context 'json' do 19 | let(:segments) { [build(:segment), build(:segment)] } 20 | let(:time) { Time.now } 21 | let(:priority) { 10 } 22 | let(:filters) do 23 | [Filter.last_session.lesser_than(2).hours_ago!, 24 | Filter.session_count.equals(5), 25 | Filter::OR, 26 | Filter.country.equals('IT')] 27 | end 28 | let(:email_subject) { Faker::Lorem.sentence } 29 | let(:email_body) { '

fake body

' } 30 | let(:sounds) { build :sounds } 31 | let(:targets) { IncludedTargets.new include_email_tokens: 'test', include_external_user_ids: 'test' } 32 | let(:icons) { build :icons } 33 | 34 | subject do 35 | build :notification, 36 | contents: contents, 37 | headings: headings, 38 | included_segments: segments, 39 | excluded_segments: segments, 40 | send_after: time, 41 | priority: priority, 42 | filters: filters, 43 | sounds: sounds, 44 | included_targets: targets, 45 | email_subject: email_subject, 46 | email_body: email_body, 47 | icons: icons 48 | end 49 | 50 | it 'serializes as json' do 51 | expect(subject.as_json).to eq( 52 | 'contents' => contents.as_json, 53 | 'headings' => headings.as_json, 54 | 'send_after' => time.to_s, 55 | 'priority' => priority, 56 | 'included_segments' => segments.as_json, 57 | 'excluded_segments' => segments.as_json, 58 | 'data' => subject.attachments.data.as_json, 59 | 'url' => subject.attachments.url, 60 | 'ios_attachments' => subject.attachments.ios_attachments.as_json, 61 | 'big_picture' => subject.attachments.android_picture, 62 | 'adm_big_picture' => subject.attachments.amazon_picture, 63 | 'chrome_big_picture' => subject.attachments.chrome_picture, 64 | 'filters' => filters.as_json, 65 | 'ios_sound' => sounds.ios.as_json, 66 | 'android_sound' => sounds.android.as_json, 67 | 'adm_sound' => sounds.amazon.as_json, 68 | 'wp_wns_sound' => sounds.windows.as_json, 69 | 'include_email_tokens' => targets.include_email_tokens, 70 | 'include_external_user_ids' => targets.include_external_user_ids, 71 | 'email_subject' => email_subject, 72 | 'email_body' => email_body, 73 | 'small_icon' => icons.small_icon, 74 | 'huawei_small_icon' => icons.huawei_small_icon, 75 | 'large_icon' => icons.large_icon, 76 | 'huawei_large_icon' => icons.huawei_large_icon, 77 | 'adm_small_icon' => icons.adm_small_icon, 78 | 'adm_large_icon' => icons.adm_large_icon, 79 | 'chrome_web_icon' => icons.chrome_web_icon, 80 | 'firefox_icon' => icons.firefox_icon, 81 | 'chrome_icon' => icons.chrome_icon 82 | ) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/onesignal/filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OneSignal 4 | class Filter 5 | OR = { operator: 'OR' }.freeze 6 | 7 | attr_reader :field, :key, :relation, :value, :hours_ago, :location 8 | 9 | class << self 10 | def last_session 11 | FilterBuilder.new 'last_session' 12 | end 13 | 14 | def first_session 15 | FilterBuilder.new 'first_session' 16 | end 17 | 18 | def session_count 19 | FilterBuilder.new 'session_count' 20 | end 21 | 22 | def session_time 23 | FilterBuilder.new 'session_time' 24 | end 25 | 26 | def amount_spent 27 | FilterBuilder.new 'amount_spent' 28 | end 29 | 30 | def bought_sku sku 31 | FilterBuilder.new 'bought_sku', key: sku 32 | end 33 | 34 | def tag tag 35 | FilterBuilder.new 'tag', key: tag 36 | end 37 | 38 | def language 39 | FilterBuilder.new 'language' 40 | end 41 | 42 | def app_version 43 | FilterBuilder.new 'app_version' 44 | end 45 | 46 | def country 47 | FilterBuilder.new 'country' 48 | end 49 | 50 | def location radius:, lat:, long: 51 | location = OpenStruct.new radius: radius, latitude: lat, longitude: long 52 | new FilterBuilder.new('location', location: location) 53 | end 54 | 55 | def email email 56 | new(FilterBuilder.new('email', value: email)) 57 | end 58 | end 59 | 60 | def hours_ago! 61 | @hours_ago ||= @value 62 | @value = nil 63 | self 64 | end 65 | 66 | def as_json options = nil 67 | super(options).select { |_k, v| v.present? } 68 | end 69 | 70 | private 71 | 72 | def initialize builder 73 | @field = builder.b_field 74 | @key = builder.b_key 75 | @relation = builder.b_relation 76 | @value = builder.b_value 77 | @hours_ago = builder.b_hours_ago 78 | @location = builder.b_location 79 | end 80 | 81 | class FilterBuilder 82 | attr_reader :b_field, :b_key, :b_relation, :b_value, :b_hours_ago, :b_location 83 | 84 | def initialize field, params = {} 85 | @b_field = field 86 | @b_key = params[:key] 87 | @b_location = params[:location] 88 | @b_value = params[:value] 89 | end 90 | 91 | def lesser_than value 92 | @b_relation = '<' 93 | @b_value = value.to_s 94 | build 95 | end 96 | 97 | alias < lesser_than 98 | 99 | def greater_than value 100 | @b_relation = '>' 101 | @b_value = value.to_s 102 | build 103 | end 104 | 105 | alias > greater_than 106 | 107 | def equals value 108 | @b_relation = '=' 109 | @b_value = value.to_s 110 | build 111 | end 112 | 113 | alias == equals 114 | 115 | def not_equals value 116 | @b_relation = '!=' 117 | @b_value = value.to_s 118 | build 119 | end 120 | 121 | alias != not_equals 122 | 123 | def exists 124 | @b_relation = 'exists' 125 | build 126 | end 127 | 128 | def not_exists 129 | @b_relation = 'not_exists' 130 | build 131 | end 132 | 133 | private 134 | 135 | def build 136 | Filter.new self 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at matteo.joliveau@mikamai.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /spec/api_calls_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | include OneSignal 5 | 6 | describe 'Live API Testing', remote: true do 7 | before(:all) do 8 | VCR.configure do |c| 9 | c.allow_http_connections_when_no_cassette = true 10 | end 11 | end 12 | 13 | let(:app_id) { ENV.fetch('ONESIGNAL_APP_ID', 'test') } 14 | let(:api_key) { ENV.fetch('ONESIGNAL_API_KEY', 'test') } 15 | 16 | let(:notification) do 17 | Notification.new(contents: Notification::Contents.new(en: 'Live Test'), 18 | headings: Notification::Headings.new(en: 'This is a live test for OneSignal'), 19 | included_segments: ['Test Users']) 20 | end 21 | 22 | it 'sends a notification' do 23 | VCR.use_cassette('os-send-noti') do 24 | response = OneSignal.send_notification notification 25 | expect(response).to be_instance_of OneSignal::Responses::Notification 26 | @notification_id = response.id 27 | expect(response.id).to eq @notification_id 28 | end 29 | end 30 | 31 | it 'fetches a notification by id' do 32 | VCR.use_cassette('os-fetch-noti') do 33 | response = OneSignal.fetch_notification @notification_id 34 | expect(response).to be_instance_of OneSignal::Responses::Notification 35 | expect(response.id).to eq @notification_id 36 | end 37 | end 38 | 39 | it 'fetches notifications' do 40 | VCR.use_cassette('os-fetch-notifications', allow_playback_repeats: true) do 41 | response = OneSignal.fetch_notifications 42 | notification = response.first 43 | expect(notification).to be_instance_of OneSignal::Responses::Notification 44 | expect(response.count).to eq 51 45 | # Ensure the Enumerator doesn't cache data improperly 46 | expect(response.count).to eq 51 47 | end 48 | end 49 | 50 | it 'fectches all players' do 51 | VCR.use_cassette('os-fetch-players') do 52 | player = OneSignal.fetch_players.first 53 | expect(player).to be_instance_of OneSignal::Responses::Player 54 | @player_id = player.id 55 | end 56 | end 57 | 58 | it 'fectches one players by id' do 59 | VCR.use_cassette('os-fetch-player') do 60 | player = OneSignal.fetch_player @player_id 61 | expect(player).to be_instance_of OneSignal::Responses::Player 62 | expect(player.id).to eq @player_id 63 | end 64 | end 65 | 66 | it 'deletes one player by id' do 67 | VCR.use_cassette('os-delete-player', :record => :new_episodes) do 68 | player = OneSignal.delete_player @player_id 69 | expect(player).to be_instance_of OneSignal::Responses::Player 70 | expect(player.id).to eq @player_id 71 | end 72 | end 73 | 74 | 75 | context 'with keys' do 76 | around do |example| 77 | OneSignal.config.app_id = app_id 78 | OneSignal.config.api_key = api_key 79 | example.run 80 | OneSignal.config.app_id = nil 81 | OneSignal.config.api_key = nil 82 | end 83 | 84 | it 'fetches CSV export data' do 85 | VCR.use_cassette('os-csv-export') do 86 | response = OneSignal.csv_export 87 | expect(response).to be_instance_of OneSignal::Responses::CsvExport 88 | expect(response.csv_file_url).to eq 'https://onesignal.s3.amazonaws.com/csv_exports/test/users_abc123.csv.gz' 89 | end 90 | end 91 | 92 | it 'fetches CSV export data with params' do 93 | VCR.use_cassette('os-csv-export', match_requests_on: [:body_as_json]) do 94 | response = OneSignal.csv_export last_active_since: Time.at(1568419200) 95 | expect(response).to be_instance_of OneSignal::Responses::CsvExport 96 | expect(response.csv_file_url).to eq 'https://onesignal.s3.amazonaws.com/csv_exports/test/users_def456.csv.gz' 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/os-csv-export.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://onesignal.com/api/v1/players/csv_export?app_id=test 6 | body: 7 | encoding: UTF-8 8 | string: '{"app_id":"test"}' 9 | headers: 10 | User-Agent: 11 | - Faraday v0.15.4 12 | Content-Type: 13 | - application/json 14 | Authorization: 15 | - Basic ZjI3NDAwY2ItNTM1Yy00OTkwLWE1OTAtNzI0ZjdmNTg1ZTlj 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | Accept: 19 | - "*/*" 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Date: 26 | - Tue, 17 Sep 2019 14:33:01 GMT 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Transfer-Encoding: 30 | - chunked 31 | Connection: 32 | - keep-alive 33 | Set-Cookie: 34 | - __cfduid=d10f3912512be2fb143a35a8fb1af30331568730781; expires=Wed, 16-Sep-20 35 | 14:33:01 GMT; path=/; domain=.onesignal.com; HttpOnly 36 | Status: 37 | - 200 OK 38 | Cache-Control: 39 | - max-age=0, private, must-revalidate 40 | Access-Control-Allow-Origin: 41 | - "*" 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Request-Id: 45 | - b5baa4c3-9884-4a26-ad57-9695fe9c85a4 46 | Access-Control-Allow-Headers: 47 | - SDK-Version 48 | Etag: 49 | - W/"7965e366a680d7fa0c3b52547dfd736a" 50 | X-Frame-Options: 51 | - SAMEORIGIN 52 | X-Runtime: 53 | - '0.169620' 54 | X-Content-Type-Options: 55 | - nosniff 56 | X-Powered-By: 57 | - Phusion Passenger 5.3.7 58 | Expect-Ct: 59 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 60 | Server: 61 | - cloudflare 62 | Cf-Ray: 63 | - 517bcaf63e2ed45b-HAM 64 | body: 65 | encoding: ASCII-8BIT 66 | string: '{"csv_file_url":"https://onesignal.s3.amazonaws.com/csv_exports/test/users_abc123.csv.gz"}' 67 | http_version: 68 | recorded_at: Tue, 17 Sep 2019 14:33:01 GMT 69 | - request: 70 | method: post 71 | uri: https://onesignal.com/api/v1/players/csv_export?app_id=test 72 | body: 73 | encoding: UTF-8 74 | string: '{"app_id":"test","last_active_since":"1568419200"}' 75 | headers: 76 | User-Agent: 77 | - Faraday v0.15.4 78 | Content-Type: 79 | - application/json 80 | Authorization: 81 | - Basic ZjI3NDAwY2ItNTM1Yy00OTkwLWE1OTAtNzI0ZjdmNTg1ZTlj 82 | Accept-Encoding: 83 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 84 | Accept: 85 | - "*/*" 86 | response: 87 | status: 88 | code: 200 89 | message: OK 90 | headers: 91 | Date: 92 | - Tue, 17 Sep 2019 14:33:01 GMT 93 | Content-Type: 94 | - application/json; charset=utf-8 95 | Transfer-Encoding: 96 | - chunked 97 | Connection: 98 | - keep-alive 99 | Set-Cookie: 100 | - __cfduid=d10f3912512be2fb143a35a8fb1af30331568730781; expires=Wed, 16-Sep-20 101 | 14:33:01 GMT; path=/; domain=.onesignal.com; HttpOnly 102 | Status: 103 | - 200 OK 104 | Cache-Control: 105 | - max-age=0, private, must-revalidate 106 | Access-Control-Allow-Origin: 107 | - "*" 108 | X-Xss-Protection: 109 | - 1; mode=block 110 | X-Request-Id: 111 | - b5baa4c3-9884-4a26-ad57-9695fe9c85a4 112 | Access-Control-Allow-Headers: 113 | - SDK-Version 114 | Etag: 115 | - W/"7965e366a680d7fa0c3b52547dfd736a" 116 | X-Frame-Options: 117 | - SAMEORIGIN 118 | X-Runtime: 119 | - '0.169620' 120 | X-Content-Type-Options: 121 | - nosniff 122 | X-Powered-By: 123 | - Phusion Passenger 5.3.7 124 | Expect-Ct: 125 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 126 | Server: 127 | - cloudflare 128 | Cf-Ray: 129 | - 517bcaf63e2ed45b-HAM 130 | body: 131 | encoding: ASCII-8BIT 132 | string: '{"csv_file_url":"https://onesignal.s3.amazonaws.com/csv_exports/test/users_def456.csv.gz"}' 133 | http_version: 134 | recorded_at: Tue, 17 Sep 2019 14:33:01 GMT 135 | recorded_with: VCR 4.0.0 136 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/os-fetch-player.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://onesignal.com/api/v1/players/?app_id 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Faraday v0.15.2 12 | Content-Type: 13 | - application/json 14 | Authorization: 15 | - Basic test 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | Accept: 19 | - "*/*" 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Date: 26 | - Thu, 28 Jun 2018 15:30:58 GMT 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Transfer-Encoding: 30 | - chunked 31 | Connection: 32 | - keep-alive 33 | Set-Cookie: 34 | - __cfduid=d162ae07cbc70fafa075f6d10a6694cc61530199858; expires=Fri, 28-Jun-19 35 | 15:30:58 GMT; path=/; domain=.onesignal.com; HttpOnly 36 | Status: 37 | - 200 OK 38 | Cache-Control: 39 | - max-age=0, private, must-revalidate 40 | Access-Control-Allow-Origin: 41 | - "*" 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Request-Id: 45 | - 768dd4da-9f24-4a93-8a1b-3efbc3bf22b2 46 | Access-Control-Allow-Headers: 47 | - SDK-Version 48 | Etag: 49 | - W/"cee038b261c95e403233f1ada96658e3" 50 | X-Frame-Options: 51 | - SAMEORIGIN 52 | X-Runtime: 53 | - '0.029141' 54 | X-Content-Type-Options: 55 | - nosniff 56 | X-Powered-By: 57 | - Phusion Passenger 5.3.2 58 | Expect-Ct: 59 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 60 | Server: 61 | - cloudflare 62 | Cf-Ray: 63 | - 4321329ccab1430a-MXP 64 | body: 65 | encoding: ASCII-8BIT 66 | string: '{"total_count":8,"offset":0,"limit":300,"players":[{"id":"14c8141f-d57d-4652-8911-960a00ff14fa","identifier":"52812f0ca9b3cc1c1f6a13fd1467c3a2a9f7ef3db5ee56cb951b00fb6bb78665","session_count":10,"language":"it","timezone":7200,"game_version":"1","device_os":"11.4","device_type":0,"device_model":"iPhone10,5","ad_id":"7A30EDB9-A4EA-404E-BA5E-2AEA86C74AA7","tags":{},"last_active":1530184068,"playtime":1219,"amount_spent":0.0,"created_at":1530177751,"invalid_identifier":false,"badge_count":0,"sdk":"020805","test_type":1,"ip":null},{"id":"16c0eee0-edc0-4630-8792-1c432a2e7da1","identifier":null,"session_count":2,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530192672,"playtime":0,"amount_spent":0.0,"created_at":1530192553,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"066e3db6-1ed1-49f0-9340-624327e3faf8","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530193737,"playtime":0,"amount_spent":0.0,"created_at":1530193737,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"cf435718-f137-4fd8-ba43-b9ada7e88846","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530194294,"playtime":0,"amount_spent":0.0,"created_at":1530194294,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"ba4b2ab3-cc89-4bd6-8c1a-9f69e7a2aa55","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530197017,"playtime":0,"amount_spent":0.0,"created_at":1530197017,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"152c5092-beb7-4b54-a0c0-168460ff41c0","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 67 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530198026,"playtime":0,"amount_spent":0.0,"created_at":1530198026,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"16421e68-fa8f-4072-82db-2d2d1765c317","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 68 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530198244,"playtime":0,"amount_spent":0.0,"created_at":1530198244,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"e818b09c-3eca-41be-b0ee-8379cab19d93","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 69 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530199447,"playtime":0,"amount_spent":0.0,"created_at":1530199447,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null}]}' 70 | http_version: 71 | recorded_at: Thu, 28 Jun 2018 15:30:58 GMT 72 | recorded_with: VCR 4.0.0 73 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/os-fetch-players.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://onesignal.com/api/v1/players?app_id 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Faraday v0.15.2 12 | Content-Type: 13 | - application/json 14 | Authorization: 15 | - Basic test 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | Accept: 19 | - "*/*" 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Date: 26 | - Thu, 28 Jun 2018 15:30:58 GMT 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Transfer-Encoding: 30 | - chunked 31 | Connection: 32 | - keep-alive 33 | Set-Cookie: 34 | - __cfduid=db617b0b45cf8c5a16bdbc61edef5a1a81530199858; expires=Fri, 28-Jun-19 35 | 15:30:58 GMT; path=/; domain=.onesignal.com; HttpOnly 36 | Status: 37 | - 200 OK 38 | Cache-Control: 39 | - max-age=0, private, must-revalidate 40 | Access-Control-Allow-Origin: 41 | - "*" 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Request-Id: 45 | - 1dbdac59-c104-4c1d-9e92-428badf0dacf 46 | Access-Control-Allow-Headers: 47 | - SDK-Version 48 | Etag: 49 | - W/"cee038b261c95e403233f1ada96658e3" 50 | X-Frame-Options: 51 | - SAMEORIGIN 52 | X-Runtime: 53 | - '0.051957' 54 | X-Content-Type-Options: 55 | - nosniff 56 | X-Powered-By: 57 | - Phusion Passenger 5.3.2 58 | Expect-Ct: 59 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 60 | Server: 61 | - cloudflare 62 | Cf-Ray: 63 | - 4321329b4893434c-MXP 64 | body: 65 | encoding: ASCII-8BIT 66 | string: '{"total_count":8,"offset":0,"limit":300,"players":[{"id":"14c8141f-d57d-4652-8911-960a00ff14fa","identifier":"52812f0ca9b3cc1c1f6a13fd1467c3a2a9f7ef3db5ee56cb951b00fb6bb78665","session_count":10,"language":"it","timezone":7200,"game_version":"1","device_os":"11.4","device_type":0,"device_model":"iPhone10,5","ad_id":"7A30EDB9-A4EA-404E-BA5E-2AEA86C74AA7","tags":{},"last_active":1530184068,"playtime":1219,"amount_spent":0.0,"created_at":1530177751,"invalid_identifier":false,"badge_count":0,"sdk":"020805","test_type":1,"ip":null},{"id":"16c0eee0-edc0-4630-8792-1c432a2e7da1","identifier":null,"session_count":2,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530192672,"playtime":0,"amount_spent":0.0,"created_at":1530192553,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"066e3db6-1ed1-49f0-9340-624327e3faf8","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530193737,"playtime":0,"amount_spent":0.0,"created_at":1530193737,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"cf435718-f137-4fd8-ba43-b9ada7e88846","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530194294,"playtime":0,"amount_spent":0.0,"created_at":1530194294,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"ba4b2ab3-cc89-4bd6-8c1a-9f69e7a2aa55","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530197017,"playtime":0,"amount_spent":0.0,"created_at":1530197017,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"152c5092-beb7-4b54-a0c0-168460ff41c0","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 67 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530198026,"playtime":0,"amount_spent":0.0,"created_at":1530198026,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"16421e68-fa8f-4072-82db-2d2d1765c317","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 68 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530198244,"playtime":0,"amount_spent":0.0,"created_at":1530198244,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"e818b09c-3eca-41be-b0ee-8379cab19d93","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 69 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530199447,"playtime":0,"amount_spent":0.0,"created_at":1530199447,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null}]}' 70 | http_version: 71 | recorded_at: Thu, 28 Jun 2018 15:30:58 GMT 72 | recorded_with: VCR 4.0.0 73 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/os-delete-player.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: https://onesignal.com/api/v1/players/?app_id 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Faraday v0.15.2 12 | Content-Type: 13 | - application/json 14 | Authorization: 15 | - Basic test 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | Accept: 19 | - "*/*" 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Date: 26 | - Thu, 28 Jun 2018 15:30:58 GMT 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Transfer-Encoding: 30 | - chunked 31 | Connection: 32 | - keep-alive 33 | Set-Cookie: 34 | - __cfduid=d162ae07cbc70fafa075f6d10a6694cc61530199858; expires=Fri, 28-Jun-19 35 | 15:30:58 GMT; path=/; domain=.onesignal.com; HttpOnly 36 | Status: 37 | - 200 OK 38 | Cache-Control: 39 | - max-age=0, private, must-revalidate 40 | Access-Control-Allow-Origin: 41 | - "*" 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Request-Id: 45 | - 768dd4da-9f24-4a93-8a1b-3efbc3bf22b2 46 | Access-Control-Allow-Headers: 47 | - SDK-Version 48 | Etag: 49 | - W/"cee038b261c95e403233f1ada96658e3" 50 | X-Frame-Options: 51 | - SAMEORIGIN 52 | X-Runtime: 53 | - '0.029141' 54 | X-Content-Type-Options: 55 | - nosniff 56 | X-Powered-By: 57 | - Phusion Passenger 5.3.2 58 | Expect-Ct: 59 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 60 | Server: 61 | - cloudflare 62 | Cf-Ray: 63 | - 4321329ccab1430a-MXP 64 | body: 65 | encoding: ASCII-8BIT 66 | string: '{"total_count":8,"offset":0,"limit":300,"players":[{"id":"14c8141f-d57d-4652-8911-960a00ff14fa","identifier":"52812f0ca9b3cc1c1f6a13fd1467c3a2a9f7ef3db5ee56cb951b00fb6bb78665","session_count":10,"language":"it","timezone":7200,"game_version":"1","device_os":"11.4","device_type":0,"device_model":"iPhone10,5","ad_id":"7A30EDB9-A4EA-404E-BA5E-2AEA86C74AA7","tags":{},"last_active":1530184068,"playtime":1219,"amount_spent":0.0,"created_at":1530177751,"invalid_identifier":false,"badge_count":0,"sdk":"020805","test_type":1,"ip":null},{"id":"16c0eee0-edc0-4630-8792-1c432a2e7da1","identifier":null,"session_count":2,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530192672,"playtime":0,"amount_spent":0.0,"created_at":1530192553,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"066e3db6-1ed1-49f0-9340-624327e3faf8","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530193737,"playtime":0,"amount_spent":0.0,"created_at":1530193737,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"cf435718-f137-4fd8-ba43-b9ada7e88846","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530194294,"playtime":0,"amount_spent":0.0,"created_at":1530194294,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"ba4b2ab3-cc89-4bd6-8c1a-9f69e7a2aa55","identifier":null,"session_count":1,"language":"en","timezone":7200,"game_version":"1","device_os":"7.1.2","device_type":1,"device_model":"MotoG3","ad_id":null,"tags":{},"last_active":1530197017,"playtime":0,"amount_spent":0.0,"created_at":1530197017,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"152c5092-beb7-4b54-a0c0-168460ff41c0","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 67 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530198026,"playtime":0,"amount_spent":0.0,"created_at":1530198026,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"16421e68-fa8f-4072-82db-2d2d1765c317","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 68 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530198244,"playtime":0,"amount_spent":0.0,"created_at":1530198244,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null},{"id":"e818b09c-3eca-41be-b0ee-8379cab19d93","identifier":null,"session_count":1,"language":"en","timezone":0,"game_version":"1","device_os":"8.1.0","device_type":1,"device_model":"Android 69 | SDK built for x86","ad_id":null,"tags":{},"last_active":1530199447,"playtime":0,"amount_spent":0.0,"created_at":1530199447,"invalid_identifier":true,"badge_count":0,"sdk":"030901","test_type":null,"ip":null}]}' 70 | http_version: 71 | recorded_at: Thu, 28 Jun 2018 15:30:58 GMT 72 | recorded_with: VCR 4.0.0 73 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/os-send-noti.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://onesignal.com/api/v1/notifications 6 | body: 7 | encoding: UTF-8 8 | string: '{"contents":{"en":"Live Test"},"headings":{"en":"This is a live test 9 | for OneSignal"},"included_segments":["Active Users"],"app_id":"22bc6dec-5150-4d6d-8628-377259d2dd14"}' 10 | headers: 11 | User-Agent: 12 | - Faraday v0.15.2 13 | Content-Type: 14 | - application/json 15 | Authorization: 16 | - Basic test 17 | Accept-Encoding: 18 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 19 | Accept: 20 | - "*/*" 21 | response: 22 | status: 23 | code: 200 24 | message: OK 25 | headers: 26 | Date: 27 | - Thu, 28 Jun 2018 15:30:57 GMT 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Transfer-Encoding: 31 | - chunked 32 | Connection: 33 | - keep-alive 34 | Set-Cookie: 35 | - __cfduid=daaa29327ffbbadef32c14c4f68e2ddcc1530199857; expires=Fri, 28-Jun-19 36 | 15:30:57 GMT; path=/; domain=.onesignal.com; HttpOnly 37 | Status: 38 | - 200 OK 39 | Cache-Control: 40 | - max-age=0, private, must-revalidate 41 | Access-Control-Allow-Origin: 42 | - "*" 43 | X-Xss-Protection: 44 | - 1; mode=block 45 | X-Request-Id: 46 | - 12e56155-1b2a-4ae2-9c43-959cf76d1219 47 | Access-Control-Allow-Headers: 48 | - SDK-Version 49 | Etag: 50 | - W/"dcc6500f0081d2c4c38f7f6490657d92" 51 | X-Frame-Options: 52 | - SAMEORIGIN 53 | X-Runtime: 54 | - '0.067198' 55 | X-Content-Type-Options: 56 | - nosniff 57 | X-Powered-By: 58 | - Phusion Passenger 5.3.2 59 | Expect-Ct: 60 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 61 | Server: 62 | - cloudflare 63 | Cf-Ray: 64 | - 432132947a73430a-MXP 65 | body: 66 | encoding: ASCII-8BIT 67 | string: '{"id":"29e9c2ad-41da-46f0-94c3-e71ee5c103dc","recipients":1}' 68 | http_version: 69 | recorded_at: Thu, 28 Jun 2018 15:30:57 GMT 70 | - request: 71 | method: get 72 | uri: https://onesignal.com/api/v1/notifications/29e9c2ad-41da-46f0-94c3-e71ee5c103dc?app_id 73 | body: 74 | encoding: US-ASCII 75 | string: '' 76 | headers: 77 | User-Agent: 78 | - Faraday v0.15.2 79 | Content-Type: 80 | - application/json 81 | Authorization: 82 | - Basic NTc3Y2RhMTAtMjQzZC00NDIxLWE3MWUtNWU0OWM3YTVhZTlh 83 | Accept-Encoding: 84 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 85 | Accept: 86 | - "*/*" 87 | response: 88 | status: 89 | code: 200 90 | message: OK 91 | headers: 92 | Date: 93 | - Thu, 28 Jun 2018 15:30:57 GMT 94 | Content-Type: 95 | - application/json; charset=utf-8 96 | Transfer-Encoding: 97 | - chunked 98 | Connection: 99 | - keep-alive 100 | Set-Cookie: 101 | - __cfduid=daaa29327ffbbadef32c14c4f68e2ddcc1530199857; expires=Fri, 28-Jun-19 102 | 15:30:57 GMT; path=/; domain=.onesignal.com; HttpOnly 103 | Status: 104 | - 200 OK 105 | Cache-Control: 106 | - public, max-age=7200 107 | Access-Control-Allow-Origin: 108 | - "*" 109 | X-Xss-Protection: 110 | - 1; mode=block 111 | X-Request-Id: 112 | - 713da936-b783-4ff4-aae6-4ec89a5e9ef7 113 | Access-Control-Allow-Headers: 114 | - SDK-Version 115 | Etag: 116 | - W/"8e772220e2df249cd3a196949cc81bfa" 117 | X-Frame-Options: 118 | - SAMEORIGIN 119 | X-Runtime: 120 | - '0.016556' 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Powered-By: 124 | - Phusion Passenger 5.3.2 125 | Cf-Cache-Status: 126 | - MISS 127 | Vary: 128 | - Accept-Encoding 129 | Expires: 130 | - Thu, 28 Jun 2018 17:30:57 GMT 131 | Expect-Ct: 132 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 133 | Server: 134 | - cloudflare 135 | Cf-Ray: 136 | - 432132961c5b430a-MXP 137 | body: 138 | encoding: ASCII-8BIT 139 | string: '{"adm_big_picture":null,"adm_group":null,"adm_group_message":null,"adm_large_icon":null,"adm_small_icon":null,"adm_sound":null,"spoken_text":null,"alexa_ssml":null,"alexa_display_title":null,"amazon_background_data":null,"android_accent_color":null,"android_group":null,"android_group_message":null,"android_led_color":null,"android_sound":null,"android_visibility":null,"app_id":"22bc6dec-5150-4d6d-8628-377259d2dd14","big_picture":null,"buttons":null,"canceled":false,"chrome_big_picture":null,"chrome_icon":null,"chrome_web_icon":null,"chrome_web_image":null,"chrome_web_badge":null,"content_available":null,"contents":{"en":"Live 140 | Test"},"converted":0,"data":null,"delayed_option":null,"delivery_time_of_day":null,"errored":0,"excluded_segments":[],"failed":0,"firefox_icon":null,"headings":{"en":"This 141 | is a live test for OneSignal"},"id":"29e9c2ad-41da-46f0-94c3-e71ee5c103dc","include_player_ids":null,"included_segments":["Active 142 | Users"],"ios_badgeCount":null,"ios_badgeType":null,"ios_category":null,"ios_sound":null,"apns_alert":null,"isAdm":false,"isAndroid":true,"isChrome":false,"isChromeWeb":false,"isAlexa":false,"isFirefox":false,"isIos":true,"isSafari":false,"isWP":false,"isWP_WNS":false,"isEdge":false,"large_icon":null,"priority":null,"queued_at":1530199857,"remaining":1,"send_after":1530199857,"completed_at":null,"small_icon":null,"successful":0,"tags":null,"filters":null,"template_id":null,"ttl":null,"url":null,"web_buttons":null,"web_push_topic":null,"wp_sound":null,"wp_wns_sound":null}' 143 | http_version: 144 | recorded_at: Thu, 28 Jun 2018 15:30:57 GMT 145 | recorded_with: VCR 4.0.0 146 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | # Cop names are not displayed in offense messages by default. Change behavior 3 | # by overriding DisplayCopNames, or by giving the `-D/--display-cop-names` 4 | # option. 5 | DisplayCopNames: true 6 | 7 | # Style guide URLs are not displayed in offense messages by default. Change 8 | # behavior by overriding `DisplayStyleGuide`, or by giving the 9 | # `-S/--display-style-guide` option. 10 | DisplayStyleGuide: true 11 | 12 | # Extra details are not displayed in offense messages by default. Change 13 | # behavior by overriding ExtraDetails, or by giving the 14 | # `-E/--extra-details` option. 15 | ExtraDetails: true 16 | 17 | # What MRI version of the Ruby interpreter is the inspected code intended to 18 | # run on? (If there is more than one, set this to the lowest version.) 19 | # If a value is specified for TargetRubyVersion then it is used. 20 | # Else if .ruby-version exists and it contains an MRI version it is used. 21 | # Otherwise we fallback to the oldest officially supported Ruby version (2.1). 22 | TargetRubyVersion: 2.4 23 | 24 | # Align the elements of a hash literal if they span more than one line. 25 | Layout/AlignHash: 26 | # Alignment of entries using hash rocket as separator. Valid values are: 27 | # 28 | # key - left alignment of keys 29 | # 'a' => 2 30 | # 'bb' => 3 31 | # separator - alignment of hash rockets, keys are right aligned 32 | # 'a' => 2 33 | # 'bb' => 3 34 | # table - left alignment of keys, hash rockets, and values 35 | # 'a' => 2 36 | # 'bb' => 3 37 | EnforcedHashRocketStyle: table 38 | SupportedHashRocketStyles: 39 | - key 40 | - separator 41 | - table 42 | # Alignment of entries using colon as separator. Valid values are: 43 | # 44 | # key - left alignment of keys 45 | # a: 0 46 | # bb: 1 47 | # separator - alignment of colons, keys are right aligned 48 | # a: 0 49 | # bb: 1 50 | # table - left alignment of keys and values 51 | # a: 0 52 | # bb: 1 53 | EnforcedColonStyle: table 54 | SupportedColonStyles: 55 | - key 56 | - separator 57 | - table 58 | # Select whether hashes that are the last argument in a method call should be 59 | # inspected? Valid values are: 60 | # 61 | # always_inspect - Inspect both implicit and explicit hashes. 62 | # Registers an offense for: 63 | # function(a: 1, 64 | # b: 2) 65 | # Registers an offense for: 66 | # function({a: 1, 67 | # b: 2}) 68 | # always_ignore - Ignore both implicit and explicit hashes. 69 | # Accepts: 70 | # function(a: 1, 71 | # b: 2) 72 | # Accepts: 73 | # function({a: 1, 74 | # b: 2}) 75 | # ignore_implicit - Ignore only implicit hashes. 76 | # Accepts: 77 | # function(a: 1, 78 | # b: 2) 79 | # Registers an offense for: 80 | # function({a: 1, 81 | # b: 2}) 82 | # ignore_explicit - Ignore only explicit hashes. 83 | # Accepts: 84 | # function({a: 1, 85 | # b: 2}) 86 | # Registers an offense for: 87 | # function(a: 1, 88 | # b: 2) 89 | EnforcedLastArgumentHashStyle: ignore_implicit 90 | SupportedLastArgumentHashStyles: 91 | - always_inspect 92 | - always_ignore 93 | - ignore_implicit 94 | - ignore_explicit 95 | 96 | Layout/EndOfLine: 97 | # The `native` style means that CR+LF (Carriage Return + Line Feed) is 98 | # enforced on Windows, and LF is enforced on other platforms. The other styles 99 | # mean LF and CR+LF, respectively. 100 | EnforcedStyle: lf 101 | SupportedStyles: 102 | - native 103 | - lf 104 | - crlf 105 | 106 | Style/BlockDelimiters: 107 | EnforcedStyle: braces_for_chaining 108 | SupportedStyles: 109 | # The `line_count_based` style enforces braces around single line blocks and 110 | # do..end around multi-line blocks. 111 | - line_count_based 112 | # The `semantic` style enforces braces around functional blocks, where the 113 | # primary purpose of the block is to return a value and do..end for 114 | # procedural blocks, where the primary purpose of the block is its 115 | # side-effects. 116 | # 117 | # This looks at the usage of a block's method to determine its type (e.g. is 118 | # the result of a `map` assigned to a variable or passed to another 119 | # method) but exceptions are permitted in the `ProceduralMethods`, 120 | # `FunctionalMethods` and `IgnoredMethods` sections below. 121 | - semantic 122 | # The `braces_for_chaining` style enforces braces around single line blocks 123 | # and do..end around multi-line blocks, except for multi-line blocks whose 124 | # return value is being chained with another method (in which case braces 125 | # are enforced). 126 | - braces_for_chaining 127 | 128 | Style/Documentation: 129 | Enabled: false 130 | 131 | Style/MethodDefParentheses: 132 | EnforcedStyle: require_no_parentheses 133 | SupportedStyles: 134 | - require_parentheses 135 | - require_no_parentheses 136 | - require_no_parentheses_except_multiline 137 | 138 | Style/RedundantReturn: 139 | # When `true` allows code like `return x, y`. 140 | AllowMultipleReturnValues: true 141 | 142 | Style/Semicolon: 143 | # Allow `;` to separate several expressions on the same line. 144 | AllowAsExpressionSeparator: true 145 | 146 | Metrics/BlockLength: 147 | Exclude: 148 | - "**/*_spec.rb" 149 | 150 | Metrics/LineLength: 151 | Max: 100 152 | # To make it possible to copy or click on URIs in the code, we allow lines 153 | # containing a URI to be longer than Max. 154 | AllowHeredoc: true 155 | AllowURI: true 156 | URISchemes: 157 | - http 158 | - https 159 | - ftp 160 | # The IgnoreCopDirectives option causes the LineLength rule to ignore cop 161 | # directives like '# rubocop: enable ...' when calculating a line's length. 162 | IgnoreCopDirectives: false 163 | # The IgnoredPatterns option is a list of !ruby/regexp and/or string 164 | # elements. Strings will be converted to Regexp objects. A line that matches 165 | # any regular expression listed in this option will be ignored by LineLength. 166 | IgnoredPatterns: [] 167 | 168 | # Align ends correctly. 169 | Lint/EndAlignment: 170 | # The value `keyword` means that `end` should be aligned with the matching 171 | # keyword (`if`, `while`, etc.). 172 | # The value `variable` means that in assignments, `end` should be aligned 173 | # with the start of the variable on the left hand side of `=`. In all other 174 | # situations, `end` should still be aligned with the keyword. 175 | # The value `start_of_line` means that `end` should be aligned with the start 176 | # of the line which the matching keyword appears on. 177 | EnforcedStyleAlignWith: variable 178 | SupportedStylesAlignWith: 179 | - keyword 180 | - variable 181 | - start_of_line 182 | AutoCorrect: false 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OneSignal Ruby Client 2 | [![Gem Version](https://badge.fury.io/rb/onesignal-ruby.svg)](https://badge.fury.io/rb/onesignal-ruby) 3 | [![CircleCI](https://circleci.com/gh/mikamai/onesignal-ruby.svg?style=svg)](https://circleci.com/gh/mikamai/onesignal-ruby) 4 | 5 | A simple, pure Ruby client to the [OneSignal Push Notification API](https://onesignal.com/). OneSignal provides a self-serve customer engagement solution for Push Notifications, Email, SMS & In-App. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'onesignal-ruby' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install onesignal-ruby 22 | 23 | ## Configuration 24 | OneSignal requires an App ID and an API Key, which can be found 25 | on the OneSignal dashboard. 26 | By default, OneSignal Ruby looks for them in the environment, loading 27 | `ONESIGNAL_APP_ID` and `ONESIGNAL_API_KEY` variables. 28 | 29 | It also defaults to `https://onesignal.com/api/v1` as the API URL. 30 | 31 | You can also turn off OneSignal entirely with a boolean flag (for example to avoid sending 32 | notification while in test or development environments) 33 | 34 | It will also use an internal instance of the Ruby Logger at `INFO` level. 35 | 36 | To customize those values, call the following snippet during your 37 | initialization phase. 38 | 39 | ```ruby 40 | require 'onesignal' 41 | 42 | OneSignal.configure do |config| 43 | config.app_id = 'my_app_id' 44 | config.api_key = 'my_api_key' 45 | config.api_url = 'http://my_api_url' 46 | config.active = false 47 | config.logger = Logger.new # Any Logger compliant implementation 48 | end 49 | ``` 50 | ## Usage 51 | 52 | ### Create a notification 53 | 54 | Create a `Notification` object. 55 | ```ruby 56 | # Create headings for different languages. English is required. 57 | headings = OneSignal::Notification::Headings.new(en: 'Hello!', it: 'Ciao!') 58 | 59 | # Create contents for different languages. English is required. 60 | contents = OneSignal::Notification::Contents.new(en: "I'm a notification!", it: 'Sono una notifica!') 61 | 62 | # Select the included (and/or excluded) segments to target 63 | included_segments = [OneSignal::Segment::ACTIVE_USERS, 'My custom segment'] 64 | 65 | # Create the Notification object 66 | notification = OneSignal::Notification.new(headings: headings, contents: contents, included_segments: included_segments) 67 | ``` 68 | 69 | Then send it. 70 | ```ruby 71 | response = OneSignal.send_notification(notification) 72 | # => # the created notification 73 | ``` 74 | 75 | ### Fetch a notification 76 | You can fetch an existing notification given its ID. 77 | ```ruby 78 | response = OneSignal.fetch_notification(notification_id) 79 | # => # the created notification 80 | ``` 81 | `OneSignal::Responses::Notification` has the following fields. 82 | ```ruby 83 | id # Notification UUID 84 | successful # Number of successful deliveries 85 | failed # Number of failed deliveries 86 | converted # Number of users who have clicked / tapped on your notification. 87 | remaining # Number of notifications that have not been sent out yet 88 | queued_at # Unix timestamp of enqueuing time 89 | send_after # Unix timestamp indicating when notification delivery should begin 90 | completed_at # Unix timestamp indicating when notification delivery completed. 91 | url # URL associated with the notification 92 | data # Custom metadata 93 | canceled # Boolean, has the notification been canceled 94 | headings # Map of locales to title strings 95 | contents # Map of locales to content strings 96 | 97 | response.id # => fe82c1ae-54c2-458b-8aad-7edc3e8a96c4 98 | ``` 99 | 100 | ### Attachments 101 | You can add files, data or images to a notification, or an external URL to open. 102 | ```ruby 103 | attachments = OneSignal::Attachments.new( 104 | data: { 'test' => 'test' }, 105 | url: 'http://example.com', 106 | ios_attachments: { 'something' => 'drawable resource name or URL.' }, 107 | android_picture: 'drawable resource name or URL.', 108 | amazon_picture: 'drawable resource name or URL.', 109 | chrome_picture: 'drawable resource name or URL.' 110 | ) 111 | 112 | OneSignal::Notification.new(attachments: attachments) 113 | ``` 114 | 115 | ### Buttons 116 | You can add interactive buttons to a notification. See https://documentation.onesignal.com/docs/action-buttons for more details. 117 | 118 | ```ruby 119 | buttons = OneSignal::Buttons.new( 120 | buttons: [{id: 'option_a', text: 'Option A' }, {id: 'option_b', text: 'Option B' }] 121 | ) 122 | 123 | OneSignal::Notification.new(buttons: buttons) 124 | ``` 125 | 126 | ### Fetch players 127 | You can fetch all players and devices with a simple method. 128 | 129 | ```ruby 130 | players = OneSignal.fetch_players 131 | # => Array of OneSignal::Responses::Player 132 | ``` 133 | 134 | Or you can fetch a single player by its ID. 135 | ```ruby 136 | player = OneSignal.fetch_player(player_id) 137 | # => # 138 | ``` 139 | 140 | ### Delete players 141 | You can delete a single player by its ID. 142 | ```ruby 143 | OneSignal.delete_player(player_id) 144 | # 145 | ``` 146 | 147 | ### Filters 148 | 149 | Filters can be created with a simple DSL. It closely matches the [JSON reference](https://documentation.onesignal.com/reference#section-send-to-users-based-on-filters), with a few touches of syntax 150 | sugar. 151 | 152 | **Example** 153 | ```ruby 154 | filters = [ 155 | OneSignal::Filter.last_session.lesser_than(2).hours_ago!, 156 | OneSignal::Filter.session_count.equals(5), 157 | OneSignal::Filter::OR, 158 | OneSignal::Filter.country.equals('IT') 159 | ] 160 | 161 | OneSignal::Notification.new(filters: filters) 162 | ``` 163 | Becomes 164 | ```json 165 | [ 166 | {"field":"last_session","relation":"<","hours_ago":"2"}, 167 | {"field":"session_count","relation":"=","value":"5"}, 168 | {"operator":"OR"}, 169 | {"field":"country","relation":"=","value":"IT"} 170 | ] 171 | ``` 172 | 173 | The operator methods (`#lesser_than`, `#greater_than`, `#equals`, `#not_equals`) are also available through the following shorthands: `<`, `>`, `=`, `!=`. 174 | 175 | **Example** 176 | ```ruby 177 | filters = [ 178 | OneSignal::Filter.tag('userId') == 5, 179 | OneSignal::Filter.session_count < 2, 180 | OneSignal::Filter.language != 'en' 181 | ] 182 | 183 | OneSignal::Notification.new(filters: filters) 184 | ``` 185 | 186 | ### Custom Sounds 187 | You can customize notification sounds by passing a `OneSignal::Sounds` object. 188 | ```ruby 189 | sounds = OneSignal::Sounds.new(ios: 'ping.wav', android: 'ping') 190 | OneSignal::Notification.new(sounds: sounds) 191 | ``` 192 | 193 | ### Specific Targets 194 | If you want to send a notification only to specific targets (a particular user's email or device) you can 195 | pass a `OneSignal::IncludedTargets` to the notification object. 196 | See [the official documentation](https://documentation.onesignal.com/reference#section-send-to-specific-devices) for a list of available params. 197 | ```ruby 198 | included_targets = OneSignal::IncludedTargets.new(include_player_ids: ['test-id-12345']) 199 | OneSignal::Notification.new(included_targets: included_targets) 200 | ``` 201 | 202 | ### Icons 203 | You can customize notification icons by passing a `OneSignal::Icons` object. 204 | ```ruby 205 | icons = OneSignal::Icons.new( 206 | small_icon: 'image URL', 207 | huawei_small_icon: 'image URL', 208 | large_icon: 'image URL', 209 | huawei_large_icon: 'image URL', 210 | adm_small_icon: 'image URL', 211 | adm_large_icon: 'image URL', 212 | chrome_web_icon: 'image URL', 213 | firefox_icon: 'image URL', 214 | chrome_icon: 'image URL' 215 | ) 216 | OneSignal::Notification.new(icons: icons) 217 | ``` 218 | 219 | **WARNING** 220 | Passing `include_player_ids` alongside other params is prohibited and will raise an `ArgumentError`. 221 | Either use `include_player_ids` or use the other params. 222 | 223 | ## Development 224 | 225 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 226 | 227 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 228 | 229 | ## Contributing 230 | 231 | Bug reports and pull requests are welcome on GitHub at https://github.com/mikamai/onesignal-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 232 | 233 | This repo is managed following the [Git Flow](https://danielkummer.github.io/git-flow-cheatsheet/) principles. 234 | - `master` is the stable, production-ready branch. Never work directly on it. The gem is published from this branch. 235 | - `develop` is the active development branch. It is supposed to be somewhat stable. Every new feature is merged here once completed, before being released to master. 236 | - `feature/my-awesome-branch` are personal, dedicated branches for working on actual features. They are merged in develop once completed and then deleted. 237 | - `hotfix/my-awesome-fix` are special branches dedicated to bugfixes that compromise the library functionality. They are merged 238 | in both master and develop and then deleted. 239 | 240 | [CHANGELOG](CHANGELOG.md) entries MUST be added for every change made to the source code. 241 | 242 | ## License 243 | 244 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 245 | 246 | ## Code of Conduct 247 | 248 | Everyone interacting in the OneSignal Ruby project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md). 249 | -------------------------------------------------------------------------------- /spec/onesignal/filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | include OneSignal 5 | 6 | describe Filter do 7 | context 'builder' do 8 | it 'builds a last session filter with lesser_than' do 9 | filter = described_class.last_session.lesser_than(1.1).hours_ago! 10 | expect(filter).to be_instance_of described_class 11 | expect(filter.field).to eq 'last_session' 12 | expect(filter.relation).to eq '<' 13 | expect(filter.hours_ago).to eq '1.1' 14 | end 15 | 16 | it 'builds a last session filter with greater_than' do 17 | filter = described_class.last_session.greater_than(1.1).hours_ago! 18 | expect(filter).to be_instance_of described_class 19 | expect(filter.field).to eq 'last_session' 20 | expect(filter.relation).to eq '>' 21 | expect(filter.hours_ago).to eq '1.1' 22 | end 23 | 24 | it 'builds a first session filter with lesser_than' do 25 | filter = described_class.first_session.lesser_than(1.1).hours_ago! 26 | expect(filter).to be_instance_of described_class 27 | expect(filter.field).to eq 'first_session' 28 | expect(filter.relation).to eq '<' 29 | expect(filter.hours_ago).to eq '1.1' 30 | end 31 | 32 | it 'builds a first session filter with greater_than' do 33 | filter = described_class.first_session.greater_than(1.1).hours_ago! 34 | expect(filter).to be_instance_of described_class 35 | expect(filter.field).to eq 'first_session' 36 | expect(filter.relation).to eq '>' 37 | expect(filter.hours_ago).to eq '1.1' 38 | end 39 | 40 | it 'builds a session count filter with lesser_than' do 41 | filter = described_class.session_count.lesser_than(1) 42 | expect(filter).to be_instance_of described_class 43 | expect(filter.field).to eq 'session_count' 44 | expect(filter.relation).to eq '<' 45 | expect(filter.value).to eq '1' 46 | end 47 | 48 | it 'builds a session count filter with greater_than' do 49 | filter = described_class.session_count.greater_than(1) 50 | expect(filter).to be_instance_of described_class 51 | expect(filter.field).to eq 'session_count' 52 | expect(filter.relation).to eq '>' 53 | expect(filter.value).to eq '1' 54 | end 55 | 56 | it 'builds a session count filter with equals' do 57 | filter = described_class.session_count.equals(1) 58 | expect(filter).to be_instance_of described_class 59 | expect(filter.field).to eq 'session_count' 60 | expect(filter.relation).to eq '=' 61 | expect(filter.value).to eq '1' 62 | end 63 | 64 | it 'builds a session count filter with not_equals' do 65 | filter = described_class.session_count.not_equals(1) 66 | expect(filter).to be_instance_of described_class 67 | expect(filter.field).to eq 'session_count' 68 | expect(filter.relation).to eq '!=' 69 | expect(filter.value).to eq '1' 70 | end 71 | 72 | it 'builds a session time filter with lesser_than' do 73 | filter = described_class.session_time.lesser_than(1600) 74 | expect(filter).to be_instance_of described_class 75 | expect(filter.field).to eq 'session_time' 76 | expect(filter.relation).to eq '<' 77 | expect(filter.value).to eq '1600' 78 | end 79 | 80 | it 'builds a session time filter with greater_than' do 81 | filter = described_class.session_time.greater_than(1600) 82 | expect(filter).to be_instance_of described_class 83 | expect(filter.field).to eq 'session_time' 84 | expect(filter.relation).to eq '>' 85 | expect(filter.value).to eq '1600' 86 | end 87 | 88 | it 'builds an amount spent filter with lesser_than' do 89 | filter = described_class.amount_spent.lesser_than(1600) 90 | expect(filter).to be_instance_of described_class 91 | expect(filter.field).to eq 'amount_spent' 92 | expect(filter.relation).to eq '<' 93 | expect(filter.value).to eq '1600' 94 | end 95 | 96 | it 'builds an amount spent filter with greater_than' do 97 | filter = described_class.amount_spent.greater_than(1600) 98 | expect(filter).to be_instance_of described_class 99 | expect(filter.field).to eq 'amount_spent' 100 | expect(filter.relation).to eq '>' 101 | expect(filter.value).to eq '1600' 102 | end 103 | 104 | it 'builds an amount spent filter with equals' do 105 | filter = described_class.amount_spent.equals(1600) 106 | expect(filter).to be_instance_of described_class 107 | expect(filter.field).to eq 'amount_spent' 108 | expect(filter.relation).to eq '=' 109 | expect(filter.value).to eq '1600' 110 | end 111 | 112 | it 'builds a bought sku filter with lesser_than' do 113 | filter = described_class.bought_sku('test').lesser_than(0.99) 114 | expect(filter).to be_instance_of described_class 115 | expect(filter.field).to eq 'bought_sku' 116 | expect(filter.key).to eq 'test' 117 | expect(filter.relation).to eq '<' 118 | expect(filter.value).to eq '0.99' 119 | end 120 | 121 | it 'builds a bought sku filter with greater_than' do 122 | filter = described_class.bought_sku('test').greater_than(0.99) 123 | expect(filter).to be_instance_of described_class 124 | expect(filter.field).to eq 'bought_sku' 125 | expect(filter.key).to eq 'test' 126 | expect(filter.relation).to eq '>' 127 | expect(filter.value).to eq '0.99' 128 | end 129 | 130 | it 'builds a bought sku filter with equals' do 131 | filter = described_class.bought_sku('test').equals(0.99) 132 | expect(filter).to be_instance_of described_class 133 | expect(filter.field).to eq 'bought_sku' 134 | expect(filter.key).to eq 'test' 135 | expect(filter.relation).to eq '=' 136 | expect(filter.value).to eq '0.99' 137 | end 138 | 139 | it 'builds a tag filter with lesser_than' do 140 | filter = described_class.tag('test').lesser_than('t') 141 | expect(filter).to be_instance_of described_class 142 | expect(filter.field).to eq 'tag' 143 | expect(filter.key).to eq 'test' 144 | expect(filter.relation).to eq '<' 145 | expect(filter.value).to eq 't' 146 | end 147 | 148 | it 'builds a tag filter with greater_than' do 149 | filter = described_class.tag('test').greater_than('t') 150 | expect(filter).to be_instance_of described_class 151 | expect(filter.field).to eq 'tag' 152 | expect(filter.key).to eq 'test' 153 | expect(filter.relation).to eq '>' 154 | expect(filter.value).to eq 't' 155 | end 156 | 157 | it 'builds a tag filter with equals' do 158 | filter = described_class.tag('test').equals('t') 159 | expect(filter).to be_instance_of described_class 160 | expect(filter.field).to eq 'tag' 161 | expect(filter.key).to eq 'test' 162 | expect(filter.relation).to eq '=' 163 | expect(filter.value).to eq 't' 164 | end 165 | 166 | it 'builds a tag filter with not_equals' do 167 | filter = described_class.tag('test').not_equals('t') 168 | expect(filter).to be_instance_of described_class 169 | expect(filter.field).to eq 'tag' 170 | expect(filter.key).to eq 'test' 171 | expect(filter.relation).to eq '!=' 172 | expect(filter.value).to eq 't' 173 | end 174 | 175 | it 'builds a tag filter with exists' do 176 | filter = described_class.tag('test').exists 177 | expect(filter).to be_instance_of described_class 178 | expect(filter.field).to eq 'tag' 179 | expect(filter.key).to eq 'test' 180 | expect(filter.relation).to eq 'exists' 181 | end 182 | 183 | it 'builds a tag filter with not_exists' do 184 | filter = described_class.tag('test').not_exists 185 | expect(filter).to be_instance_of described_class 186 | expect(filter.field).to eq 'tag' 187 | expect(filter.key).to eq 'test' 188 | expect(filter.relation).to eq 'not_exists' 189 | end 190 | 191 | it 'builds a language filter with equals' do 192 | filter = described_class.language.equals(:en) 193 | expect(filter).to be_instance_of described_class 194 | expect(filter.field).to eq 'language' 195 | expect(filter.relation).to eq '=' 196 | expect(filter.value).to eq 'en' 197 | end 198 | 199 | it 'builds a language filter with not_equals' do 200 | filter = described_class.language.not_equals(:en) 201 | expect(filter).to be_instance_of described_class 202 | expect(filter.field).to eq 'language' 203 | expect(filter.relation).to eq '!=' 204 | expect(filter.value).to eq 'en' 205 | end 206 | 207 | it 'builds an app version filter with lesser_than' do 208 | filter = described_class.app_version.lesser_than('t') 209 | expect(filter).to be_instance_of described_class 210 | expect(filter.field).to eq 'app_version' 211 | expect(filter.relation).to eq '<' 212 | expect(filter.value).to eq 't' 213 | end 214 | 215 | it 'builds an app version filter with greater_than' do 216 | filter = described_class.app_version.greater_than('t') 217 | expect(filter).to be_instance_of described_class 218 | expect(filter.field).to eq 'app_version' 219 | expect(filter.relation).to eq '>' 220 | expect(filter.value).to eq 't' 221 | end 222 | 223 | it 'builds an app version filter with equals' do 224 | filter = described_class.app_version.equals('t') 225 | expect(filter).to be_instance_of described_class 226 | expect(filter.field).to eq 'app_version' 227 | expect(filter.relation).to eq '=' 228 | expect(filter.value).to eq 't' 229 | end 230 | 231 | it 'builds an app version filter with not_equals' do 232 | filter = described_class.app_version.not_equals('t') 233 | expect(filter).to be_instance_of described_class 234 | expect(filter.field).to eq 'app_version' 235 | expect(filter.relation).to eq '!=' 236 | expect(filter.value).to eq 't' 237 | end 238 | 239 | it 'builds an location filter' do 240 | filter = described_class.location(radius: 15, lat: '1243434', long: '12314325') 241 | expect(filter).to be_instance_of described_class 242 | expect(filter.field).to eq 'location' 243 | expect(filter.location.radius).to eq 15 244 | expect(filter.location.latitude).to eq '1243434' 245 | expect(filter.location.longitude).to eq '12314325' 246 | end 247 | 248 | it 'builds an email filter' do 249 | filter = described_class.email('test@example.com') 250 | expect(filter).to be_instance_of described_class 251 | expect(filter.field).to eq 'email' 252 | expect(filter.value).to eq 'test@example.com' 253 | end 254 | 255 | it 'builds a country filter' do 256 | filter = described_class.country.equals('US') 257 | expect(filter).to be_instance_of described_class 258 | expect(filter.field).to eq 'country' 259 | expect(filter.relation).to eq '=' 260 | expect(filter.value).to eq 'US' 261 | end 262 | end 263 | 264 | context 'validations' do 265 | skip('TODO') # TODO: implement validations 266 | end 267 | 268 | context 'json' do 269 | it 'serializes OR operator correctly' do 270 | expect(described_class::OR.to_json).to eq '{"operator":"OR"}' 271 | end 272 | 273 | it 'serializes a chain of filters correctly' do 274 | filters = [described_class.last_session.lesser_than(2).hours_ago!, 275 | described_class.session_count.equals(5), 276 | described_class::OR, 277 | described_class.country.equals('IT')] 278 | json = '[{"field":"last_session","relation":"<","hours_ago":"2"},'\ 279 | '{"field":"session_count","relation":"=","value":"5"},{"operator":"OR"},'\ 280 | '{"field":"country","relation":"=","value":"IT"}]' 281 | 282 | expect(filters.to_json).to eq json 283 | end 284 | end 285 | 286 | context Filter::FilterBuilder do 287 | subject { described_class.new 'test' } 288 | context 'aliases' do 289 | it 'has all builder method aliased' do 290 | [ 291 | %i[lesser_than <], 292 | %i[greater_than >], 293 | %i[equals ==], 294 | %i[not_equals !=] 295 | ].each do |method_name| 296 | name, al = method_name 297 | expect(subject.method(name)).to eq subject.method(al) 298 | end 299 | end 300 | end 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/os-fetch-noti.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://onesignal.com/api/v1/notifications/?app_id 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Faraday v0.15.2 12 | Content-Type: 13 | - application/json 14 | Authorization: 15 | - Basic test 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | Accept: 19 | - "*/*" 20 | response: 21 | status: 22 | code: 200 23 | message: OK 24 | headers: 25 | Date: 26 | - Thu, 28 Jun 2018 15:30:58 GMT 27 | Content-Type: 28 | - application/json; charset=utf-8 29 | Transfer-Encoding: 30 | - chunked 31 | Connection: 32 | - keep-alive 33 | Set-Cookie: 34 | - __cfduid=df0001a10af0412855ad6ed0276b466461530199857; expires=Fri, 28-Jun-19 35 | 15:30:57 GMT; path=/; domain=.onesignal.com; HttpOnly 36 | Status: 37 | - 200 OK 38 | Cache-Control: 39 | - public, max-age=7200 40 | Access-Control-Allow-Origin: 41 | - "*" 42 | X-Xss-Protection: 43 | - 1; mode=block 44 | X-Request-Id: 45 | - 5fc751d2-1bc0-46c7-b33f-5579f956f7d2 46 | Access-Control-Allow-Headers: 47 | - SDK-Version 48 | Etag: 49 | - W/"ff3055c61c4d74053c0069ab107cbe35" 50 | X-Frame-Options: 51 | - SAMEORIGIN 52 | X-Runtime: 53 | - '0.257959' 54 | X-Content-Type-Options: 55 | - nosniff 56 | X-Powered-By: 57 | - Phusion Passenger 5.3.2 58 | Cf-Cache-Status: 59 | - EXPIRED 60 | Vary: 61 | - Accept-Encoding 62 | Expires: 63 | - Thu, 28 Jun 2018 17:30:58 GMT 64 | Expect-Ct: 65 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 66 | Server: 67 | - cloudflare 68 | Cf-Ray: 69 | - 432132975a284334-MXP 70 | body: 71 | encoding: ASCII-8BIT 72 | string: !binary |- 73 | eyJ0b3RhbF9jb3VudCI6MjYsIm9mZnNldCI6MCwibGltaXQiOjUwLCJub3RpZmljYXRpb25zIjpbeyJhZG1fYmlnX3BpY3R1cmUiOm51bGwsImFkbV9ncm91cCI6bnVsbCwiYWRtX2dyb3VwX21lc3NhZ2UiOm51bGwsImFkbV9sYXJnZV9pY29uIjpudWxsLCJhZG1fc21hbGxfaWNvbiI6bnVsbCwiYWRtX3NvdW5kIjpudWxsLCJzcG9rZW5fdGV4dCI6bnVsbCwiYWxleGFfc3NtbCI6bnVsbCwiYWxleGFfZGlzcGxheV90aXRsZSI6bnVsbCwiYW1hem9uX2JhY2tncm91bmRfZGF0YSI6bnVsbCwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOm51bGwsImFuZHJvaWRfZ3JvdXAiOm51bGwsImFuZHJvaWRfZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYW5kcm9pZF9sZWRfY29sb3IiOm51bGwsImFuZHJvaWRfc291bmQiOm51bGwsImFuZHJvaWRfdmlzaWJpbGl0eSI6bnVsbCwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOm51bGwsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjpudWxsLCJjaHJvbWVfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ltYWdlIjpudWxsLCJjaHJvbWVfd2ViX2JhZGdlIjpudWxsLCJjb250ZW50X2F2YWlsYWJsZSI6bnVsbCwiY29udGVudHMiOnsiZW4iOiJMaXZlIFRlc3QifSwiY29udmVydGVkIjowLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6bnVsbCwiZGVsaXZlcnlfdGltZV9vZl9kYXkiOm51bGwsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOm51bGwsImhlYWRpbmdzIjp7ImVuIjoiVGhpcyBpcyBhIGxpdmUgdGVzdCBmb3IgT25lU2lnbmFsIn0sImlkIjoiMjllOWMyYWQtNDFkYS00NmYwLTk0YzMtZTcxZWU1YzEwM2RjIiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBY3RpdmUgVXNlcnMiXSwiaW9zX2JhZGdlQ291bnQiOm51bGwsImlvc19iYWRnZVR5cGUiOm51bGwsImlvc19jYXRlZ29yeSI6bnVsbCwiaW9zX3NvdW5kIjpudWxsLCJhcG5zX2FsZXJ0IjpudWxsLCJpc0FkbSI6ZmFsc2UsImlzQW5kcm9pZCI6dHJ1ZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjpudWxsLCJwcmlvcml0eSI6bnVsbCwicXVldWVkX2F0IjoxNTMwMTk5ODU3LCJyZW1haW5pbmciOjEsInNlbmRfYWZ0ZXIiOjE1MzAxOTk4NTcsImNvbXBsZXRlZF9hdCI6bnVsbCwic21hbGxfaWNvbiI6bnVsbCwic3VjY2Vzc2Z1bCI6MCwidGFncyI6bnVsbCwiZmlsdGVycyI6bnVsbCwidGVtcGxhdGVfaWQiOm51bGwsInR0bCI6bnVsbCwidXJsIjpudWxsLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjpudWxsLCJ3cF93bnNfc291bmQiOm51bGx9LHsiYWRtX2JpZ19waWN0dXJlIjpudWxsLCJhZG1fZ3JvdXAiOm51bGwsImFkbV9ncm91cF9tZXNzYWdlIjpudWxsLCJhZG1fbGFyZ2VfaWNvbiI6bnVsbCwiYWRtX3NtYWxsX2ljb24iOm51bGwsImFkbV9zb3VuZCI6bnVsbCwic3Bva2VuX3RleHQiOm51bGwsImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOm51bGwsImFuZHJvaWRfYWNjZW50X2NvbG9yIjpudWxsLCJhbmRyb2lkX2dyb3VwIjpudWxsLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOm51bGwsImFuZHJvaWRfbGVkX2NvbG9yIjpudWxsLCJhbmRyb2lkX3NvdW5kIjpudWxsLCJhbmRyb2lkX3Zpc2liaWxpdHkiOm51bGwsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjpudWxsLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6bnVsbCwiY2hyb21lX2ljb24iOm51bGwsImNocm9tZV93ZWJfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pbWFnZSI6bnVsbCwiY2hyb21lX3dlYl9iYWRnZSI6bnVsbCwiY29udGVudF9hdmFpbGFibGUiOm51bGwsImNvbnRlbnRzIjp7ImVuIjoiTGl2ZSBUZXN0In0sImNvbnZlcnRlZCI6MCwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOm51bGwsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjpudWxsLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjpudWxsLCJoZWFkaW5ncyI6eyJlbiI6IlRoaXMgaXMgYSBsaXZlIHRlc3QgZm9yIE9uZVNpZ25hbCJ9LCJpZCI6IjNiM2ZhMzdiLTk2NDgtNDEwMS1iZjdiLTMwYzRkYzQ2NWMwMyIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWN0aXZlIFVzZXJzIl0sImlvc19iYWRnZUNvdW50IjpudWxsLCJpb3NfYmFkZ2VUeXBlIjpudWxsLCJpb3NfY2F0ZWdvcnkiOm51bGwsImlvc19zb3VuZCI6bnVsbCwiYXBuc19hbGVydCI6bnVsbCwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOnRydWUsImlzQ2hyb21lIjpmYWxzZSwiaXNDaHJvbWVXZWIiOmZhbHNlLCJpc0FsZXhhIjpmYWxzZSwiaXNGaXJlZm94IjpmYWxzZSwiaXNJb3MiOnRydWUsImlzU2FmYXJpIjpmYWxzZSwiaXNXUCI6ZmFsc2UsImlzV1BfV05TIjpmYWxzZSwiaXNFZGdlIjpmYWxzZSwibGFyZ2VfaWNvbiI6bnVsbCwicHJpb3JpdHkiOm51bGwsInF1ZXVlZF9hdCI6MTUzMDE5OTc4MywicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTk5NzgzLCJjb21wbGV0ZWRfYXQiOjE1MzAxOTk3ODQsInNtYWxsX2ljb24iOm51bGwsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6bnVsbCwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6bnVsbCwid3Bfd25zX3NvdW5kIjpudWxsfSx7ImFkbV9iaWdfcGljdHVyZSI6bnVsbCwiYWRtX2dyb3VwIjpudWxsLCJhZG1fZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYWRtX2xhcmdlX2ljb24iOm51bGwsImFkbV9zbWFsbF9pY29uIjpudWxsLCJhZG1fc291bmQiOm51bGwsInNwb2tlbl90ZXh0IjpudWxsLCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpudWxsLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6bnVsbCwiYW5kcm9pZF9ncm91cCI6bnVsbCwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjpudWxsLCJhbmRyb2lkX2xlZF9jb2xvciI6bnVsbCwiYW5kcm9pZF9zb3VuZCI6bnVsbCwiYW5kcm9pZF92aXNpYmlsaXR5IjpudWxsLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6bnVsbCwiYnV0dG9ucyI6bnVsbCwiY2FuY2VsZWQiOmZhbHNlLCJjaHJvbWVfYmlnX3BpY3R1cmUiOm51bGwsImNocm9tZV9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ljb24iOm51bGwsImNocm9tZV93ZWJfaW1hZ2UiOm51bGwsImNocm9tZV93ZWJfYmFkZ2UiOm51bGwsImNvbnRlbnRfYXZhaWxhYmxlIjpudWxsLCJjb250ZW50cyI6eyJlbiI6IkxpdmUgVGVzdCJ9LCJjb252ZXJ0ZWQiOjAsImRhdGEiOm51bGwsImRlbGF5ZWRfb3B0aW9uIjpudWxsLCJkZWxpdmVyeV90aW1lX29mX2RheSI6bnVsbCwiZXJyb3JlZCI6MCwiZXhjbHVkZWRfc2VnbWVudHMiOltdLCJmYWlsZWQiOjAsImZpcmVmb3hfaWNvbiI6bnVsbCwiaGVhZGluZ3MiOnsiZW4iOiJUaGlzIGlzIGEgbGl2ZSB0ZXN0IGZvciBPbmVTaWduYWwifSwiaWQiOiIyNmEyODI5MC1iYTNjLTRmMTYtOTA0MS1jZTcyMjdmM2FkZGIiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIkFjdGl2ZSBVc2VycyJdLCJpb3NfYmFkZ2VDb3VudCI6bnVsbCwiaW9zX2JhZGdlVHlwZSI6bnVsbCwiaW9zX2NhdGVnb3J5IjpudWxsLCJpb3Nfc291bmQiOm51bGwsImFwbnNfYWxlcnQiOm51bGwsImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjp0cnVlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOm51bGwsInByaW9yaXR5IjpudWxsLCJxdWV1ZWRfYXQiOjE1MzAxOTk3MTYsInJlbWFpbmluZyI6MCwic2VuZF9hZnRlciI6MTUzMDE5OTcxNiwiY29tcGxldGVkX2F0IjoxNTMwMTk5NzE2LCJzbWFsbF9pY29uIjpudWxsLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOm51bGwsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOm51bGwsIndwX3duc19zb3VuZCI6bnVsbH0seyJhZG1fYmlnX3BpY3R1cmUiOm51bGwsImFkbV9ncm91cCI6bnVsbCwiYWRtX2dyb3VwX21lc3NhZ2UiOm51bGwsImFkbV9sYXJnZV9pY29uIjpudWxsLCJhZG1fc21hbGxfaWNvbiI6bnVsbCwiYWRtX3NvdW5kIjpudWxsLCJzcG9rZW5fdGV4dCI6bnVsbCwiYWxleGFfc3NtbCI6bnVsbCwiYWxleGFfZGlzcGxheV90aXRsZSI6bnVsbCwiYW1hem9uX2JhY2tncm91bmRfZGF0YSI6bnVsbCwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOm51bGwsImFuZHJvaWRfZ3JvdXAiOm51bGwsImFuZHJvaWRfZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYW5kcm9pZF9sZWRfY29sb3IiOm51bGwsImFuZHJvaWRfc291bmQiOm51bGwsImFuZHJvaWRfdmlzaWJpbGl0eSI6bnVsbCwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOm51bGwsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjpudWxsLCJjaHJvbWVfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ltYWdlIjpudWxsLCJjaHJvbWVfd2ViX2JhZGdlIjpudWxsLCJjb250ZW50X2F2YWlsYWJsZSI6bnVsbCwiY29udGVudHMiOnsiZW4iOiJMaXZlIFRlc3QifSwiY29udmVydGVkIjowLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6bnVsbCwiZGVsaXZlcnlfdGltZV9vZl9kYXkiOm51bGwsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOm51bGwsImhlYWRpbmdzIjp7ImVuIjoiVGhpcyBpcyBhIGxpdmUgdGVzdCBmb3IgT25lU2lnbmFsIn0sImlkIjoiYjA0MjAwYjEtYTA4Yy00ZGJjLWJkNTEtN2U3Mzc1N2UxNzI2IiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBY3RpdmUgVXNlcnMiXSwiaW9zX2JhZGdlQ291bnQiOm51bGwsImlvc19iYWRnZVR5cGUiOm51bGwsImlvc19jYXRlZ29yeSI6bnVsbCwiaW9zX3NvdW5kIjpudWxsLCJhcG5zX2FsZXJ0IjpudWxsLCJpc0FkbSI6ZmFsc2UsImlzQW5kcm9pZCI6dHJ1ZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjpudWxsLCJwcmlvcml0eSI6bnVsbCwicXVldWVkX2F0IjoxNTMwMTk5MDQ2LCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxOTkwNDYsImNvbXBsZXRlZF9hdCI6MTUzMDE5OTA0Nywic21hbGxfaWNvbiI6bnVsbCwic3VjY2Vzc2Z1bCI6MSwidGFncyI6bnVsbCwiZmlsdGVycyI6bnVsbCwidGVtcGxhdGVfaWQiOm51bGwsInR0bCI6bnVsbCwidXJsIjpudWxsLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjpudWxsLCJ3cF93bnNfc291bmQiOm51bGx9LHsiYWRtX2JpZ19waWN0dXJlIjpudWxsLCJhZG1fZ3JvdXAiOm51bGwsImFkbV9ncm91cF9tZXNzYWdlIjpudWxsLCJhZG1fbGFyZ2VfaWNvbiI6bnVsbCwiYWRtX3NtYWxsX2ljb24iOm51bGwsImFkbV9zb3VuZCI6bnVsbCwic3Bva2VuX3RleHQiOm51bGwsImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOm51bGwsImFuZHJvaWRfYWNjZW50X2NvbG9yIjpudWxsLCJhbmRyb2lkX2dyb3VwIjpudWxsLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOm51bGwsImFuZHJvaWRfbGVkX2NvbG9yIjpudWxsLCJhbmRyb2lkX3NvdW5kIjpudWxsLCJhbmRyb2lkX3Zpc2liaWxpdHkiOm51bGwsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjpudWxsLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6bnVsbCwiY2hyb21lX2ljb24iOm51bGwsImNocm9tZV93ZWJfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pbWFnZSI6bnVsbCwiY2hyb21lX3dlYl9iYWRnZSI6bnVsbCwiY29udGVudF9hdmFpbGFibGUiOm51bGwsImNvbnRlbnRzIjp7ImVuIjoiTGl2ZSBUZXN0In0sImNvbnZlcnRlZCI6MCwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOm51bGwsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjpudWxsLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjpudWxsLCJoZWFkaW5ncyI6eyJlbiI6IlRoaXMgaXMgYSBsaXZlIHRlc3QgZm9yIE9uZVNpZ25hbCJ9LCJpZCI6IjQ3N2MwNTQ5LWNhNzUtNDc0MC04ODIyLThkZmI4ZjUzYTlhMyIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWN0aXZlIFVzZXJzIl0sImlvc19iYWRnZUNvdW50IjpudWxsLCJpb3NfYmFkZ2VUeXBlIjpudWxsLCJpb3NfY2F0ZWdvcnkiOm51bGwsImlvc19zb3VuZCI6bnVsbCwiYXBuc19hbGVydCI6bnVsbCwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOnRydWUsImlzQ2hyb21lIjpmYWxzZSwiaXNDaHJvbWVXZWIiOmZhbHNlLCJpc0FsZXhhIjpmYWxzZSwiaXNGaXJlZm94IjpmYWxzZSwiaXNJb3MiOnRydWUsImlzU2FmYXJpIjpmYWxzZSwiaXNXUCI6ZmFsc2UsImlzV1BfV05TIjpmYWxzZSwiaXNFZGdlIjpmYWxzZSwibGFyZ2VfaWNvbiI6bnVsbCwicHJpb3JpdHkiOm51bGwsInF1ZXVlZF9hdCI6MTUzMDE5ODk5OSwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTk4OTk5LCJjb21wbGV0ZWRfYXQiOjE1MzAxOTkwMDAsInNtYWxsX2ljb24iOm51bGwsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6bnVsbCwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6bnVsbCwid3Bfd25zX3NvdW5kIjpudWxsfSx7ImFkbV9iaWdfcGljdHVyZSI6IiIsImFkbV9ncm91cCI6IiIsImFkbV9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFkbV9sYXJnZV9pY29uIjoiIiwiYWRtX3NtYWxsX2ljb24iOiIiLCJhZG1fc291bmQiOiIiLCJzcG9rZW5fdGV4dCI6e30sImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOmZhbHNlLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6IiIsImFuZHJvaWRfZ3JvdXAiOiIiLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOnsiZW4iOiIifSwiYW5kcm9pZF9sZWRfY29sb3IiOiIiLCJhbmRyb2lkX3NvdW5kIjoiIiwiYW5kcm9pZF92aXNpYmlsaXR5IjoxLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6IiIsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjoiIiwiY2hyb21lX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ltYWdlIjoiIiwiY2hyb21lX3dlYl9iYWRnZSI6IiIsImNvbnRlbnRfYXZhaWxhYmxlIjpmYWxzZSwiY29udGVudHMiOnsiZW4iOiJnZmRnZmRnZCJ9LCJjb252ZXJ0ZWQiOjAsImRhdGEiOm51bGwsImRlbGF5ZWRfb3B0aW9uIjoiaW1tZWRpYXRlIiwiZGVsaXZlcnlfdGltZV9vZl9kYXkiOiIzOjMyUE0iLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjoiIiwiaGVhZGluZ3MiOnsiZW4iOiJkc2Fkc2FkcyJ9LCJpZCI6ImE0MmIzNjA0LTkwOTYtNDE0YS1hMzliLTBiMjNjMjcyZGRhNCIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWxsIl0sImlvc19iYWRnZUNvdW50IjoxLCJpb3NfYmFkZ2VUeXBlIjoiTm9uZSIsImlvc19jYXRlZ29yeSI6IiIsImlvc19zb3VuZCI6IiIsImFwbnNfYWxlcnQiOnt9LCJpc0FkbSI6ZmFsc2UsImlzQW5kcm9pZCI6dHJ1ZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjoiIiwicHJpb3JpdHkiOjUsInF1ZXVlZF9hdCI6MTUzMDE5Mjc2OCwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTkyNzY4LCJjb21wbGV0ZWRfYXQiOjE1MzAxOTI3NjksInNtYWxsX2ljb24iOiIiLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOiIiLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjoiIiwid3Bfd25zX3NvdW5kIjoiIn0seyJhZG1fYmlnX3BpY3R1cmUiOiIiLCJhZG1fZ3JvdXAiOiIiLCJhZG1fZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhZG1fbGFyZ2VfaWNvbiI6IiIsImFkbV9zbWFsbF9pY29uIjoiIiwiYWRtX3NvdW5kIjoiIiwic3Bva2VuX3RleHQiOnt9LCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpmYWxzZSwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOiIiLCJhbmRyb2lkX2dyb3VwIjoiIiwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFuZHJvaWRfbGVkX2NvbG9yIjoiIiwiYW5kcm9pZF9zb3VuZCI6IiIsImFuZHJvaWRfdmlzaWJpbGl0eSI6MSwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOiIiLCJidXR0b25zIjoiW3tcImlkXCI6XCJvbmVcIixcInRleHRcIjpcIk9uZVwiLFwiaWNvblwiOlwiaWNfbWVudV9zaGFyZVwifV0iLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6IiIsImNocm9tZV9pY29uIjoiIiwiY2hyb21lX3dlYl9pY29uIjoiIiwiY2hyb21lX3dlYl9pbWFnZSI6IiIsImNocm9tZV93ZWJfYmFkZ2UiOiIiLCJjb250ZW50X2F2YWlsYWJsZSI6ZmFsc2UsImNvbnRlbnRzIjp7ImVuIjoiZHNkc2QifSwiY29udmVydGVkIjoxLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6ImltbWVkaWF0ZSIsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjoiMTowMVBNIiwiZXJyb3JlZCI6MCwiZXhjbHVkZWRfc2VnbWVudHMiOltdLCJmYWlsZWQiOjAsImZpcmVmb3hfaWNvbiI6IiIsImhlYWRpbmdzIjp7ImVuIjoiVGVzdCBQdWxzYW50aSJ9LCJpZCI6Ijk1NzQ1MWUwLWJhZjMtNDhlOC1iYjZhLTM3NzUyMjBiZWM5NSIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWxsIl0sImlvc19iYWRnZUNvdW50IjoxLCJpb3NfYmFkZ2VUeXBlIjoiSW5jcmVhc2UiLCJpb3NfY2F0ZWdvcnkiOiIiLCJpb3Nfc291bmQiOiIiLCJhcG5zX2FsZXJ0Ijp7fSwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOmZhbHNlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOiIiLCJwcmlvcml0eSI6NSwicXVldWVkX2F0IjoxNTMwMTgzODMzLCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxODM4MzMsImNvbXBsZXRlZF9hdCI6MTUzMDE4MzgzNCwic21hbGxfaWNvbiI6IiIsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6IiIsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOiIiLCJ3cF93bnNfc291bmQiOiIifSx7ImFkbV9iaWdfcGljdHVyZSI6IiIsImFkbV9ncm91cCI6IiIsImFkbV9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFkbV9sYXJnZV9pY29uIjoiIiwiYWRtX3NtYWxsX2ljb24iOiIiLCJhZG1fc291bmQiOiIiLCJzcG9rZW5fdGV4dCI6e30sImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOmZhbHNlLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6IiIsImFuZHJvaWRfZ3JvdXAiOiIiLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOnsiZW4iOiIifSwiYW5kcm9pZF9sZWRfY29sb3IiOiIiLCJhbmRyb2lkX3NvdW5kIjoiIiwiYW5kcm9pZF92aXNpYmlsaXR5IjoxLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6IiIsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjoiIiwiY2hyb21lX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ltYWdlIjoiIiwiY2hyb21lX3dlYl9iYWRnZSI6IiIsImNvbnRlbnRfYXZhaWxhYmxlIjpmYWxzZSwiY29udGVudHMiOnsiZW4iOiJkc2ZkYXNmZGFzIn0sImNvbnZlcnRlZCI6MCwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOiJpbW1lZGlhdGUiLCJkZWxpdmVyeV90aW1lX29mX2RheSI6IjEyOjUyUE0iLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjoiIiwiaGVhZGluZ3MiOnsiZW4iOiJmZHNmZGFzZiJ9LCJpZCI6ImYyNDZmNmFlLTUwZmQtNGUwNi1hOWRkLTQ1MDgwZDRlMzI5MCIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWxsIl0sImlvc19iYWRnZUNvdW50IjoxLCJpb3NfYmFkZ2VUeXBlIjoiSW5jcmVhc2UiLCJpb3NfY2F0ZWdvcnkiOiIiLCJpb3Nfc291bmQiOiJyZXdhcmQud2F2IiwiYXBuc19hbGVydCI6e30sImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjoiIiwicHJpb3JpdHkiOjUsInF1ZXVlZF9hdCI6MTUzMDE4MzE0NSwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTgzMTQ1LCJjb21wbGV0ZWRfYXQiOjE1MzAxODMxNDUsInNtYWxsX2ljb24iOiIiLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOiIiLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjoiIiwid3Bfd25zX3NvdW5kIjoiIn0seyJhZG1fYmlnX3BpY3R1cmUiOiIiLCJhZG1fZ3JvdXAiOiIiLCJhZG1fZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhZG1fbGFyZ2VfaWNvbiI6IiIsImFkbV9zbWFsbF9pY29uIjoiIiwiYWRtX3NvdW5kIjoiIiwic3Bva2VuX3RleHQiOnt9LCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpmYWxzZSwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOiIiLCJhbmRyb2lkX2dyb3VwIjoiIiwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFuZHJvaWRfbGVkX2NvbG9yIjoiIiwiYW5kcm9pZF9zb3VuZCI6IiIsImFuZHJvaWRfdmlzaWJpbGl0eSI6MSwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOiIiLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6IiIsImNocm9tZV9pY29uIjoiIiwiY2hyb21lX3dlYl9pY29uIjoiIiwiY2hyb21lX3dlYl9pbWFnZSI6IiIsImNocm9tZV93ZWJfYmFkZ2UiOiIiLCJjb250ZW50X2F2YWlsYWJsZSI6ZmFsc2UsImNvbnRlbnRzIjp7ImVuIjoidGVzdCB0ZXN0IDEifSwiY29udmVydGVkIjowLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6ImltbWVkaWF0ZSIsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjoiMTI6NTFQTSIsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOiIiLCJoZWFkaW5ncyI6eyJlbiI6InRlc3QgdGVzdCJ9LCJpZCI6IjllODI3NTAxLTNmNmQtNGYyNy05ZDUwLThlY2Y1YjZlNDNlMSIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWxsIl0sImlvc19iYWRnZUNvdW50IjoxLCJpb3NfYmFkZ2VUeXBlIjoiSW5jcmVhc2UiLCJpb3NfY2F0ZWdvcnkiOiIiLCJpb3Nfc291bmQiOiJyZXdhcmQud2F2IiwiYXBuc19hbGVydCI6e30sImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjoiIiwicHJpb3JpdHkiOjUsInF1ZXVlZF9hdCI6MTUzMDE4MzEwNywicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTgzMTA3LCJjb21wbGV0ZWRfYXQiOjE1MzAxODMxMDgsInNtYWxsX2ljb24iOiIiLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOiIiLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjoiIiwid3Bfd25zX3NvdW5kIjoiIn0seyJhZG1fYmlnX3BpY3R1cmUiOiIiLCJhZG1fZ3JvdXAiOiIiLCJhZG1fZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhZG1fbGFyZ2VfaWNvbiI6IiIsImFkbV9zbWFsbF9pY29uIjoiIiwiYWRtX3NvdW5kIjoiIiwic3Bva2VuX3RleHQiOnt9LCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpmYWxzZSwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOiIiLCJhbmRyb2lkX2dyb3VwIjoiIiwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFuZHJvaWRfbGVkX2NvbG9yIjoiIiwiYW5kcm9pZF9zb3VuZCI6IiIsImFuZHJvaWRfdmlzaWJpbGl0eSI6MSwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOiIiLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6IiIsImNocm9tZV9pY29uIjoiIiwiY2hyb21lX3dlYl9pY29uIjoiIiwiY2hyb21lX3dlYl9pbWFnZSI6IiIsImNocm9tZV93ZWJfYmFkZ2UiOiIiLCJjb250ZW50X2F2YWlsYWJsZSI6ZmFsc2UsImNvbnRlbnRzIjp7ImVuIjoidGVzdCAyIn0sImNvbnZlcnRlZCI6MCwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOiJpbW1lZGlhdGUiLCJkZWxpdmVyeV90aW1lX29mX2RheSI6IjEyOjIwUE0iLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjoiIiwiaGVhZGluZ3MiOnsiZW4iOiJUZXN0IDIifSwiaWQiOiI5OTI1MzAyOC1iNThmLTQ3YjEtYjgxZC01NDAwZjFkZjI5ODUiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIkFsbCJdLCJpb3NfYmFkZ2VDb3VudCI6MSwiaW9zX2JhZGdlVHlwZSI6Ik5vbmUiLCJpb3NfY2F0ZWdvcnkiOiIiLCJpb3Nfc291bmQiOiJuZXdDYW1wYWlnbi53YXYiLCJhcG5zX2FsZXJ0Ijp7fSwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOmZhbHNlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOiIiLCJwcmlvcml0eSI6NSwicXVldWVkX2F0IjoxNTMwMTgxMjgwLCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxODEyODAsImNvbXBsZXRlZF9hdCI6MTUzMDE4MTI4MCwic21hbGxfaWNvbiI6IiIsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6IiIsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOiIiLCJ3cF93bnNfc291bmQiOiIifSx7ImFkbV9iaWdfcGljdHVyZSI6IiIsImFkbV9ncm91cCI6IiIsImFkbV9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFkbV9sYXJnZV9pY29uIjoiIiwiYWRtX3NtYWxsX2ljb24iOiIiLCJhZG1fc291bmQiOiIiLCJzcG9rZW5fdGV4dCI6e30sImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOmZhbHNlLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6IiIsImFuZHJvaWRfZ3JvdXAiOiIiLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOnsiZW4iOiIifSwiYW5kcm9pZF9sZWRfY29sb3IiOiIiLCJhbmRyb2lkX3NvdW5kIjoiIiwiYW5kcm9pZF92aXNpYmlsaXR5IjoxLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6IiIsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjoiIiwiY2hyb21lX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ltYWdlIjoiIiwiY2hyb21lX3dlYl9iYWRnZSI6IiIsImNvbnRlbnRfYXZhaWxhYmxlIjpmYWxzZSwiY29udGVudHMiOnsiZW4iOiJ0ZXN0IDEifSwiY29udmVydGVkIjowLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6ImltbWVkaWF0ZSIsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjoiMTI6MThQTSIsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOiIiLCJoZWFkaW5ncyI6eyJlbiI6IlJld2FyZCAxIn0sImlkIjoiOTVjNGI0NDktN2I3Yi00NDVkLWEzNmItMjZhZTYzOTcxNmI0IiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBbGwiXSwiaW9zX2JhZGdlQ291bnQiOjEsImlvc19iYWRnZVR5cGUiOiJOb25lIiwiaW9zX2NhdGVnb3J5IjoiIiwiaW9zX3NvdW5kIjoicmV3YXJkLndhdiIsImFwbnNfYWxlcnQiOnt9LCJpc0FkbSI6ZmFsc2UsImlzQW5kcm9pZCI6ZmFsc2UsImlzQ2hyb21lIjpmYWxzZSwiaXNDaHJvbWVXZWIiOmZhbHNlLCJpc0FsZXhhIjpmYWxzZSwiaXNGaXJlZm94IjpmYWxzZSwiaXNJb3MiOnRydWUsImlzU2FmYXJpIjpmYWxzZSwiaXNXUCI6ZmFsc2UsImlzV1BfV05TIjpmYWxzZSwiaXNFZGdlIjpmYWxzZSwibGFyZ2VfaWNvbiI6IiIsInByaW9yaXR5Ijo1LCJxdWV1ZWRfYXQiOjE1MzAxODExNTgsInJlbWFpbmluZyI6MCwic2VuZF9hZnRlciI6MTUzMDE4MTE1OCwiY29tcGxldGVkX2F0IjoxNTMwMTgxMTU5LCJzbWFsbF9pY29uIjoiIiwic3VjY2Vzc2Z1bCI6MSwidGFncyI6bnVsbCwiZmlsdGVycyI6bnVsbCwidGVtcGxhdGVfaWQiOm51bGwsInR0bCI6bnVsbCwidXJsIjoiIiwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6IiIsIndwX3duc19zb3VuZCI6IiJ9LHsiYWRtX2JpZ19waWN0dXJlIjoiIiwiYWRtX2dyb3VwIjoiIiwiYWRtX2dyb3VwX21lc3NhZ2UiOnsiZW4iOiIifSwiYWRtX2xhcmdlX2ljb24iOiIiLCJhZG1fc21hbGxfaWNvbiI6IiIsImFkbV9zb3VuZCI6IiIsInNwb2tlbl90ZXh0Ijp7fSwiYWxleGFfc3NtbCI6bnVsbCwiYWxleGFfZGlzcGxheV90aXRsZSI6bnVsbCwiYW1hem9uX2JhY2tncm91bmRfZGF0YSI6ZmFsc2UsImFuZHJvaWRfYWNjZW50X2NvbG9yIjoiIiwiYW5kcm9pZF9ncm91cCI6IiIsImFuZHJvaWRfZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhbmRyb2lkX2xlZF9jb2xvciI6IiIsImFuZHJvaWRfc291bmQiOiIiLCJhbmRyb2lkX3Zpc2liaWxpdHkiOjEsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjoiIiwiYnV0dG9ucyI6Ilt7XCJpZFwiOlwiMVwiLFwidGV4dFwiOlwicGlwcG9cIixcImljb25cIjpcIlwifSx7XCJpZFwiOlwiMlwiLFwidGV4dFwiOlwicGx1dG9cIixcImljb25cIjpcIlwifSx7XCJpZFwiOlwiM1wiLFwidGV4dFwiOlwicXVhcXVhXCIsXCJpY29uXCI6XCJcIn1dIiwiY2FuY2VsZWQiOmZhbHNlLCJjaHJvbWVfYmlnX3BpY3R1cmUiOiIiLCJjaHJvbWVfaWNvbiI6IiIsImNocm9tZV93ZWJfaWNvbiI6IiIsImNocm9tZV93ZWJfaW1hZ2UiOiIiLCJjaHJvbWVfd2ViX2JhZGdlIjoiIiwiY29udGVudF9hdmFpbGFibGUiOmZhbHNlLCJjb250ZW50cyI6eyJlbiI6ImZzZmEifSwiY29udmVydGVkIjoxLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6ImltbWVkaWF0ZSIsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjoiMTE6NDZBTSIsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOiIiLCJoZWFkaW5ncyI6eyJlbiI6ImRzYWRhc2RhIn0sImlkIjoiZmFmNThkMmMtZjQ1ZS00YWI1LTk1NzMtMDc3ZTQ2NDM1MTdiIiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBbGwiXSwiaW9zX2JhZGdlQ291bnQiOjEsImlvc19iYWRnZVR5cGUiOiJOb25lIiwiaW9zX2NhdGVnb3J5IjoiIiwiaW9zX3NvdW5kIjoiIiwiYXBuc19hbGVydCI6e30sImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjoiIiwicHJpb3JpdHkiOjUsInF1ZXVlZF9hdCI6MTUzMDE3OTIwMSwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTc5MjAxLCJjb21wbGV0ZWRfYXQiOjE1MzAxNzkyMDEsInNtYWxsX2ljb24iOiIiLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOiIiLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjoiIiwid3Bfd25zX3NvdW5kIjoiIn0seyJhZG1fYmlnX3BpY3R1cmUiOiIiLCJhZG1fZ3JvdXAiOiIiLCJhZG1fZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhZG1fbGFyZ2VfaWNvbiI6IiIsImFkbV9zbWFsbF9pY29uIjoiIiwiYWRtX3NvdW5kIjoiIiwic3Bva2VuX3RleHQiOnt9LCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpmYWxzZSwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOiIiLCJhbmRyb2lkX2dyb3VwIjoiIiwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFuZHJvaWRfbGVkX2NvbG9yIjoiIiwiYW5kcm9pZF9zb3VuZCI6IiIsImFuZHJvaWRfdmlzaWJpbGl0eSI6MSwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOiIiLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6IiIsImNocm9tZV9pY29uIjoiIiwiY2hyb21lX3dlYl9pY29uIjoiIiwiY2hyb21lX3dlYl9pbWFnZSI6IiIsImNocm9tZV93ZWJfYmFkZ2UiOiIiLCJjb250ZW50X2F2YWlsYWJsZSI6ZmFsc2UsImNvbnRlbnRzIjp7ImVuIjoidmlzaXRhIGlsIG5vc3RybyBzaXRvIHdlYiAyIn0sImNvbnZlcnRlZCI6MSwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOiJpbW1lZGlhdGUiLCJkZWxpdmVyeV90aW1lX29mX2RheSI6IjExOjQ0QU0iLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjoiIiwiaGVhZGluZ3MiOnsiZW4iOiJkYXNkc2FkIn0sImlkIjoiMDJhNGYyZmItYTZiNi00MmM4LWE5NWQtYjVhYTg3MjM0YTk5IiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBbGwiXSwiaW9zX2JhZGdlQ291bnQiOjEsImlvc19iYWRnZVR5cGUiOiJOb25lIiwiaW9zX2NhdGVnb3J5IjoiIiwiaW9zX3NvdW5kIjoiIiwiYXBuc19hbGVydCI6e30sImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjoiIiwicHJpb3JpdHkiOjUsInF1ZXVlZF9hdCI6MTUzMDE3OTEyNiwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTc5MTI2LCJjb21wbGV0ZWRfYXQiOjE1MzAxNzkxMjYsInNtYWxsX2ljb24iOiIiLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOiJodHRwOi8vd29yZHByZXNzLTE4NDEyMi01NDEyODEuY2xvdWR3YXlzYXBwcy5jb20vIiwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6IiIsIndwX3duc19zb3VuZCI6IiJ9LHsiYWRtX2JpZ19waWN0dXJlIjoiIiwiYWRtX2dyb3VwIjoiIiwiYWRtX2dyb3VwX21lc3NhZ2UiOnsiZW4iOiIifSwiYWRtX2xhcmdlX2ljb24iOiIiLCJhZG1fc21hbGxfaWNvbiI6IiIsImFkbV9zb3VuZCI6IiIsInNwb2tlbl90ZXh0Ijp7fSwiYWxleGFfc3NtbCI6bnVsbCwiYWxleGFfZGlzcGxheV90aXRsZSI6bnVsbCwiYW1hem9uX2JhY2tncm91bmRfZGF0YSI6ZmFsc2UsImFuZHJvaWRfYWNjZW50X2NvbG9yIjoiIiwiYW5kcm9pZF9ncm91cCI6IiIsImFuZHJvaWRfZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhbmRyb2lkX2xlZF9jb2xvciI6IiIsImFuZHJvaWRfc291bmQiOiIiLCJhbmRyb2lkX3Zpc2liaWxpdHkiOjEsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjoiIiwiYnV0dG9ucyI6bnVsbCwiY2FuY2VsZWQiOmZhbHNlLCJjaHJvbWVfYmlnX3BpY3R1cmUiOiIiLCJjaHJvbWVfaWNvbiI6IiIsImNocm9tZV93ZWJfaWNvbiI6IiIsImNocm9tZV93ZWJfaW1hZ2UiOiIiLCJjaHJvbWVfd2ViX2JhZGdlIjoiIiwiY29udGVudF9hdmFpbGFibGUiOmZhbHNlLCJjb250ZW50cyI6eyJlbiI6InZpc2l0YSBpbCBub3N0cm8gc2l0byB3ZWIifSwiY29udmVydGVkIjoxLCJkYXRhIjp7ImtleTEiOiJwaXBwbyJ9LCJkZWxheWVkX29wdGlvbiI6ImltbWVkaWF0ZSIsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjoiMTE6MzhBTSIsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOiIiLCJoZWFkaW5ncyI6eyJlbiI6IkNpYW8gY2lhb25lIn0sImlkIjoiNjkyZjk5OGUtZjczZC00ZGRlLWJmNTktNDU4NjkzNTg2YjdmIiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBbGwiXSwiaW9zX2JhZGdlQ291bnQiOjEsImlvc19iYWRnZVR5cGUiOiJOb25lIiwiaW9zX2NhdGVnb3J5IjoiIiwiaW9zX3NvdW5kIjoiIiwiYXBuc19hbGVydCI6e30sImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjoiIiwicHJpb3JpdHkiOjUsInF1ZXVlZF9hdCI6MTUzMDE3OTA2MSwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTc5MDYxLCJjb21wbGV0ZWRfYXQiOjE1MzAxNzkwNjIsInNtYWxsX2ljb24iOiIiLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOiJodHRwOi8vd29yZHByZXNzLTE4NDEyMi01NDEyODEuY2xvdWR3YXlzYXBwcy5jb20vIiwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6IiIsIndwX3duc19zb3VuZCI6IiJ9LHsiYWRtX2JpZ19waWN0dXJlIjpudWxsLCJhZG1fZ3JvdXAiOm51bGwsImFkbV9ncm91cF9tZXNzYWdlIjpudWxsLCJhZG1fbGFyZ2VfaWNvbiI6bnVsbCwiYWRtX3NtYWxsX2ljb24iOm51bGwsImFkbV9zb3VuZCI6bnVsbCwic3Bva2VuX3RleHQiOm51bGwsImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOm51bGwsImFuZHJvaWRfYWNjZW50X2NvbG9yIjpudWxsLCJhbmRyb2lkX2dyb3VwIjpudWxsLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOm51bGwsImFuZHJvaWRfbGVkX2NvbG9yIjpudWxsLCJhbmRyb2lkX3NvdW5kIjpudWxsLCJhbmRyb2lkX3Zpc2liaWxpdHkiOm51bGwsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjpudWxsLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6bnVsbCwiY2hyb21lX2ljb24iOm51bGwsImNocm9tZV93ZWJfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pbWFnZSI6bnVsbCwiY2hyb21lX3dlYl9iYWRnZSI6bnVsbCwiY29udGVudF9hdmFpbGFibGUiOm51bGwsImNvbnRlbnRzIjp7ImVuIjoiTGl2ZSBUZXN0In0sImNvbnZlcnRlZCI6MSwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOm51bGwsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjpudWxsLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjpudWxsLCJoZWFkaW5ncyI6eyJlbiI6IlRoaXMgaXMgYSBsaXZlIHRlc3QgZm9yIE9uZVNpZ25hbCJ9LCJpZCI6ImZlODJjMWFlLTU0YzItNDU4Yi04YWFkLTdlZGMzZThhOTZjNCIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWN0aXZlIFVzZXJzIl0sImlvc19iYWRnZUNvdW50IjpudWxsLCJpb3NfYmFkZ2VUeXBlIjpudWxsLCJpb3NfY2F0ZWdvcnkiOm51bGwsImlvc19zb3VuZCI6bnVsbCwiYXBuc19hbGVydCI6bnVsbCwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOmZhbHNlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOm51bGwsInByaW9yaXR5IjpudWxsLCJxdWV1ZWRfYXQiOjE1MzAxNzg5MjUsInJlbWFpbmluZyI6MCwic2VuZF9hZnRlciI6MTUzMDE3ODkyNSwiY29tcGxldGVkX2F0IjoxNTMwMTc4OTI1LCJzbWFsbF9pY29uIjpudWxsLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOm51bGwsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOm51bGwsIndwX3duc19zb3VuZCI6bnVsbH0seyJhZG1fYmlnX3BpY3R1cmUiOm51bGwsImFkbV9ncm91cCI6bnVsbCwiYWRtX2dyb3VwX21lc3NhZ2UiOm51bGwsImFkbV9sYXJnZV9pY29uIjpudWxsLCJhZG1fc21hbGxfaWNvbiI6bnVsbCwiYWRtX3NvdW5kIjpudWxsLCJzcG9rZW5fdGV4dCI6bnVsbCwiYWxleGFfc3NtbCI6bnVsbCwiYWxleGFfZGlzcGxheV90aXRsZSI6bnVsbCwiYW1hem9uX2JhY2tncm91bmRfZGF0YSI6bnVsbCwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOm51bGwsImFuZHJvaWRfZ3JvdXAiOm51bGwsImFuZHJvaWRfZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYW5kcm9pZF9sZWRfY29sb3IiOm51bGwsImFuZHJvaWRfc291bmQiOm51bGwsImFuZHJvaWRfdmlzaWJpbGl0eSI6bnVsbCwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOm51bGwsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjpudWxsLCJjaHJvbWVfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ltYWdlIjpudWxsLCJjaHJvbWVfd2ViX2JhZGdlIjpudWxsLCJjb250ZW50X2F2YWlsYWJsZSI6bnVsbCwiY29udGVudHMiOnsiZW4iOiJMaXZlIFRlc3QifSwiY29udmVydGVkIjowLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6bnVsbCwiZGVsaXZlcnlfdGltZV9vZl9kYXkiOm51bGwsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOm51bGwsImhlYWRpbmdzIjp7ImVuIjoiVGhpcyBpcyBhIGxpdmUgdGVzdCBmb3IgT25lU2lnbmFsIn0sImlkIjoiMjg1M2FmYzMtYTZmNC00OTAwLTg2ZDQtM2IwMmI5NDlmMDBkIiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBY3RpdmUgVXNlcnMiXSwiaW9zX2JhZGdlQ291bnQiOm51bGwsImlvc19iYWRnZVR5cGUiOm51bGwsImlvc19jYXRlZ29yeSI6bnVsbCwiaW9zX3NvdW5kIjpudWxsLCJhcG5zX2FsZXJ0IjpudWxsLCJpc0FkbSI6ZmFsc2UsImlzQW5kcm9pZCI6ZmFsc2UsImlzQ2hyb21lIjpmYWxzZSwiaXNDaHJvbWVXZWIiOmZhbHNlLCJpc0FsZXhhIjpmYWxzZSwiaXNGaXJlZm94IjpmYWxzZSwiaXNJb3MiOnRydWUsImlzU2FmYXJpIjpmYWxzZSwiaXNXUCI6ZmFsc2UsImlzV1BfV05TIjpmYWxzZSwiaXNFZGdlIjpmYWxzZSwibGFyZ2VfaWNvbiI6bnVsbCwicHJpb3JpdHkiOm51bGwsInF1ZXVlZF9hdCI6MTUzMDE3ODg0NiwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTc4ODQ2LCJjb21wbGV0ZWRfYXQiOjE1MzAxNzg4NDYsInNtYWxsX2ljb24iOm51bGwsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6bnVsbCwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6bnVsbCwid3Bfd25zX3NvdW5kIjpudWxsfSx7ImFkbV9iaWdfcGljdHVyZSI6bnVsbCwiYWRtX2dyb3VwIjpudWxsLCJhZG1fZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYWRtX2xhcmdlX2ljb24iOm51bGwsImFkbV9zbWFsbF9pY29uIjpudWxsLCJhZG1fc291bmQiOm51bGwsInNwb2tlbl90ZXh0IjpudWxsLCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpudWxsLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6bnVsbCwiYW5kcm9pZF9ncm91cCI6bnVsbCwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjpudWxsLCJhbmRyb2lkX2xlZF9jb2xvciI6bnVsbCwiYW5kcm9pZF9zb3VuZCI6bnVsbCwiYW5kcm9pZF92aXNpYmlsaXR5IjpudWxsLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6bnVsbCwiYnV0dG9ucyI6bnVsbCwiY2FuY2VsZWQiOmZhbHNlLCJjaHJvbWVfYmlnX3BpY3R1cmUiOm51bGwsImNocm9tZV9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ljb24iOm51bGwsImNocm9tZV93ZWJfaW1hZ2UiOm51bGwsImNocm9tZV93ZWJfYmFkZ2UiOm51bGwsImNvbnRlbnRfYXZhaWxhYmxlIjpudWxsLCJjb250ZW50cyI6eyJlbiI6IkxpdmUgVGVzdCJ9LCJjb252ZXJ0ZWQiOjEsImRhdGEiOm51bGwsImRlbGF5ZWRfb3B0aW9uIjpudWxsLCJkZWxpdmVyeV90aW1lX29mX2RheSI6bnVsbCwiZXJyb3JlZCI6MCwiZXhjbHVkZWRfc2VnbWVudHMiOltdLCJmYWlsZWQiOjAsImZpcmVmb3hfaWNvbiI6bnVsbCwiaGVhZGluZ3MiOnsiZW4iOiJUaGlzIGlzIGEgbGl2ZSB0ZXN0IGZvciBPbmVTaWduYWwifSwiaWQiOiJiYThhOTNlMC03OTYyLTRlMzItYTIwMS1lODFlYjhhNDY0MTUiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIkFjdGl2ZSBVc2VycyJdLCJpb3NfYmFkZ2VDb3VudCI6bnVsbCwiaW9zX2JhZGdlVHlwZSI6bnVsbCwiaW9zX2NhdGVnb3J5IjpudWxsLCJpb3Nfc291bmQiOm51bGwsImFwbnNfYWxlcnQiOm51bGwsImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjpudWxsLCJwcmlvcml0eSI6bnVsbCwicXVldWVkX2F0IjoxNTMwMTc4NzgwLCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxNzg3ODAsImNvbXBsZXRlZF9hdCI6MTUzMDE3ODc4MSwic21hbGxfaWNvbiI6bnVsbCwic3VjY2Vzc2Z1bCI6MSwidGFncyI6bnVsbCwiZmlsdGVycyI6bnVsbCwidGVtcGxhdGVfaWQiOm51bGwsInR0bCI6bnVsbCwidXJsIjpudWxsLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjpudWxsLCJ3cF93bnNfc291bmQiOm51bGx9LHsiYWRtX2JpZ19waWN0dXJlIjpudWxsLCJhZG1fZ3JvdXAiOm51bGwsImFkbV9ncm91cF9tZXNzYWdlIjpudWxsLCJhZG1fbGFyZ2VfaWNvbiI6bnVsbCwiYWRtX3NtYWxsX2ljb24iOm51bGwsImFkbV9zb3VuZCI6bnVsbCwic3Bva2VuX3RleHQiOm51bGwsImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOm51bGwsImFuZHJvaWRfYWNjZW50X2NvbG9yIjpudWxsLCJhbmRyb2lkX2dyb3VwIjpudWxsLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOm51bGwsImFuZHJvaWRfbGVkX2NvbG9yIjpudWxsLCJhbmRyb2lkX3NvdW5kIjpudWxsLCJhbmRyb2lkX3Zpc2liaWxpdHkiOm51bGwsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjpudWxsLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6bnVsbCwiY2hyb21lX2ljb24iOm51bGwsImNocm9tZV93ZWJfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pbWFnZSI6bnVsbCwiY2hyb21lX3dlYl9iYWRnZSI6bnVsbCwiY29udGVudF9hdmFpbGFibGUiOm51bGwsImNvbnRlbnRzIjp7ImVuIjoiTGl2ZSBUZXN0In0sImNvbnZlcnRlZCI6MSwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOm51bGwsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjpudWxsLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjpudWxsLCJoZWFkaW5ncyI6eyJlbiI6IlRoaXMgaXMgYSBsaXZlIHRlc3QgZm9yIE9uZVNpZ25hbCJ9LCJpZCI6ImRmMzlkZGJjLTY2NmItNGVjNS1hMmQ0LWY5ZTEyNDhjMmYzZiIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWN0aXZlIFVzZXJzIl0sImlvc19iYWRnZUNvdW50IjpudWxsLCJpb3NfYmFkZ2VUeXBlIjpudWxsLCJpb3NfY2F0ZWdvcnkiOm51bGwsImlvc19zb3VuZCI6bnVsbCwiYXBuc19hbGVydCI6bnVsbCwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOmZhbHNlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOm51bGwsInByaW9yaXR5IjpudWxsLCJxdWV1ZWRfYXQiOjE1MzAxNzg3MzQsInJlbWFpbmluZyI6MCwic2VuZF9hZnRlciI6MTUzMDE3ODczNCwiY29tcGxldGVkX2F0IjoxNTMwMTc4NzM0LCJzbWFsbF9pY29uIjpudWxsLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOm51bGwsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOm51bGwsIndwX3duc19zb3VuZCI6bnVsbH0seyJhZG1fYmlnX3BpY3R1cmUiOiIiLCJhZG1fZ3JvdXAiOiIiLCJhZG1fZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhZG1fbGFyZ2VfaWNvbiI6IiIsImFkbV9zbWFsbF9pY29uIjoiIiwiYWRtX3NvdW5kIjoiIiwic3Bva2VuX3RleHQiOnt9LCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpmYWxzZSwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOiIiLCJhbmRyb2lkX2dyb3VwIjoiIiwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFuZHJvaWRfbGVkX2NvbG9yIjoiIiwiYW5kcm9pZF9zb3VuZCI6IiIsImFuZHJvaWRfdmlzaWJpbGl0eSI6MSwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOiIiLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6IiIsImNocm9tZV9pY29uIjoiIiwiY2hyb21lX3dlYl9pY29uIjoiIiwiY2hyb21lX3dlYl9pbWFnZSI6IiIsImNocm9tZV93ZWJfYmFkZ2UiOiIiLCJjb250ZW50X2F2YWlsYWJsZSI6ZmFsc2UsImNvbnRlbnRzIjp7ImVuIjoiU2dyaWduaWduw6wifSwiY29udmVydGVkIjoxLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6ImltbWVkaWF0ZSIsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjoiMTE6MzNBTSIsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOiIiLCJoZWFkaW5ncyI6eyJlbiI6IkNpYW8ifSwiaWQiOiJiMTIyNDg5YS00ODU0LTQ2NTMtODFiMy1mMTYyOThkYzRkOTYiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIkFjdGl2ZSBVc2VycyJdLCJpb3NfYmFkZ2VDb3VudCI6MSwiaW9zX2JhZGdlVHlwZSI6Ik5vbmUiLCJpb3NfY2F0ZWdvcnkiOiIiLCJpb3Nfc291bmQiOiIiLCJhcG5zX2FsZXJ0Ijp7fSwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOmZhbHNlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOiIiLCJwcmlvcml0eSI6NSwicXVldWVkX2F0IjoxNTMwMTc4NTg2LCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxNzg1ODcsImNvbXBsZXRlZF9hdCI6MTUzMDE3ODU4Nywic21hbGxfaWNvbiI6IiIsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6IiIsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOiIiLCJ3cF93bnNfc291bmQiOiIifSx7ImFkbV9iaWdfcGljdHVyZSI6bnVsbCwiYWRtX2dyb3VwIjpudWxsLCJhZG1fZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYWRtX2xhcmdlX2ljb24iOm51bGwsImFkbV9zbWFsbF9pY29uIjpudWxsLCJhZG1fc291bmQiOm51bGwsInNwb2tlbl90ZXh0IjpudWxsLCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpudWxsLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6bnVsbCwiYW5kcm9pZF9ncm91cCI6bnVsbCwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjpudWxsLCJhbmRyb2lkX2xlZF9jb2xvciI6bnVsbCwiYW5kcm9pZF9zb3VuZCI6bnVsbCwiYW5kcm9pZF92aXNpYmlsaXR5IjpudWxsLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6bnVsbCwiYnV0dG9ucyI6bnVsbCwiY2FuY2VsZWQiOmZhbHNlLCJjaHJvbWVfYmlnX3BpY3R1cmUiOm51bGwsImNocm9tZV9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ljb24iOm51bGwsImNocm9tZV93ZWJfaW1hZ2UiOm51bGwsImNocm9tZV93ZWJfYmFkZ2UiOm51bGwsImNvbnRlbnRfYXZhaWxhYmxlIjpudWxsLCJjb250ZW50cyI6eyJlbiI6IkxpdmUgVGVzdCJ9LCJjb252ZXJ0ZWQiOjAsImRhdGEiOm51bGwsImRlbGF5ZWRfb3B0aW9uIjpudWxsLCJkZWxpdmVyeV90aW1lX29mX2RheSI6bnVsbCwiZXJyb3JlZCI6MCwiZXhjbHVkZWRfc2VnbWVudHMiOltdLCJmYWlsZWQiOjAsImZpcmVmb3hfaWNvbiI6bnVsbCwiaGVhZGluZ3MiOnsiZW4iOiJUaGlzIGlzIGEgbGl2ZSB0ZXN0IGZvciBPbmVTaWduYWwifSwiaWQiOiJjM2ZiMGVlNC1iY2RkLTRiNzctYTkyZC0wMjJiNDYzYTg5OGYiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIkFjdGl2ZSBVc2VycyJdLCJpb3NfYmFkZ2VDb3VudCI6bnVsbCwiaW9zX2JhZGdlVHlwZSI6bnVsbCwiaW9zX2NhdGVnb3J5IjpudWxsLCJpb3Nfc291bmQiOm51bGwsImFwbnNfYWxlcnQiOm51bGwsImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjpudWxsLCJwcmlvcml0eSI6bnVsbCwicXVldWVkX2F0IjoxNTMwMTc4NDk0LCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxNzg0OTQsImNvbXBsZXRlZF9hdCI6MTUzMDE3ODQ5NCwic21hbGxfaWNvbiI6bnVsbCwic3VjY2Vzc2Z1bCI6MSwidGFncyI6bnVsbCwiZmlsdGVycyI6bnVsbCwidGVtcGxhdGVfaWQiOm51bGwsInR0bCI6bnVsbCwidXJsIjpudWxsLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjpudWxsLCJ3cF93bnNfc291bmQiOm51bGx9LHsiYWRtX2JpZ19waWN0dXJlIjpudWxsLCJhZG1fZ3JvdXAiOm51bGwsImFkbV9ncm91cF9tZXNzYWdlIjpudWxsLCJhZG1fbGFyZ2VfaWNvbiI6bnVsbCwiYWRtX3NtYWxsX2ljb24iOm51bGwsImFkbV9zb3VuZCI6bnVsbCwic3Bva2VuX3RleHQiOm51bGwsImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOm51bGwsImFuZHJvaWRfYWNjZW50X2NvbG9yIjpudWxsLCJhbmRyb2lkX2dyb3VwIjpudWxsLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOm51bGwsImFuZHJvaWRfbGVkX2NvbG9yIjpudWxsLCJhbmRyb2lkX3NvdW5kIjpudWxsLCJhbmRyb2lkX3Zpc2liaWxpdHkiOm51bGwsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjpudWxsLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6bnVsbCwiY2hyb21lX2ljb24iOm51bGwsImNocm9tZV93ZWJfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pbWFnZSI6bnVsbCwiY2hyb21lX3dlYl9iYWRnZSI6bnVsbCwiY29udGVudF9hdmFpbGFibGUiOm51bGwsImNvbnRlbnRzIjp7ImVuIjoiTGl2ZSBUZXN0In0sImNvbnZlcnRlZCI6MCwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOm51bGwsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjpudWxsLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjpudWxsLCJoZWFkaW5ncyI6eyJlbiI6IlRoaXMgaXMgYSBsaXZlIHRlc3QgZm9yIE9uZVNpZ25hbCJ9LCJpZCI6IjkwMmUyNThmLTIzYzYtNGQ1ZS1iMDM4LWNjYzE0ZWY3NDUwOCIsImluY2x1ZGVfcGxheWVyX2lkcyI6bnVsbCwiaW5jbHVkZWRfc2VnbWVudHMiOlsiQWN0aXZlIFVzZXJzIl0sImlvc19iYWRnZUNvdW50IjpudWxsLCJpb3NfYmFkZ2VUeXBlIjpudWxsLCJpb3NfY2F0ZWdvcnkiOm51bGwsImlvc19zb3VuZCI6bnVsbCwiYXBuc19hbGVydCI6bnVsbCwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOmZhbHNlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOm51bGwsInByaW9yaXR5IjpudWxsLCJxdWV1ZWRfYXQiOjE1MzAxNzg0NzEsInJlbWFpbmluZyI6MCwic2VuZF9hZnRlciI6MTUzMDE3ODQ3MSwiY29tcGxldGVkX2F0IjoxNTMwMTc4NDcyLCJzbWFsbF9pY29uIjpudWxsLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOm51bGwsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOm51bGwsIndwX3duc19zb3VuZCI6bnVsbH0seyJhZG1fYmlnX3BpY3R1cmUiOm51bGwsImFkbV9ncm91cCI6bnVsbCwiYWRtX2dyb3VwX21lc3NhZ2UiOm51bGwsImFkbV9sYXJnZV9pY29uIjpudWxsLCJhZG1fc21hbGxfaWNvbiI6bnVsbCwiYWRtX3NvdW5kIjpudWxsLCJzcG9rZW5fdGV4dCI6bnVsbCwiYWxleGFfc3NtbCI6bnVsbCwiYWxleGFfZGlzcGxheV90aXRsZSI6bnVsbCwiYW1hem9uX2JhY2tncm91bmRfZGF0YSI6bnVsbCwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOm51bGwsImFuZHJvaWRfZ3JvdXAiOm51bGwsImFuZHJvaWRfZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYW5kcm9pZF9sZWRfY29sb3IiOm51bGwsImFuZHJvaWRfc291bmQiOm51bGwsImFuZHJvaWRfdmlzaWJpbGl0eSI6bnVsbCwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOm51bGwsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjpudWxsLCJjaHJvbWVfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ltYWdlIjpudWxsLCJjaHJvbWVfd2ViX2JhZGdlIjpudWxsLCJjb250ZW50X2F2YWlsYWJsZSI6bnVsbCwiY29udGVudHMiOnsiZW4iOiJTZ3JpZ25pbsOsIn0sImNvbnZlcnRlZCI6MSwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOm51bGwsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjpudWxsLCJlcnJvcmVkIjowLCJleGNsdWRlZF9zZWdtZW50cyI6W10sImZhaWxlZCI6MCwiZmlyZWZveF9pY29uIjpudWxsLCJoZWFkaW5ncyI6eyJlbiI6IlNncmlnbmluw6wifSwiaWQiOiI4OGU1MWMyOC01NTkyLTQ1NmYtYjdlNS1mZDA2Y2I1YmUxNDgiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIkFjdGl2ZSBVc2VycyIsIkFsbCJdLCJpb3NfYmFkZ2VDb3VudCI6bnVsbCwiaW9zX2JhZGdlVHlwZSI6bnVsbCwiaW9zX2NhdGVnb3J5IjpudWxsLCJpb3Nfc291bmQiOm51bGwsImFwbnNfYWxlcnQiOm51bGwsImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjpudWxsLCJwcmlvcml0eSI6bnVsbCwicXVldWVkX2F0IjoxNTMwMTc4Mjg3LCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxNzgyODcsImNvbXBsZXRlZF9hdCI6MTUzMDE3ODI4OCwic21hbGxfaWNvbiI6bnVsbCwic3VjY2Vzc2Z1bCI6MSwidGFncyI6bnVsbCwiZmlsdGVycyI6bnVsbCwidGVtcGxhdGVfaWQiOm51bGwsInR0bCI6bnVsbCwidXJsIjpudWxsLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjpudWxsLCJ3cF93bnNfc291bmQiOm51bGx9LHsiYWRtX2JpZ19waWN0dXJlIjpudWxsLCJhZG1fZ3JvdXAiOm51bGwsImFkbV9ncm91cF9tZXNzYWdlIjpudWxsLCJhZG1fbGFyZ2VfaWNvbiI6bnVsbCwiYWRtX3NtYWxsX2ljb24iOm51bGwsImFkbV9zb3VuZCI6bnVsbCwic3Bva2VuX3RleHQiOm51bGwsImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOm51bGwsImFuZHJvaWRfYWNjZW50X2NvbG9yIjpudWxsLCJhbmRyb2lkX2dyb3VwIjpudWxsLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOm51bGwsImFuZHJvaWRfbGVkX2NvbG9yIjpudWxsLCJhbmRyb2lkX3NvdW5kIjpudWxsLCJhbmRyb2lkX3Zpc2liaWxpdHkiOm51bGwsImFwcF9pZCI6IjIyYmM2ZGVjLTUxNTAtNGQ2ZC04NjI4LTM3NzI1OWQyZGQxNCIsImJpZ19waWN0dXJlIjpudWxsLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6bnVsbCwiY2hyb21lX2ljb24iOm51bGwsImNocm9tZV93ZWJfaWNvbiI6bnVsbCwiY2hyb21lX3dlYl9pbWFnZSI6bnVsbCwiY2hyb21lX3dlYl9iYWRnZSI6bnVsbCwiY29udGVudF9hdmFpbGFibGUiOm51bGwsImNvbnRlbnRzIjp7ImVuIjoiU2dyaWduaW7DrCJ9LCJjb252ZXJ0ZWQiOjEsImRhdGEiOm51bGwsImRlbGF5ZWRfb3B0aW9uIjpudWxsLCJkZWxpdmVyeV90aW1lX29mX2RheSI6bnVsbCwiZXJyb3JlZCI6MCwiZXhjbHVkZWRfc2VnbWVudHMiOltdLCJmYWlsZWQiOjAsImZpcmVmb3hfaWNvbiI6bnVsbCwiaGVhZGluZ3MiOnsiZW4iOiJTZ3JpZ25pbsOsIn0sImlkIjoiNGFiMGYwNzMtOTc0OC00MTg5LThhYTctOTBkNzY1MDUwMzI1IiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBY3RpdmUgVXNlcnMiLCJBbGwiXSwiaW9zX2JhZGdlQ291bnQiOm51bGwsImlvc19iYWRnZVR5cGUiOm51bGwsImlvc19jYXRlZ29yeSI6bnVsbCwiaW9zX3NvdW5kIjpudWxsLCJhcG5zX2FsZXJ0IjpudWxsLCJpc0FkbSI6ZmFsc2UsImlzQW5kcm9pZCI6ZmFsc2UsImlzQ2hyb21lIjpmYWxzZSwiaXNDaHJvbWVXZWIiOmZhbHNlLCJpc0FsZXhhIjpmYWxzZSwiaXNGaXJlZm94IjpmYWxzZSwiaXNJb3MiOnRydWUsImlzU2FmYXJpIjpmYWxzZSwiaXNXUCI6ZmFsc2UsImlzV1BfV05TIjpmYWxzZSwiaXNFZGdlIjpmYWxzZSwibGFyZ2VfaWNvbiI6bnVsbCwicHJpb3JpdHkiOm51bGwsInF1ZXVlZF9hdCI6MTUzMDE3ODI2OCwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTc4MjY4LCJjb21wbGV0ZWRfYXQiOjE1MzAxNzgyNjksInNtYWxsX2ljb24iOm51bGwsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6bnVsbCwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6bnVsbCwid3Bfd25zX3NvdW5kIjpudWxsfSx7ImFkbV9iaWdfcGljdHVyZSI6IiIsImFkbV9ncm91cCI6IiIsImFkbV9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFkbV9sYXJnZV9pY29uIjoiIiwiYWRtX3NtYWxsX2ljb24iOiIiLCJhZG1fc291bmQiOiIiLCJzcG9rZW5fdGV4dCI6e30sImFsZXhhX3NzbWwiOm51bGwsImFsZXhhX2Rpc3BsYXlfdGl0bGUiOm51bGwsImFtYXpvbl9iYWNrZ3JvdW5kX2RhdGEiOmZhbHNlLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6IiIsImFuZHJvaWRfZ3JvdXAiOiIiLCJhbmRyb2lkX2dyb3VwX21lc3NhZ2UiOnsiZW4iOiIifSwiYW5kcm9pZF9sZWRfY29sb3IiOiIiLCJhbmRyb2lkX3NvdW5kIjoiIiwiYW5kcm9pZF92aXNpYmlsaXR5IjoxLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6IiIsImJ1dHRvbnMiOm51bGwsImNhbmNlbGVkIjpmYWxzZSwiY2hyb21lX2JpZ19waWN0dXJlIjoiIiwiY2hyb21lX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ljb24iOiIiLCJjaHJvbWVfd2ViX2ltYWdlIjoiIiwiY2hyb21lX3dlYl9iYWRnZSI6IiIsImNvbnRlbnRfYXZhaWxhYmxlIjpmYWxzZSwiY29udGVudHMiOnsiZW4iOiJTZ3JpZ25pZ27DrCJ9LCJjb252ZXJ0ZWQiOjEsImRhdGEiOm51bGwsImRlbGF5ZWRfb3B0aW9uIjoiaW1tZWRpYXRlIiwiZGVsaXZlcnlfdGltZV9vZl9kYXkiOiIxMToyNEFNIiwiZXJyb3JlZCI6MCwiZXhjbHVkZWRfc2VnbWVudHMiOltdLCJmYWlsZWQiOjAsImZpcmVmb3hfaWNvbiI6IiIsImhlYWRpbmdzIjp7ImVuIjoiQ2lhbyAyIn0sImlkIjoiOTljMmE5ODQtNTczMy00MGViLWIwYjYtY2I2MTVlYjI0YmE5IiwiaW5jbHVkZV9wbGF5ZXJfaWRzIjpudWxsLCJpbmNsdWRlZF9zZWdtZW50cyI6WyJBbGwiXSwiaW9zX2JhZGdlQ291bnQiOjEsImlvc19iYWRnZVR5cGUiOiJOb25lIiwiaW9zX2NhdGVnb3J5IjoiIiwiaW9zX3NvdW5kIjoiIiwiYXBuc19hbGVydCI6e30sImlzQWRtIjpmYWxzZSwiaXNBbmRyb2lkIjpmYWxzZSwiaXNDaHJvbWUiOmZhbHNlLCJpc0Nocm9tZVdlYiI6ZmFsc2UsImlzQWxleGEiOmZhbHNlLCJpc0ZpcmVmb3giOmZhbHNlLCJpc0lvcyI6dHJ1ZSwiaXNTYWZhcmkiOmZhbHNlLCJpc1dQIjpmYWxzZSwiaXNXUF9XTlMiOmZhbHNlLCJpc0VkZ2UiOmZhbHNlLCJsYXJnZV9pY29uIjoiIiwicHJpb3JpdHkiOjUsInF1ZXVlZF9hdCI6MTUzMDE3NzkwNSwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTc3OTA1LCJjb21wbGV0ZWRfYXQiOjE1MzAxNzc5MDYsInNtYWxsX2ljb24iOiIiLCJzdWNjZXNzZnVsIjoxLCJ0YWdzIjpudWxsLCJmaWx0ZXJzIjpudWxsLCJ0ZW1wbGF0ZV9pZCI6bnVsbCwidHRsIjpudWxsLCJ1cmwiOiIiLCJ3ZWJfYnV0dG9ucyI6bnVsbCwid2ViX3B1c2hfdG9waWMiOm51bGwsIndwX3NvdW5kIjoiIiwid3Bfd25zX3NvdW5kIjoiIn0seyJhZG1fYmlnX3BpY3R1cmUiOiIiLCJhZG1fZ3JvdXAiOiIiLCJhZG1fZ3JvdXBfbWVzc2FnZSI6eyJlbiI6IiJ9LCJhZG1fbGFyZ2VfaWNvbiI6IiIsImFkbV9zbWFsbF9pY29uIjoiIiwiYWRtX3NvdW5kIjoiIiwic3Bva2VuX3RleHQiOnt9LCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpmYWxzZSwiYW5kcm9pZF9hY2NlbnRfY29sb3IiOiIiLCJhbmRyb2lkX2dyb3VwIjoiIiwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjp7ImVuIjoiIn0sImFuZHJvaWRfbGVkX2NvbG9yIjoiIiwiYW5kcm9pZF9zb3VuZCI6IiIsImFuZHJvaWRfdmlzaWJpbGl0eSI6MSwiYXBwX2lkIjoiMjJiYzZkZWMtNTE1MC00ZDZkLTg2MjgtMzc3MjU5ZDJkZDE0IiwiYmlnX3BpY3R1cmUiOiIiLCJidXR0b25zIjpudWxsLCJjYW5jZWxlZCI6ZmFsc2UsImNocm9tZV9iaWdfcGljdHVyZSI6IiIsImNocm9tZV9pY29uIjoiIiwiY2hyb21lX3dlYl9pY29uIjoiIiwiY2hyb21lX3dlYl9pbWFnZSI6IiIsImNocm9tZV93ZWJfYmFkZ2UiOiIiLCJjb250ZW50X2F2YWlsYWJsZSI6ZmFsc2UsImNvbnRlbnRzIjp7ImVuIjoiU2dyaWduaWduw6AifSwiY29udmVydGVkIjoxLCJkYXRhIjpudWxsLCJkZWxheWVkX29wdGlvbiI6ImltbWVkaWF0ZSIsImRlbGl2ZXJ5X3RpbWVfb2ZfZGF5IjoiMTE6MjNBTSIsImVycm9yZWQiOjAsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOiIiLCJoZWFkaW5ncyI6eyJlbiI6IkNpYW8ifSwiaWQiOiJjOGQ1YTkzYi1jZDU4LTQ0MzktOTk2Ni1hZTc3YWNmMTE3MGQiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIkFjdGl2ZSBVc2VycyJdLCJpb3NfYmFkZ2VDb3VudCI6MSwiaW9zX2JhZGdlVHlwZSI6Ik5vbmUiLCJpb3NfY2F0ZWdvcnkiOiIiLCJpb3Nfc291bmQiOiIiLCJhcG5zX2FsZXJ0Ijp7fSwiaXNBZG0iOmZhbHNlLCJpc0FuZHJvaWQiOmZhbHNlLCJpc0Nocm9tZSI6ZmFsc2UsImlzQ2hyb21lV2ViIjpmYWxzZSwiaXNBbGV4YSI6ZmFsc2UsImlzRmlyZWZveCI6ZmFsc2UsImlzSW9zIjp0cnVlLCJpc1NhZmFyaSI6ZmFsc2UsImlzV1AiOmZhbHNlLCJpc1dQX1dOUyI6ZmFsc2UsImlzRWRnZSI6ZmFsc2UsImxhcmdlX2ljb24iOiIiLCJwcmlvcml0eSI6NSwicXVldWVkX2F0IjoxNTMwMTc3ODc3LCJyZW1haW5pbmciOjAsInNlbmRfYWZ0ZXIiOjE1MzAxNzc4NzcsImNvbXBsZXRlZF9hdCI6MTUzMDE3Nzg3OCwic21hbGxfaWNvbiI6IiIsInN1Y2Nlc3NmdWwiOjEsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6IiIsIndlYl9idXR0b25zIjpudWxsLCJ3ZWJfcHVzaF90b3BpYyI6bnVsbCwid3Bfc291bmQiOiIiLCJ3cF93bnNfc291bmQiOiIifSx7ImFkbV9iaWdfcGljdHVyZSI6bnVsbCwiYWRtX2dyb3VwIjpudWxsLCJhZG1fZ3JvdXBfbWVzc2FnZSI6bnVsbCwiYWRtX2xhcmdlX2ljb24iOm51bGwsImFkbV9zbWFsbF9pY29uIjpudWxsLCJhZG1fc291bmQiOm51bGwsInNwb2tlbl90ZXh0IjpudWxsLCJhbGV4YV9zc21sIjpudWxsLCJhbGV4YV9kaXNwbGF5X3RpdGxlIjpudWxsLCJhbWF6b25fYmFja2dyb3VuZF9kYXRhIjpudWxsLCJhbmRyb2lkX2FjY2VudF9jb2xvciI6bnVsbCwiYW5kcm9pZF9ncm91cCI6bnVsbCwiYW5kcm9pZF9ncm91cF9tZXNzYWdlIjpudWxsLCJhbmRyb2lkX2xlZF9jb2xvciI6bnVsbCwiYW5kcm9pZF9zb3VuZCI6bnVsbCwiYW5kcm9pZF92aXNpYmlsaXR5IjpudWxsLCJhcHBfaWQiOiIyMmJjNmRlYy01MTUwLTRkNmQtODYyOC0zNzcyNTlkMmRkMTQiLCJiaWdfcGljdHVyZSI6bnVsbCwiYnV0dG9ucyI6bnVsbCwiY2FuY2VsZWQiOmZhbHNlLCJjaHJvbWVfYmlnX3BpY3R1cmUiOm51bGwsImNocm9tZV9pY29uIjpudWxsLCJjaHJvbWVfd2ViX2ljb24iOm51bGwsImNocm9tZV93ZWJfaW1hZ2UiOm51bGwsImNocm9tZV93ZWJfYmFkZ2UiOm51bGwsImNvbnRlbnRfYXZhaWxhYmxlIjpudWxsLCJjb250ZW50cyI6e30sImNvbnZlcnRlZCI6MCwiZGF0YSI6bnVsbCwiZGVsYXllZF9vcHRpb24iOiJpbW1lZGlhdGUiLCJkZWxpdmVyeV90aW1lX29mX2RheSI6IjU6MTJQTSIsImVycm9yZWQiOjEsImV4Y2x1ZGVkX3NlZ21lbnRzIjpbXSwiZmFpbGVkIjowLCJmaXJlZm94X2ljb24iOm51bGwsImhlYWRpbmdzIjp7fSwiaWQiOiJiNDVjZTE5Ny1hNzliLTRlMzYtOTkyMy01NGE5MjU5YzE0NWIiLCJpbmNsdWRlX3BsYXllcl9pZHMiOm51bGwsImluY2x1ZGVkX3NlZ21lbnRzIjpbIlRlc3QgVXNlcnMiXSwiaW9zX2JhZGdlQ291bnQiOm51bGwsImlvc19iYWRnZVR5cGUiOm51bGwsImlvc19jYXRlZ29yeSI6bnVsbCwiaW9zX3NvdW5kIjpudWxsLCJhcG5zX2FsZXJ0Ijp7fSwiaXNBZG0iOm51bGwsImlzQW5kcm9pZCI6bnVsbCwiaXNDaHJvbWUiOm51bGwsImlzQ2hyb21lV2ViIjpudWxsLCJpc0FsZXhhIjpudWxsLCJpc0ZpcmVmb3giOm51bGwsImlzSW9zIjpudWxsLCJpc1NhZmFyaSI6bnVsbCwiaXNXUCI6bnVsbCwiaXNXUF9XTlMiOm51bGwsImlzRWRnZSI6bnVsbCwibGFyZ2VfaWNvbiI6bnVsbCwicHJpb3JpdHkiOm51bGwsInF1ZXVlZF9hdCI6MTUzMDExMjQ2MSwicmVtYWluaW5nIjowLCJzZW5kX2FmdGVyIjoxNTMwMTEyNDYxLCJjb21wbGV0ZWRfYXQiOjE1MzAxMTI0NjMsInNtYWxsX2ljb24iOm51bGwsInN1Y2Nlc3NmdWwiOjAsInRhZ3MiOm51bGwsImZpbHRlcnMiOm51bGwsInRlbXBsYXRlX2lkIjpudWxsLCJ0dGwiOm51bGwsInVybCI6bnVsbCwid2ViX2J1dHRvbnMiOm51bGwsIndlYl9wdXNoX3RvcGljIjpudWxsLCJ3cF9zb3VuZCI6bnVsbCwid3Bfd25zX3NvdW5kIjpudWxsfV19 74 | http_version: 75 | recorded_at: Thu, 28 Jun 2018 15:30:58 GMT 76 | recorded_with: VCR 4.0.0 77 | --------------------------------------------------------------------------------