gooddata_marketo 0.0.1-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +9 -0
  3. data/Gemfile.lock +131 -0
  4. data/README.md +207 -0
  5. data/bin/Gemfile +10 -0
  6. data/bin/auth.json +16 -0
  7. data/bin/main.rb +24 -0
  8. data/bin/process.rbx +541 -0
  9. data/examples/all_lead_changes.rb +119 -0
  10. data/examples/all_leads.rb +249 -0
  11. data/examples/lead_changes_to_ads.rb +63 -0
  12. data/gooddata_marketo.gemspec +25 -0
  13. data/lib/gooddata_marketo/adapters/rest.rb +287 -0
  14. data/lib/gooddata_marketo/client.rb +373 -0
  15. data/lib/gooddata_marketo/data/activity_types.rb +104 -0
  16. data/lib/gooddata_marketo/data/reserved_sql_keywords.rb +205 -0
  17. data/lib/gooddata_marketo/helpers/s3.rb +141 -0
  18. data/lib/gooddata_marketo/helpers/stringwizard.rb +32 -0
  19. data/lib/gooddata_marketo/helpers/table.rb +323 -0
  20. data/lib/gooddata_marketo/helpers/webdav.rb +118 -0
  21. data/lib/gooddata_marketo/loads.rb +235 -0
  22. data/lib/gooddata_marketo/models/campaigns.rb +57 -0
  23. data/lib/gooddata_marketo/models/channels.rb +30 -0
  24. data/lib/gooddata_marketo/models/child/activity.rb +104 -0
  25. data/lib/gooddata_marketo/models/child/criteria.rb +17 -0
  26. data/lib/gooddata_marketo/models/child/lead.rb +118 -0
  27. data/lib/gooddata_marketo/models/child/mobj.rb +68 -0
  28. data/lib/gooddata_marketo/models/etl.rb +75 -0
  29. data/lib/gooddata_marketo/models/leads.rb +493 -0
  30. data/lib/gooddata_marketo/models/load.rb +17 -0
  31. data/lib/gooddata_marketo/models/mobjects.rb +121 -0
  32. data/lib/gooddata_marketo/models/streams.rb +137 -0
  33. data/lib/gooddata_marketo/models/tags.rb +35 -0
  34. data/lib/gooddata_marketo/models/validate.rb +46 -0
  35. data/lib/gooddata_marketo.rb +24 -0
  36. data/process.rb +517 -0
  37. metadata +177 -0
