distribution_wrappers 0.2.2

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +102 -0
  3. data/Gemfile.lock +295 -0
  4. data/README.md +22 -0
  5. data/Rakefile +53 -0
  6. data/VERSION +1 -0
  7. data/distribution_wrappers.gemspec +173 -0
  8. data/lib/config/key_chain.yml +70 -0
  9. data/lib/config/key_chain_dev.yml +71 -0
  10. data/lib/distribution_wrappers.rb +64 -0
  11. data/lib/distribution_wrappers/_base.rb +68 -0
  12. data/lib/distribution_wrappers/backstitch/backstitch.rb +267 -0
  13. data/lib/distribution_wrappers/email/_email.rb +48 -0
  14. data/lib/distribution_wrappers/email/template.html.haml +29 -0
  15. data/lib/distribution_wrappers/google/google.rb +129 -0
  16. data/lib/distribution_wrappers/hipchat/hipchat.rb +80 -0
  17. data/lib/distribution_wrappers/office365/office365.rb +51 -0
  18. data/lib/distribution_wrappers/sendgrid/sendgrid.rb +35 -0
  19. data/lib/distribution_wrappers/slack/slack.rb +87 -0
  20. data/lib/helpers/keys.rb +10 -0
  21. data/lib/helpers/text_helper.rb +9 -0
  22. data/lib/models/device_session_distro.rb +5 -0
  23. data/lib/models/distribution_channel_distro.rb +4 -0
  24. data/lib/models/feed_distro.rb +11 -0
  25. data/lib/models/organization_admin_distro.rb +5 -0
  26. data/lib/models/organization_distro.rb +10 -0
  27. data/lib/models/studio_post_version_contact_distro.rb +5 -0
  28. data/lib/models/studio_post_version_distro.rb +5 -0
  29. data/lib/models/team_distro.rb +13 -0
  30. data/lib/models/team_feed_distro.rb +5 -0
  31. data/lib/models/team_member_distro.rb +6 -0
  32. data/lib/models/topic_distro.rb +11 -0
  33. data/lib/models/topic_feed_distro.rb +5 -0
  34. data/lib/models/topic_subscription_distro.rb +9 -0
  35. data/lib/models/user_distro.rb +34 -0
  36. data/test/helper.rb +34 -0
  37. data/test/test_distribution_wrappers.rb +7 -0
  38. metadata +556 -0
