facebook_ads 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +16 -0
  5. data/Gemfile.lock +129 -0
  6. data/README.markdown +380 -0
  7. data/bin/console +13 -0
  8. data/facebook_ads.gemspec +26 -0
  9. data/lib/facebook_ads.rb +46 -0
  10. data/lib/facebook_ads/ad.rb +34 -0
  11. data/lib/facebook_ads/ad_account.rb +126 -0
  12. data/lib/facebook_ads/ad_campaign.rb +54 -0
  13. data/lib/facebook_ads/ad_creative.rb +66 -0
  14. data/lib/facebook_ads/ad_image.rb +28 -0
  15. data/lib/facebook_ads/ad_insight.rb +24 -0
  16. data/lib/facebook_ads/ad_set.rb +35 -0
  17. data/lib/facebook_ads/ad_targeting.rb +61 -0
  18. data/lib/facebook_ads/base.rb +151 -0
  19. data/spec/ad_account_spec.rb +78 -0
  20. data/spec/ad_campaign_spec.rb +13 -0
  21. data/spec/ad_creative_spec.rb +14 -0
  22. data/spec/ad_image_spec.rb +11 -0
  23. data/spec/ad_insight_spec.rb +11 -0
  24. data/spec/ad_set_spec.rb +13 -0
  25. data/spec/ad_spec.rb +13 -0
  26. data/spec/ad_targeting_spec.rb +4 -0
  27. data/spec/spec_helper.rb +15 -0
  28. data/spec/support/fixtures.sh +17 -0
  29. data/spec/support/fixtures/6057330925170.json +1 -0
  30. data/spec/support/fixtures/6057810634370.json +1 -0
  31. data/spec/support/fixtures/6057810946970.json +1 -0
  32. data/spec/support/fixtures/6057824295570.json +1 -0
  33. data/spec/support/fixtures/act_861827983860489.json +1 -0
  34. data/spec/support/fixtures/act_861827983860489/adcreatives.json +1 -0
  35. data/spec/support/fixtures/act_861827983860489/adimages.json +1 -0
  36. data/spec/support/fixtures/act_861827983860489/ads.json +1 -0
  37. data/spec/support/fixtures/act_861827983860489/adsets.json +1 -0
  38. data/spec/support/fixtures/act_861827983860489/campaigns.json +1 -0
  39. data/spec/support/fixtures/me/adaccounts.json +1 -0
  40. data/spec/support/rack_facebook.rb +22 -0
  41. metadata +127 -0
