canvas-lms/app/models/message.rb

887 lines
27 KiB
Ruby
Raw Normal View History

2011-02-01 09:57:29 +08:00
#
# Copyright (C) 2011 - 2015 Instructure, Inc.
2011-02-01 09:57:29 +08:00
#
# 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/>.
#
require 'hey'
2011-02-01 09:57:29 +08:00
class Message < ActiveRecord::Base
# Included modules
include Rails.application.routes.url_helpers
include ERB::Util
2011-02-01 09:57:29 +08:00
include SendToStream
include TextHelper
include HtmlTextHelper
include Workflow
include Messages::PeerReviewsHelper
2011-02-01 09:57:29 +08:00
extend TextHelper
# Associations
belongs_to :asset_context, :polymorphic => true
2011-02-01 09:57:29 +08:00
belongs_to :communication_channel
belongs_to :context, :polymorphic => true
include NotificationPreloader
2011-02-01 09:57:29 +08:00
belongs_to :user
belongs_to :root_account, :class_name => 'Account'
has_many :attachments, :as => :context
2011-02-01 09:57:29 +08:00
attr_accessible :to, :from, :subject, :body, :delay_for, :context, :path_type,
:from_name, :reply_to_name, :sent_at, :notification, :user, :communication_channel,
Add API for admins to access user messages There are three parts to this: - API access for site admins and account admins. Account admins can only see messages for their account. - Includes start and end time parameters for basic searching - Summary notification are gathered up per account now, so account admins will not see a summary notification from multiple accounts. fixes CNVS-3695 test plan: - Verify Admin Access: - Have a user enrolled in two courses in separate root accounts. - Send the user ASAP notification from each account. - Go to /users/:id/messages (the old interface) and verify the messages appear there. - As a site admin, get /api/v1/comm_messages for the user (see documentation for the api). - Verify that data for both messages are returned. - Use the console to give an account admin on one of the accounts :read_messages permissions. - As the account admin, get /api/v1/comm_messages for the user. - Verify that only the message for the admin's account is returned. - Verify start_time and end_time parameters - Use the console to modify the created_at field for the user's messages - As a site admin, get /api/v1/comm_messages for the user, specifying various combinations of start_time and end_time, and make sure the appropriate messages are returned. - Verify account based summaries - Set the notification policies for the user to a summary setting (daily or weekly) - Send multiple notifications to the user from each account. - Use the console to run the SummaryMessageConsolidator::process function. - View /messages for the user and verify that separate summaries are sent for each account. Change-Id: Ie33ec02cc2033a1cc2f1fcbe538b76792aab0e6c Reviewed-on: https://gerrit.instructure.com/18586 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Mark Ericksen <marke@instructure.com> QA-Review: Marc LeGendre <marc@instructure.com> Product-Review: Marc LeGendre <marc@instructure.com>
2013-03-13 23:11:14 +08:00
:notification_name, :asset_context, :data, :root_account_id
attr_writer :delayed_messages
attr_accessor :output_buffer
# Callbacks
after_save :stage_message
2011-02-01 09:57:29 +08:00
before_save :infer_defaults
before_save :move_dashboard_messages
before_save :move_messages_for_deleted_users
2011-02-01 09:57:29 +08:00
before_save :set_asset_context_code
# Validations
validates :body, length: {maximum: maximum_text_length}, allow_nil: true, allow_blank: true
validates :html_body, length: {maximum: maximum_text_length}, allow_nil: true, allow_blank: true
validates :transmission_errors, length: {maximum: maximum_text_length}, allow_nil: true, allow_blank: true
validates :to, length: {maximum: maximum_text_length}, allow_nil: true, allow_blank: true
validates :from, length: {maximum: maximum_text_length}, allow_nil: true, allow_blank: true
validates :url, length: {maximum: maximum_text_length}, allow_nil: true, allow_blank: true
validates :subject, length: {maximum: maximum_string_length}, allow_nil: true, allow_blank: true
validates :from_name, length: {maximum: maximum_string_length}, allow_nil: true, allow_blank: true
validates :reply_to_name, length: {maximum: maximum_string_length}, allow_nil: true, allow_blank: true
# Stream policy
2011-02-01 09:57:29 +08:00
on_create_send_to_streams do
if to == 'dashboard' && Notification.types_to_show_in_feed.include?(notification_name)
user_id
2011-02-01 09:57:29 +08:00
else
[]
end
end
# State machine
2011-02-01 09:57:29 +08:00
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
2011-02-01 09:57:29 +08:00
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
2011-02-01 09:57:29 +08:00
state :bounced do
event :close, :transitions_to => :closed
end
2011-02-01 09:57:29 +08:00
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
# Named scopes
scope :for_asset_context_codes, lambda { |context_codes| where(:asset_context_code => context_codes) }
scope :for, lambda { |context| where(:context_type => context.class.base_class.to_s, :context_id => context) }
scope :after, lambda { |date| where("messages.created_at>?", date) }
scope :to_dispatch, -> {
where("messages.workflow_state='staged' AND messages.dispatch_at<=? AND 'messages.to'<>'dashboard'", Time.now.utc)
}
scope :to_email, -> { where(:path_type => ['email', 'sms']) }
scope :not_to_email, -> { where("messages.path_type NOT IN ('email', 'sms')") }
scope :by_name, lambda { |notification_name| where(:notification_name => notification_name) }
scope :before, lambda { |date| where("messages.created_at<?", date) }
scope :for_user, lambda { |user| where(:user_id => user)}
# messages that can be moved to the 'cancelled' state. dashboard messages
# can be closed by calling 'cancel', but aren't included
scope :cancellable, -> { where(:workflow_state => ['created', 'staged', 'sending']) }
# 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.
scope :staged, -> { where("messages.workflow_state='staged' AND messages.dispatch_at>?", Time.now.utc) }
scope :in_state, lambda { |state| where(:workflow_state => Array(state).map(&:to_s)) }
#Public: Helper methods for grabbing a user via the "from" field and using it to
#populate the avatar, name, and email in the conversation email notification
def author
@_author ||= begin
if context.has_attribute?(:user_id)
User.find(context.user_id)
elsif context.has_attribute?(:author_id)
User.find(context.author_id)
else
nil
end
end
end
def avatar_enabled?
return false unless author_account.present?
author_account.service_enabled?(:avatars)
end
def author_account
# Root account is populated during save
return nil unless author.present?
root_account_id ? Account.find(root_account_id) : author.account
end
def author_avatar_url
url = author.try(:avatar_url)
URI.join("#{HostUrl.protocol}://#{HostUrl.context_host(author_account)}", url).to_s if url
end
def author_short_name
author.try(:short_name)
end
def author_email_address
if context_root_account.try(:author_email_in_notifications?)
author.try(:email)
end
end
# Public: Helper to generate a URI for the given subject. Overrides Rails'
# built-in ActionController::PolymorphicRoutes#polymorphic_url method because
# it forces option defaults for protocol and host.
use url helpers in messages fixes CNVS-11838 and then choose the host more intelligently to use one the user can actually log in to test plan - have accounts on two shards, referred to as "account 1" and "account 2" from now on - have account domains that are resolvable for the accounts, referred to as account1.canvas.dev and account2.canvas.dev from now on - have account 2 trust account 1 by POSTing to account2.canvas.dev/api/v1/accounts/<account 2 id>/trust_links with a site admin's auth token and setting trust_link[managing_account_id] to account 1's global id. you can now use users from account 1 on account 2 - regression test notifications, especially considering notifications generated by account 2 that are sent to account 1 users. - account 1 users should have links that use account1.canvas.dev and have global ids for referenced objects that live on account 2's shard, e.g. account1.canvas.dev/courses/2~1/discussion_topics/2~30 - avatars should use the avatar owner's account domain - account 2 users should have links that use account2.canvas.dev and have local ids, e.g. account2.canvas.dev/courses/1/discussion_topics/30 - following the links when the user receiving the notification is logged in should take the user to the linked object - following the links when not logged in should take the user to a login page that they are able to log in to - be sure to at least check: context file downloads links (e.g. discussion topic attachments) file downloads links (e.g. conversation message attachments) avatars links conversation notifications discussion notifications peer review notifications Change-Id: Idebd247fee99a2b973d3fa6f4f2fca0e723d99a5 Reviewed-on: https://gerrit.instructure.com/31867 Tested-by: Jenkins Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Joel Hough <joel@instructure.com>
2014-03-14 01:44:09 +08:00
def default_url_options
{ protocol: HostUrl.protocol, host: HostUrl.context_host(link_root_account) }
end
# Public: Helper to generate JSON suitable for publishing via Amazon SNS
#
# Currently pulls data from email template contents
#
# Returns a JSON string
def sns_json
@sns_json ||= begin
custom_data = {
html_url: self.url,
user_id: self.user.global_id
}
custom_data[:api_url] = content(:api_url) if content(:api_url) # no templates define this right now
{
default: self.subject,
GCM: {
data: {
alert: self.subject,
}.merge(custom_data)
}.to_json,
APNS_SANDBOX: {
aps: {
alert: self.subject
}
}.merge(custom_data).to_json,
APNS: {
aps: {
alert: self.subject
}
}.merge(custom_data).to_json
}.to_json
end
end
use url helpers in messages fixes CNVS-11838 and then choose the host more intelligently to use one the user can actually log in to test plan - have accounts on two shards, referred to as "account 1" and "account 2" from now on - have account domains that are resolvable for the accounts, referred to as account1.canvas.dev and account2.canvas.dev from now on - have account 2 trust account 1 by POSTing to account2.canvas.dev/api/v1/accounts/<account 2 id>/trust_links with a site admin's auth token and setting trust_link[managing_account_id] to account 1's global id. you can now use users from account 1 on account 2 - regression test notifications, especially considering notifications generated by account 2 that are sent to account 1 users. - account 1 users should have links that use account1.canvas.dev and have global ids for referenced objects that live on account 2's shard, e.g. account1.canvas.dev/courses/2~1/discussion_topics/2~30 - avatars should use the avatar owner's account domain - account 2 users should have links that use account2.canvas.dev and have local ids, e.g. account2.canvas.dev/courses/1/discussion_topics/30 - following the links when the user receiving the notification is logged in should take the user to the linked object - following the links when not logged in should take the user to a login page that they are able to log in to - be sure to at least check: context file downloads links (e.g. discussion topic attachments) file downloads links (e.g. conversation message attachments) avatars links conversation notifications discussion notifications peer review notifications Change-Id: Idebd247fee99a2b973d3fa6f4f2fca0e723d99a5 Reviewed-on: https://gerrit.instructure.com/31867 Tested-by: Jenkins Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Joel Hough <joel@instructure.com>
2014-03-14 01:44:09 +08:00
# infer a root account associated with the context that the user can log in to
def link_root_account
@root_account ||= begin
context = self.context
context = context.assignment if context.respond_to?(:assignment) && context.assignment
context = context.rubric_association.context if context.respond_to?(:rubric_association) && context.rubric_association
context = context.appointment_group.contexts.first if context.respond_to?(:appointment_group) && context.appointment_group
context = context.context if context.respond_to?(:context)
context = context.account if context.respond_to?(:account)
context = context.root_account if context.respond_to?(:root_account)
if context
p = SisPseudonym.for(user, context)
use url helpers in messages fixes CNVS-11838 and then choose the host more intelligently to use one the user can actually log in to test plan - have accounts on two shards, referred to as "account 1" and "account 2" from now on - have account domains that are resolvable for the accounts, referred to as account1.canvas.dev and account2.canvas.dev from now on - have account 2 trust account 1 by POSTing to account2.canvas.dev/api/v1/accounts/<account 2 id>/trust_links with a site admin's auth token and setting trust_link[managing_account_id] to account 1's global id. you can now use users from account 1 on account 2 - regression test notifications, especially considering notifications generated by account 2 that are sent to account 1 users. - account 1 users should have links that use account1.canvas.dev and have global ids for referenced objects that live on account 2's shard, e.g. account1.canvas.dev/courses/2~1/discussion_topics/2~30 - avatars should use the avatar owner's account domain - account 2 users should have links that use account2.canvas.dev and have local ids, e.g. account2.canvas.dev/courses/1/discussion_topics/30 - following the links when the user receiving the notification is logged in should take the user to the linked object - following the links when not logged in should take the user to a login page that they are able to log in to - be sure to at least check: context file downloads links (e.g. discussion topic attachments) file downloads links (e.g. conversation message attachments) avatars links conversation notifications discussion notifications peer review notifications Change-Id: Idebd247fee99a2b973d3fa6f4f2fca0e723d99a5 Reviewed-on: https://gerrit.instructure.com/31867 Tested-by: Jenkins Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Joel Hough <joel@instructure.com>
2014-03-14 01:44:09 +08:00
p ||= user.find_pseudonym_for_account(context, true)
context = p.account if p
else
# nothing? okay, just something the user can log in to
context = user.pseudonym.try(:account)
context ||= self.context
end
context
end
end
# infer a root account time zone
def root_account_time_zone
link_root_account.time_zone if link_root_account.respond_to?(:time_zone)
end
# Internal: Store any transmission errors in the database to help with later
# debugging.
#
# val - An error string.
#
# Returns nothing.
def transmission_errors=(val)
write_attribute(:transmission_errors, val[0, self.class.maximum_text_length])
2011-02-01 09:57:29 +08:00
end
# Public: Custom getter that delegates and caches notification category to
# associated notification
#
# Returns a notification category string.
def notification_category
@cat ||= notification.try(:category)
end
# Public: Return associated notification's display category.
#
# Returns notification display category string.
def notification_display_category
notification.try(:display_category)
end
# Public: Skip message dispatch during stage transition. Used when batch
# dispatching.
#
# Returns nothing.
def stage_without_dispatch!
@stage_without_dispatch = true
end
# Public: Stage the message during the dispatch process. Messages travel
# from created -> staged -> sending -> sent.
#
# Returns nothing.
2011-02-01 09:57:29 +08:00
def stage_message
stage if state == :created
if dashboard?
messages = Message.in_state(:dashboard).where(
:notification_id => notification_id,
:context_id => context_id,
:context_type => context_type,
:user_id => user_id
)
(messages - [self]).each(&:close)
2011-02-01 09:57:29 +08:00
end
end
class UnescapedBuffer < String # acts like safe buffer except for the actually being safe part
alias :append= :<<
alias :safe_concat :concat
alias :safe_append= :concat
end
# Public: Store content in a message_content_... instance variable.
#
# name - The symbol name of the content.
# block - ?
#
# Returns an empty string.
2011-02-01 09:57:29 +08:00
def define_content(name, &block)
if name == :subject || name == :user_name
old_output_buffer, @output_buffer = [@output_buffer, UnescapedBuffer.new]
else
old_output_buffer, @output_buffer = [@output_buffer, @output_buffer.dup.clear]
end
2011-02-01 09:57:29 +08:00
yield
instance_variable_set(:"@message_content_#{name}",
@output_buffer.to_s.strip)
@output_buffer = old_output_buffer.sub(/\n\z/, '')
if old_output_buffer.is_a?(ActiveSupport::SafeBuffer) && old_output_buffer.html_safe?
@output_buffer = old_output_buffer.class.new(@output_buffer)
end
''
end
# Public: Get a message_content_... instance variable.
#
# name - The name of the message content variable as a symbol.
#
# Returns value of instance variable (should be a string?).
2011-02-01 09:57:29 +08:00
def content(name)
instance_variable_get(:"@message_content_#{name}")
2011-02-01 09:57:29 +08:00
end
# Public: Custom getter for @message_content_link.
#
# Returns string content from @message_content_link.
2011-02-01 09:57:29 +08:00
def main_link
content(:link)
end
# Public: Load a message template from app/messages. Also sets @i18n_scope.
#
# filename - The string path to the template (e.g. "/var/web/canvas/app/messages/template.email.erb")
#
# Returns a template string or false if it can't be found.
def get_template(filename)
path = Canvas::MessageHelper.find_message_path(filename)
if !(File.exist?(path) rescue false)
filename = self.notification.name.downcase.gsub(/\s/, '_') + ".email.erb"
path = Canvas::MessageHelper.find_message_path(filename)
end
@i18n_scope = "messages." + filename.sub(/\.erb\z/, '')
if (File.exist?(path) rescue false)
File.read(path)
else
false
end
end
# Public: Get the template name based on the path type.
#
# path_type - The path to send the message across, e.g, 'email'.
#
# Returns file name for erb template
def template_filename(path_type=nil)
self.notification.name.parameterize.underscore + "." + path_type + ".erb"
end
# Public: Apply an HTML email template to this message.
#
# _binding - The binding to attach to the template.
#
# Returns an HTML template (or nil).
def apply_html_template(_binding)
orig_i18n_scope = @i18n_scope
@i18n_scope = "#{@i18n_scope}.html"
return nil unless template = load_html_template
# Add the attribute 'inner_html' with the value of inner_html into the _binding
@output_buffer = nil
inner_html = ActionView::Template::Handlers::Erubis.new(template, :bufvar => '@output_buffer').result(_binding)
setter = eval "inner_html = nil; lambda { |v| inner_html = v }", _binding
setter.call(inner_html)
layout_path = Canvas::MessageHelper.find_message_path('_layout.email.html.erb')
@output_buffer = nil
ActionView::Template::Handlers::Erubis.new(File.read(layout_path)).result(_binding)
ensure
@i18n_scope = orig_i18n_scope
end
def load_html_template
html_file = template_filename('email.html')
html_path = Canvas::MessageHelper.find_message_path(html_file)
File.read(html_path) if File.exist?(html_path)
end
# Public: Assign the body, subject and url to the message.
#
# message_body_template - Raw template body
# path_type - Path to send the message across, e.g, 'email'.
# _binding - Message binding
#
# Returns message body
use url helpers in messages fixes CNVS-11838 and then choose the host more intelligently to use one the user can actually log in to test plan - have accounts on two shards, referred to as "account 1" and "account 2" from now on - have account domains that are resolvable for the accounts, referred to as account1.canvas.dev and account2.canvas.dev from now on - have account 2 trust account 1 by POSTing to account2.canvas.dev/api/v1/accounts/<account 2 id>/trust_links with a site admin's auth token and setting trust_link[managing_account_id] to account 1's global id. you can now use users from account 1 on account 2 - regression test notifications, especially considering notifications generated by account 2 that are sent to account 1 users. - account 1 users should have links that use account1.canvas.dev and have global ids for referenced objects that live on account 2's shard, e.g. account1.canvas.dev/courses/2~1/discussion_topics/2~30 - avatars should use the avatar owner's account domain - account 2 users should have links that use account2.canvas.dev and have local ids, e.g. account2.canvas.dev/courses/1/discussion_topics/30 - following the links when the user receiving the notification is logged in should take the user to the linked object - following the links when not logged in should take the user to a login page that they are able to log in to - be sure to at least check: context file downloads links (e.g. discussion topic attachments) file downloads links (e.g. conversation message attachments) avatars links conversation notifications discussion notifications peer review notifications Change-Id: Idebd247fee99a2b973d3fa6f4f2fca0e723d99a5 Reviewed-on: https://gerrit.instructure.com/31867 Tested-by: Jenkins Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Joel Hough <joel@instructure.com>
2014-03-14 01:44:09 +08:00
def populate_body(message_body_template, path_type, _binding, filename)
# Build the body content based on the path type
self.body = Erubis::Eruby.new(message_body_template,
use url helpers in messages fixes CNVS-11838 and then choose the host more intelligently to use one the user can actually log in to test plan - have accounts on two shards, referred to as "account 1" and "account 2" from now on - have account domains that are resolvable for the accounts, referred to as account1.canvas.dev and account2.canvas.dev from now on - have account 2 trust account 1 by POSTing to account2.canvas.dev/api/v1/accounts/<account 2 id>/trust_links with a site admin's auth token and setting trust_link[managing_account_id] to account 1's global id. you can now use users from account 1 on account 2 - regression test notifications, especially considering notifications generated by account 2 that are sent to account 1 users. - account 1 users should have links that use account1.canvas.dev and have global ids for referenced objects that live on account 2's shard, e.g. account1.canvas.dev/courses/2~1/discussion_topics/2~30 - avatars should use the avatar owner's account domain - account 2 users should have links that use account2.canvas.dev and have local ids, e.g. account2.canvas.dev/courses/1/discussion_topics/30 - following the links when the user receiving the notification is logged in should take the user to the linked object - following the links when not logged in should take the user to a login page that they are able to log in to - be sure to at least check: context file downloads links (e.g. discussion topic attachments) file downloads links (e.g. conversation message attachments) avatars links conversation notifications discussion notifications peer review notifications Change-Id: Idebd247fee99a2b973d3fa6f4f2fca0e723d99a5 Reviewed-on: https://gerrit.instructure.com/31867 Tested-by: Jenkins Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Joel Hough <joel@instructure.com>
2014-03-14 01:44:09 +08:00
bufvar: '@output_buffer', filename: filename).result(_binding)
self.html_body = apply_html_template(_binding) if path_type == 'email'
# Append a footer to the body if the path type is email
if path_type == 'email'
raw_footer_message = File.read(Canvas::MessageHelper.find_message_path('_email_footer.email.erb'))
footer_message = Erubis::Eruby.new(raw_footer_message, :bufvar => "@output_buffer").result(_binding)
if footer_message.present?
self.body = <<-END.strip_heredoc
#{self.body}
________________________________________
#{footer_message}
END
end
end
self.body
end
# Public: Prepare a message for delivery by setting body, subject, etc.
#
# path_type - The path to send the message across, e.g, 'email'.
#
# Returns nothing.
2011-02-01 09:57:29 +08:00
def parse!(path_type=nil)
raise StandardError, "Cannot parse without a context" unless self.context
# Get the users timezone but maintain the original timezone in order to set it back at the end
original_time_zone = Time.zone.name || "UTC"
user_time_zone = self.user.try(:time_zone) || root_account_time_zone || original_time_zone
Time.zone = user_time_zone
# (temporarily) override course name with user's nickname for the course
hacked_course = apply_course_nickname_to_asset(self.context, self.user)
# Ensure we have a path_type
path_type = 'dashboard' if to == 'summary'
unless path_type
path_type = communication_channel.try(:path_type) || 'email'
end
# Determine the message template file to be used in the message
filename = template_filename(path_type)
message_body_template = get_template(filename)
context, asset, user, delayed_messages, asset_context, data = [self.context,
self.context, self.user, @delayed_messages, self.asset_context, @data]
use url helpers in messages fixes CNVS-11838 and then choose the host more intelligently to use one the user can actually log in to test plan - have accounts on two shards, referred to as "account 1" and "account 2" from now on - have account domains that are resolvable for the accounts, referred to as account1.canvas.dev and account2.canvas.dev from now on - have account 2 trust account 1 by POSTing to account2.canvas.dev/api/v1/accounts/<account 2 id>/trust_links with a site admin's auth token and setting trust_link[managing_account_id] to account 1's global id. you can now use users from account 1 on account 2 - regression test notifications, especially considering notifications generated by account 2 that are sent to account 1 users. - account 1 users should have links that use account1.canvas.dev and have global ids for referenced objects that live on account 2's shard, e.g. account1.canvas.dev/courses/2~1/discussion_topics/2~30 - avatars should use the avatar owner's account domain - account 2 users should have links that use account2.canvas.dev and have local ids, e.g. account2.canvas.dev/courses/1/discussion_topics/30 - following the links when the user receiving the notification is logged in should take the user to the linked object - following the links when not logged in should take the user to a login page that they are able to log in to - be sure to at least check: context file downloads links (e.g. discussion topic attachments) file downloads links (e.g. conversation message attachments) avatars links conversation notifications discussion notifications peer review notifications Change-Id: Idebd247fee99a2b973d3fa6f4f2fca0e723d99a5 Reviewed-on: https://gerrit.instructure.com/31867 Tested-by: Jenkins Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Joel Hough <joel@instructure.com>
2014-03-14 01:44:09 +08:00
link_root_account.shard.activate do
if message_body_template.present? && path_type.present?
populate_body(message_body_template, path_type, binding, filename)
use url helpers in messages fixes CNVS-11838 and then choose the host more intelligently to use one the user can actually log in to test plan - have accounts on two shards, referred to as "account 1" and "account 2" from now on - have account domains that are resolvable for the accounts, referred to as account1.canvas.dev and account2.canvas.dev from now on - have account 2 trust account 1 by POSTing to account2.canvas.dev/api/v1/accounts/<account 2 id>/trust_links with a site admin's auth token and setting trust_link[managing_account_id] to account 1's global id. you can now use users from account 1 on account 2 - regression test notifications, especially considering notifications generated by account 2 that are sent to account 1 users. - account 1 users should have links that use account1.canvas.dev and have global ids for referenced objects that live on account 2's shard, e.g. account1.canvas.dev/courses/2~1/discussion_topics/2~30 - avatars should use the avatar owner's account domain - account 2 users should have links that use account2.canvas.dev and have local ids, e.g. account2.canvas.dev/courses/1/discussion_topics/30 - following the links when the user receiving the notification is logged in should take the user to the linked object - following the links when not logged in should take the user to a login page that they are able to log in to - be sure to at least check: context file downloads links (e.g. discussion topic attachments) file downloads links (e.g. conversation message attachments) avatars links conversation notifications discussion notifications peer review notifications Change-Id: Idebd247fee99a2b973d3fa6f4f2fca0e723d99a5 Reviewed-on: https://gerrit.instructure.com/31867 Tested-by: Jenkins Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Joel Hough <joel@instructure.com>
2014-03-14 01:44:09 +08:00
# Set the subject and url
self.subject = @message_content_subject || t('#message.default_subject', 'Canvas Alert')
self.url = @message_content_link || nil
else
# Message doesn't exist so we flag the message as an error
main_link = Erubis::Eruby.new(self.notification.main_link || "").result(binding)
self.subject = Erubis::Eruby.new(subject).result(binding)
self.body = Erubis::Eruby.new(body).result(binding)
self.transmission_errors = "couldn't find #{Canvas::MessageHelper.find_message_path(filename)}"
end
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
self.body
ensure
# Set the timezone back to what it originally was
Time.zone = original_time_zone if original_time_zone.present?
hacked_course.apply_nickname_for!(nil) if hacked_course
@i18n_scope = nil
2011-02-01 09:57:29 +08:00
end
# Public: Deliver this message.
#
# Returns nothing.
2011-02-01 09:57:29 +08:00
def deliver
# don't dispatch canceled or already-sent messages.
return nil unless dispatch
unless path_type.present?
logger.warn "Could not find a path type for #{inspect}"
return nil
2011-02-01 09:57:29 +08:00
end
delivery_method = "deliver_via_#{path_type}".to_sym
if not delivery_method or not respond_to?(delivery_method, true)
logger.warn("Could not set delivery_method from #{path_type}")
return nil
2011-02-01 09:57:29 +08:00
end
if user.account.feature_enabled?(:notification_service) && path_type != "yo"
enqueue_to_sqs
else
send(delivery_method)
end
end
def enqueue_to_sqs
message_body = path_type == "email" ? Mailer.create_message(self).to_s : body
NotificationService.process(message_body, path_type, to, remote_configuration)
complete_dispatch
rescue AWS::SQS::Errors::ServiceError => e
Canvas::Errors.capture(
e,
message: 'Message delivery failed',
to: to,
object: inspect.to_s
)
error_string = "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
self.transmission_errors = error_string
self.errored_dispatch
raise
2011-02-01 09:57:29 +08:00
end
class RemoteConfigurationError < StandardError; end
# Public: Determine the remote configuration for notification_service
#
# Returns string remote configuration (eventually a hash).
def remote_configuration
case path_type
when "email"
return "email.amazonaws.com"
when "push"
return "push.com"
when "twitter"
return 'twitter'
when "sms"
return if to =~ /^\+[0-9]+$/ ? "Twilio.com" : "email.amazonaws.com"
else
raise RemoteConfigurationError, "No matching path types for notification service"
end
end
# Public: Fetch the dashboard messages for the given messages.
#
# messages - An array of message objects.
#
# Returns an array of dashboard messages.
2011-02-01 09:57:29 +08:00
def self.dashboard_messages(messages)
message_types = messages.inject({}) do |types, message|
type = message.notification.category rescue 'Other'
if type.present?
types[type] ||= []
types[type] << message
2011-02-01 09:57:29 +08:00
end
hash
2011-02-01 09:57:29 +08:00
end
# not sure what this is even doing?
message_types.to_a.sort_by { |m| m[0] == 'Other' ? CanvasSort::Last : m[0] }
2011-02-01 09:57:29 +08:00
end
# Public: Get the root account of this message's context.
#
# Returns an account.
def context_root_account
unbounded_loop_paranoia_counter = 10
current_context = context
until current_context.respond_to?(:root_account)
Add API for admins to access user messages There are three parts to this: - API access for site admins and account admins. Account admins can only see messages for their account. - Includes start and end time parameters for basic searching - Summary notification are gathered up per account now, so account admins will not see a summary notification from multiple accounts. fixes CNVS-3695 test plan: - Verify Admin Access: - Have a user enrolled in two courses in separate root accounts. - Send the user ASAP notification from each account. - Go to /users/:id/messages (the old interface) and verify the messages appear there. - As a site admin, get /api/v1/comm_messages for the user (see documentation for the api). - Verify that data for both messages are returned. - Use the console to give an account admin on one of the accounts :read_messages permissions. - As the account admin, get /api/v1/comm_messages for the user. - Verify that only the message for the admin's account is returned. - Verify start_time and end_time parameters - Use the console to modify the created_at field for the user's messages - As a site admin, get /api/v1/comm_messages for the user, specifying various combinations of start_time and end_time, and make sure the appropriate messages are returned. - Verify account based summaries - Set the notification policies for the user to a summary setting (daily or weekly) - Send multiple notifications to the user from each account. - Use the console to run the SummaryMessageConsolidator::process function. - View /messages for the user and verify that separate summaries are sent for each account. Change-Id: Ie33ec02cc2033a1cc2f1fcbe538b76792aab0e6c Reviewed-on: https://gerrit.instructure.com/18586 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Mark Ericksen <marke@instructure.com> QA-Review: Marc LeGendre <marc@instructure.com> Product-Review: Marc LeGendre <marc@instructure.com>
2013-03-13 23:11:14 +08:00
return nil if unbounded_loop_paranoia_counter <= 0 || current_context.nil?
return nil unless current_context.respond_to?(:context)
current_context = current_context.context
unbounded_loop_paranoia_counter -= 1
end
current_context.root_account
end
def custom_logo
context_root_account && context_root_account.settings[:email_logo]
end
# Internal: Set default values before save.
#
# Returns true.
2011-02-01 09:57:29 +08:00
def infer_defaults
if notification
self.notification_name ||= notification.name
self.notification_category ||= notification_category
end
self.path_type ||= communication_channel.try(:path_type)
self.path_type = 'summary' if to == 'dashboard'
self.path_type = 'email' if context_type == 'ErrorReport'
self.to_email = true if %w[email sms].include?(path_type)
Add API for admins to access user messages There are three parts to this: - API access for site admins and account admins. Account admins can only see messages for their account. - Includes start and end time parameters for basic searching - Summary notification are gathered up per account now, so account admins will not see a summary notification from multiple accounts. fixes CNVS-3695 test plan: - Verify Admin Access: - Have a user enrolled in two courses in separate root accounts. - Send the user ASAP notification from each account. - Go to /users/:id/messages (the old interface) and verify the messages appear there. - As a site admin, get /api/v1/comm_messages for the user (see documentation for the api). - Verify that data for both messages are returned. - Use the console to give an account admin on one of the accounts :read_messages permissions. - As the account admin, get /api/v1/comm_messages for the user. - Verify that only the message for the admin's account is returned. - Verify start_time and end_time parameters - Use the console to modify the created_at field for the user's messages - As a site admin, get /api/v1/comm_messages for the user, specifying various combinations of start_time and end_time, and make sure the appropriate messages are returned. - Verify account based summaries - Set the notification policies for the user to a summary setting (daily or weekly) - Send multiple notifications to the user from each account. - Use the console to run the SummaryMessageConsolidator::process function. - View /messages for the user and verify that separate summaries are sent for each account. Change-Id: Ie33ec02cc2033a1cc2f1fcbe538b76792aab0e6c Reviewed-on: https://gerrit.instructure.com/18586 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Mark Ericksen <marke@instructure.com> QA-Review: Marc LeGendre <marc@instructure.com> Product-Review: Marc LeGendre <marc@instructure.com>
2013-03-13 23:11:14 +08:00
root_account = context_root_account
self.root_account_id ||= root_account.try(:id)
self.from_name = infer_from_name
self.reply_to_name = message_context.reply_to_name
2011-02-01 09:57:29 +08:00
true
end
# Public: Convenience method for translation calls.
#
# key - The translation key.
# default - The English default of the key.
# options - An options hash passed to translate (default: {}).
#
# Returns a translated string.
def translate(*args)
key, options = I18nliner::CallHelpers.infer_arguments(args)
# Add scope if it's present in the model and missing from the key.
if !options[:i18nliner_inferred_key] && @i18n_scope && key !~ /\A#/
key = "##{@i18n_scope}.#{key}"
end
super(key, options)
end
alias :t :translate
# Public: Store data on the message for use at delivery-time.
#
# values_hash - A hash of values to store in the model's data attribute.
#
# Returns nothing.
def data=(values_hash)
@data = OpenStruct.new(values_hash)
end
# Public: Before save, close this message if it has no user or a deleted
# user and isn't for an ErrorReport.
#
# Returns nothing.
def move_messages_for_deleted_users
if context_type != 'ErrorReport' && (!user || user.deleted?)
self.workflow_state = 'closed'
end
end
# Public: Before save, prepare dashboard messages for display on dashboard.
#
# Returns nothing.
def move_dashboard_messages
if to == 'dashboard' && !cancelled? && !closed?
self.workflow_state = 'dashboard'
end
end
# Public: Before save, set the proper asset_context_code for the model.
#
# Returns an asset_context_code string or nil.
def set_asset_context_code
self.asset_context_code = "#{context_type.underscore}_#{context_id}"
rescue
nil
end
Add API for admins to access user messages There are three parts to this: - API access for site admins and account admins. Account admins can only see messages for their account. - Includes start and end time parameters for basic searching - Summary notification are gathered up per account now, so account admins will not see a summary notification from multiple accounts. fixes CNVS-3695 test plan: - Verify Admin Access: - Have a user enrolled in two courses in separate root accounts. - Send the user ASAP notification from each account. - Go to /users/:id/messages (the old interface) and verify the messages appear there. - As a site admin, get /api/v1/comm_messages for the user (see documentation for the api). - Verify that data for both messages are returned. - Use the console to give an account admin on one of the accounts :read_messages permissions. - As the account admin, get /api/v1/comm_messages for the user. - Verify that only the message for the admin's account is returned. - Verify start_time and end_time parameters - Use the console to modify the created_at field for the user's messages - As a site admin, get /api/v1/comm_messages for the user, specifying various combinations of start_time and end_time, and make sure the appropriate messages are returned. - Verify account based summaries - Set the notification policies for the user to a summary setting (daily or weekly) - Send multiple notifications to the user from each account. - Use the console to run the SummaryMessageConsolidator::process function. - View /messages for the user and verify that separate summaries are sent for each account. Change-Id: Ie33ec02cc2033a1cc2f1fcbe538b76792aab0e6c Reviewed-on: https://gerrit.instructure.com/18586 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Mark Ericksen <marke@instructure.com> QA-Review: Marc LeGendre <marc@instructure.com> Product-Review: Marc LeGendre <marc@instructure.com>
2013-03-13 23:11:14 +08:00
# Public: Return the message as JSON filtered to selected fields and
# flattened appropriately.
#
# Returns json hash.
def as_json(options = {})
super(:only => [:id, :created_at, :sent_at, :workflow_state, :from, :to, :reply_to, :subject, :body, :html_body])['message']
end
protected
# Internal: Deliver the message through email.
#
# Returns nothing.
# Raises Net::SMTPServerBusy if the message cannot be sent.
# Raises Timeout::Error if the remote server times out.
def deliver_via_email
res = nil
logger.info "Delivering mail: #{self.inspect}"
begin
res = Mailer.create_message(self).deliver
rescue Net::SMTPServerBusy => e
@exception = e
logger.error "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
cancel if e.message.try(:match, /Bad recipient/)
rescue StandardError, Timeout::Error => e
@exception = e
logger.error "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
2011-02-01 09:57:29 +08:00
end
if res
complete_dispatch
elsif @exception
raise_error = @exception.to_s !~ /^450/
log_error = raise_error && !@exception.is_a?(Timeout::Error)
if log_error
Canvas::Errors.capture(
@exception,
message: 'Message delivery failed',
to: to,
object: inspect.to_s
)
end
self.errored_dispatch
if raise_error
raise @exception
else
return false
end
end
true
end
# Internal: Deliver the message through Twitter.
#
# The template should define the content for :link and not place into the body of the template itself
#
# Returns nothing.
def deliver_via_twitter
twitter_service = user.user_services.where(service: 'twitter').first
host = HostUrl.short_host(self.asset_context)
msg_id = AssetSignature.generate(self)
Twitter::Messenger.new(self, twitter_service, host, msg_id).deliver
complete_dispatch
end
# Internal: Deliver the message through Yo.
#
# Returns nothing.
def deliver_via_yo
plugin = Canvas::Plugin.find(:yo)
if plugin && plugin.enabled? && plugin.setting(:api_token)
service = self.user.user_services.where(service: 'yo').first
Hey.api_token ||= plugin.setting(:api_token)
Hey::Yo.user(service.service_user_id, link: self.url)
complete_dispatch
else
cancel
end
end
Send messages via Twilio Fixes CNVS-21548, CNVS-20625, CNVS-21580 Test plan: - Copy config/twilio.yml.example to config/twilio.yml - Configure config/twilio.yml with credentials from a Twilio account - Create a user - Enable the international_sms feature flag for the account of the user you created - Create a communication channel from a Rails console, using a phone number with which you can test. Assuming 1-801-555-0100 as the phone number, and assuming that the id of the user that you created is 42, you can do that with: User.find(42).communication_channels.create!( path_type: 'sms', path: '+18015550100') - As a site admin, confirm the user's communication channel - Cause a notification to be sent to the user - Ensure that you receive a text message - Multiple outbound numbers (will need a paid Twilio account to test): - Configure config/twilio.yml with credentials from a Twilio account that has multiple outbound phone numbers - Create multiple users and add new phone numbers for each using the above steps, and confirm them - Cause notifications to be generated for each user and ensure that they come from different phone numbers - You may need to test several phone numbers before a notification is sent from a different number - Cause more notifications to be sent and ensure that each phone number receives notifications from the same outbound phone number every time Change-Id: I103c93a8096acaaabd29530b0a0b5c43bc05c26b Reviewed-on: https://gerrit.instructure.com/59901 Tested-by: Jenkins Reviewed-by: Joel Hough <joel@instructure.com> QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Peyton Craighill <pcraighill@instructure.com>
2015-08-18 03:26:39 +08:00
# Internal: Send the message through SMS. This currently sends it via Twilio if the recipient is a E.164 phone
# number, or via email otherwise.
#
# Returns nothing.
def deliver_via_sms
Send messages via Twilio Fixes CNVS-21548, CNVS-20625, CNVS-21580 Test plan: - Copy config/twilio.yml.example to config/twilio.yml - Configure config/twilio.yml with credentials from a Twilio account - Create a user - Enable the international_sms feature flag for the account of the user you created - Create a communication channel from a Rails console, using a phone number with which you can test. Assuming 1-801-555-0100 as the phone number, and assuming that the id of the user that you created is 42, you can do that with: User.find(42).communication_channels.create!( path_type: 'sms', path: '+18015550100') - As a site admin, confirm the user's communication channel - Cause a notification to be sent to the user - Ensure that you receive a text message - Multiple outbound numbers (will need a paid Twilio account to test): - Configure config/twilio.yml with credentials from a Twilio account that has multiple outbound phone numbers - Create multiple users and add new phone numbers for each using the above steps, and confirm them - Cause notifications to be generated for each user and ensure that they come from different phone numbers - You may need to test several phone numbers before a notification is sent from a different number - Cause more notifications to be sent and ensure that each phone number receives notifications from the same outbound phone number every time Change-Id: I103c93a8096acaaabd29530b0a0b5c43bc05c26b Reviewed-on: https://gerrit.instructure.com/59901 Tested-by: Jenkins Reviewed-by: Joel Hough <joel@instructure.com> QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Peyton Craighill <pcraighill@instructure.com>
2015-08-18 03:26:39 +08:00
if to =~ /^\+[0-9]+$/
begin
unless user.account.feature_enabled?(:international_sms)
raise "International SMS is currently disabled for this user's account"
end
Try to send messages from the recipient's country Fixes CNVS-24733 Test plan: - Set up config/twilio.yml - Ensure that your Twilio account owns phone numbers in the U.S. and phone numbers in at least one other country - Enable both the "International SMS" and "International SMS - Send from Recipient's Country" feature flags on the account you'll be working with - Add a phone number in the U.S. to your profile - Cause a notification to be sent to this phone number - Ensure that the text message you receive is from one of the U.S. phone numbers - Add a phone number in a country in which your Twilio account owns outbound phone numbers to your profile - Cause a notification to be sent to this phone number - Ensure that the text message you receive is from one of the phone numbers in this country - Add a phone number in a country in which your Twilio account does not own any outbound numbers - Cause a notification to be sent to this phone number - Ensure that the text message you receive is from one of the U.S. phone numbers - Disable the "International SMS - Send from Recipient's Country" feature flag - Repeat the above steps, but ensure that all text messages to all phone numbers are sent from a U.S. phone number Change-Id: I65b4a7c2e201f8afc5e6068ad80a3b4f9ce8710c Reviewed-on: https://gerrit.instructure.com/66320 Reviewed-by: Matthew Wheeler <mwheeler@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Allison Weiss <allison@instructure.com>
2015-11-03 05:19:22 +08:00
if Canvas::Twilio.enabled?
Canvas::Twilio.deliver(
to,
body,
from_recipient_country: user.account.feature_enabled?(:international_sms_from_recipient_country)
)
end
Send messages via Twilio Fixes CNVS-21548, CNVS-20625, CNVS-21580 Test plan: - Copy config/twilio.yml.example to config/twilio.yml - Configure config/twilio.yml with credentials from a Twilio account - Create a user - Enable the international_sms feature flag for the account of the user you created - Create a communication channel from a Rails console, using a phone number with which you can test. Assuming 1-801-555-0100 as the phone number, and assuming that the id of the user that you created is 42, you can do that with: User.find(42).communication_channels.create!( path_type: 'sms', path: '+18015550100') - As a site admin, confirm the user's communication channel - Cause a notification to be sent to the user - Ensure that you receive a text message - Multiple outbound numbers (will need a paid Twilio account to test): - Configure config/twilio.yml with credentials from a Twilio account that has multiple outbound phone numbers - Create multiple users and add new phone numbers for each using the above steps, and confirm them - Cause notifications to be generated for each user and ensure that they come from different phone numbers - You may need to test several phone numbers before a notification is sent from a different number - Cause more notifications to be sent and ensure that each phone number receives notifications from the same outbound phone number every time Change-Id: I103c93a8096acaaabd29530b0a0b5c43bc05c26b Reviewed-on: https://gerrit.instructure.com/59901 Tested-by: Jenkins Reviewed-by: Joel Hough <joel@instructure.com> QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Peyton Craighill <pcraighill@instructure.com>
2015-08-18 03:26:39 +08:00
rescue StandardError => e
logger.error "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
Canvas::Errors.capture(
e,
message: 'SMS delivery failed',
to: to,
object: inspect.to_s,
tags: {
type: :sms_message
}
)
cancel
else
complete_dispatch
end
else
deliver_via_email
end
end
# Internal: Deliver the message using AWS SNS.
#
# Returns nothing.
def deliver_via_push
begin
self.user.notification_endpoints.each do |notification_endpoint|
notification_endpoint.destroy unless notification_endpoint.push_json(sns_json)
end
complete_dispatch
rescue StandardError => e
@exception = e
error_string = "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
logger.error error_string
transmission_errors = error_string
cancel
raise e
end
end
private
def infer_from_name
if asset_context
return message_context.from_name if message_context.from_name.present?
return asset_context.name if can_use_asset_name_for_from?
end
if root_account && root_account.settings[:outgoing_email_default_name]
return root_account.settings[:outgoing_email_default_name]
end
HostUrl.outgoing_email_default_name
end
def can_use_asset_name_for_from?
!asset_context.is_a?(Account) && asset_context.name && notification.dashboard? rescue false
end
def message_context
@_message_context ||= Messages::AssetContext.new(context, notification_name)
end
def apply_course_nickname_to_asset(asset, user)
hacked_course = if asset.is_a?(Course)
asset
elsif asset.respond_to?(:context) && asset.context.is_a?(Course)
asset.context
elsif asset.respond_to?(:course) && asset.course.is_a?(Course)
asset.course
end
hacked_course.apply_nickname_for!(user) if hacked_course
hacked_course
end
2011-02-01 09:57:29 +08:00
end