mosaic-lyris 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/Gemfile +3 -0
  2. data/README +4 -0
  3. data/Rakefile +31 -0
  4. data/VERSION +1 -0
  5. data/init.rb +4 -0
  6. data/lib/lyris.rb +2 -0
  7. data/lib/mosaic/lyris.rb +12 -0
  8. data/lib/mosaic/lyris/demographic.rb +65 -0
  9. data/lib/mosaic/lyris/filter.rb +6 -0
  10. data/lib/mosaic/lyris/list.rb +73 -0
  11. data/lib/mosaic/lyris/message.rb +95 -0
  12. data/lib/mosaic/lyris/object.rb +227 -0
  13. data/lib/mosaic/lyris/partner.rb +6 -0
  14. data/lib/mosaic/lyris/record.rb +101 -0
  15. data/lib/mosaic/lyris/trigger.rb +113 -0
  16. data/lib/mosaic/lyris/upload.rb +89 -0
  17. data/lib/mosaic/lyris_mailer.rb +102 -0
  18. data/mosaic-lyris.gemspec +143 -0
  19. data/test/demographic_test.rb +176 -0
  20. data/test/filter_test.rb +6 -0
  21. data/test/http_responder.rb +88 -0
  22. data/test/list_test.rb +49 -0
  23. data/test/message_test.rb +6 -0
  24. data/test/partner_test.rb +6 -0
  25. data/test/record_test.rb +177 -0
  26. data/test/responses/demographic/add_error_name_already_exists.xml +2 -0
  27. data/test/responses/demographic/add_success_12345.xml +2 -0
  28. data/test/responses/demographic/query_all_success.xml +19 -0
  29. data/test/responses/demographic/query_enabled_details_success.xml +12 -0
  30. data/test/responses/demographic/query_enabled_success.xml +13 -0
  31. data/test/responses/list/add_error_name_already_exists.xml +2 -0
  32. data/test/responses/list/add_success_12345.xml +2 -0
  33. data/test/responses/list/delete_error_not_found_99999.xml +2 -0
  34. data/test/responses/list/delete_success_12345.xml +2 -0
  35. data/test/responses/list/query_list_data_success.xml +20 -0
  36. data/test/responses/record/add_error_email_already_exists.xml +2 -0
  37. data/test/responses/record/add_success.xml +2 -0
  38. data/test/responses/record/query_all_success.xml +94 -0
  39. data/test/responses/record/query_all_success_empty.xml +2 -0
  40. data/test/responses/record/query_all_success_page_1.xml +54 -0
  41. data/test/responses/record/query_all_success_page_2.xml +34 -0
  42. data/test/responses/record/query_all_success_page_3.xml +14 -0
  43. data/test/responses/record/query_email_error_not_found.xml +2 -0
  44. data/test/responses/record/query_email_success_active.xml +14 -0
  45. data/test/responses/record/query_email_success_admin_trashed.xml +14 -0
  46. data/test/responses/record/query_email_success_bounced.xml +14 -0
  47. data/test/responses/record/query_email_success_unsubscribed.xml +14 -0
  48. data/test/responses/record/update_error_not_found.xml +2 -0
  49. data/test/responses/record/update_success.xml +2 -0
  50. data/test/responses/triggers/fire_error_invalid_recipients.xml +2 -0
  51. data/test/responses/triggers/fire_error_invalid_trigger_id.xml +2 -0
  52. data/test/responses/triggers/fire_success.xml +2 -0
  53. data/test/test_helper.rb +68 -0
  54. data/test/trigger_test.rb +45 -0
  55. metadata +342 -0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README ADDED
