zuck 1.0.0 → 2.0.0
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.
- checksums.yaml +5 -13
- data/CHANGELOG.markdown +35 -5
- data/Gemfile +3 -2
- data/Gemfile.lock +13 -10
- data/README.markdown +26 -38
- data/VERSION +1 -1
- data/console +8 -2
- data/lib/zuck/facebook/ad_account.rb +22 -5
- data/lib/zuck/facebook/ad_campaign.rb +12 -15
- data/lib/zuck/facebook/ad_creative.rb +14 -14
- data/lib/zuck/facebook/ad_group.rb +12 -12
- data/lib/zuck/facebook/ad_interest.rb +12 -15
- data/lib/zuck/facebook/ad_set.rb +30 -0
- data/lib/zuck/facebook/targeting_spec.rb +4 -4
- data/lib/zuck/fb_object/dsl.rb +15 -7
- data/lib/zuck/fb_object/hash_delegator.rb +20 -3
- data/lib/zuck/fb_object/write.rb +18 -6
- data/lib/zuck/fb_object.rb +1 -1
- data/lib/zuck/helpers.rb +20 -1
- data/spec/fixtures/a_single_account.yml +146 -29
- data/spec/fixtures/a_single_campaign.yml +61 -12
- data/spec/fixtures/a_single_group.yml +65 -12
- data/spec/fixtures/ad_interest_search_disney.yml +16 -14
- data/spec/fixtures/ad_interest_search_moviepilot.yml +15 -98
- data/spec/fixtures/ad_interest_search_nonexistant.yml +7 -5
- data/spec/fixtures/create_ad_campaign.yml +334 -9
- data/spec/fixtures/create_ad_group.yml +154 -9
- data/spec/fixtures/create_ad_set.yml +95 -0
- data/spec/fixtures/delete_ad_group.yml +55 -8
- data/spec/fixtures/find_a_single_group_and_update_it.yml +122 -95
- data/spec/fixtures/list_of_ad_accounts.yml +77 -58
- data/spec/fixtures/list_of_ad_campaigns.yml +77 -55
- data/spec/fixtures/list_of_ad_creatives.yml +61 -13
- data/spec/fixtures/list_of_ad_groups.yml +77 -60
- data/spec/fixtures/list_of_all_ad_creatives_of_account.yml +77 -73
- data/spec/fixtures/reach_for_invalid_interest.yml +29 -111
- data/spec/fixtures/reach_for_valid_keywords.yml +11 -54
- data/spec/fixtures/reach_for_valid_keywords_male_young.yml +11 -60
- data/spec/lib/zuck/facebook/ad_account_spec.rb +1 -1
- data/spec/lib/zuck/facebook/ad_interest_spec.rb +2 -2
- data/spec/lib/zuck/facebook/targeting_spec_spec.rb +13 -21
- data/spec/lib/zuck_spec.rb +34 -23
- data/spec/spec_helper.rb +2 -5
- data/test_access_token +1 -0
- data/zuck.gemspec +15 -9
- metadata +63 -46
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZjllZWZjNTgxYzdhYmRiNjQ1YmE2OTY5NTI5MmMyNmFjY2M2NDgyYQ==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 40becd6a7fffa32053f38e6e550c76ba84480044
|
4
|
+
data.tar.gz: 9ad567305d626e899c9b3bc1563297df9f49164b
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
MWM5OWYwYTAyNmM1ODA4MTM1MmExYzI0MWM4YjM1N2MxNzc4NGFmMGYzMDc5
|
11
|
-
ODMwNzkwNTEzMThhYzgzYTE4ZWEyMWQ3NDM4NTdmMmZlMmY5Nzk=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
NTZkMDQ0NzZlYzQyMGVmMmJiNjcwZmU2NmYzOTgwZTM1OWY2MGEwNjJhODQ3
|
14
|
-
YjcxMThmNzkxNDllOWYxMDhkMzhmMTBkNDk2ODg1ODFjY2RmYzBiODRjZDY2
|
15
|
-
MWMzZjkxNjhkNThmYjJkN2UxYTM3ZjM3ODE4Njg2ZGU1NDBlM2I=
|
6
|
+
metadata.gz: 5fc4c51ef900b5b8d4084500aa339ee2d0e3320d2c89baa9ccf1ce044ee965eb379e91c22010d79e68c47281c7f1988d0622f42ca1d296d6d0b3904def9fe5e0
|
7
|
+
data.tar.gz: 42e866685f4fefc664e59871b98db3428a0407144a7b4ce015fc68fc358fbc4e313318b8f27a73fd8ec187292555be917ce9152ac849591e2ecbd05a42b7f592
|
data/CHANGELOG.markdown
CHANGED
@@ -1,5 +1,35 @@
|
|
1
|
+
2.0.0
|
2
|
+
=====
|
3
|
+
|
4
|
+
- update Koala to 1.10.1
|
5
|
+
- use the `/search?type=adinterestq=interest` endpoint to validate ad interests
|
6
|
+
instead of `/search?type=adinterestvalid&interest_list=['interest']`
|
7
|
+
- use json objects instead of string values for ad interests
|
8
|
+
- don't escape commas with %2C when passing searches on to koala
|
9
|
+
- update ad account properties
|
10
|
+
- fix consecutive `#save`s without a `#reload` in between
|
11
|
+
|
12
|
+
Breaking changes:
|
13
|
+
----------------
|
14
|
+
|
15
|
+
If you upgrade from 1.x, make sure you read the following:
|
16
|
+
|
17
|
+
- `Zuck::AdInterest.search` results use the `audience_size` key in order to use
|
18
|
+
the same names as the facebook api. In zuck 1.x it was `audience`
|
19
|
+
- `Zuck::TargetingSpec`
|
20
|
+
- `initialize` does not take string interests anymore,
|
21
|
+
but expects `{id: '123', name: 'foo'} objects, see
|
22
|
+
"[Specifying values for interests as a string will be sunset in favor of a JSON object of id and name](https://developers.facebook.com/docs/apps/migrations/ads-api-changes-2014-04-09)"
|
23
|
+
- in your targeting spec, don't specify `{countries: ['US']}`, but
|
24
|
+
`{geo_locations: { countries: ['US']}}`
|
25
|
+
- `Zuck::AdInterest.best_guess` returns an ad interest object, what was
|
26
|
+
formerly the `:interest` key is now called `:name`
|
27
|
+
- Ad Campaigns are now called Ad Sets, and Ad Sets can be grouped
|
28
|
+
together in Ad Campaigns [see docs](https://developers.facebook.com/docs/reference/ads-api/adcampaign/v2.2)
|
29
|
+
|
30
|
+
|
1
31
|
1.0.0
|
2
|
-
|
32
|
+
=====
|
3
33
|
- Implement Facebook's April 9 breaking changes, which also change this
|
4
34
|
gem
|
5
35
|
- AdKeyword/keywords are gone, welcome AdInterest/interests
|
@@ -10,16 +40,16 @@ docs](https://developers.facebook.com/docs/reference/ads-api/interest-targeting)
|
|
10
40
|
docs](https://developers.facebook.com/docs/reference/ads-api/targeting-specs))
|
11
41
|
|
12
42
|
0.2.0
|
13
|
-
|
43
|
+
=====
|
14
44
|
- implement Facebook's [Oct 2013 breaking changes](https://developers.facebook.com/roadmap/#q4_2013)
|
15
45
|
|
16
46
|
0.0.9
|
17
|
-
|
47
|
+
=====
|
18
48
|
- add Zuck::TargetingSpec.valid_countries? method to allow for country
|
19
49
|
validation from the outside
|
20
50
|
|
21
51
|
0.0.8
|
22
|
-
|
52
|
+
=====
|
23
53
|
- validate the countries before fetching a reach estimate
|
24
54
|
|
25
55
|
0.0.7
|
@@ -28,5 +58,5 @@ docs](https://developers.facebook.com/docs/reference/ads-api/targeting-specs))
|
|
28
58
|
with a # as a prefix
|
29
59
|
|
30
60
|
0.0.4
|
31
|
-
|
61
|
+
=====
|
32
62
|
- integrated targeting spec to fetch reach estimates
|
data/Gemfile
CHANGED
@@ -2,7 +2,7 @@ source 'http://rubygems.org'
|
|
2
2
|
|
3
3
|
gem 'rvm'
|
4
4
|
gem 'bundler'
|
5
|
-
gem 'koala'
|
5
|
+
gem 'koala'
|
6
6
|
gem 'activesupport'
|
7
7
|
|
8
8
|
group :development do
|
@@ -16,11 +16,12 @@ group :development, :test do
|
|
16
16
|
gem 'webmock', '~>1.8.0'
|
17
17
|
gem 'rspec'
|
18
18
|
gem 'vcr'
|
19
|
+
gem 'pry'
|
19
20
|
end
|
20
21
|
|
21
22
|
platform :ruby do
|
22
23
|
group :development do
|
23
|
-
gem 'guard'
|
24
|
+
gem 'guard-ctags-bundler'
|
24
25
|
gem 'guard-rspec'
|
25
26
|
gem 'guard-bundler'
|
26
27
|
gem 'guard-yard'
|
data/Gemfile.lock
CHANGED
@@ -4,15 +4,15 @@ GEM
|
|
4
4
|
activesupport (3.2.9)
|
5
5
|
i18n (~> 0.6)
|
6
6
|
multi_json (~> 1.0)
|
7
|
-
addressable (2.3.
|
7
|
+
addressable (2.3.6)
|
8
8
|
bourne (1.1.2)
|
9
9
|
mocha (= 0.10.5)
|
10
10
|
builder (3.2.2)
|
11
11
|
coderay (1.0.8)
|
12
12
|
crack (0.3.1)
|
13
13
|
diff-lcs (1.1.3)
|
14
|
-
faraday (0.8.
|
15
|
-
multipart-post (~> 1.
|
14
|
+
faraday (0.8.9)
|
15
|
+
multipart-post (~> 1.2.0)
|
16
16
|
git (1.2.6)
|
17
17
|
github_api (0.10.2)
|
18
18
|
addressable
|
@@ -30,6 +30,8 @@ GEM
|
|
30
30
|
guard-bundler (1.0.0)
|
31
31
|
bundler (~> 1.0)
|
32
32
|
guard (~> 1.1)
|
33
|
+
guard-ctags-bundler (0.2.0)
|
34
|
+
guard (>= 1.1)
|
33
35
|
guard-rspec (2.3.3)
|
34
36
|
guard (>= 1.1)
|
35
37
|
rspec (~> 2.11)
|
@@ -52,10 +54,10 @@ GEM
|
|
52
54
|
json (1.8.1-java)
|
53
55
|
jwt (0.1.11)
|
54
56
|
multi_json (>= 1.5)
|
55
|
-
koala (1.
|
56
|
-
addressable
|
57
|
-
faraday
|
58
|
-
multi_json
|
57
|
+
koala (1.10.1)
|
58
|
+
addressable
|
59
|
+
faraday
|
60
|
+
multi_json
|
59
61
|
listen (0.6.0)
|
60
62
|
lumberjack (1.0.2)
|
61
63
|
metaclass (0.0.1)
|
@@ -63,7 +65,7 @@ GEM
|
|
63
65
|
mini_portile (0.5.3)
|
64
66
|
mocha (0.10.5)
|
65
67
|
metaclass (~> 0.0.1)
|
66
|
-
multi_json (1.
|
68
|
+
multi_json (1.10.1)
|
67
69
|
multi_xml (0.5.5)
|
68
70
|
multipart-post (1.2.0)
|
69
71
|
nokogiri (1.6.1)
|
@@ -122,12 +124,13 @@ DEPENDENCIES
|
|
122
124
|
activesupport
|
123
125
|
bundler
|
124
126
|
growl
|
125
|
-
guard
|
126
127
|
guard-bundler
|
128
|
+
guard-ctags-bundler
|
127
129
|
guard-rspec
|
128
130
|
guard-yard
|
129
131
|
jeweler (~> 2.0.1)
|
130
|
-
koala
|
132
|
+
koala
|
133
|
+
pry
|
131
134
|
rb-fsevent
|
132
135
|
rdoc (~> 3.12)
|
133
136
|
redcarpet
|
data/README.markdown
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
Zuck; use facebook's advertisement API with ruby
|
4
4
|
================
|
5
5
|
|
6
|
-
This is a little gem that makes access to facebook's
|
6
|
+
This is a little gem that makes access to facebook's
|
7
7
|
ads API a little easier. Check out facebook's
|
8
8
|
[documentation](https://developers.facebook.com/docs/reference/ads-api/)
|
9
9
|
for a nice diagram that explains how things work.
|
@@ -28,16 +28,12 @@ accounts = Zuck::AdAccount.all
|
|
28
28
|
|
29
29
|
# Let's look at an account
|
30
30
|
my_account = accounts.first
|
31
|
-
=> #<Zuck::AdAccount id: "act_10150585630710217", account_id: "10150585630710217", name: "", account_status: 1, currency: "USD", timezone_id: 47, timezone_name: "Europe/Berlin", timezone_offset_hours_utc: 2, is_personal: 0, business_name: "Big Mike Alright UG (haftungsbeschr\u00e4nkt)", business_street: "Big Mike Alright UG (haftungsbeschr\u00e4nkt)", business_street2: "J\u00e4gerndorfer Zeile 61", business_city: "Berlin", business_state: "Berlin", business_zip: "12209", business_country_code: "DE", vat_status: 3, daily_spend_limit: 25000, users: [{"uid":501730216,"permissions":[1,2,3,4,5,7],"role":1001}], notification_settings: {"501730216":{"1000":{"1":1},"1001":{"1":1},"1002":{"1":1,"2":60},"1003":{"1":1,"2":60},"1004":{"1":1},"1005":{"1":1},"1006":{"1":1},"1009":{"1":1},"1010":{"1":1},"1011":{"1":1},"2000":{"1":1,"2":60},"2001":{"1":1,"2":60},"2002":{"2":60},"2003":{"1":1,"2":60},"2004":{"1":1,"2":60},"2005":{"1":1,"2":60},"3000":{"1":1,"2":60},"3001":{"1":1,"2":60},"3002":{"2":60},"3003":{"1":1,"2":60},"5000":{"1":1},"6000":{"1":1},"6001":{"1":1},"9000":{"1":1,"2":60},"8000":{"1":1,"2":60}}}, capabilities: [], balance: 0, moo_default_conversion_bid: 1000, moo_default_bid: 1000>
|
31
|
+
=> #<Zuck::AdAccount id: "act_10150585630710217", account_id: "10150585630710217", name: "", account_status: 1, currency: "USD", timezone_id: 47, timezone_name: "Europe/Berlin", timezone_offset_hours_utc: 2, is_personal: 0, business_name: "Big Mike Alright UG (haftungsbeschr\u00e4nkt)", business_street: "Big Mike Alright UG (haftungsbeschr\u00e4nkt)", business_street2: "J\u00e4gerndorfer Zeile 61", business_city: "Berlin", business_state: "Berlin", business_zip: "12209", business_country_code: "DE", vat_status: 3, daily_spend_limit: 25000, users: [{"uid":501730216,"permissions":[1,2,3,4,5,7],"role":1001}], notification_settings: {"501730216":{"1000":{"1":1},"1001":{"1":1},"1002":{"1":1,"2":60},"1003":{"1":1,"2":60},"1004":{"1":1},"1005":{"1":1},"1006":{"1":1},"1009":{"1":1},"1010":{"1":1},"1011":{"1":1},"2000":{"1":1,"2":60},"2001":{"1":1,"2":60},"2002":{"2":60},"2003":{"1":1,"2":60},"2004":{"1":1,"2":60},"2005":{"1":1,"2":60},"3000":{"1":1,"2":60},"3001":{"1":1,"2":60},"3002":{"2":60},"3003":{"1":1,"2":60},"5000":{"1":1},"6000":{"1":1},"6001":{"1":1},"9000":{"1":1,"2":60},"8000":{"1":1,"2":60}}}, capabilities: [], balance: 0, moo_default_conversion_bid: 1000, moo_default_bid: 1000>
|
32
32
|
|
33
33
|
# Aha. How do I access properties? The documented properties
|
34
34
|
# have getters:
|
35
35
|
my_account.currency
|
36
|
-
=> "USD"
|
37
|
-
|
38
|
-
# But facebook also returns some non documented stuff
|
39
|
-
my_account[:moo_default_bid]
|
40
|
-
=> 1000
|
36
|
+
=> "USD"
|
41
37
|
|
42
38
|
# Let's fetch the campaigns for this account
|
43
39
|
my_campaign = my_account.ad_campaigns.first
|
@@ -67,12 +63,12 @@ creative = '{"type":25,"action_spec":{"action.type":"like", "post":1015042041088
|
|
67
63
|
o = { bid_type: 1,
|
68
64
|
max_bid: 1,
|
69
65
|
name: "My first ad group",
|
70
|
-
targeting: '{"countries":["US"]}',
|
66
|
+
targeting: '{"geo_locations": {"countries":["US"]}}',
|
71
67
|
creative: creative}
|
72
|
-
|
68
|
+
|
73
69
|
# Create it in the context of my_campaign
|
74
70
|
group = my_campaign.create_ad_group(o)
|
75
|
-
=> #<Zuck::AdGroup adgroup_id: 6005851390151, ad_id: 6005851390151, campaign_id: 6005851032951, name: "My first ad group", adgroup_status: 4, bid_type: 1, max_bid: "1", bid_info: {"1":"1"}, ad_status: 4, account_id: "10150585630710217", id: "6005851390151", creative_ids: [6005851371551], targeting: {"countries":["US"],"friends_of_connections":[{"id":"6005851366351","name":null}]}, conversion_specs: [{"action.type":"like","post":"10150420410887685"}], start_time: null, end_time: null, updated_time: 1343916568, created_time: 1343916568>
|
71
|
+
=> #<Zuck::AdGroup adgroup_id: 6005851390151, ad_id: 6005851390151, campaign_id: 6005851032951, name: "My first ad group", adgroup_status: 4, bid_type: 1, max_bid: "1", bid_info: {"1":"1"}, ad_status: 4, account_id: "10150585630710217", id: "6005851390151", creative_ids: [6005851371551], targeting: {"geo_locations": {"countries":["US"]},"friends_of_connections":[{"id":"6005851366351","name":null}]}, conversion_specs: [{"action.type":"like","post":"10150420410887685"}], start_time: null, end_time: null, updated_time: 1343916568, created_time: 1343916568>
|
76
72
|
|
77
73
|
# Shoot, that was the wrong name
|
78
74
|
group.name = "My serious ad group"
|
@@ -94,31 +90,31 @@ AdInterest convenience methods
|
|
94
90
|
```ruby
|
95
91
|
graph = Zuck.graph
|
96
92
|
|
97
|
-
# Search for
|
93
|
+
# Search for interests (to auto complete, for example) (yes, facebook sometimes returns ids as string and sometimes as numbers)
|
98
94
|
Zuck::AdInterest.search(graph, "Auto")
|
99
95
|
=> [
|
100
|
-
{:
|
101
|
-
{:
|
102
|
-
{:
|
103
|
-
{:
|
96
|
+
{:interest=>"Auto", :id=>"6003156165433", :audience=>nil},
|
97
|
+
{:interest=>"#Automobile", :id=>6003176678152, :audience=>97900000},
|
98
|
+
{:interest=>"#Auto racing", :id=>6003146718552, :audience=>21800000},
|
99
|
+
{:interest=>"#Auto mechanic", :id=>6003109384433, :audience=>14600000}
|
104
100
|
]
|
105
101
|
|
106
|
-
# Quickly check if a
|
102
|
+
# Quickly check if a interest is valid
|
107
103
|
Zuck::AdInterest.validate(graph, '#Eminem')
|
108
104
|
=> {"#Eminem" => true}
|
109
105
|
|
110
|
-
# Quickly check a couple of
|
106
|
+
# Quickly check a couple of interests
|
111
107
|
Zuck::AdInterest.validate(graph, ['#Eminem', 'Wil Ferel', 'Bronson'])
|
112
108
|
=> {"#Eminem"=>true, "Bronson"=>true, "Wil Ferel"=>false}
|
113
109
|
|
114
|
-
# Make a best guess on how a
|
110
|
+
# Make a best guess on how a interest is called on Facebook
|
115
111
|
Zuck::AdInterest.best_guess(graph, 'Disney')
|
116
|
-
=> {:
|
112
|
+
=> {:interest=>"#The Walt Disney Company", :id=>6003270522085, :audience=>72500000}
|
117
113
|
|
118
|
-
# Sometimes a best guess does not return a
|
119
|
-
# means that we don't know the audience of that
|
114
|
+
# Sometimes a best guess does not return a interest with a # prefix, and that
|
115
|
+
# means that we don't know the audience of that interest:
|
120
116
|
Zuck::AdInterest.best_guess(graph, 'Moviepilot')
|
121
|
-
=> {:
|
117
|
+
=> {:interest=>"Moviepilot", :id=>6003327847780, :audience=>nil}
|
122
118
|
|
123
119
|
```
|
124
120
|
|
@@ -137,27 +133,19 @@ Here's a support chart:
|
|
137
133
|
<th style="text-align:center">.save</th>
|
138
134
|
<th style="text-align:center">.destroy</th>
|
139
135
|
<th style="text-align:center">parent.create_obj*</th>
|
140
|
-
<th style="text-align:center">Convenience methods**</th>
|
141
136
|
</tr>
|
142
|
-
<tr><td style="text-align: right">Ad account</td> <td>✔</td><td>-</td><td>✔</td><td>✔</td><td>-</td
|
143
|
-
<tr><td style="text-align: right">Ad account group</td> <td>-</td><td>-</td><td>-</td><td>-</td><td>-</td
|
144
|
-
<tr><td style="text-align: right">Ad campaign</td> <td>✔</td><td>✔</td><td>✔</td><td>✔</td><td>✔</td
|
145
|
-
<tr><td style="text-align: right">Ad creative</td> <td>✔</td><td>-</td><td>-</td><td>-</td><td>-</td
|
146
|
-
<tr><td style="text-align: right">Ad group</td> <td>✔</td><td>✔</td><td>✔</td><td>✔</td><td>✔</td
|
147
|
-
<tr><td style="text-align: right">Ad
|
148
|
-
<tr><td style="text-align: right">Ad user</td> <td>-</td><td>-</td><td>-</td><td>-</td><td>-</td
|
137
|
+
<tr><td style="text-align: right">Ad account</td> <td>✔</td><td>-</td><td>✔</td><td>✔</td><td>-</td></tr>
|
138
|
+
<tr><td style="text-align: right">Ad account group</td> <td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
139
|
+
<tr><td style="text-align: right">Ad campaign</td> <td>✔</td><td>✔</td><td>✔</td><td>✔</td><td>✔</td></tr>
|
140
|
+
<tr><td style="text-align: right">Ad creative</td> <td>✔</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
141
|
+
<tr><td style="text-align: right">Ad group</td> <td>✔</td><td>✔</td><td>✔</td><td>✔</td><td>✔</td></tr>
|
142
|
+
<tr><td style="text-align: right">Ad set</td> <td>✔</td><td>✔</td><td>✔</td><td>✔</td><td>✔</td></tr>
|
143
|
+
<tr><td style="text-align: right">Ad user</td> <td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
149
144
|
</table>
|
150
145
|
|
151
|
-
(*) This means
|
146
|
+
(*) This means if you can, for example, create a new ad group by calling
|
152
147
|
`my_campaign.create_ad_group(data)` or not.
|
153
148
|
|
154
|
-
(**) Right now, everything goes right to facebook, but we'll want some
|
155
|
-
convenience methods that tell you, for example, what
|
156
|
-
`ad_group.ad_status == 3` actually means
|
157
|
-
|
158
|
-
( ) These don't exist as their own objects in this gem but live in their
|
159
|
-
parents. This means you can, for now, only read them:
|
160
|
-
|
161
149
|
Users don't exist as objects yet, but you can list all ad users of
|
162
150
|
an account via `my_ad_account.users` and you will get an array of hashes.
|
163
151
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.0.0
|
data/console
CHANGED
@@ -4,10 +4,11 @@ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
|
|
4
4
|
require 'bundler'
|
5
5
|
Bundler.require
|
6
6
|
|
7
|
-
require '
|
7
|
+
require 'pry'
|
8
8
|
require 'zuck'
|
9
9
|
|
10
10
|
def reload!
|
11
|
+
Zuck.graph = Koala::Facebook::API.new(File.read("test_access_token"))
|
11
12
|
@loaded_files ||= {}
|
12
13
|
count = 0
|
13
14
|
|
@@ -23,4 +24,9 @@ def reload!
|
|
23
24
|
"reloaded #{count} files"
|
24
25
|
end
|
25
26
|
|
26
|
-
|
27
|
+
def graph
|
28
|
+
Zuck.graph
|
29
|
+
end
|
30
|
+
|
31
|
+
Zuck.graph = Koala::Facebook::API.new(File.read("test_access_token"))
|
32
|
+
binding.pry
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Zuck
|
2
2
|
class AdAccount < RawFbObject
|
3
|
+
include Zuck::Helpers
|
3
4
|
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# actually returned.
|
5
|
+
# Known keys as per
|
6
|
+
# [fb docs](https://developers.facebook.com/docs/reference/ads-api/adaccount/)
|
7
7
|
known_keys :account_groups,
|
8
8
|
:account_id,
|
9
9
|
:account_status,
|
@@ -19,25 +19,42 @@ module Zuck
|
|
19
19
|
:business_street,
|
20
20
|
:business_zip,
|
21
21
|
:capabilities,
|
22
|
+
:created_time,
|
22
23
|
:currency,
|
23
24
|
:daily_spend_limit,
|
25
|
+
:end_advertiser,
|
26
|
+
:funding_source,
|
27
|
+
:funding_source_details,
|
24
28
|
:id,
|
25
29
|
:is_personal,
|
30
|
+
:media_agency,
|
26
31
|
:name,
|
32
|
+
:offsite_pixels_tos_accepted,
|
33
|
+
:partner,
|
27
34
|
:spend_cap,
|
28
35
|
:timezone_id,
|
29
36
|
:timezone_name,
|
30
37
|
:timezone_offset_hours_utc,
|
31
38
|
:tos_accepted,
|
32
39
|
:users,
|
33
|
-
:
|
40
|
+
:tax_id_status
|
34
41
|
|
35
42
|
|
36
43
|
list_path 'me/adaccounts'
|
37
|
-
connections :ad_campaigns
|
44
|
+
connections :ad_campaigns, :ad_sets, :ad_groups
|
38
45
|
|
39
46
|
def self.all(graph = Zuck.graph)
|
40
47
|
super(graph)
|
41
48
|
end
|
49
|
+
|
50
|
+
def path
|
51
|
+
normalize_account_id(id)
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_data(data)
|
55
|
+
super
|
56
|
+
self.id = normalize_account_id(id)
|
57
|
+
end
|
58
|
+
|
42
59
|
end
|
43
60
|
end
|
@@ -1,23 +1,20 @@
|
|
1
1
|
module Zuck
|
2
2
|
class AdCampaign < RawFbObject
|
3
3
|
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
known_keys :
|
8
|
-
:
|
9
|
-
:
|
10
|
-
:daily_imps,
|
11
|
-
:end_time,
|
12
|
-
:id,
|
13
|
-
:lifetime_budget,
|
4
|
+
# Known keys as per
|
5
|
+
# the [fb docs](https://developers.facebook.com/docs/reference/ads-api/adcampaign/v2.2)
|
6
|
+
# as well as undocumented keys returned by the Graph API
|
7
|
+
known_keys :id,
|
8
|
+
:account_id,
|
9
|
+
:objective,
|
14
10
|
:name,
|
15
|
-
:
|
16
|
-
:
|
11
|
+
:adgroups,
|
12
|
+
:campaign_group_status,
|
13
|
+
:buying_type
|
17
14
|
|
18
|
-
parent_object :ad_account
|
19
|
-
list_path :
|
20
|
-
connections :ad_groups
|
15
|
+
parent_object :ad_account, as: :account_id
|
16
|
+
list_path :adcampaign_groups
|
17
|
+
connections :ad_groups, :ad_campaigns
|
21
18
|
|
22
19
|
end
|
23
20
|
end
|
@@ -4,25 +4,25 @@ module Zuck
|
|
4
4
|
# Can't create this directly (yet)
|
5
5
|
read_only
|
6
6
|
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
known_keys :name,
|
11
|
-
:type,
|
12
|
-
:object_id,
|
7
|
+
# Known keys as per
|
8
|
+
# [fb docs](https://developers.facebook.com/docs/reference/ads-api/adaccount/)
|
9
|
+
known_keys :actor_id,
|
13
10
|
:body,
|
11
|
+
:call_to_action_type,
|
12
|
+
:follow_redirect,
|
13
|
+
:image_crops,
|
14
|
+
:image_file,
|
14
15
|
:image_hash,
|
15
16
|
:image_url,
|
16
|
-
:id,
|
17
|
-
:title,
|
18
17
|
:link_url,
|
18
|
+
:name,
|
19
|
+
:object_id,
|
20
|
+
:object_story_id,
|
21
|
+
:object_story_spec,
|
22
|
+
:object_url,
|
23
|
+
:title,
|
19
24
|
:url_tags,
|
20
|
-
:
|
21
|
-
:related_fan_page,
|
22
|
-
:follow_redirect,
|
23
|
-
:auto_update,
|
24
|
-
:story_id,
|
25
|
-
:action_spec
|
25
|
+
:id
|
26
26
|
|
27
27
|
parent_object :ad_group
|
28
28
|
list_path :adcreatives
|
@@ -3,20 +3,20 @@ require 'zuck/facebook/ad_creative'
|
|
3
3
|
module Zuck
|
4
4
|
class AdGroup < RawFbObject
|
5
5
|
|
6
|
-
#
|
7
|
-
#
|
8
|
-
|
9
|
-
|
6
|
+
# Known keys as per
|
7
|
+
# [fb docs](https://developers.facebook.com/docs/reference/ads-api/adgroup/v2.2)
|
8
|
+
known_keys :id,
|
9
|
+
:account_id,
|
10
10
|
:adgroup_status,
|
11
|
-
:bid_info,
|
12
11
|
:bid_type,
|
12
|
+
:bid_info,
|
13
|
+
:conversion_specs,
|
13
14
|
:campaign_id,
|
15
|
+
:campaign_group_id,
|
14
16
|
:conversion_specs,
|
15
17
|
:created_time,
|
16
18
|
:creative_ids,
|
17
|
-
:
|
18
|
-
:disapprove_reason_descriptions,
|
19
|
-
:last_updated_by_app_id,
|
19
|
+
:failed_delivery_checks,
|
20
20
|
:name,
|
21
21
|
:targeting,
|
22
22
|
:tracking_specs,
|
@@ -27,10 +27,10 @@ module Zuck
|
|
27
27
|
list_path :adgroups
|
28
28
|
connections :ad_creatives
|
29
29
|
|
30
|
-
def self.create(graph, data,
|
31
|
-
path =
|
32
|
-
data['campaign_id'] =
|
33
|
-
super(graph, data,
|
30
|
+
def self.create(graph, data, ad_set)
|
31
|
+
path = ad_set.ad_account.path
|
32
|
+
data['campaign_id'] = ad_set.id
|
33
|
+
super(graph, data, ad_set, path)
|
34
34
|
end
|
35
35
|
|
36
36
|
end
|
@@ -9,35 +9,32 @@ module Zuck
|
|
9
9
|
# popular one
|
10
10
|
def best_guess(graph, interest)
|
11
11
|
search(graph, interest).sort do |a,b|
|
12
|
-
if a[:
|
13
|
-
a[:
|
12
|
+
if a[:audience_size].to_i > 0 || b[:audience_size].to_i > 0
|
13
|
+
a[:audience_size].to_i <=> b[:audience_size].to_i
|
14
14
|
else
|
15
|
-
b[:
|
15
|
+
b[:name].length <=> a[:name].length
|
16
16
|
end
|
17
17
|
end.last
|
18
18
|
end
|
19
19
|
|
20
|
-
# Checks the ad api to see if the given interests are valid
|
20
|
+
# Checks the ad api to see if the given interests are valid. You can either
|
21
|
+
# pass it an array of strings, or an array of hashes with `:name` and `:id` keys.
|
21
22
|
# @return [Hash] The keys are the (lowercased) interests and the values their validity
|
22
23
|
def validate(graph, interests)
|
23
|
-
interests = normalize_array(interests).map{|k| k.gsub(',', '%2C')}
|
24
|
-
search = graph.search(nil, type: 'adinterestvalid', interest_list: [interests].flatten)
|
25
24
|
results = {}
|
26
|
-
|
27
|
-
|
25
|
+
normalized = normalize_array(values_from_string_or_object_interests(interests))
|
26
|
+
normalized.each do |interest|
|
27
|
+
# The interest is valid if we found at least one match that
|
28
|
+
# has the exactly the same name (ignoring case)
|
29
|
+
hits = search(graph, interest).select{|d| d['name'].downcase == interest.downcase}
|
30
|
+
results[interest] = hits.count > 0
|
28
31
|
end
|
29
32
|
results
|
30
33
|
end
|
31
34
|
|
32
35
|
# Ad interest search
|
33
36
|
def search(graph, interest)
|
34
|
-
|
35
|
-
{
|
36
|
-
interest: r['name'],
|
37
|
-
id: r['id'],
|
38
|
-
audience: r['audience_size']
|
39
|
-
}
|
40
|
-
end
|
37
|
+
graph.search(interest, type: :adinterest).map(&:with_indifferent_access)
|
41
38
|
end
|
42
39
|
end
|
43
40
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'zuck/facebook/ad_creative'
|
2
|
+
|
3
|
+
module Zuck
|
4
|
+
class AdSet < RawFbObject
|
5
|
+
|
6
|
+
# Known keys as per
|
7
|
+
# [fb docs](https://developers.facebook.com/docs/reference/ads-api/adset/v2.2)
|
8
|
+
known_keys :id,
|
9
|
+
:name,
|
10
|
+
:account_id,
|
11
|
+
:bid_type,
|
12
|
+
:bid_info,
|
13
|
+
:campaign_group_id,
|
14
|
+
:campaign_status,
|
15
|
+
:start_time,
|
16
|
+
:end_time,
|
17
|
+
:updated_time,
|
18
|
+
:created_time,
|
19
|
+
:daily_budget,
|
20
|
+
:lifetime_budget,
|
21
|
+
:budget_remaining,
|
22
|
+
:targeting,
|
23
|
+
:promoted_object
|
24
|
+
|
25
|
+
parent_object :ad_account, as: :account_id
|
26
|
+
list_path :adcampaigns # Yes, this is correct, "for legacy reasons"
|
27
|
+
connections :ad_groups, :ad_creatives
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -70,7 +70,7 @@ module Zuck
|
|
70
70
|
def initialize(graph, ad_account, spec = nil)
|
71
71
|
@validated_interests = {}
|
72
72
|
@graph = graph
|
73
|
-
@ad_account =
|
73
|
+
@ad_account = normalize_account_id(ad_account)
|
74
74
|
self.spec = spec
|
75
75
|
end
|
76
76
|
|
@@ -104,7 +104,7 @@ module Zuck
|
|
104
104
|
# @return boolean
|
105
105
|
def validate_interest(interest)
|
106
106
|
if @validated_interests[interest] == nil
|
107
|
-
interests =
|
107
|
+
interests = [@spec[:interests]] + [interest]
|
108
108
|
@validated_interests = Zuck::AdInterest.validate(@graph, interests)
|
109
109
|
end
|
110
110
|
@validated_interests[interest] == true
|
@@ -153,7 +153,7 @@ module Zuck
|
|
153
153
|
private
|
154
154
|
|
155
155
|
def validate_spec
|
156
|
-
@spec[:interests]
|
156
|
+
@spec[:interests] = @spec[:interests]
|
157
157
|
@spec[:broad_age] ||= false
|
158
158
|
validate_countries
|
159
159
|
unless @spec[:interests].present? or @spec[:connections].present?
|
@@ -195,7 +195,7 @@ module Zuck
|
|
195
195
|
@spec[:genders] = [2] if gender.to_s == 'female'
|
196
196
|
|
197
197
|
interest = spec.delete(:interest)
|
198
|
-
@spec[:interests] =
|
198
|
+
@spec[:interests] = [interest, @spec[:interests]].compact.flatten
|
199
199
|
|
200
200
|
country = spec.delete(:country)
|
201
201
|
@spec[:countries] = normalize_countries([country, @spec[:countries]])
|