mosaic-lyris 1.0.1

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 (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