@@ -0,0 +1,4 @@
1
+
2
+ gem 'lyris', '~> 1.0.0'
3
+ bundle
4
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ task :default => [:test]
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.pattern = 'test/*_test.rb'
8
+ t.verbose = true
9
+ end
10
+
11
+ Rake::Task[:test].comment = "Run all tests"
12
+
13
+ begin
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gemspec|
16
+ gemspec.name = "mosaic-lyris"
17
+ gemspec.summary = "Lyris/EmailLabs API"
18
+ gemspec.description = "A wrapper for the Lyris/EmailLabs API to simplify integration"
19
+ gemspec.email = "brent.faulkner@mosaic.com"
20
+ gemspec.homepage = "http://github.com/mosaicxm/mosaic-lyris"
21
+ gemspec.authors = ["S. Brent Faulkner"]
22
+ gemspec.add_dependency('builder')
23
+ gemspec.add_dependency('active_support')
24
+ gemspec.add_dependency('htmlentities')
25
+ gemspec.add_dependency('nokogiri')
26
+ gemspec.add_dependency('tzinfo')
27
+ gemspec.add_development_dependency('mocha')
28
+ end
29
+ rescue LoadError
30
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
31
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
data/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ gem 'nokogiri'
2
+ gem 'htmlentities'
3
+
4
+ require 'lyris'
data/lib/lyris.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'mosaic/lyris'
2
+ require 'mosaic/lyris_mailer'
@@ -0,0 +1,12 @@
1
+ %w(
2
+ object
3
+ demographic
4
+ filter
5
+ list
6
+ message
7
+ partner
8
+ record
9
+ trigger
10
+ ).each do |file|
11
+ require File.join(File.dirname(__FILE__),'lyris',file)
12
+ end
@@ -0,0 +1,65 @@
1
+ module Mosaic
2
+ module Lyris
3
+ class Demographic < Object
4
+ attr_reader :enabled,
5
+ :group,
6
+ :id,
7
+ :list_id,
8
+ :name,
9
+ :options,
10
+ :size,
11
+ :type
12
+
13
+ class << self
14
+ def add(type, name, options = {})
15
+ validate_options!(type, options)
16
+ reply = post('demographic', 'add') do |request|
17
+ request.MLID options[:list_id] if options[:list_id]
18
+ put_data(request, demographic_type(type), name)
19
+ put_array_data(request, 'option', options[:option])
20
+ put_data(request, 'state', 'enabled') if options[:enabled]
21
+ put_data(request, 'size', options[:size]) if options[:size]
22
+ end
23
+ new(options.merge(:id => reply.at('/DATASET/DATA').inner_html.to_i, :name => name, :type => type))
24
+ end
25
+
26
+ def query(what, options = {})
27
+ reply = post('demographic', query_type(what)) do |request|
28
+ request.MLID options[:list_id] if options[:list_id]
29
+ end
30
+ reply.search('/DATASET/RECORD').collect do |record|
31
+ new :enabled => ([:enabled, :enabled_details].include?(what) || get_boolean_data(record, 'state', 'enabled')),
32
+ :group => get_data(record, "group"),
33
+ :id => get_integer_data(record, 'id'),
34
+ :list_id => options[:list_id],
35
+ :name => get_data(record, 'name'),
36
+ :options => get_array_data(record, 'option'),
37
+ :size => get_integer_data(record, 'size'),
38
+ :type => get_data(record, 'type').downcase.gsub(/ /,'_').to_sym
39
+ end
40
+ end
41
+
42
+ protected
43
+ def demographic_type(type)
44
+ raise ArgumentError, "expected :checkbox, :date, :multiple_checkbox, :multiple_select_list, :radio_button, :select_list, :text or :textarea; got #{type.inspect}" unless %w(checkbox date multiple_checkbox multiple_select_list radio_button select_list text textarea).include?(type.to_s)
45
+ type.to_s.gsub(/_/,' ')
46
+ end
47
+
48
+ def query_type(what)
49
+ raise ArgumentError, "expected :all, :enabled or :enabled_details; got #{what.inspect}" unless %w(all enabled enabled_details).include?(what.to_s)
50
+ "query-#{what}".gsub(/_/,'-')
51
+ end
52
+
53
+ def validate_options!(type, options)
54
+ if %w(multiple_checkbox multiple_select_list radio_button select_list).include?(type.to_s)
55
+ raise ArgumentError, "missing options for #{type.inspect} demographic" unless options[:options]
56
+ else
57
+ raise ArgumentError, "#{type.inspect} demographic does not support options" if options[:options]
58
+ end
59
+ raise ArgumentError, "#{type.inspect} demographic does not support :size option" if options[:size] unless %w(multiple_select_list select_list).include?(type.to_s)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,6 @@
1
+ module Mosaic
2
+ module Lyris
3
+ class Filter < Object
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,73 @@
1
+ require 'time'
2
+
3
+ module Mosaic
4
+ module Lyris
5
+ class List < Object
6
+ attr_reader :cache_time,
7
+ :clickthru_url,
8
+ :footer_html,
9
+ :footer_text,
10
+ :handle_autoreply,
11
+ :handle_autoreply_email,
12
+ :handle_unsubscribe,
13
+ :handle_unsubscribe_email,
14
+ :id,
15
+ :last_sent,
16
+ :members,
17
+ :messages,
18
+ :name,
19
+ :reply_forward_email,
20
+ :reply_forward_subject,
21
+ :reply_from_email,
22
+ :reply_from_name,
23
+ :status
24
+
25
+ def active?
26
+ status == 'active'
27
+ end
28
+
29
+ def archived?
30
+ status == 'archived'
31
+ end
32
+
33
+ class << self
34
+ def add(name, attributes = {})
35
+ reply = post('list', 'add') do |request|
36
+ put_data(request, 'name', name)
37
+ put_extra_data(request, 'CLICKTHRU_URL', attributes[:clickthru_url])
38
+ end
39
+ new attributes.merge(:id => reply.at('/DATASET/DATA').inner_html.to_i, :name => name)
40
+ end
41
+
42
+ def delete(id)
43
+ reply = post('list', 'delete') do |request|
44
+ request.MLID id
45
+ end
46
+ new :id => id
47
+ end
48
+
49
+ def query(what)
50
+ reply = post('list', query_type(what))
51
+ reply.search('/DATASET/RECORD').collect do |record|
52
+ new :cache_time => get_xml_time_data(record, 'cache-time'),
53
+ :id => get_integer_data(record, 'name', :id),
54
+ :last_sent => get_date_data(record, 'last-sent'),
55
+ :members => get_integer_data(record, 'members'),
56
+ :messages => get_integer_data(record, 'messages'),
57
+ :name => get_data(record, 'name'),
58
+ :status => get_data(record, 'status')
59
+ end
60
+ end
61
+
62
+ protected
63
+ def query_type(what)
64
+ if what == :all
65
+ 'query-listdata'
66
+ else
67
+ raise ArgumentError, "expected :all, got #{what.inspect}"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,95 @@
1
+ module Mosaic
2
+ module Lyris
3
+ class Message < Object
4
+ attr_reader :id,
5
+ :aol,
6
+ :category,
7
+ :charset,
8
+ :clickthru,
9
+ :clickthru_text,
10
+ :edited_at,
11
+ :format,
12
+ :from_email,
13
+ :from_name,
14
+ :html,
15
+ :htmlencoding,
16
+ :name,
17
+ :rule,
18
+ :segment,
19
+ :segment_id,
20
+ :sent,
21
+ :sent_at,
22
+ :stats_sent,
23
+ :subject,
24
+ :text,
25
+ :type
26
+
27
+ class << self
28
+ def query(what, options = {})
29
+ if what == :all
30
+ query_all(options)
31
+ else
32
+ query_one(what, options)
33
+ end
34
+ end
35
+
36
+ protected
37
+ def query_all(options)
38
+ reply = post('message', 'query-listdata') do |request|
39
+ request.MLID options[:list_id] if options[:list_id]
40
+ end
41
+ reply.search('/DATASET/RECORD').collect do |record|
42
+ edit_time = get_time_data(record, 'last-edit-time')
43
+ edit_date = get_date_data(record, 'last-edit-date')
44
+ sent = get_data(record, 'sent')
45
+ sent_at = (sent == 'yes') ? get_date_data(record, 'date').to_time + get_time_offset_data(record, 'delivery') : nil
46
+ subject = get_data(record, 'subject')
47
+ new :id => get_integer_data(record, 'mid'),
48
+ :category => get_data(record, 'category'),
49
+ :edited_at => edit_date && (edit_date.to_time + edit_time.to_i - edit_time.to_date.to_time.to_i),
50
+ :format => get_data(record, 'message-format'),
51
+ :segment_id => get_integer_data(record, 'segment-id'),
52
+ :segment => get_data(record, 'segment'),
53
+ :sent => sent,
54
+ :sent_at => sent_at,
55
+ :stats_sent => get_integer_data(record, 'stats-sent'),
56
+ :subject => subject,
57
+ :type => get_data(record, 'mlid').blank? ? 'system' : (subject =~ /\APROOF: / ? 'test' : 'user')
58
+ end
59
+ end
60
+
61
+ def query_one(id, options)
62
+ reply = post('message', 'query-data') do |request|
63
+ request.MLID options[:list_id] if options[:list_id]
64
+ request.MID id
65
+ end
66
+ record = reply.at('/DATASET/RECORD')
67
+ sent = get_data(record, 'sent')
68
+ sent_at = (sent == 'yes') ? get_date_data(record, 'date').to_time + get_time_offset_data(record, 'delivery') : nil
69
+ new :id => id,
70
+ :aol => get_data(record, 'message-aol'),
71
+ :category => get_data(record, 'category'),
72
+ :charset => get_data(record, 'charset'),
73
+ :clickthru => get_boolean_data(record, 'clickthru', 'on'),
74
+ :clickthru_text => get_boolean_data(record, 'clickthru-text', 'on'),
75
+ :format => get_data(record, 'message-format'),
76
+ :from_email => get_data(record, 'from-email'),
77
+ :from_name => get_data(record, 'from-name'),
78
+ :html => get_data(record, 'message-html'),
79
+ :htmlencoding => get_data(record, 'htmlencoding'),
80
+ :segment_id => get_integer_data(record, 'rule'),
81
+ :segment => get_data(record, 'rule-name'),
82
+ :name => get_data(record, 'name'),
83
+ :sent => sent,
84
+ :sent_at => sent_at,
85
+ :subject => get_data(record, 'subject'),
86
+ :text => get_data(record, 'message-text')
87
+ end
88
+ end
89
+
90
+ def sent?
91
+ sent == 'yes'
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,227 @@
1
+ require 'builder'
2
+ require 'net/https'
3
+ require 'nokogiri'
4
+ require 'uri'
5
+ require 'htmlentities'
6
+
7
+ require 'active_support/core_ext/object/blank'
8
+ require 'active_support/core_ext/time/calculations'
9
+
10
+ module Mosaic
11
+ module Lyris
12
+ class Error < RuntimeError; end
13
+
14
+ class Object
15
+ private_class_method :new
16
+
17
+ @@logger = nil
18
+
19
+ def initialize(attributes)
20
+ attributes.each do |attribute,value|
21
+ instance_variable_set "@#{attribute}", value unless value.nil?
22
+ end
23
+ end
24
+
25
+ def to_param
26
+ id && id.to_s
27
+ end
28
+
29
+ class << self
30
+ def configuration
31
+ @@configuration ||= load_configuration
32
+ end
33
+
34
+ def configuration=(value)
35
+ @@configuration = value
36
+ end
37
+
38
+ def load_configuration
39
+ configuration = YAML.load_file(File.join(::Rails.root.to_s,'config','lyris.yml')) rescue {}
40
+ configuration = configuration[::Rails.env] if configuration.include?(::Rails.env)
41
+ configuration
42
+ end
43
+ end
44
+
45
+ protected
46
+ class << self
47
+ def callback_url
48
+ configuration['callback_url']
49
+ end
50
+
51
+ def default_list_id
52
+ configuration['list_id']
53
+ end
54
+
55
+ def default_trigger_id
56
+ configuration['trigger_id']
57
+ end
58
+
59
+ def get_array_data(record, type)
60
+ if record.at("DATA[@type='#{type}']")
61
+ record.search("DATA[@type='#{type}']").collect do |data|
62
+ if block_given?
63
+ yield data
64
+ else
65
+ data.inner_html
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def get_boolean_data(record, type, value, attribute = nil, conditions = {})
72
+ if data = get_data(record, type, attribute, conditions)
73
+ data == value
74
+ end
75
+ end
76
+
77
+ def get_data(record, type, attribute = nil, conditions = {})
78
+ xpath = "DATA[@type='#{type}']"
79
+ xpath << conditions.collect { |a,v| "[@#{a}='#{v}']" }.join
80
+ if element = record.at(xpath)
81
+ data = attribute ? element[attribute] : element.inner_html
82
+ # TODO: fix encoding if necessary... seems like we should need to convert to UTF-8 here, but this works for now
83
+ HTMLEntities.new.decode(data.gsub(/&#(\d+);/) { Integer($1).chr })
84
+ end
85
+ end
86
+
87
+ def get_date_data(record, type, attribute = nil, conditions = {})
88
+ data = get_data(record, type, attribute, conditions)
89
+ Date.parse(data) unless data.blank?
90
+ end
91
+
92
+ def get_demographic_data(record)
93
+ if data = get_array_data(record, 'demographic') { |d| [ d[:id].to_i, d.inner_html ] }
94
+ data.inject({}) do |h,(k,v)|
95
+ case h[k]
96
+ when NilClass
97
+ h[k] = v
98
+ when Array
99
+ h[k] << v
100
+ else
101
+ h[k] = Array(h[k])
102
+ h[k] << v
103
+ end
104
+ h
105
+ end
106
+ end
107
+ end
108
+
109
+ def get_element(record, element)
110
+ if data = record.at("/#{element}")
111
+ data.inner_html
112
+ end
113
+ end
114
+
115
+ def get_integer_data(record, type, attribute = nil, conditions = {})
116
+ if data = get_data(record, type, attribute, conditions)
117
+ data.gsub(/,/,'').to_i
118
+ end
119
+ end
120
+
121
+ def get_integer_element(record, element)
122
+ get_element(record, element).to_i
123
+ end
124
+
125
+ def get_time_data(record, type, attribute = nil, conditions = {})
126
+ if data = get_data(record, type, attribute, conditions)
127
+ Time.use_zone('Pacific Time (US & Canada)') do
128
+ Time.zone.parse(data)
129
+ end
130
+ end
131
+ end
132
+
133
+ def get_time_element(record, element)
134
+ if data = get_element(record, element)
135
+ Time.parse(data) + (Time.zone.utc_offset - Time.zone_offset('PST'))
136
+ end
137
+ end
138
+
139
+ def get_time_offset_data(record, type, attribute = nil, conditions = {})
140
+ if offset = get_integer_data(record, type, attribute, conditions)
141
+ offset + (Time.zone.utc_offset - Time.zone_offset('PST'))
142
+ end
143
+ end
144
+
145
+ def get_xml_time_data(record, type, attribute = nil, conditions = {})
146
+ if data = get_data(record, type, attribute, conditions)
147
+ Time.xmlschema(data)
148
+ end
149
+ end
150
+
151
+ def logger
152
+ @@logger ||= nil
153
+ end
154
+
155
+ def logger=(logger)
156
+ @@logger = logger
157
+ end
158
+
159
+ def password
160
+ configuration['password']
161
+ end
162
+
163
+ def post(type, activity, &block)
164
+ xml = Builder::XmlMarkup.new(:indent => 2)
165
+ xml.instruct!
166
+ xml.DATASET do
167
+ xml.SITE_ID site_id
168
+ put_extra_data(xml, 'password', password)
169
+ block.call(xml) if block
170
+ end
171
+ input = xml.target!
172
+
173
+ request = Net::HTTP::Post.new("/API/mailing_list.html")
174
+ logger.debug ">>>>> REQUEST:\ntype=#{type}\nactivity=#{activity}\ninput=#{input}\n>>>>>" if logger
175
+ request.set_form_data('type' => type, 'activity' => activity, 'input' => input)
176
+
177
+ conn = Net::HTTP.new(server, 443)
178
+ conn.use_ssl = true
179
+ conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
180
+
181
+ conn.start do |http|
182
+ # TODO: parse encoding from declaration? update declaration after conversion?
183
+ reply = http.request(request).body
184
+ logger.debug ">>>>> REPLY:\n#{reply}\n>>>>>" if logger
185
+ document = Nokogiri.XML(reply)
186
+ raise Error, (document % '/DATASET/DATA').inner_html unless document % "/DATASET/TYPE[.='success']"
187
+ document
188
+ end
189
+ end
190
+
191
+ def put_array_data(request, type, values)
192
+ Array(values).each do |value|
193
+ put_data(request, type, value)
194
+ end
195
+ end
196
+
197
+ def put_data(request, type, value, attributes = {})
198
+ request.DATA value, {:type => type}.merge(attributes) unless value.nil?
199
+ end
200
+
201
+ def put_demographic_data(request, demographics)
202
+ Array(demographics).each do |id, value|
203
+ Array(value).each do |v|
204
+ put_data(request, 'demographic', v, :id => id)
205
+ end
206
+ end
207
+ end
208
+
209
+ def put_extra_data(request, id, value)
210
+ put_data(request, 'extra', value, :id => id)
211
+ end
212
+
213
+ def server
214
+ configuration['server']
215
+ end
216
+
217
+ def site_id
218
+ configuration['site_id']
219
+ end
220
+
221
+ def triggers
222
+ configuration['triggers']
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end