data/bin/console ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'facebook_ads'
5
+ require 'awesome_print'
6
+ require 'pry'
7
+
8
+ FacebookAds.access_token = File.read('test_access_token').squish
9
+ FacebookAds.logger = Logger.new(STDOUT)
10
+ FacebookAds.logger.level = Logger::Severity::DEBUG
11
+
12
+ AwesomePrint.pry!
13
+ binding.pry
@@ -0,0 +1,26 @@
1
+ # To release a new version:
2
+ # gem build facebook_ads.gemspec
3
+ # gem push facebook_ads-0.1.5.gem
4
+ Gem::Specification.new do |s|
5
+ s.name = 'facebook_ads'
6
+ s.version = '0.1.5'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.licenses = ['MIT']
9
+ s.authors = ['Chris Estreich']
10
+ s.email = 'cestreich@gmail.com'
11
+ s.homepage = 'https://github.com/cte/facebook-ads-sdk-ruby'
12
+ s.summary = "Facebook Marketing API SDK for Ruby."
13
+ s.description = "This gem allows to easily manage your Facebook ads via Facebook's Marketing API in ruby."
14
+
15
+ s.extra_rdoc_files = ['README.markdown']
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
19
+ s.require_paths = ['lib']
20
+
21
+ s.required_ruby_version = '~> 2.0'
22
+
23
+ s.add_dependency 'activesupport', '~> 4.2'
24
+ s.add_dependency 'httmultiparty', '~> 0.3'
25
+ s.add_dependency 'hashie', '~> 3.4'
26
+ end
@@ -0,0 +1,46 @@
1
+ # External requires.
2
+ require 'active_support/all'
3
+ require 'httmultiparty'
4
+ require 'hashie'
5
+
6
+ # Internal requires.
7
+ require 'facebook_ads/base'
8
+ Dir[File.expand_path('../facebook_ads/*.rb', __FILE__)].each { |f| require f }
9
+
10
+ # The primary namespace for this gem.
11
+ module FacebookAds
12
+
13
+ def self.logger=(logger)
14
+ @logger = logger
15
+ end
16
+
17
+ def self.logger
18
+ unless defined?(@logger)
19
+ @logger = Logger.new('/dev/null')
20
+ @logger.level = Logger::Severity::UNKNOWN
21
+ end
22
+
23
+ @logger
24
+ end
25
+
26
+ def self.base_uri=(base_uri)
27
+ @base_uri = base_uri
28
+ end
29
+
30
+ def self.base_uri
31
+ unless defined?(@base_uri)
32
+ @base_uri = 'https://graph.facebook.com/v2.6'
33
+ end
34
+
35
+ @base_uri
36
+ end
37
+
38
+ def self.access_token=(access_token)
39
+ @access_token = access_token
40
+ end
41
+
42
+ def self.access_token
43
+ @access_token
44
+ end
45
+
46
+ end
@@ -0,0 +1,34 @@
1
+ module FacebookAds
2
+ # An ad belongs to an ad set. It is created using an ad creative.
3
+ # https://developers.facebook.com/docs/marketing-api/reference/adgroup
4
+ class Ad < Base
5
+
6
+ FIELDS = %w(id account_id campaign_id adset_id adlabels bid_amount bid_info bid_type configured_status conversion_specs created_time creative effective_status last_updated_by_app_id name tracking_specs updated_time ad_review_feedback)
7
+ STATUSES = %w(ACTIVE PAUSED DELETED PENDING_REVIEW DISAPPROVED PREAPPROVED PENDING_BILLING_INFO CAMPAIGN_PAUSED ARCHIVED ADSET_PAUSED)
8
+
9
+ # belongs_to ad_account
10
+
11
+ def ad_account
12
+ @ad_set ||= FacebookAds::AdAccount.find(account_id)
13
+ end
14
+
15
+ # belongs_to ad_campaign
16
+
17
+ def ad_campaign
18
+ @ad_set ||= FacebookAds::AdCampaign.find(campaign_id)
19
+ end
20
+
21
+ # belongs_to ad_set
22
+
23
+ def ad_set
24
+ @ad_set ||= FacebookAds::AdSet.find(adset_id)
25
+ end
26
+
27
+ # belongs_to ad_creative
28
+
29
+ def ad_creative
30
+ @ad_creative ||= FacebookAds::AdCreative.find(creative['id'])
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,126 @@
1
+ module FacebookAds
2
+ # An ad account has many ad campaigns, ad images, and ad creatives.
3
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-account
4
+ class AdAccount < Base
5
+
6
+ FIELDS = %w(id account_id account_status age created_time currency name last_used_time)
7
+
8
+ class << self
9
+ def all
10
+ get('/me/adaccounts', objectify: true)
11
+ end
12
+
13
+ def find_by(conditions)
14
+ all.detect do |ad_account|
15
+ conditions.all? do |key, value|
16
+ ad_account.send(key) == value
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ # has_many campaigns
23
+
24
+ def ad_campaigns(effective_status: ['ACTIVE'], limit: 100)
25
+ FacebookAds::AdCampaign.paginate("/#{id}/campaigns", query: { effective_status: effective_status, limit: limit })
26
+ end
27
+
28
+ def create_ad_campaign(name:, objective:, status: 'ACTIVE')
29
+ raise Exception, "Objective must be one of: #{FacebookAds::AdCampaign::OBJECTIVES.to_sentence}" unless FacebookAds::AdCampaign::OBJECTIVES.include?(objective)
30
+ raise Exception, "Status must be one of: #{FacebookAds::AdCampaign::STATUSES.to_sentence}" unless FacebookAds::AdCampaign::STATUSES.include?(status)
31
+ campaign = FacebookAds::AdCampaign.post("/#{id}/campaigns", query: { name: name, objective: objective, status: status }, objectify: true)
32
+ FacebookAds::AdCampaign.find(campaign.id)
33
+ end
34
+
35
+ # has_many ad_images
36
+
37
+ def ad_images(hashes: nil, limit: 100)
38
+ if hashes.present?
39
+ FacebookAds::AdImage.get("/#{id}/adimages", query: { hashes: hashes }, objectify: true)
40
+ else
41
+ FacebookAds::AdImage.paginate("/#{id}/adimages", query: { limit: limit })
42
+ end
43
+ end
44
+
45
+ def create_ad_images(urls)
46
+ files = urls.collect do |url|
47
+ pathname = Pathname.new(url)
48
+ name = "#{pathname.dirname.basename}.jpg"
49
+ data = HTTParty.get(url).body
50
+ file = File.open("/tmp/#{name}", 'w') # Assume *nix-based system.
51
+ file.binmode
52
+ file.write(data)
53
+ file.close
54
+ [name, File.open(file.path)]
55
+ end.to_h
56
+
57
+ response = FacebookAds::AdImage.post("/#{id}/adimages", query: files, objectify: false)
58
+ files.values.each { |file| File.delete(file.path) }
59
+
60
+ if response['images'].present?
61
+ hashes = response['images'].map { |key, hash| hash['hash'] }
62
+ ad_images(hashes: hashes)
63
+ else
64
+ []
65
+ end
66
+ end
67
+
68
+ # has_many ad_creatives
69
+
70
+ def ad_creatives(limit: 100)
71
+ FacebookAds::AdCreative.paginate("/#{id}/adcreatives", query: { limit: limit })
72
+ end
73
+
74
+ def create_ad_creative(creative, carousel: true)
75
+ optional = %i(instagram_actor_id)
76
+
77
+ required = if carousel
78
+ %i(name page_id link message assets call_to_action_type multi_share_optimized multi_share_end_card)
79
+ else
80
+ %i(name page_id message link link_title image_hash call_to_action_type)
81
+ end
82
+
83
+ if (keys = required - creative.keys).present?
84
+ raise Exception, "Creative is missing the following: #{keys.to_sentence}"
85
+ end
86
+
87
+ raise Exception, "Creative call_to_action_type must be one of: #{FacebookAds::AdCreative::CALL_TO_ACTION_TYPES.to_sentence}" unless FacebookAds::AdCreative::CALL_TO_ACTION_TYPES.include?(creative[:call_to_action_type])
88
+
89
+ query = if carousel
90
+ FacebookAds::AdCreative.carousel(creative)
91
+ else
92
+ FacebookAds::AdCreative.photo(creative)
93
+ end
94
+
95
+ creative = FacebookAds::AdCreative.post("/#{id}/adcreatives", query: query, objectify: true) # Returns a FacebookAds::AdCreative instance.
96
+ FacebookAds::AdCreative.find(creative.id)
97
+ end
98
+
99
+ # has_many ad_sets
100
+
101
+ def ad_sets(effective_status: ['ACTIVE'], limit: 100)
102
+ FacebookAds::AdSet.paginate("/#{id}/adsets", query: { effective_status: effective_status, limit: limit })
103
+ end
104
+
105
+ # has_many ads
106
+
107
+ def ads(effective_status: ['ACTIVE'], limit: 100)
108
+ FacebookAds::Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
109
+ end
110
+
111
+ # has_many ad_insights
112
+
113
+ def ad_insights(range: Date.today..Date.today, level: 'ad', time_increment: 1)
114
+ ad_campaigns.map do |ad_campaign|
115
+ ad_campaign.ad_insights(range: range, level: level, time_increment: time_increment)
116
+ end.flatten
117
+ end
118
+
119
+ # has_many applications
120
+
121
+ def applications
122
+ self.class.get("/#{id}/advertisable_applications", objectify: false)
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,54 @@
1
+ module FacebookAds
2
+ # An ad campaign has many ad sets and belongs to an ad account.
3
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group
4
+ class AdCampaign < Base
5
+
6
+ FIELDS = %w(id account_id buying_type can_use_spend_cap configured_status created_time effective_status name objective start_time stop_time updated_time spend_cap)
7
+ STATUSES = %w(ACTIVE PAUSED DELETED PENDING_REVIEW DISAPPROVED PREAPPROVED PENDING_BILLING_INFO CAMPAIGN_PAUSED ARCHIVED ADSET_PAUSED)
8
+ OBJECTIVES = %w(CONVERSIONS MOBILE_APP_INSTALLS) # %w(BRAND_AWARENESS CANVAS_APP_ENGAGEMENT CANVAS_APP_INSTALLS CONVERSIONS EVENT_RESPONSES EXTERNAL LEAD_GENERATION LINK_CLICKS LOCAL_AWARENESS MOBILE_APP_ENGAGEMENT MOBILE_APP_INSTALLS OFFER_CLAIMS PAGE_LIKES POST_ENGAGEMENT PRODUCT_CATALOG_SALES REACH VIDEO_VIEWS)
9
+
10
+ # belongs_to ad_account
11
+
12
+ def ad_account
13
+ @ad_account ||= FacebookAds::AdAccount.find("act_#{account_id}")
14
+ end
15
+
16
+ # has_many ad_sets
17
+
18
+ def ad_sets(effective_status: ['ACTIVE'], limit: 100)
19
+ FacebookAds::AdSet.paginate("/#{id}/adsets", query: { effective_status: effective_status, limit: limit })
20
+ end
21
+
22
+ def create_ad_set(name:, promoted_object:, targeting:, daily_budget:, optimization_goal:, billing_event: 'IMPRESSIONS', status: 'ACTIVE', is_autobid: true)
23
+ raise Exception, "Optimization goal must be one of: #{FacebookAds::AdSet::OPTIMIZATION_GOALS.to_sentence}" unless FacebookAds::AdSet::OPTIMIZATION_GOALS.include?(optimization_goal)
24
+ raise Exception, "Billing event must be one of: #{FacebookAds::AdSet::BILLING_EVENTS.to_sentence}" unless FacebookAds::AdSet::BILLING_EVENTS.include?(billing_event)
25
+
26
+ targeting.validate! # Will raise if invalid.
27
+
28
+ ad_set = FacebookAds::AdSet.post("/act_#{account_id}/adsets", query: { # Returns a FacebookAds::AdSet instance.
29
+ campaign_id: id,
30
+ name: name,
31
+ targeting: targeting.to_hash.to_json,
32
+ promoted_object: promoted_object.to_json,
33
+ optimization_goal: optimization_goal,
34
+ daily_budget: daily_budget,
35
+ billing_event: billing_event,
36
+ status: status,
37
+ is_autobid: is_autobid
38
+ }, objectify: true)
39
+
40
+ FacebookAds::AdSet.find(ad_set.id)
41
+ end
42
+
43
+ # has_many ad_insights
44
+
45
+ def ad_insights(range: Date.today..Date.today, level: 'ad', time_increment: 1)
46
+ FacebookAds::AdInsight.paginate("/#{id}/insights", query: {
47
+ level: level,
48
+ time_increment: time_increment,
49
+ time_range: { 'since': range.first.to_s, 'until': range.last.to_s }
50
+ })
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,66 @@
1
+ module FacebookAds
2
+ # Ad ad creative has many ad images and belongs to an ad account.
3
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-creative
4
+ class AdCreative < Base
5
+
6
+ FIELDS = %w(id name object_story_id object_story_spec object_type thumbnail_url run_status)
7
+ CALL_TO_ACTION_TYPES = %w(SHOP_NOW INSTALL_MOBILE_APP USE_MOBILE_APP SIGN_UP DOWNLOAD BUY_NOW) # %w(OPEN_LINK LIKE_PAGE SHOP_NOW PLAY_GAME INSTALL_APP USE_APP INSTALL_MOBILE_APP USE_MOBILE_APP BOOK_TRAVEL LISTEN_MUSIC WATCH_VIDEO LEARN_MORE SIGN_UP DOWNLOAD WATCH_MORE NO_BUTTON CALL_NOW BUY_NOW GET_OFFER GET_OFFER_VIEW GET_DIRECTIONS MESSAGE_PAGE SUBSCRIBE SELL_NOW DONATE_NOW GET_QUOTE CONTACT_US RECORD_NOW VOTE_NOW OPEN_MOVIES)
8
+
9
+ class << self
10
+
11
+ def photo(name:, page_id:, instagram_actor_id: nil, message:, link:, link_title:, image_hash:, call_to_action_type:)
12
+ object_story_spec = {
13
+ 'page_id' => page_id, # 300664329976860
14
+ 'instagram_actor_id' => instagram_actor_id, # 503391023081924
15
+ 'link_data' => {
16
+ 'link' => link, # https://tophatter.com/, https://itunes.apple.com/app/id619460348, http://play.google.com/store/apps/details?id=com.tophatter
17
+ 'message' => message,
18
+ 'image_hash' => image_hash,
19
+ 'call_to_action' => {
20
+ 'type' => call_to_action_type,
21
+ 'value' => {
22
+ # 'application' =>,
23
+ 'link' => link,
24
+ 'link_title' => link_title
25
+ }
26
+ }
27
+ }
28
+ }
29
+ {
30
+ name: name,
31
+ object_story_spec: object_story_spec.to_json
32
+ }
33
+ end
34
+
35
+ # https://developers.facebook.com/docs/marketing-api/guides/carousel-ads/v2.6
36
+ def carousel(name:, page_id:, instagram_actor_id: nil, link:, message:, assets:, call_to_action_type:, multi_share_optimized:, multi_share_end_card:)
37
+ object_story_spec = {
38
+ 'page_id' => page_id, # 300664329976860
39
+ 'instagram_actor_id' => instagram_actor_id, # 503391023081924
40
+ 'link_data' => {
41
+ 'link' => link, # https://tophatter.com/, https://itunes.apple.com/app/id619460348, http://play.google.com/store/apps/details?id=com.tophatter
42
+ 'message' => message,
43
+ 'call_to_action' => { 'type' => call_to_action_type },
44
+ 'child_attachments' => assets.collect { |asset|
45
+ {
46
+ 'link' => link,
47
+ 'image_hash' => asset[:hash],
48
+ 'name' => asset[:title],
49
+ # 'description' => asset[:title],
50
+ 'call_to_action' => { 'type' => call_to_action_type } # Redundant?
51
+ }
52
+ },
53
+ 'multi_share_optimized' => multi_share_optimized,
54
+ 'multi_share_end_card' => multi_share_end_card
55
+ }
56
+ }
57
+ {
58
+ name: name,
59
+ object_story_spec: object_story_spec.to_json
60
+ }
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,28 @@
1
+ module FacebookAds
2
+ # An ad image belongs to an ad account.
3
+ # An image will always produce the same hash.
4
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-image
5
+ class AdImage < Base
6
+
7
+ FIELDS = %w(id hash account_id name permalink_url original_width original_height)
8
+
9
+ class << self
10
+ def find(id)
11
+ raise Exception, 'NOT IMPLEMENTED'
12
+ end
13
+ end
14
+
15
+ def hash
16
+ self[:hash]
17
+ end
18
+
19
+ def update(data)
20
+ raise Exception, 'NOT IMPLEMENTED'
21
+ end
22
+
23
+ def destroy
24
+ super(path: "/act_#{account_id}/adimages", query: { hash: hash })
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module FacebookAds
2
+ # Ad insights exist for ad accounts, ad campaigns, ad sets, and ads.
3
+ # A lot more can be done here.
4
+ # https://developers.facebook.com/docs/marketing-api/insights/overview
5
+ class AdInsight < Base
6
+
7
+ FIELDS = %w(ad_id objective impressions unique_actions cost_per_unique_action_type clicks cpc cpm ctr spend)
8
+
9
+ class << self
10
+ def find(id)
11
+ raise Exception, 'NOT IMPLEMENTED'
12
+ end
13
+ end
14
+
15
+ def update(data)
16
+ raise Exception, 'NOT IMPLEMENTED'
17
+ end
18
+
19
+ def destroy
20
+ raise Exception, 'NOT IMPLEMENTED'
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ module FacebookAds
2
+ # An ad set belongs to a campaign and has many ads.
3
+ # https://developers.facebook.com/docs/marketing-api/reference/ad-campaign
4
+ class AdSet < Base
5
+
6
+ FIELDS = %w(id account_id campaign_id adlabels adset_schedule bid_amount bid_info billing_event configured_status created_time creative_sequence effective_status end_time frequency_cap frequency_cap_reset_period frequency_control_specs is_autobid lifetime_frequency_cap lifetime_imps name optimization_goal promoted_object rf_prediction_id rtb_flag start_time targeting updated_time use_new_app_click pacing_type budget_remaining daily_budget lifetime_budget)
7
+ STATUSES = %w(ACTIVE PAUSED DELETED PENDING_REVIEW DISAPPROVED PREAPPROVED PENDING_BILLING_INFO CAMPAIGN_PAUSED ARCHIVED ADSET_PAUSED)
8
+ BILLING_EVENTS = %w(APP_INSTALLS IMPRESSIONS) # %w(CLICKS LINK_CLICKS OFFER_CLAIMS PAGE_LIKES POST_ENGAGEMENT VIDEO_VIEWS)
9
+ OPTIMIZATION_GOALS = %w(APP_INSTALLS OFFSITE_CONVERSIONS) # %w(NONE BRAND_AWARENESS CLICKS ENGAGED_USERS EXTERNAL EVENT_RESPONSES IMPRESSIONS LEAD_GENERATION LINK_CLICKS OFFER_CLAIMS PAGE_ENGAGEMENT PAGE_LIKES POST_ENGAGEMENT REACH SOCIAL_IMPRESSIONS VIDEO_VIEWS)
10
+
11
+ # belongs_to ad_account
12
+
13
+ def ad_account
14
+ @ad_set ||= FacebookAds::AdAccount.find(account_id)
15
+ end
16
+
17
+ # belongs_to ad_campaign
18
+
19
+ def ad_campaign
20
+ @campaign ||= FacebookAds::AdCampaign.find(campaign_id)
21
+ end
22
+
23
+ # has_many ads
24
+
25
+ def ads(effective_status: ['ACTIVE'], limit: 100)
26
+ FacebookAds::Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
27
+ end
28
+
29
+ def create_ad(name:, creative_id:)
30
+ ad = FacebookAds::Ad.post("/act_#{account_id}/ads", query: { name: name, adset_id: id, creative: { creative_id: creative_id }.to_json }, objectify: true) # Returns a FacebookAds::Ad instance.
31
+ FacebookAds::Ad.find(ad.id)
32
+ end
33
+
34
+ end
35
+ end