439 lines
15 KiB
Ruby
439 lines
15 KiB
Ruby
#
|
|
# Copyright (C) 2011 Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
class Message < ActiveRecord::Base
|
|
include Workflow
|
|
include SendToStream
|
|
include Twitter
|
|
include TextHelper
|
|
|
|
has_many :attachments, :as => :context
|
|
belongs_to :notification
|
|
belongs_to :context, :polymorphic => true
|
|
belongs_to :communication_channel
|
|
belongs_to :user
|
|
belongs_to :asset_context, :polymorphic => true
|
|
|
|
attr_accessible :to, :from, :subject, :body, :delay_for, :context, :path_type, :from_name, :sent_at, :notification, :user, :communication_channel, :notification_name
|
|
|
|
after_save :stage_message
|
|
before_save :move_messages_for_deleted_users
|
|
before_save :infer_defaults
|
|
before_save :move_dashboard_messages
|
|
before_save :set_asset_context_code
|
|
validates_length_of :body, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
|
|
validates_length_of :transmission_errors, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
|
|
|
|
def move_messages_for_deleted_users
|
|
self.workflow_state = 'closed' if self.context_type != "ErrorReport" && (!self.user || self.user.deleted?)
|
|
end
|
|
|
|
def transmission_errors=(val)
|
|
if !val || val.length < self.class.maximum_text_length
|
|
write_attribute(:transmission_errors, val)
|
|
else
|
|
write_attribute(:transmission_errors, val[0,self.class.maximum_text_length])
|
|
end
|
|
end
|
|
|
|
on_create_send_to_streams do
|
|
if self.to == "dashboard" && Notification.types_to_show_in_feed.include?(self.notification_name)
|
|
self.user_id
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def move_dashboard_messages
|
|
self.workflow_state = 'dashboard' if self.to == 'dashboard' && !self.cancelled? && !self.closed?
|
|
end
|
|
|
|
def set_asset_context_code
|
|
self.asset_context_code = "#{self.context_type.underscore}_#{self.context_id}" rescue nil
|
|
end
|
|
|
|
named_scope :for_asset_context_codes, lambda { |context_codes| {
|
|
:conditions => {:asset_context_code => context_codes} }
|
|
}
|
|
|
|
# TODO: DOES ANYTHING USE THIS? IT IS DEPRECATED BY THE asset_context_code COLUMN
|
|
def context_code
|
|
self.asset_context_code
|
|
end
|
|
|
|
def notification_category
|
|
@cat ||= self.notification.category
|
|
end
|
|
|
|
named_scope :for, lambda { |context|
|
|
{ :conditions => ['messages.context_type = ? and messages.context_id = ?', context.class.base_ar_class.to_s, context.id]}
|
|
}
|
|
named_scope :after, lambda{ |date|
|
|
{ :conditions => ['messages.created_at > ?', date] }
|
|
}
|
|
|
|
named_scope :to_dispatch, lambda {
|
|
{ :conditions => ["messages.workflow_state = ? and messages.dispatch_at <= ? and 'messages.to' != ?", 'staged', Time.now.utc, 'dashboard' ]}
|
|
}
|
|
named_scope :to_email, lambda{
|
|
{ :conditions => ['messages.path_type = ? OR messages.path_type = ?', 'email', 'sms'] }
|
|
}
|
|
named_scope :to_facebook, lambda{
|
|
{ :conditions => ['messages.path_type = ? AND messages.workflow_state = ?', 'facebook', 'sent'], :order => 'sent_at DESC', :limit => 25 }
|
|
}
|
|
named_scope :not_to_email, lambda{
|
|
{ :conditions => ['messages.path_type != ? AND messages.path_type != ?', 'email', 'sms'] }
|
|
}
|
|
|
|
named_scope :by_name, lambda { |notification_name|
|
|
{ :conditions => ['messages.notification_name = ?', notification_name]}
|
|
}
|
|
|
|
named_scope :directed_to, lambda { |to|
|
|
if to == "dashboard"
|
|
{ :conditions => ['messages.to = ?', to]}
|
|
else
|
|
{ :conditions => ['messages.communication_channel_id = ?', CommunicationChannel.find_by_path(to)]}
|
|
end
|
|
}
|
|
|
|
named_scope :before, lambda { |date|
|
|
{:conditions => ['messages.created_at < ?', date] }
|
|
}
|
|
|
|
def self.old_dashboard
|
|
res = []
|
|
# TODO i18n
|
|
Notification.find_all_by_category("Course Content").each do |notification|
|
|
res += Message.in_state(:dashboard).by_name(notification.name).before(2.weeks.ago)[0..10]
|
|
end
|
|
res
|
|
end
|
|
|
|
named_scope :for_user, lambda { |user|
|
|
{ :conditions => {:user_id => user}}
|
|
}
|
|
|
|
# For finding a very particular message:
|
|
# Message.for(context).by_name(name).directed_to(to).for_user(user), or
|
|
# messages.for(context).by_name(name).directed_to(to).for_user(user)
|
|
# Where user can be a User or id, name needs to be the Notification name.
|
|
named_scope :staged, lambda {
|
|
{ :conditions => ['messages.workflow_state = ? and messages.dispatch_at > ?', 'staged', DateTime.now.utc.to_s(:db) ]}
|
|
}
|
|
|
|
named_scope :in_state, lambda { |state|
|
|
case state
|
|
when Array
|
|
{ :conditions => { :workflow_state => state.map{|f| f.to_s} } }
|
|
else
|
|
{ :conditions => {:workflow_state => state.to_s } }
|
|
end
|
|
}
|
|
|
|
workflow do
|
|
state :created do
|
|
event :stage, :transitions_to => :staged do
|
|
self.dispatch_at = Time.now.utc + self.delay_for
|
|
if self.to != 'dashboard' && !@stage_without_dispatch
|
|
MessageDispatcher.dispatch(self)
|
|
end
|
|
end
|
|
event :cancel, :transitions_to => :cancelled
|
|
event :close, :transitions_to => :closed # needed for dashboard messages
|
|
end
|
|
|
|
state :staged do
|
|
event :dispatch, :transitions_to => :sending
|
|
event :cancel, :transitions_to => :cancelled
|
|
event :close, :transitions_to => :closed # needed for dashboard messages
|
|
end
|
|
|
|
state :sending do
|
|
event :complete_dispatch, :transitions_to => :sent do
|
|
self.sent_at ||= Time.now
|
|
end
|
|
event :cancel, :transitions_to => :cancelled
|
|
event :close, :transitions_to => :closed
|
|
event :errored_dispatch, :transitions_to => :staged do
|
|
# A little delay so we don't churn so much when the server is down.
|
|
self.dispatch_at = Time.now.utc + 5.minutes
|
|
end
|
|
end
|
|
|
|
state :sent do
|
|
event :close, :transitions_to => :closed
|
|
event :bounce, :transitions_to => :bounced do
|
|
# Permenant reminder that this bounced.
|
|
self.communication_channel.bounce_count += 1
|
|
self.communication_channel.save!
|
|
self.is_bounced = true
|
|
end
|
|
event :recycle, :transitions_to => :staged
|
|
end
|
|
|
|
state :bounced do
|
|
event :close, :transitions_to => :closed
|
|
end
|
|
|
|
state :dashboard do
|
|
event :close, :transitions_to => :closed
|
|
event :cancel, :transitions_to => :closed
|
|
end
|
|
state :cancelled
|
|
|
|
state :closed do
|
|
event :send_message, :transitions_to => :closed do
|
|
self.sent_at ||= Time.now
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
# skip dispatching the message during the stage transition, useful when batch
|
|
# dispatching.
|
|
def stage_without_dispatch!
|
|
@stage_without_dispatch = true
|
|
end
|
|
|
|
# Sets a few defaults and gets it on its way to be dispatched.
|
|
# The path: created -> staged -> sending -> sent
|
|
def stage_message
|
|
self.stage if self.state == :created
|
|
if self.dashboard?
|
|
messages = Message.in_state(:dashboard).find_all_by_notification_id_and_context_id_and_context_type_and_user_id(self.notification_id, self.context_id, self.context_type, self.user_id)
|
|
(messages - [self]).each{|m| m.close }
|
|
end
|
|
end
|
|
|
|
def define_content(name, &block)
|
|
old_output, @output_buffer = @output_buffer, ''
|
|
yield
|
|
self.instance_variable_set("@message_content_#{name.to_s}".to_sym, @output_buffer.to_s.strip)
|
|
old_output.sub!(/\n\z/, '')
|
|
@output_buffer = old_output
|
|
""
|
|
end
|
|
|
|
attr_writer :delayed_messages
|
|
|
|
def content(name)
|
|
self.instance_variable_get("@message_content_#{name.to_s}".to_sym)
|
|
end
|
|
|
|
def main_link
|
|
content(:link)
|
|
end
|
|
|
|
def parse!(path_type=nil)
|
|
raise StandardError, "Cannot parse without a context" unless self.context
|
|
@user = self.user
|
|
old_time_zone = Time.zone.name || "UTC"
|
|
Time.zone = (@user && @user.time_zone) || old_time_zone
|
|
# This makes me sad every time I see it. What we call context on a
|
|
# message is different than what we call context anywhere else in the
|
|
# app. In message templates you should use "asset" instead of "context"
|
|
# to prevent confusion.
|
|
@context = self.context
|
|
@asset = @context
|
|
context, asset, user, delayed_messages = [@context, @asset, @user, @delayed_messages]
|
|
@time_zone = Time.zone
|
|
time_zone = Time.zone
|
|
path_type ||= self.communication_channel.path_type rescue path_type
|
|
path_type = "summary" if self.to == 'dashboard'
|
|
path_type ||= "email"
|
|
filename = self.notification.name.downcase.gsub(/\s/, '_') + "." + path_type + ".erb" #rescue "not.found"
|
|
path = Canvas::MessageHelper.find_message_path(filename)
|
|
if !(File.exist?(path) rescue false)
|
|
filename = self.notification.name.downcase.gsub(/\s/, '_') + ".email.erb" #rescue "not.found"
|
|
path = Canvas::MessageHelper.find_message_path(filename)
|
|
end
|
|
@i18n_scope = "messages." + filename.sub(/\.erb\z/, '')
|
|
if (File.exist?(path) rescue false)
|
|
message = File.read(path)
|
|
@message_content_link = nil; @message_content_subject = nil
|
|
self.extend TextHelper
|
|
self.extend ERB::Util
|
|
b = binding
|
|
|
|
if path_type == 'facebook'
|
|
# this will ensure we escape anything that's not already safe
|
|
self.body = RailsXss::Erubis.new(message).result(b)
|
|
else
|
|
self.body = Erubis::Eruby.new(message, :bufvar => "@output_buffer").result(b)
|
|
end
|
|
if path_type == 'email'
|
|
message = File.read(Canvas::MessageHelper.find_message_path('_email_footer.email.erb'))
|
|
comm_message = Erubis::Eruby.new(message, :bufvar => "@output_buffer").result(b) rescue nil
|
|
self.body = self.body + "\n\n\n\n\n\n________________________________________\n" + comm_message if comm_message
|
|
end
|
|
self.subject = @message_content_subject || t('#message.default_subject', 'Canvas Alert')
|
|
self.url = @message_content_link || nil
|
|
self.body
|
|
else
|
|
self.extend TextHelper
|
|
b = binding
|
|
main_link = Erubis::Eruby.new(self.notification.main_link || "").result(b)
|
|
b = binding
|
|
self.subject = Erubis::Eruby.new(self.subject).result(b)
|
|
self.body = Erubis::Eruby.new(self.body).result(b)
|
|
self.transmission_errors = "couldn't find #{path}"
|
|
end
|
|
Time.zone = old_time_zone
|
|
self.body
|
|
ensure
|
|
@i18n_scope = nil
|
|
end
|
|
|
|
def reply_to_secure_id
|
|
Canvas::Security.hmac_sha1(self.id.to_s)
|
|
end
|
|
|
|
def reply_to_address
|
|
res = (self.forced_reply_to || nil) rescue nil
|
|
res = nil if self.path_type == 'sms' rescue false
|
|
res = self.from if self.context_type == 'ErrorReport'
|
|
unless res
|
|
addr, domain = HostUrl.outgoing_email_address.split(/@/)
|
|
res = "#{addr}+#{self.reply_to_secure_id}-#{self.id}@#{domain}"
|
|
end
|
|
end
|
|
|
|
def deliver
|
|
self.dispatch
|
|
|
|
if not self.path_type
|
|
logger.warn("Could not find a path type for #{self.inspect}")
|
|
return
|
|
end
|
|
|
|
delivery_method = "deliver_via_#{self.path_type}".to_sym
|
|
if not delivery_method or not self.respond_to?(delivery_method)
|
|
logger.warn("Could not set delivery_method from #{self.path_type}")
|
|
return
|
|
end
|
|
|
|
self.send(delivery_method)
|
|
end
|
|
|
|
def self.dashboard_messages(messages)
|
|
message_types = {}
|
|
messages.each do |m|
|
|
# TODO i18n
|
|
type = m.notification.category rescue "Other"
|
|
if type
|
|
message_types[type] ||= []
|
|
message_types[type] << m
|
|
end
|
|
end
|
|
message_types.to_a.sort_by{|m| m[0] == "Other" ? "ZZZZ" : m[0]}
|
|
end
|
|
|
|
def formatted_body
|
|
if path_type == 'facebook'
|
|
res = (body || "").gsub(/\n/, "<br/>\n").gsub(/(\s\s+)/) {|str| str.gsub(/\s/, " ") }
|
|
elsif path == 'email'
|
|
self.extend TextHelper
|
|
res = format_message(body).first
|
|
res
|
|
else
|
|
body
|
|
end
|
|
end
|
|
|
|
def infer_defaults
|
|
self.notification_name ||= self.notification.name if self.notification
|
|
self.notification_category ||= self.notification.category if self.notification
|
|
self.path_type ||= self.communication_channel.path_type rescue nil
|
|
self.path_type = 'summary' if self.to == 'dashboard'
|
|
self.path_type = 'email' if self.context_type == 'ErrorReport'
|
|
self.to_email = true if self.path_type == 'email' || self.path_type == 'sms'
|
|
self.from_name = HostUrl.outgoing_email_default_name
|
|
self.from_name = self.asset_context.name if (self.asset_context && self.asset_context.name && self.notification.dashboard? rescue false)
|
|
self.from_name = self.from_name if self.respond_to?(:from_name)
|
|
true
|
|
end
|
|
|
|
def translate(key, default, options={})
|
|
key = "\##{@i18n_scope}.#{key}" if @i18n_scope && key !~ /\A#/
|
|
super(key, default, options)
|
|
end
|
|
alias :t :translate
|
|
|
|
protected
|
|
|
|
def deliver_via_email
|
|
logger.info "Delivering mail: #{self.inspect}"
|
|
res = nil
|
|
begin
|
|
res = Mailer.deliver_message(self)
|
|
rescue Net::SMTPServerBusy => e
|
|
@exception = e
|
|
logger.error "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
|
if e.message && e.message.match(/Bad recipient/)
|
|
self.cancel
|
|
end
|
|
rescue Timeout::Error => e
|
|
@exception = e
|
|
logger.error "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
|
rescue => e
|
|
@exception = e
|
|
logger.error "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
|
end
|
|
if res
|
|
complete_dispatch
|
|
elsif @exception
|
|
if !@exception.is_a?(Timeout::Error)
|
|
ErrorReport.log_exception(:default, @exception, {
|
|
:message => "Message delivery failed",
|
|
:to => self.to,
|
|
:object => self.inspect.to_s,
|
|
})
|
|
end
|
|
self.errored_dispatch
|
|
raise @exception
|
|
end
|
|
true
|
|
end
|
|
|
|
def deliver_via_chat
|
|
# record_delivered
|
|
end
|
|
|
|
def deliver_via_twitter
|
|
@twitter_service = self.user.user_services.find_by_service('twitter')
|
|
url = "http://#{HostUrl.short_host(self.asset_context)}/mr/#{self.id}"
|
|
body = self.body[0, 139 - url.length]
|
|
twitter_self_dm(@twitter_service, "#{body} #{url}") if @twitter_service
|
|
complete_dispatch
|
|
end
|
|
|
|
def deliver_via_facebook
|
|
facebook_user_id = self.to.to_i.to_s
|
|
service = self.user.user_services.for_service('facebook').find_by_service_user_id(facebook_user_id)
|
|
Facebook.dashboard_increment_count(service) if service && service.token
|
|
complete_dispatch
|
|
end
|
|
|
|
def deliver_via_sms
|
|
# for now, this is good.
|
|
deliver_via_email
|
|
end
|
|
|
|
end
|