canvas-lms/app/models/message.rb

1155 lines
38 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2011-02-01 09:57:29 +08:00
#
# Copyright (C) 2011 - present 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/>.
#
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
include Messages::SendStudentNamesHelper
2011-02-01 09:57:29 +08:00
include CanvasPartman::Concerns::Partitioned
self.partitioning_strategy = :by_date
self.partitioning_interval = :weeks
extend TextHelper
MAX_TWITTER_MESSAGE_LENGTH = 140
class QueuedNotFound < StandardError; end
class Queued
# use this to queue messages for delivery so we find them using the created_at in the scope
# instead of using id alone when reconstituting the AR object
attr_accessor :id, :created_at
def initialize(id, created_at)
@id, @created_at = id, created_at
end
delegate :dispatch_at, :to => :message
def deliver
message.deliver
rescue QueuedNotFound => e
raise Delayed::RetriableError, "Message does not (yet?) exist"
end
def message
return @message if @message.present?
@message = Message.in_partition('id' => id, 'created_at' => @created_at).where(:id => @id, :created_at => @created_at).first || Message.where(:id => @id).first
raise QueuedNotFound if @message.nil?
@message
end
end
def for_queue
Queued.new(self.id, self.created_at)
end
# Associations
2011-02-01 09:57:29 +08:00
belongs_to :communication_channel
belongs_to :context, polymorphic: [], exhaustive: false
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, :inverse_of => :context
2011-02-01 09:57:29 +08:00
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
before_save :truncate_invalid_message
# Validations
validate :prevent_updates
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_text_length}, allow_nil: true, allow_blank: true
validates :from_name, length: {maximum: maximum_text_length}, allow_nil: true, allow_blank: true
validates :reply_to_name, length: {maximum: maximum_string_length}, allow_nil: true, allow_blank: true
def prevent_updates
unless self.new_record?
# e.g. Message.where(:id => self.id, :created_at => self.created_at).update_all(...)
self.errors.add(:base, "Regular saving on messages is disabled - use save_using_update_all")
end
end
# 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'
2011-02-01 09:57:29 +08:00
MessageDispatcher.dispatch(self)
end
end
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
event :set_transmission_error, :transitions_to => :transmission_error
2011-02-01 09:57:29 +08:00
event :cancel, :transitions_to => :cancelled
event :close, :transitions_to => :closed # needed for dashboard messages
end
state :staged do
event :dispatch, :transitions_to => :sending
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
event :set_transmission_error, :transitions_to => :transmission_error
2011-02-01 09:57:29 +08:00
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
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
event :set_transmission_error, :transitions_to => :transmission_error
2011-02-01 09:57:29 +08:00
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
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
event :set_transmission_error, :transitions_to => :transmission_error
2011-02-01 09:57:29 +08:00
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
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
event :set_transmission_error, :transitions_to => :transmission_error
2011-02-01 09:57:29 +08:00
event :close, :transitions_to => :closed
event :cancel, :transitions_to => :closed
end
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
2011-02-01 09:57:29 +08:00
state :cancelled
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
state :transmission_error do
event :close, :transitions_to => :closed
end
2011-02-01 09:57:29 +08:00
state :closed do
Add notification failure processor Using the new notification_service allows us to provide more specific failure feedback to canvas. When we enqueue a message to the notification service, we pass along the canvas global message id. If the message fails to send, we enqueue a failure message to a "notification_failure" sqs queue, and reference the global message id. This allows us to write failure information off to the canvas message object and put it into an error state. Test Plan: * Start local fake_sqs environment If using docker `$ docker pull feathj/fake-sqs` `$ docker run -it -p 9494:9494 -e VIRTUAL_HOST=sqs.docker feathj/fake-sqs` If running native `$ gem install fake_sqs` `$ fake_sqs` * Create `<canvas>/config/notification_failures.yml` file and place the following in it: If using docker ``` development: use_ssl: false sqs_endpoint: sqs.docker sqs_port: 9494 access_key_id: access key id secret_access_key: secret access key ``` If running native ``` development: use_ssl: false sqs_endpoint: localhost sqs_port: 4568 access_key_id: access key id secret_access_key: secret access key ``` * Create a canvas message to put in error state * Login to canvas * Create new conversation message * Open rails console and confirm that message.state is not "transmission_error", also take note of message id * Start canvas jobs, from canvas-lms directory: `$ bundle exec script/delayed_job run` * Manually enqueue failure message to fake_sqs ``` require 'yaml' require 'aws-sdk' require 'aws-sdk-core' require 'aws-sdk-resources' require 'aws-sdk-v1' client = AWS::SQS::Client.new( use_ssl: false, sqs_endpoint: '<YOUR_SQS_HOST>', sqs_port: <YOUR_SQS_PORT>, access_key_id: 'access key id', secret_access_key: 'secret access key' ) client.create_queue(queue_name: 'notification-service-failures') rescue nil queue_url = client .list_queues[:queue_urls] .reject { |queue| /dead/i.match(queue) } .detect { |queue| /notification-service-failures/.match(queue) } puts queue_url puts client.send_message(queue_url: queue_url, message_body: { 'global_id' => <YOUR_MESSAGE_ID>, 'error' => 'the message failed to send amigo' }.to_json) ``` * Verify that message is state is set to "transmission_error" and the transmission_errors field has your error message closes CNVS-26442 Change-Id: Ic379142727d4e186ae3032241caca1b1e4c5e074 Reviewed-on: https://gerrit.instructure.com/70447 Reviewed-by: Christina Wuest <cwuest@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Tested-by: Jenkins QA-Review: Heath Hales <hhales@instructure.com> Product-Review: Jonathan Featherstone <jfeatherstone@instructure.com>
2016-01-16 08:03:06 +08:00
event :set_transmission_error, :transitions_to => :transmission_error
2011-02-01 09:57:29 +08:00
event :send_message, :transitions_to => :closed do
self.sent_at ||= Time.now
end
end
end
# turns out we can override this method inside the workflow gem to get a custom save for workflow transitions
def persist_workflow_state(new_state)
self.workflow_state = new_state
self.save_using_update_all
end
def save_using_update_all
self.shard.activate do
self.updated_at = Time.now.utc
updates = Hash[self.changes_to_save.map{|k, v| [k, v.last]}]
self.class.in_partition(attributes).where(:id => self.id, :created_at => self.created_at).update_all(updates)
self.clear_changes_information
end
end
# Named scopes
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 :more_recent_than, lambda { |date| where("messages.created_at>? AND messages.dispatch_at>?", date, 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)) }
scope :at_timestamp, lambda { |timestamp| where("created_at >= ? AND created_at < ?", Time.at(timestamp.to_i), Time.at(timestamp.to_i + 1)) }
# an optimization for queries that would otherwise target the main table to
# make them target the specific partition table. Naturally this only works if
# the records all reside within the same partition!!!
#
# for example, this takes us from:
#
# Message.where(id: 3)
# => SELECT "messages".* FROM "messages" WHERE "messages"."id" = 3
# to:
#
# Message.in_partition(Message.last.attributes).where(id: 3)
# => SELECT "messages_2020_35".* FROM "messages_2020_35" WHERE "messages_2020_35"."id" = 3
#
scope :in_partition, lambda { |attrs|
dup.instance_eval do
tap do
@table = klass.arel_table_from_key_values(attrs)
@predicate_builder = predicate_builder.dup
@predicate_builder.instance_variable_set('@table', ActiveRecord::TableMetadata.new(klass, @table))
end
end
}
#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)
# The User model currently supports storing either a path or full
# URL for an avatar. Because of this, alternatives to URI.encode
# such as CGI.escape end up escaping too much for full URLs. In
# order to escape just the path, we'd need to utilize URI.parse
# which can't handle URLs with spaces. As that is the root cause
# of this change, we'll just use the deprecated URI.encode method.
#
# rubocop:disable Lint/UriEscapeUnescape
URI.join("#{HostUrl.protocol}://#{HostUrl.context_host(author_account)}", URI.encode(url)).to_s if url
# rubocop:enable Lint/UriEscapeUnescape
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
transpose_ids in html body to be shard aware fixes VICE-623 Original attempt to fix this was made on https://gerrit.instructure.com/c/canvas-lms/+/244178. This issue was a result of the Announcement/DiscussionTopic models not having a RootAccount association in spite of having the associated foreign key. MRA attempted to use the context's root account for domain lookup (see lib/multiple_root_accounts/extensions/host_url.rb) Instead, we now use the linked root account as is already determined by the Message itself. Due to the separation of functionality, testing for the full flow was decided against given the complexity and small ROI. test plan: - set up two accounts with a trust (hmu if you need help) - in one account, create a course - enroll a user from the other account into that course - ensure the user is set up to receive notifications for discussion topics - create an announcement in the course - add a link to the announcement to content in that account - publish the announcement to trigger a notification - check the content of the user's message - the link should point to the correct domain for the second account qa risk: med; this has already caused issues on production Change-Id: Ib643c5ce7a8ab78783f28253d7e42935fdf3d25b Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/248491 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Rob Orton <rob@instructure.com> QA-Review: Rob Orton <rob@instructure.com> Product-Review: Rob Orton <rob@instructure.com>
2020-09-25 04:35:18 +08:00
# overwrite existing html_to_text so that messages with links can have the ids
# translated to be shard aware while preserving the link_root_account for the
# host.
def html_to_text(html, *opts)
super(transpose_url_ids(html), *opts)
end
# overwrite existing html_to_simple_html so that messages with links can have
# the ids translated to be shard aware while preserving the link_root_account
# for the host.
def html_to_simple_html(html, *opts)
super(transpose_url_ids(html), *opts)
end
def transpose_url_ids(html)
url_helper = Api::Html::UrlProxy.new(self, self.context,
HostUrl.context_host(self.link_root_account),
HostUrl.protocol,
target_shard: self.link_root_account.shard)
Api::Html::Content.rewrite_outgoing(html, self.link_root_account, url_helper)
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(pre_loaded_account: nil)
context = pre_loaded_account
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
@root_account ||= begin
context ||= self.context
if context.is_a?(CommunicationChannel) && @data&.root_account_id
root_account = Account.where(id: @data.root_account_id).first
context = root_account if root_account
end
# root_account is on lots of objects, use it when we can.
context = context.root_account if context.respond_to?(:root_account)
# some of these `context =` may not be relevant now that we have
# root_account on many classes, but root_account doesn't respond to them
# and so it's fast, and there are a lot of ways to generate a message.
context = context.assignment.root_account if context.respond_to?(:assignment) && context.assignment
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
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.master_template.course if context.respond_to?(:master_template) && context.master_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
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)
# Going through SisPseudonym.for is important since the account could change
if context && context.respond_to?(:root_account)
p = SisPseudonym.for(user, context, type: :implicit, require_sis: false)
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
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!
return if state == :bounced
self.dispatch_at = Time.now.utc + self.delay_for
self.workflow_state = 'staged'
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)
return false if filename.include?('slack')
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.
#
# Returns an HTML template (or nil).
def apply_html_template(binding)
orig_i18n_scope = @i18n_scope
@i18n_scope = "#{@i18n_scope}.html"
template, template_path = load_html_template
return nil unless template
# Add the attribute 'inner_html' with the value of inner_html into the _binding
@output_buffer = ActionView::OutputBuffer.new
inner_html = eval(ActionView::Template::Handlers::ERB::Erubi.new(template, :bufvar => '@output_buffer').src, binding, template_path)
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 = ActionView::OutputBuffer.new
eval(ActionView::Template::Handlers::ERB::Erubi.new(File.read(layout_path)).src, binding, layout_path)
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), 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'.
#
# Returns message body
def populate_body(message_body_template, path_type, binding, filename)
# Build the body content based on the path type
self.body = eval(Erubi::Engine.new(message_body_template, bufvar: '@output_buffer').src, binding, filename)
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'
footer_path = Canvas::MessageHelper.find_message_path('_email_footer.email.erb')
raw_footer_message = File.read(footer_path)
footer_message = eval(Erubi::Engine.new(raw_footer_message, :bufvar => "@output_buffer").src, nil, footer_path)
# currently, _email_footer.email.erb only contains a way for users to change notification prefs
# they can only change it if they are registered in the first place
# do not show this for emails telling users to register
if footer_message.present? && !self.notification&.registration?
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.
def parse!(path_type=nil, root_account: nil)
2011-02-01 09:57:29 +08:00
raise StandardError, "Cannot parse without a context" unless self.context
# set @root_account using our pre_loaded_account, because link_root_account
# is called many times.
link_root_account(pre_loaded_account: root_account)
# 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)
path_type ||= communication_channel.try(:path_type) || 'email'
# Determine the message template file to be used in the message
filename = template_filename(path_type)
message_body_template = get_template(filename)
if !message_body_template && path_type == 'slack'
filename = template_filename('sms')
message_body_template = get_template(filename)
end
context, asset, user, delayed_messages, data = [self.context,
self.context, self.user, @delayed_messages, @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?
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
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 = eval(Erubi::Engine.new(self.notification.main_link || "").src)
self.subject = eval(Erubi::Engine.new(subject).src)
self.body = eval(Erubi::Engine.new(body).src)
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
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
if path_type == 'slack' && !context_root_account.settings[:encrypted_slack_key]
logger.warn('Could not send slack message without configured key')
return nil
2011-02-01 09:57:29 +08:00
end
check_acct = root_account || user&.account || Account.site_admin
if path_type == 'sms'
if Notification.types_to_send_in_sms(check_acct).exclude?(notification_name)
return skip_and_cancel
end
end
if path_type == "push"
if Notification.types_to_send_in_push.exclude?(notification_name) || !check_acct.enable_push_notifications?
return skip_and_cancel
end
end
InstStatsd::Statsd.increment("message.deliver.#{path_type}.#{notification_name}",
short_stat: 'message.deliver',
tags: {path_type: path_type, notification_name: notification_name})
global_account_id = Shard.global_id_for(root_account_id, self.shard)
InstStatsd::Statsd.increment("message.deliver.#{path_type}.#{global_account_id}",
short_stat: 'message.deliver_per_account',
tags: {path_type: path_type, root_account_id: global_account_id})
if check_acct.feature_enabled?(:notification_service)
enqueue_to_sqs
else
delivery_method = "deliver_via_#{path_type}".to_sym
if !delivery_method || !respond_to?(delivery_method, true)
logger.warn("Could not set delivery_method from #{path_type}")
return nil
end
send(delivery_method)
end
end
def skip_and_cancel
InstStatsd::Statsd.increment("message.skip.#{path_type}.#{notification_name}",
short_stat: 'message.skip',
tags: { path_type: path_type, notification_name: notification_name })
self.cancel
end
# Public: Enqueues a message to the notification_service's sqs queue
#
# Returns nothing
def enqueue_to_sqs
targets = notification_targets
if targets.empty?
# Log no_targets_specified error to DataDog
InstStatsd::Statsd.increment("message.no_targets_specified",
short_stat: 'message.no_targets_specified',
tags: {path_type: path_type})
self.transmission_errors = "No notification targets specified"
self.set_transmission_error
else
targets.each do |target|
Services::NotificationService.process(
notification_service_id,
notification_message,
path_type,
target,
self.notification&.priority?
)
end
complete_dispatch
end
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
# Public: Determines the message body for a notification endpoint
#
# Returns target notification message body
def notification_message
case path_type
when "email"
Mailer.create_message(self).to_s
when "push"
sns_json
when "twitter"
url = self.main_link || self.url
message_length = MAX_TWITTER_MESSAGE_LENGTH - url.length - 1
truncated_body = HtmlTextHelper.strip_and_truncate(body, max_length: message_length)
"#{truncated_body} #{url}"
else
if to =~ /^\+[0-9]+$/ || path_type == 'slack'
body
else
Mailer.create_message(self).to_s
end
end
end
# Public: Returns all notification_service targets to send to
#
# Returns the targets in which to send the notification to
def notification_targets
case path_type
when "push"
self.user.notification_endpoints.pluck(:arn)
when "twitter"
twitter_service = user.user_services.where(service: 'twitter').first
[
"access_token"=> twitter_service.token,
"access_token_secret"=> twitter_service.secret,
"user_id"=> twitter_service.service_user_id
]
when 'slack'
[
'recipient'=> to,
'access_token'=> Canvas::Security.decrypt_password(context_root_account.settings[:encrypted_slack_key],
context_root_account.settings[:encrypted_slack_key_salt], 'instructure_slack_encrypted_key')
]
else
[to]
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: Message to use if the message is unavailable to send.
#
# Returns a string
def self.unavailable_message
I18n.t('message preview unavailable')
end
# Public: Get the root account of this message's context.
#
# Returns an account.
def context_root_account
if context.is_a?(AccountNotification)
return context.account.root_account
end
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
# This is a dumb name, but it's the context (course/group/account/user) of
# the message.context (which should really be message.asset)
def context_context
@context_context ||= begin
unbounded_loop_paranoia_counter = 10
current_context = context
until current_context&.is_a_context?
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
end
end
def media_context
context = self.context
context = context.context if context.respond_to?(:context)
return context if context.is_a?(Course)
context = (context.respond_to?(:course) && context.course) ? context.course : link_root_account
context
end
def notification_service_id
"#{self.global_id}+#{self.created_at.to_i}"
end
def self.parse_notification_service_id(service_id)
if service_id.to_s.include?("+")
service_id.split("+")
else
[service_id, nil]
end
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
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 = name_helper.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: Truncate the message if it exceeds 64kb
#
# Returns nothing.
def truncate_invalid_message
[:body, :html_body].each do |attr|
if self.send(attr) && self.send(attr).bytesize > self.class.maximum_text_length
self.send("#{attr}=", Message.unavailable_message)
end
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
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, :from_name, :to, :reply_to, :subject, :body, :html_body])['message']
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
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_now
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.context_host(link_root_account)
msg_id = AssetSignature.generate(self)
Twitter::Messenger.new(self, twitter_service, host, msg_id).deliver
complete_dispatch
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)
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
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: true
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
)
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
return name_helper.from_name if name_helper.from_name.present?
if name_helper.asset.is_a? AppointmentGroup
if !name_helper.asset.contexts_for_user(user).nil?
names = name_helper.asset.contexts_for_user(user).map(&:name).join(", ")
if names == ""
return name_helper.asset.context.name
else
return names
end
end
end
return context_context.nickname_for(user) if can_use_name_for_from?(context_context)
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_name_for_from?(c)
c && !c.is_a?(Account) && notification&.dashboard? &&
c.respond_to?(:name) && c.name.present?
end
def name_helper
@name_helper ||= Messages::NameHelper.new(
asset: context,
message_recipient: self.user,
notification_name: 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