@@ -0,0 +1,493 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'gooddata_marketo/models/child/lead'
4
+ require 'gooddata_marketo/models/child/activity'
5
+
6
+ class GoodDataMarketo::Leads
7
+
8
+ attr_reader :client
9
+
10
+ def initialize config = {}
11
+
12
+ @client = config[:client]
13
+
14
+ end
15
+
16
+ def [](a)
17
+ if a.include? '@'
18
+ self.get_by_email(a)
19
+ else
20
+ self.get_by_id(a)
21
+ end
22
+ end
23
+
24
+ # POSSIBLE KEY TYPES FOR LEAD QUERIES
25
+ #
26
+ # IDNUM: The Marketo ID (e.g. 64)
27
+ # COOKIE: The value generated by the Munchkin Javascript. (e.g. id:561-HYG-937&token:_mch-marketo.com-1258067434006-50277)
28
+ # EMAIL: The email address associated with the lead. (e.g. rufus@marketo.com)
29
+ # SFDCLEADID: The lead ID from SalesForce
30
+ # LEADOWNEREMAIL: The Lead Owner Email
31
+ # SFDCACCOUNTID: The Account ID from SalesForce
32
+ # SFDCCONTACTID: The Contact ID from SalesForce
33
+ # SFDCLEADID: TheLead ID from SalesForce
34
+ # SFDCLEADOWNERID: The Lead owner ID from SalesForce
35
+ # SFDCOPPTYID: The Opportunity ID from SalesForce
36
+
37
+ def get_by_email email # http://developers.marketo.com/documentation/soap/getlead/
38
+ type = 'EMAIL'
39
+ request = { :lead_key => { :key_type => type, :key_value => email } }
40
+ response = client.call(:get_lead, request)
41
+ # GoodDataMarketo::Lead.new response[:lead_record_list][:lead_record], :client => client (CLIENT REMOVED DUE TO PERFORMANCE ISSUE)
42
+ GoodDataMarketo::Lead.new response[:lead_record_list][:lead_record]
43
+
44
+ end
45
+
46
+ def get_by_id id
47
+ type = 'IDNUM'
48
+ request = { :lead_key => { :key_type => type, :key_value => id } }
49
+ response = client.call(:get_lead, request)
50
+ # GoodDataMarketo::Lead.new response[:lead_record_list][:lead_record], :client => client (CLIENT REMOVED DUE TO PERFORMANCE ISSUE)
51
+ GoodDataMarketo::Lead.new response[:lead_record_list][:lead_record]
52
+
53
+ end
54
+
55
+ def get_multiple config = {} # http://developers.marketo.com/documentation/soap/getmultipleleads/
56
+
57
+ values_array = config[:ids] || config[:values]
58
+
59
+ @leads_from_call = []
60
+
61
+ # Possible types
62
+ # IDNUM, COOKIE, EMAIL, LEADOWNEREMAIL, SFDCACCOUNTID, SFDCCONTACTID, SFDCLEADID, SFDCLEADOWNERID, SFDCOPPTYID.
63
+
64
+ type = config[:type] || 'IDNUM'
65
+ xsi_type = config[:xsi_type] || 'ns1:LeadKeySelector'
66
+
67
+ if values_array.is_a? String
68
+ values_array = [values_array]
69
+ end
70
+
71
+ segments = values_array.each_slice(100).to_a
72
+
73
+ segments.each { |values|
74
+
75
+ request = {
76
+ :lead_selector => {
77
+ :key_type => type,
78
+ :key_values => {
79
+ :string_item => values
80
+ }
81
+ },
82
+ :batch_size => config[:batch_size] || "100", # Unable to determine timeout rate at the 1000 so moved to 200.
83
+ :attributes! => { :lead_selector => { 'xsi:type' => xsi_type } }
84
+ }
85
+
86
+ inc = config[:include_attributes] || config[:include] || config[:types]
87
+ if inc
88
+ if inc.is_a? String
89
+ inc = [inc]
90
+ end
91
+ request[:activity_filter][:include_attributes] = inc
92
+ end
93
+
94
+ exc = config[:exclude_attributes] || config[:exclude]
95
+ if exc
96
+ if exc.is_a? String
97
+ exc = [exc]
98
+ end
99
+ request[:activity_filter][:exclude_attributes] = exc
100
+ end
101
+
102
+ activities = config[:filters] || config[:activity_name_filter] || config[:activities]
103
+ if activities
104
+ activities = [activities] if activities.is_a? String
105
+ request[:activity_name_filter] = Hash.new
106
+ request[:activity_name_filter][:string_item] = activities
107
+ end
108
+
109
+ # Ensure that the API call is not made with both.
110
+ if inc && exc
111
+ raise "Include and exclude attributes may not be used in the same call."
112
+ end
113
+
114
+ request[:start_position] = Hash.new if config[:oldest_created_at] || config[:activity_created_at] || config[:offset]
115
+
116
+ if config[:oldest_created_at]
117
+ begin
118
+ oca = StringWizard.time(config[:oldest_created_at]).to_s
119
+ request[:start_position][:oldest_created_at] = oca
120
+ rescue Exception => e
121
+ puts e if GoodDataMarketo.logging
122
+ end
123
+
124
+ end
125
+
126
+ if config[:activity_created_at]
127
+ begin
128
+ aca = StringWizard.time(config[:activity_created_at]).to_s
129
+ request[:start_position][:activity_created_at] = aca
130
+ rescue Exception => e
131
+ puts e if GoodDataMarketo.logging
132
+ end
133
+ end
134
+
135
+ if config[:latest_created_at]
136
+ begin
137
+ lca = StringWizard.time(config[:latest_created_at]).to_s
138
+ request[:start_position][:latest_created_at] = lca
139
+ rescue Exception => e
140
+ puts e if GoodDataMarketo.logging
141
+ end
142
+ end
143
+
144
+ offset_date = config[:offset]
145
+
146
+ request[:start_position][:offset] = offset_date if offset_date
147
+
148
+ # Execute a stream unless the number of leads is less than the batch limit of 100
149
+ begin
150
+
151
+ if values.length < 101 && values.length > 1
152
+
153
+ c = client.call(:get_multiple_leads, request)
154
+ c[:lead_record_list][:lead_record].each { |lead|
155
+ # l = GoodDataMarketo::Lead.new lead, :client => client (CLIENT REMOVED DUE TO PERFORMANCE ISSUE)
156
+ if client.load
157
+ l = lead.to_json
158
+ else
159
+ l = GoodDataMarketo::Lead.new lead
160
+ end
161
+
162
+ @leads_from_call << l
163
+ }
164
+
165
+ if client.load
166
+ ids = client.load.arguments[:ids].drop(values.length)
167
+ client.load.arguments[:ids] = ids
168
+ client.load.save
169
+
170
+ end
171
+
172
+
173
+ elsif values.length == 1
174
+ c = client.call(:get_multiple_leads, request)
175
+
176
+ # l = GoodDataMarketo::Lead.new c[:lead_record_list][:lead_record], :client => client (CLIENT REMOVED DUE TO PERFORMANCE ISSUE)
177
+ if client.load
178
+ l = c[:lead_record_list][:lead_record].to_json
179
+ else
180
+ l = GoodDataMarketo::Lead.new c[:lead_record_list][:lead_record]
181
+ end
182
+
183
+ @leads_from_call << l
184
+
185
+ else
186
+
187
+ c = client.stream(:get_multiple_leads, request)
188
+
189
+ if client.load
190
+
191
+ ids = client.load.arguments[:ids].drop(values.length)
192
+
193
+ client.load.arguments[:ids] = ids
194
+
195
+ client.load.save
196
+
197
+ end
198
+
199
+ puts "#{Time.now} => Marketo:Leads:#{c.storage.length}" if GoodDataMarketo.logging
200
+ c.storage.each do |request|
201
+ request[:lead_record_list][:lead_record].each { |lead|
202
+ # l = GoodDataMarketo::Lead.new lead, :client => client (CLIENT REMOVED DUE TO PERFORMANCE ISSUE)
203
+
204
+ # To conserve memory on large batches sent the raw file to load storage.
205
+ if client.load
206
+ l = lead.to_json
207
+ else
208
+ l = GoodDataMarketo::Lead.new lead
209
+ end
210
+
211
+ @leads_from_call << l
212
+ }
213
+ end
214
+
215
+ end
216
+ rescue Exception => exp
217
+ puts exp if GoodDataMarketo.logging
218
+ puts "#{Time.now} => 0 results for Marketo query."
219
+ end
220
+
221
+ puts "#{Time.now} => Marketo:Leads:Queue:#{@leads_from_call.length}"
222
+
223
+ }
224
+ client.load.log('RESPONSE') if client.load
225
+ client.load.storage = @leads_from_call if client.load
226
+ @leads_from_call
227
+
228
+ end
229
+
230
+ def get_changes(config = {}) # http://developers.marketo.com/documentation/soap/getleadchanges/
231
+
232
+ # EXAMPLE REQUEST
233
+ # request = {
234
+ # :start_position => {
235
+ # :oldest_created_at => "2013-07-01 23:58:14 -0700",
236
+ # },
237
+ # :activity_name_filter => {
238
+ # :stringItem => ["Visit Webpage", "Click Link"] },
239
+ # :batch_size => "100"
240
+ # }
241
+
242
+ # OPTIONAL ACTIVITIES
243
+ # NewLead
244
+ # AssocWithOpprtntyInSales
245
+ # DissocFromOpprtntyInSales
246
+ # UpdateOpprtntyInSales
247
+ # ChangeDataValue
248
+ # MergeLeads
249
+ # OpenEmail
250
+ # SendEmail
251
+
252
+ request = {
253
+ :start_position => Hash.new,
254
+ :batch_size => config[:batch_size] || '1000'
255
+ }
256
+
257
+ ###################
258
+ # Activity Config #
259
+ ###################
260
+
261
+ query = config[:lead] || config[:email] || config[:values] || config[:value]
262
+
263
+ if query
264
+ query = [query] if query.is_a? String
265
+ xsi_type = config[:xsi_type] || 'ns1:LeadKeySelector'
266
+
267
+ request[:lead_selector] = {}
268
+ request[:lead_selector][:key_values] = {}
269
+ request[:lead_selector][:key_type] = config[:type] || 'EMAIL'
270
+ request[:lead_selector][:key_values][:string_item] = query
271
+ request[:attributes!] = { :lead_selector => { 'xsi:type' => xsi_type } }
272
+
273
+ end
274
+
275
+ inc = config[:include_attributes] || config[:include] || config[:types]
276
+ if inc
277
+ if inc.is_a? String
278
+ inc = [inc]
279
+ end
280
+ request[:activity_filter][:include_attributes] = inc
281
+ end
282
+
283
+ exc = config[:exclude_attributes] || config[:exclude]
284
+ if exc
285
+ if exc.is_a? String
286
+ exc = [exc]
287
+ end
288
+ request[:activity_filter][:exclude_attributes] = exc
289
+ end
290
+
291
+ activities = config[:filters] || config[:activity_name_filter] || config[:activities]
292
+ if activities
293
+
294
+ activities = [activities] if activities.is_a? String
295
+ request[:activity_name_filter] = Hash.new
296
+ request[:activity_name_filter][:string_item] = activities
297
+ end
298
+
299
+ # Ensure that the API call is not made with both.
300
+ if inc && exc
301
+ raise "Include and exclude attributes may not be used in the same call."
302
+ end
303
+
304
+ #########################
305
+ # Start Position Config #
306
+ #########################
307
+
308
+ if config[:oldest_created_at]
309
+ begin
310
+ oca = StringWizard.time(config[:oldest_created_at])
311
+ request[:start_position][:oldest_created_at] = oca
312
+ rescue Exception => e
313
+ puts e if GoodDataMarketo.logging
314
+ end
315
+
316
+ end
317
+
318
+ if config[:latest_created_at]
319
+ begin
320
+ lca = StringWizard.time(config[:latest_created_at])
321
+ request[:start_position][:latest_created_at] = lca
322
+ rescue Exception => e
323
+ puts e if GoodDataMarketo.logging
324
+ end
325
+
326
+ end
327
+
328
+ if config[:activity_created_at]
329
+ begin
330
+ aca = StringWizard.time(config[:activity_created_at])
331
+ request[:start_position][:activity_created_at] = aca
332
+ rescue Exception => e
333
+ puts e if GoodDataMarketo.logging
334
+ end
335
+ end
336
+
337
+ o = config[:offset]
338
+ request[:start_position][:offset] = o if o
339
+
340
+ raise 'A start position type is required (:oldest_created_at, :offset, :activity_created_at)' unless oca || aca || o
341
+
342
+ c = client.stream(:get_lead_changes, request)
343
+
344
+ # Load all of the leads from the stream into an array for processes from a load or direct call.
345
+ # If it is a direct call, build an object, if not move the item as raw json.
346
+
347
+ leads_from_changes_call = []
348
+ begin
349
+
350
+ c.storage.pmap do |request|
351
+
352
+ stored_item = request[:lead_change_record_list][:lead_change_record]
353
+
354
+ if stored_item.is_a? Array
355
+ stored_item.each { |activity|
356
+ if client.load
357
+ l = activity.to_json
358
+ else
359
+ l = GoodDataMarketo::Activity.new activity
360
+ end
361
+
362
+ leads_from_changes_call << l
363
+ }
364
+ else
365
+ if client.load
366
+ l = stored_item.to_json
367
+ else
368
+ l = GoodDataMarketo::Activity.new activity
369
+ end
370
+ leads_from_changes_call << l
371
+ end
372
+ end
373
+
374
+ rescue
375
+
376
+ puts "#{Time.now} => 0 results for Marketo query."
377
+ end
378
+
379
+ client.load.log('RESPONSE') if client.load
380
+ client.load.storage = leads_from_changes_call if client.load
381
+
382
+ leads_from_changes_call
383
+
384
+ end
385
+
386
+ def get_activities config = {} # http://developers.marketo.com/documentation/soap/getleadactivity/
387
+
388
+ # EXAMPLE HISTORY REQUEST
389
+ # request = {
390
+ # :lead_selector => {
391
+ # :key_type => "IDNUM",
392
+ # :key_value => "example@host.com"},
393
+ # :activity_filter => {
394
+ # :include_types => {
395
+ # :activity_type => ["VisitWebpage", "FillOutForm"]
396
+ # }
397
+ # },
398
+ # :start_position => {
399
+ # :"last_created_at/" => "",
400
+ # :"offset/" => "" },
401
+ # :batch_size => "10"
402
+ # }
403
+
404
+ request = {
405
+ :lead_key => {
406
+ :key_type => config[:type] || "EMAIL",
407
+ :key_value => config[:value]},
408
+ :batch_size => config[:batch_size] || "1000"
409
+ }
410
+
411
+ activity_types = config[:activity_types] || config[:activity_type]
412
+
413
+ if activity_types
414
+
415
+ activity_types = [activity_types] if activity_types.is_a? String
416
+
417
+ request[:activity_filter] = {}
418
+ request[:activity_filter][:include_types] = {}
419
+ request[:activity_filter][:include_types] = activity_types
420
+
421
+ end
422
+
423
+ # Check if any date options were added and if so add the date hash.
424
+ if config[:last_created_at] || config[:activity_created_at] || config[:latest_created_at] || config[:oldest_created_at]
425
+ request[:start_position] = {}
426
+ end
427
+
428
+ request[:start_position][:last_created_at] = StringWizard.time(config[:last_created_at]) if config[:last_created_at]
429
+ request[:start_position][:activity_created_at] = StringWizard.time(config[:activity_created_at]) if config[:activity_created_at]
430
+ request[:start_position][:latest_created_at] = StringWizard.time(config[:latest_created_at]) if config[:latest_created_at]
431
+ request[:start_position][:oldest_created_at] = StringWizard.time(config[:oldest_created_at]) if config[:oldest_created_at]
432
+
433
+ c = client.stream(:get_lead_activity, request)
434
+
435
+ leads_from_activities_call = []
436
+ begin
437
+ c.storage.each do |request|
438
+ request[:activity_record_list][:activity_record].each { |activity|
439
+ l = GoodDataMarketo::Activity.new activity
440
+ leads_from_activities_call << l
441
+ }
442
+ end
443
+ rescue
444
+ puts "#{Time.now} => Marketo (0) results for query. Adjust the configuration of the request or confirm there has not been changes to the structure of response from Marketo."
445
+ end
446
+
447
+ client.load.log('RESPONSE') if client.load
448
+
449
+ leads_from_activities_call
450
+
451
+ end
452
+
453
+ alias :get_activity :get_activities
454
+
455
+ def get_activity_by_email email
456
+ config = {}
457
+ config[:type] = 'EMAIL'
458
+ config[:value] = email
459
+
460
+ self.get_activities config
461
+
462
+ end
463
+
464
+ def get_activity_by_id id
465
+ config = {}
466
+ config[:type] = 'IDNUM'
467
+ config[:value] = id
468
+
469
+ self.get_activity config
470
+
471
+ end
472
+
473
+ def get_multiple_by_email emails, config = {}
474
+ config[:type] = 'EMAIL'
475
+ config[:ids] = emails
476
+ self.get_multiple config
477
+ end
478
+
479
+ def get_multiple_by_id ids, config = {}
480
+ config[:type] = 'IDNUM'
481
+ config[:ids] = ids
482
+ self.get_multiple config
483
+ end
484
+
485
+ def types
486
+ m = self.methods - Object.methods
487
+ m.delete(:types)
488
+ m
489
+ end
490
+
491
+ end
492
+
493
+
@@ -0,0 +1,17 @@
1
+ class GoodDataMarketo::LoadFile
2
+
3
+ attr_accessor :name
4
+ attr_accessor :method
5
+ attr_accessor :type
6
+ attr_accessor :arguments
7
+
8
+ def initialize config = {}
9
+ @name = config[:name]
10
+ @type = config[:type]
11
+ @method = config[:method]
12
+ @arguments = {
13
+ :ids => config[:ids] || config[:values],
14
+ :type => config[:type]
15
+ }
16
+ end
17
+ end
@@ -0,0 +1,121 @@
1
+ # encoding: UTF-8
2
+ # http://developers.marketo.com/documentation/soap/getmobjects/
3
+ require 'gooddata_marketo/models/child/criteria'
4
+ require 'gooddata_marketo/models/child/mobj'
5
+
6
+ class GoodDataMarketo::MObjects
7
+
8
+ attr_reader :client
9
+
10
+ def initialize config = {}
11
+
12
+ @obj_criteria_list = []
13
+ @client = config[:client]
14
+
15
+ end
16
+
17
+ def get config = {} # http://developers.marketo.com/documentation/soap/getcampaignsforsource/
18
+
19
+ # EXAMPLE CRITERIA
20
+ # criteria = {
21
+ # :attr_name => "Id", # See the types of content it can search above.
22
+ # :comparison => "LE",
23
+ # :attr_value => "1010"
24
+ # }
25
+
26
+ # EXAMPLE ASSOCIATED OBJECT
27
+ # # m_associated_object = {
28
+ # :m_obj_type => '',
29
+ # :id=> '',
30
+ # :external_key => '' <-- Optional
31
+ # }
32
+
33
+ # EXAMPLE CRITERIA COMPARISONS
34
+ # EQ - Equals
35
+ # NE - Not Equals
36
+ # LT - Less Than
37
+ # LE - Less Than or Equals
38
+ # GT - Greater Than
39
+ # GE - Greater Than or Equals
40
+
41
+ # ACCEPTED ATTRIBUTE VALUE ITEMS
42
+ # Name: Name of the MObject
43
+ # Role: The role associated with an OpportunityPersonRole object
44
+ # Type: The type of an Opportunity object
45
+ # Stage:- The stage of an Opportunity object
46
+ # CRM Id: the CRM Id could refer to the Id of the Salesforce campaign connected to a Marketo program. Note: The SFDC Campaign ID needs to be the 18-digit ID.
47
+ # Created At: Equals, not equals, less than, less than or equal to, greater than, greater than or equal to
48
+ # Two “created dates” can be specified to create a date range
49
+ # Updated At or Tag Type (only one can be specified): Equals, not equals, less than, less than or equal to, greater than, greater than or equal to
50
+ # Two “created dates” can be specified to create a date range
51
+ # Tag Value: (Only one can be specified)
52
+ # Workspace Name: (only one can be specified)
53
+ # Workspace Id: (only one can be specified)
54
+ # Include Archive: Applicable only with Program MObject. Set it to true if you wish to include archived programs.
55
+
56
+ # AVAILABLE TYPES
57
+ # Program
58
+ # OpportunityPersonRole
59
+ # Opportunity
60
+
61
+ type = config[:type] || "Program"
62
+ request = { :type => type }
63
+
64
+ criteria = config[:criteria] || @obj_criteria_list
65
+ request[:m_obj_criteria_list] = criteria if criteria
66
+ associate = config[:association] || config[:m_obj_association] || config[:associate]
67
+
68
+ if associate
69
+ request[:m_obj_association_list] = Hash.new
70
+ request[:m_obj_association_list][:m_obj_association] = associate
71
+ end
72
+
73
+ # This field is optional and only present during `type => 'Program'` calls.
74
+ if type == "Program"
75
+ request[:include_details] = config[:include_details].to_s || "false"
76
+ end
77
+
78
+ c = client.stream(:get_m_objects, request)
79
+
80
+ mobjects_from_call = []
81
+ c.storage.each do |request|
82
+ request[:m_object_list][:m_object].each do |mobj|
83
+ m = GoodDataMarketo::MObject.new mobj
84
+ mobjects_from_call << m
85
+ end
86
+ end
87
+
88
+ binding.pry
89
+ mobjects_from_call
90
+
91
+ end
92
+
93
+ def add_criteria name, value, comparison
94
+
95
+ obj = {}
96
+ obj[:m_obj_criteria] = {
97
+ :attr_name => name.to_s,
98
+ :comparison => comparison.to_s,
99
+ :attr_value => value.to_s
100
+ }
101
+
102
+ @obj_criteria_list << obj
103
+
104
+ end
105
+
106
+ def criteria
107
+ @obj_criteria_list
108
+ end
109
+
110
+ def remove_criteria query
111
+ match = @obj_criteria_list.find {|item| item[:m_obj_criteria][:attr_name] == query }
112
+ @obj_criteria_list.delete(match)
113
+ end
114
+
115
+ alias :remove :remove_criteria
116
+
117
+ alias :add :add_criteria
118
+
119
+ end
120
+
121
+