@@ -0,0 +1,267 @@
1
+ module DistributionWrappers
2
+ class Backstitch < DistributionWrappers::Base
3
+
4
+ def send_message(recipient)
5
+ super
6
+ @response = Semantic::OrganizationResults::Article.bulk_store([@message], true, true)
7
+ results = []
8
+
9
+ @version_contact.channel_entity_identifier = @message[:reference_id]
10
+ @version_contact.save
11
+
12
+ # send push notification if priority alerts is turned on for the selected feed
13
+ feed = FeedDistro.find(recipient['identifier'].to_i)
14
+ if feed.params["priority_alerts"] && @message
15
+ pushwoosh_id = feed.owner.pushwoosh_id if feed.owner_type == "Organization" && feed.owner.pushwoosh_id
16
+ organization_id = feed.owner.id if feed.owner && feed.owner_type == "Organization"
17
+ send_notifications(@message, feed.id, organization_id, pushwoosh_id)
18
+ end
19
+ end
20
+
21
+ def get_contacts
22
+ channel = DistributionChannelDistro.find @params[:channel_id]
23
+ user = UserDistro.find(channel.owner_id)
24
+
25
+ feeds = user.managed_feeds
26
+
27
+ csv_string = ""
28
+
29
+ feeds.each_with_index do |feed, index|
30
+ csv_string += CSV.generate_line [feed.name, feed.feed_id, '{"url": null}', 'feed', @params[:channel_id]]
31
+
32
+ if index % 100 == 0
33
+ @params[:temp_file].write(csv_string)
34
+ csv_string = ""
35
+ end
36
+ end
37
+
38
+ @params[:temp_file].write(csv_string)
39
+
40
+ return true
41
+ end
42
+
43
+ private
44
+
45
+ def prepare(identifier=nil)
46
+ published_at = Time.now.utc.to_datetime
47
+ doc = Nokogiri::HTML(@params[:post].draft_content)
48
+
49
+ doc = append_parameters(doc, true)
50
+ doc = update_feedback_trackers(doc) if doc.css('td[data-cell-identifier="feedback-form-container"]').length > 0
51
+
52
+ doc = redirect_links(doc, true)
53
+
54
+ if @params[:reference_id]
55
+ result = Semantic::OrganizationResults::Article.find_by_reference_id(@params[:reference_id])
56
+ else
57
+ result = Semantic::OrganizationResults::Article.new("feed-#{identifier}-#{@params[:user].id}-#{published_at.to_i}")
58
+ end
59
+
60
+ build_result(doc, result, identifier)
61
+ end
62
+
63
+ def update_feedback_trackers(doc)
64
+ doc.css('td[data-cell-identifier="feedback-form-container"]').each do |feedback|
65
+ identifier = feedback.css('a').attr('data-url-identifier')
66
+ feedback.inner_html = "<textarea id='#{identifier}' style='width:100%;height:100%;outline:none;border:none;resize:none;'></textarea>"
67
+ end
68
+
69
+ doc.css('td[data-cell-identifier="feedback-submit-container"]').each do |feedback|
70
+ base_url = feedback.css('a').attr('href')
71
+ feedback.css('a').attr('href').value = base_url.value.gsub('feedback_form', 'record_feedback')
72
+ end
73
+
74
+ doc
75
+ end
76
+
77
+ def build_result(doc, result, identifier)
78
+ begin
79
+ published_at = Time.now.utc.to_datetime
80
+
81
+ created_by = UserDistro.find @params[:post].created_by_id
82
+ result[:published_at] = published_at
83
+ result[:title] = @params[:post].title
84
+ result[:plain_text_title] = TextHelper.clean_text(result[:title])
85
+ result[:author][:name] = @params[:user].display_name
86
+ result[:tracking] = {
87
+ :type => "text",
88
+ :feed_id => identifier.to_i
89
+ }
90
+
91
+ result[:origin][:id] = created_by.id
92
+ result[:origin][:name] = created_by.display_name
93
+ result[:origin][:icon][:url] = created_by.avatar_url
94
+
95
+ if @params[:post].draft_content.match(/<html>.*<\/html>/).nil?
96
+ result[:full_text] = "<html>#{@params[:post].draft_content}</html>"
97
+ else
98
+ result[:full_text] = @params[:post].draft_content
99
+ end
100
+
101
+ result[:images][:full_size][:url] = @params[:post].main_image if @params[:post].main_image
102
+
103
+ video_iframes = doc.css('iframe').select{|iframe| URI.unescape(iframe.attr('src')).match(/(?<=youtube\.com\/embed\/)(.+?)(?=\?|$)/)}
104
+
105
+ video_iframes.each do |iframe|
106
+ iframe['style'] = "box-sizing: content-box; max-width: 100%; border: 0px;"
107
+ iframe['height'] = '360'
108
+ iframe['width'] = '640'
109
+ end
110
+
111
+ if result[:images][:full_size][:url].nil?
112
+ if img = doc.xpath('//iframe').first
113
+ if img.attr('src').match(/youtube/) ||
114
+ img.attr('src').match(/dailymotion/) ||
115
+ img.attr('src').match(/wistia/)
116
+ VideoInfo.provider_api_keys = {youtube: Keys.google.key, vimeo: Keys.vimeo.client_id}
117
+ video = VideoInfo.new(img.attr('src'))
118
+ article[:images][:full_size][:url] = video.thumbnail_large
119
+ end
120
+ end
121
+ end
122
+
123
+ base_url = ""
124
+ if ENV['ENVIRONMENT'] == 'production'
125
+ base_url = "http://studio.backstit.ch"
126
+ else
127
+ base_url = "http://localhost:3000"
128
+ end
129
+
130
+ new_node = doc.create_element 'div'
131
+ new_node.inner_html = "<img src='#{base_url}/posts/#{@params[:post].id}/engagement/tracking.gif?contact_id=#{@params[:version_contact_id]}&version_id=#{@params[:version_id]}&organization_id=#{@params[:organization].id}&email=<<email_placeholder>>'/>"
132
+ doc.children[1].add_child(new_node)
133
+
134
+ result[:full_text] = doc.to_html
135
+
136
+ result[:plain_text] = TextHelper.clean_text(result[:full_text])
137
+
138
+ summary = Pismo::Document.new(result[:plain_text])
139
+ result[:description] = summary.lede
140
+ result[:plain_text_description] = TextHelper.clean_text(result[:description])
141
+
142
+ base_url = ""
143
+ if ENV['ENVIRONMENT'] == 'production'
144
+ base_url = "http://backstit.ch"
145
+ else
146
+ base_url = "http://localhost:3000"
147
+ end
148
+ result[:origin][:url] = "#{base_url}/feeds/#{result[:tracking][:feed_id]}/results/#{result[:reference_id]}"
149
+
150
+ return result
151
+ rescue
152
+ end
153
+ end
154
+
155
+ def send_notifications(result, feed_id, organization_id=nil, pushwoosh_id=nil)
156
+ Pushwoosh.configure do |config|
157
+ config.auth = 'VvFYDCs1zaz5PIy9WgC7KDfSmAD6Sqlykrxw1mxsXnfSgRG07k84wuCD4hBRAt97EyvDJQKl2ewy01h8olzV'
158
+ if pushwoosh_id
159
+ config.application = pushwoosh_id
160
+ else
161
+ config.application = 'FF35B-68BC0'
162
+ end
163
+ end
164
+
165
+ topics = TopicDistro.joins(:topic_feeds).where('topic_feeds.feed_id = ?', feed_id)
166
+
167
+ body = []
168
+ topics.each do |topic|
169
+ internal_source_count = topic.feeds.where("feeds.owner_id is not null").count
170
+ if topic.alias? && ((topic.promoted_alias? && internal_source_count > 0) || internal_source_count == 0)
171
+ alias_name = "#{topic.alias}"
172
+ alias_name = "#{alias_name},#{topic.promoted_alias}" if internal_source_count > 0
173
+ elsif topic.serialized_alias? && ((topic.serialized_promoted_alias? && internal_source_count > 0) || internal_source_count == 0)
174
+ indices = topic.serialized_alias["indices"]
175
+ indices.concat(topic.serialized_promoted_alias["indices"]) unless topic.serialized_promoted_alias.nil?
176
+
177
+ should = []
178
+ should << {topic.serialized_alias["action"] => topic.serialized_alias["filters"]}
179
+ should << {topic.serialized_promoted_alias["action"] => topic.serialized_promoted_alias["filters"]} unless topic.serialized_promoted_alias.nil?
180
+
181
+ alias_name = indices.uniq.join(",")
182
+ filters = {"bool" => {"should" => should}}
183
+ else
184
+ next
185
+ end
186
+
187
+ query = {
188
+ :query => {
189
+ :term => {
190
+ :reference_id => result[:reference_id]
191
+ }
192
+ }
193
+ }
194
+
195
+ query[:filter] = filters if filters
196
+
197
+ body << {:search_type => 'count', :search => query, :index => alias_name, :ignore_indices => "missing", :topic_id => topic.id}
198
+ end
199
+
200
+ options = {
201
+ :body => body
202
+ }
203
+
204
+ search = ELASTICSEARCH.msearch options
205
+
206
+ topic_ids = []
207
+
208
+ body.each_with_index do |topic, index|
209
+ if search["responses"][index]["hits"]["total"] > 0
210
+ topic_ids << topic[:topic_id]
211
+ end
212
+ end
213
+
214
+ # If a user hasn't signed into the app, send them a priority email instead of a push notification
215
+ users = UserDistro.joins(:topics).joins("left join device_sessions on device_sessions.user_id = users.id")
216
+ .where("topics.id in (?) AND device_sessions.id is null", topic_ids)
217
+ .uniq.pluck(:id).compact
218
+
219
+ # Pull device sessions for every user that's signed into the app so we can send push notifications
220
+ device_sessions = DeviceSessionDistro.joins(:user => :topics).where("topics.id in (?)", topic_ids)
221
+ .uniq.pluck(:device_token).compact.delete_if{|token| token == "" }
222
+
223
+
224
+ users.each do |user_id|
225
+ # Mailer::PriorityAlertWorker.perform_async(user_id, organization_id, result)
226
+ end
227
+
228
+ message = ""
229
+
230
+ case result[:type]
231
+ when 'article'
232
+ plain_text = ""
233
+ plain_text = " - #{result[:plain_text][0..140]}" if result[:plain_text]
234
+ message = "#{result[:title]}#{plain_text}"
235
+ when 'status', 'photo'
236
+ message = "#{result[:plain_text_description][0..140]}"
237
+ when 'video'
238
+ message = "#{result[:title]} - #{result[:description][0..140]}"
239
+ when 'email'
240
+ message = "#{result[:subject]} - #{result[:plain_text][0..140]}"
241
+ when "product", "service", "hotel"
242
+ message = "#{result[:title][:long][0..140]}"
243
+ end
244
+
245
+ options = {
246
+ "send_date" => "now",
247
+ "ignore_user_timezone" => true,
248
+ "platforms" => [1,3],
249
+ "data" => {
250
+ "ref_id" => result[:reference_id],
251
+ "feed_id" => feed_id.to_s
252
+ }
253
+ }
254
+
255
+ #ios options
256
+ options["ios_badges"] = "+1"
257
+ options["apns_trim_content"] = 1
258
+
259
+ # android options
260
+ options["android_badges"] = "+1"
261
+
262
+ device_sessions.each_slice(1000) do |tokens|
263
+ Pushwoosh.notify_devices(message, tokens, options)
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,48 @@
1
+ module DistributionWrappers
2
+ class Email < DistributionWrappers::Base
3
+
4
+ def prepare(email)
5
+ @params[:email] = email
6
+ template = Tilt::HamlTemplate.new(File.join(File.dirname(__FILE__), 'template.html.haml'))
7
+
8
+ doc = Nokogiri::HTML(@params[:post].draft_content)
9
+
10
+ doc = replace_video(doc)
11
+
12
+ doc = append_parameters(doc)
13
+
14
+ doc = redirect_links(doc)
15
+
16
+ @params[:draft_content] = html
17
+
18
+ body_html = template.render(@params)
19
+
20
+ body_html
21
+ end
22
+
23
+ private
24
+
25
+ def replace_video(doc)
26
+ VideoInfo.provider_api_keys = {youtube: '294129192803.apps.googleusercontent.com', vimeo: 'cb5e2b895a44ce8a74336fc1dde69f2e9e2ca172'}
27
+ video_iframes = doc.css('span iframe').select{|iframe| URI.unescape(iframe.attr('src')).match(/(?<=youtube\.com\/embed\/)(.+?)(?=\?|$)/)}
28
+
29
+ video_iframes.each do |iframe|
30
+ id = URI.unescape(iframe.attr('src')).scan(/(?<=youtube\.com\/embed\/)(.+?)(?=\?|$)/).flatten[0]
31
+ new_node = doc.create_element "div"
32
+ video_info = VideoInfo.new("http://www.youtube.com/watch?v=#{id}")
33
+ new_node.inner_html = """<table width='100%' border='0' cellspacing='0' cellpadding='0'>
34
+ <tr>
35
+ <td align='center'>
36
+ <a href='http://www.youtube.com/watch?v=#{id}'>
37
+ <img src='#{video_info.thumbnail_large}' style='height:360px;width:480px;display:block;'>
38
+ <img src='http://images-backstitch.s3.amazonaws.com/emails/video_info.png' style='width:480px;display:block;'>
39
+ </a>
40
+ </td>
41
+ </tr>
42
+ </table>"""
43
+ iframe.parent.replace new_node
44
+ end
45
+ doc
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ !!!
2
+
3
+ - if ENV['ENVIRONMENT'] == "development"
4
+ - base_url = "http://localhost:3000"
5
+ - else
6
+ - base_url = "http://studio.backstit.ch"
7
+
8
+ %html
9
+ %head
10
+ %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
11
+ %body{:style => "margin: 0px;"}
12
+ %table{:width => '100%', :border => 0, :cellspacing => 0, :cellpadding => '0px', :style => "background: #f1f1f1; border: 1px solid #e4e4e4; color: #3e3e3e;"}
13
+ %tr
14
+ %td{:align => 'center'}
15
+ -# container
16
+ %table{:width => '600', :border => 0, :cellspacing => 0, :cellpadding => 0, :style => "width: 600px;background: #ffffff;padding:8px"}
17
+ %tr
18
+ %td
19
+ != self[:draft_content]
20
+
21
+ %tr{:style => "font-family: Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 12px; color: #656565; text-align: center;"}
22
+ %td{:style => "padding: 10px 0px;"}
23
+ %p
24
+ %b{:style => "color: #{self[:organization].highlight_color}"}
25
+ = self[:organization].name
26
+ sent you this message using backstitch Studio.
27
+ %p{:style => "color: #656565"}
28
+ ©2016 backstitch Inc. 1528 Woodward Ave. 3rd Floor, Detroit, MI 48226
29
+ %img{:src => "#{base_url}/posts/#{self[:post].id}/engagement/tracking.gif?contact_id=#{self[:version_contact_id]}&version_id=#{self[:version_id]}&organization_id=#{self[:organization].id}"}
@@ -0,0 +1,129 @@
1
+ # require 'google/apis/gmail_v1'
2
+ # require 'googleauth'
3
+ # require 'googleauth/stores/file_token_store'
4
+ require 'csv'
5
+
6
+ module DistributionWrappers
7
+ class Google < DistributionWrappers::Email
8
+
9
+ def send_message(recipient)
10
+ super
11
+
12
+ email = recipient['identifier']
13
+ client = get_access
14
+
15
+ msg = Mail.new
16
+ msg.date = Time.now
17
+ msg.subject = @params[:subject]
18
+ msg.body = @message
19
+ msg.to = email
20
+ msg.content_type = 'text/html'
21
+
22
+ if @params[:custom_opts]['send_from']
23
+ msg.from = @params[:custom_opts]['send_from']
24
+ end
25
+
26
+ send_response = client.send_user_message('me', upload_source: StringIO.new(msg.to_s), content_type: 'message/rfc822')
27
+ return {:response => send_response, :msg => msg}
28
+ end
29
+
30
+ def get_contacts
31
+ record_count = 0
32
+ loop do
33
+ url = "https://www.google.com/m8/feeds/contacts/default/full?max-results=10000&alt=json&start-index=#{record_count+1}"
34
+ response = JSON.parse(RestClient.get(url, {"GData-Version" => "3.0", "Authorization" => "Bearer #{@auth[:key]}"}))
35
+
36
+ break if response['feed'].nil? || response['feed']['entry'].nil?
37
+
38
+ response['feed']['entry'].each_with_index do |entry, index|
39
+ new_string = convert_to_contact_entry(entry, index)
40
+ csv_string += new_string unless new_string.nil?
41
+ if index % 100 == 0
42
+ @params[:temp_file].write(csv_string)
43
+ csv_string = ""
44
+ end
45
+ end
46
+
47
+ @params[:temp_file].write(csv_string)
48
+ csv_string = ""
49
+
50
+ record_count += response['feed']['entry'].length
51
+ break if response['feed']['entry'].length < 10000
52
+ end
53
+
54
+ @params[:temp_file].rewind
55
+ return true
56
+ end
57
+
58
+ def convert_to_contact_entry(entry, index)
59
+ # creating nil fields to keep the fields consistent across other networks
60
+ name = ""
61
+ if entry['gd$name']
62
+ gd_name = entry['gd$name']
63
+ first_name = normalize_name(entry['gd$name']['gd$givenName']['$t']) if gd_name['gd$givenName']
64
+ last_name = normalize_name(entry['gd$name']['gd$familyName']['$t']) if gd_name['gd$familyName']
65
+ name = normalize_name(entry['gd$name']['gd$fullName']['$t']) if gd_name['gd$fullName']
66
+ name = full_name(first_name,last_name) if name.nil?
67
+ end
68
+
69
+ emails = []
70
+ entry['gd$email'].each do |email|
71
+ if email['rel']
72
+ split_index = email['rel'].index('#')
73
+ emails << {:name => email['rel'][split_index + 1, email['rel'].length - 1], :email => email['address']}
74
+ elsif email['label']
75
+ emails << {:name => email['label'], :email => email['address']}
76
+ end
77
+ end if entry['gd$email']
78
+
79
+ # Support older versions of the gem by keeping singular entries around
80
+ email = emails[0][:email] if emails[0]
81
+
82
+ name = email_to_name(name) if (!name.nil? && name.include?('@'))
83
+ name = email_to_name(email) if (name.nil? && emails[0] && email)
84
+
85
+ if email && email != "" && name && name != ""
86
+ return_string = CSV.generate_line [name, email, '{"url": null}', 'email', @params[:channel_id]]
87
+ else
88
+ return_string = nil
89
+ end
90
+
91
+ return_string
92
+ end
93
+
94
+ def get_access
95
+ service = Google::Google::Apis::GmailV1::GmailService.new
96
+ service.authorization = @auth[:key]
97
+ service
98
+ end
99
+
100
+ # normalize the name
101
+ def normalize_name name
102
+ return nil if name.nil?
103
+ name.chomp!
104
+ name.squeeze!(' ')
105
+ name.strip!
106
+ return name
107
+ end
108
+
109
+ # create a full name given the individual first and last name
110
+ def full_name first_name, last_name
111
+ return "#{first_name} #{last_name}" if first_name && last_name
112
+ return "#{first_name}" if first_name && !last_name
113
+ return "#{last_name}" if !first_name && last_name
114
+ return nil
115
+ end
116
+
117
+ # create a username/name from a given email
118
+ def email_to_name username_or_email
119
+ username_or_email = username_or_email.split('@').first if username_or_email.include?('@')
120
+ if group = (/(?<first>[a-z|A-Z]+)[\.|_](?<last>[a-z|A-Z]+)/).match(username_or_email)
121
+ first_name = normalize_name(group[:first])
122
+ last_name = normalize_name(group[:last])
123
+ return "#{first_name} #{last_name}"
124
+ end
125
+ username = normalize_name(username_or_email)
126
+ return username
127
+ end
128
+ end
129
+ end