facebook_ads 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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