2020-10-27 00:46:40 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
#
|
2017-04-28 03:46:17 +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
|
2013-02-14 01:13:44 +08:00
|
|
|
# Included modules
|
2014-07-24 01:14:22 +08:00
|
|
|
include Rails.application.routes.url_helpers
|
2014-01-28 05:07:09 +08:00
|
|
|
|
2013-02-21 03:10:53 +08:00
|
|
|
include ERB::Util
|
2011-02-01 09:57:29 +08:00
|
|
|
include SendToStream
|
2011-06-30 07:04:59 +08:00
|
|
|
include TextHelper
|
2014-02-05 04:53:04 +08:00
|
|
|
include HtmlTextHelper
|
2013-02-14 01:13:44 +08:00
|
|
|
include Workflow
|
2015-09-03 08:51:15 +08:00
|
|
|
include Messages::PeerReviewsHelper
|
2018-07-31 04:42:16 +08:00
|
|
|
include Messages::SendStudentNamesHelper
|
2011-02-01 09:57:29 +08:00
|
|
|
|
2018-11-02 00:09:25 +08:00
|
|
|
include CanvasPartman::Concerns::Partitioned
|
|
|
|
self.partitioning_strategy = :by_date
|
|
|
|
self.partitioning_interval = :weeks
|
|
|
|
|
2013-02-21 03:10:53 +08:00
|
|
|
extend TextHelper
|
|
|
|
|
2016-01-23 07:26:44 +08:00
|
|
|
MAX_TWITTER_MESSAGE_LENGTH = 140
|
|
|
|
|
2020-12-09 03:33:59 +08:00
|
|
|
class QueuedNotFound < StandardError; end
|
|
|
|
|
2018-11-02 00:09:25 +08:00
|
|
|
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
|
|
|
|
|
2020-12-09 03:33:59 +08:00
|
|
|
delegate :dispatch_at, :to => :message
|
|
|
|
|
|
|
|
def deliver
|
|
|
|
message.deliver
|
|
|
|
rescue QueuedNotFound => e
|
|
|
|
raise Delayed::RetriableError, "Message does not (yet?) exist"
|
|
|
|
end
|
|
|
|
|
2018-11-02 00:09:25 +08:00
|
|
|
def message
|
2020-12-09 03:33:59 +08:00
|
|
|
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
|
2018-11-02 00:09:25 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def for_queue
|
|
|
|
Queued.new(self.id, self.created_at)
|
|
|
|
end
|
2015-09-03 08:51:15 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Associations
|
2011-02-01 09:57:29 +08:00
|
|
|
belongs_to :communication_channel
|
2017-08-01 04:30:37 +08:00
|
|
|
belongs_to :context, polymorphic: [], exhaustive: false
|
2014-09-03 23:11:17 +08:00
|
|
|
include NotificationPreloader
|
2011-02-01 09:57:29 +08:00
|
|
|
belongs_to :user
|
2014-05-22 06:06:13 +08:00
|
|
|
belongs_to :root_account, :class_name => 'Account'
|
2016-12-23 04:21:33 +08:00
|
|
|
has_many :attachments, :as => :context, :inverse_of => :context
|
2011-02-01 09:57:29 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
attr_writer :delayed_messages
|
2014-02-12 09:29:31 +08:00
|
|
|
attr_accessor :output_buffer
|
2013-02-14 01:13:44 +08:00
|
|
|
|
|
|
|
# Callbacks
|
|
|
|
after_save :stage_message
|
2011-02-01 09:57:29 +08:00
|
|
|
before_save :infer_defaults
|
|
|
|
before_save :move_dashboard_messages
|
2013-02-14 01:13:44 +08:00
|
|
|
before_save :move_messages_for_deleted_users
|
2021-04-08 06:36:51 +08:00
|
|
|
before_save :truncate_invalid_message
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Validations
|
2018-11-02 00:09:25 +08:00
|
|
|
validate :prevent_updates
|
2015-12-18 05:31:08 +08:00
|
|
|
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
|
2016-02-25 06:10:08 +08:00
|
|
|
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
|
2015-12-18 05:31:08 +08:00
|
|
|
validates :reply_to_name, length: {maximum: maximum_string_length}, allow_nil: true, allow_blank: true
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2018-11-02 00:09:25 +08:00
|
|
|
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
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Stream policy
|
2011-02-01 09:57:29 +08:00
|
|
|
on_create_send_to_streams do
|
2013-02-14 01:13:44 +08:00
|
|
|
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
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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
|
2020-03-24 01:44:39 +08:00
|
|
|
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
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
state :bounced do
|
|
|
|
event :close, :transitions_to => :closed
|
|
|
|
end
|
2012-12-07 03:19:08 +08:00
|
|
|
|
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
|
2013-02-14 01:13:44 +08:00
|
|
|
end
|
|
|
|
|
2018-11-02 00:09:25 +08:00
|
|
|
# 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]}]
|
2020-08-25 01:17:41 +08:00
|
|
|
self.class.in_partition(attributes).where(:id => self.id, :created_at => self.created_at).update_all(updates)
|
2018-11-02 00:09:25 +08:00
|
|
|
self.clear_changes_information
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Named scopes
|
2014-09-26 03:17:49 +08:00
|
|
|
scope :for, lambda { |context| where(:context_type => context.class.base_class.to_s, :context_id => context) }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2013-03-21 03:38:19 +08:00
|
|
|
scope :after, lambda { |date| where("messages.created_at>?", date) }
|
2020-03-25 03:14:04 +08:00
|
|
|
scope :more_recent_than, lambda { |date| where("messages.created_at>? AND messages.dispatch_at>?", date, date) }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2014-07-02 03:38:26 +08:00
|
|
|
scope :to_dispatch, -> {
|
2013-03-21 03:38:19 +08:00
|
|
|
where("messages.workflow_state='staged' AND messages.dispatch_at<=? AND 'messages.to'<>'dashboard'", Time.now.utc)
|
2013-02-14 01:13:44 +08:00
|
|
|
}
|
|
|
|
|
2014-07-02 03:38:26 +08:00
|
|
|
scope :to_email, -> { where(:path_type => ['email', 'sms']) }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2014-07-02 03:38:26 +08:00
|
|
|
scope :not_to_email, -> { where("messages.path_type NOT IN ('email', 'sms')") }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2013-03-21 03:38:19 +08:00
|
|
|
scope :by_name, lambda { |notification_name| where(:notification_name => notification_name) }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2013-03-21 03:38:19 +08:00
|
|
|
scope :before, lambda { |date| where("messages.created_at<?", date) }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2013-03-21 03:38:19 +08:00
|
|
|
scope :for_user, lambda { |user| where(:user_id => user)}
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2013-02-28 10:11:57 +08:00
|
|
|
# messages that can be moved to the 'cancelled' state. dashboard messages
|
|
|
|
# can be closed by calling 'cancel', but aren't included
|
2014-07-02 03:38:26 +08:00
|
|
|
scope :cancellable, -> { where(:workflow_state => ['created', 'staged', 'sending']) }
|
2013-03-21 03:38:19 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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.
|
2014-07-02 03:38:26 +08:00
|
|
|
scope :staged, -> { where("messages.workflow_state='staged' AND messages.dispatch_at>?", Time.now.utc) }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2013-03-21 03:38:19 +08:00
|
|
|
scope :in_state, lambda { |state| where(:workflow_state => Array(state).map(&:to_s)) }
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2018-11-02 00:09:25 +08:00
|
|
|
scope :at_timestamp, lambda { |timestamp| where("created_at >= ? AND created_at < ?", Time.at(timestamp.to_i), Time.at(timestamp.to_i + 1)) }
|
|
|
|
|
2020-08-25 01:17:41 +08:00
|
|
|
# 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
|
|
|
|
}
|
|
|
|
|
2014-05-22 04:10:10 +08:00
|
|
|
#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
|
2014-07-16 06:37:20 +08:00
|
|
|
url = author.try(:avatar_url)
|
2021-03-05 06:37:23 +08:00
|
|
|
# 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
|
2014-05-22 04:10:10 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def author_short_name
|
|
|
|
author.try(:short_name)
|
|
|
|
end
|
|
|
|
|
|
|
|
def author_email_address
|
2014-09-09 06:47:27 +08:00
|
|
|
if context_root_account.try(:author_email_in_notifications?)
|
|
|
|
author.try(:email)
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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.
|
2014-03-14 01:44:09 +08:00
|
|
|
def default_url_options
|
|
|
|
{ protocol: HostUrl.protocol, host: HostUrl.context_host(link_root_account) }
|
2013-02-14 01:13:44 +08:00
|
|
|
end
|
|
|
|
|
2015-01-14 06:41:12 +08:00
|
|
|
# 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
|
2015-03-19 18:05:18 +08:00
|
|
|
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
|
|
|
|
|
2015-01-14 06:41:12 +08:00
|
|
|
{
|
|
|
|
default: self.subject,
|
|
|
|
GCM: {
|
|
|
|
data: {
|
|
|
|
alert: self.subject,
|
2015-03-19 18:05:18 +08:00
|
|
|
}.merge(custom_data)
|
2015-01-14 06:41:12 +08:00
|
|
|
}.to_json,
|
2015-02-26 01:24:01 +08:00
|
|
|
APNS_SANDBOX: {
|
|
|
|
aps: {
|
|
|
|
alert: self.subject
|
|
|
|
}
|
2015-03-19 18:05:18 +08:00
|
|
|
}.merge(custom_data).to_json,
|
2015-01-14 06:41:12 +08:00
|
|
|
APNS: {
|
|
|
|
aps: {
|
|
|
|
alert: self.subject
|
|
|
|
}
|
2015-03-19 18:05:18 +08:00
|
|
|
}.merge(custom_data).to_json
|
2015-01-14 06:41:12 +08:00
|
|
|
}.to_json
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
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
|
|
|
|
|
2014-03-14 01:44:09 +08:00
|
|
|
# infer a root account associated with the context that the user can log in to
|
2021-04-21 12:39:54 +08:00
|
|
|
def link_root_account(pre_loaded_account: nil)
|
|
|
|
context = pre_loaded_account
|
2014-03-14 01:44:09 +08:00
|
|
|
@root_account ||= begin
|
2021-04-21 12:39:54 +08:00
|
|
|
context ||= self.context
|
2017-07-26 06:54:57 +08:00
|
|
|
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
|
2016-06-13 21:54:45 +08:00
|
|
|
|
2021-04-21 12:39:54 +08:00
|
|
|
# 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
|
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
|
2017-05-05 22:41:31 +08:00
|
|
|
context = context.master_template.course if context.respond_to?(:master_template) && context.master_template
|
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)
|
2021-04-21 12:39:54 +08:00
|
|
|
|
|
|
|
# Going through SisPseudonym.for is important since the account could change
|
2019-04-15 03:03:55 +08:00
|
|
|
if context && context.respond_to?(:root_account)
|
2017-04-07 05:00:25 +08:00
|
|
|
p = SisPseudonym.for(user, context, type: :implicit, require_sis: false)
|
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
|
2013-04-20 06:00:15 +08:00
|
|
|
end
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2015-11-25 05:15:59 +08:00
|
|
|
# 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
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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
|
2011-02-23 05:28:50 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Public: Custom getter that delegates and caches notification category to
|
2014-01-28 05:07:09 +08:00
|
|
|
# associated notification
|
2013-02-14 01:13:44 +08:00
|
|
|
#
|
|
|
|
# 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
|
2011-02-23 05:28:50 +08:00
|
|
|
# dispatching.
|
2013-02-14 01:13:44 +08:00
|
|
|
#
|
|
|
|
# Returns nothing.
|
2011-02-23 05:28:50 +08:00
|
|
|
def stage_without_dispatch!
|
2020-07-29 08:10:18 +08:00
|
|
|
return if state == :bounced
|
2020-03-24 01:44:39 +08:00
|
|
|
self.dispatch_at = Time.now.utc + self.delay_for
|
|
|
|
self.workflow_state = 'staged'
|
2011-02-23 05:28:50 +08:00
|
|
|
end
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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
|
2013-02-14 01:13:44 +08:00
|
|
|
stage if state == :created
|
|
|
|
|
|
|
|
if dashboard?
|
2013-03-19 03:07:47 +08:00
|
|
|
messages = Message.in_state(:dashboard).where(
|
2013-02-14 01:13:44 +08:00
|
|
|
:notification_id => notification_id,
|
|
|
|
:context_id => context_id,
|
|
|
|
:context_type => context_type,
|
|
|
|
:user_id => user_id
|
2013-03-19 03:07:47 +08:00
|
|
|
)
|
2013-02-14 01:13:44 +08:00
|
|
|
|
|
|
|
(messages - [self]).each(&:close)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
end
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2014-05-13 22:18:47 +08:00
|
|
|
class UnescapedBuffer < String # acts like safe buffer except for the actually being safe part
|
|
|
|
alias :append= :<<
|
|
|
|
alias :safe_concat :concat
|
2015-07-09 04:12:07 +08:00
|
|
|
alias :safe_append= :concat
|
2014-05-13 22:18:47 +08:00
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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)
|
2014-05-13 22:18:47 +08:00
|
|
|
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
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
yield
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
instance_variable_set(:"@message_content_#{name}",
|
|
|
|
@output_buffer.to_s.strip)
|
|
|
|
@output_buffer = old_output_buffer.sub(/\n\z/, '')
|
|
|
|
|
2014-01-13 23:37:39 +08:00
|
|
|
if old_output_buffer.is_a?(ActiveSupport::SafeBuffer) && old_output_buffer.html_safe?
|
2014-02-12 09:29:31 +08:00
|
|
|
@output_buffer = old_output_buffer.class.new(@output_buffer)
|
2014-01-13 23:37:39 +08:00
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
''
|
|
|
|
end
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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)
|
2013-02-14 01:13:44 +08:00
|
|
|
instance_variable_get(:"@message_content_#{name}")
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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
|
2012-12-06 03:29:24 +08:00
|
|
|
|
|
|
|
# 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)
|
2019-09-20 05:39:37 +08:00
|
|
|
return false if filename.include?('slack')
|
2013-02-14 01:13:44 +08:00
|
|
|
filename = self.notification.name.downcase.gsub(/\s/, '_') + ".email.erb"
|
2012-12-06 03:29:24 +08:00
|
|
|
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
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2013-02-21 03:10:53 +08:00
|
|
|
# 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
|
|
|
|
|
2013-04-20 04:30:44 +08:00
|
|
|
# Public: Apply an HTML email template to this message.
|
2013-02-21 13:16:24 +08:00
|
|
|
#
|
2013-04-20 04:30:44 +08:00
|
|
|
# Returns an HTML template (or nil).
|
2018-04-11 02:09:37 +08:00
|
|
|
def apply_html_template(binding)
|
2013-07-12 00:36:36 +08:00
|
|
|
orig_i18n_scope = @i18n_scope
|
|
|
|
@i18n_scope = "#{@i18n_scope}.html"
|
2018-04-11 02:09:37 +08:00
|
|
|
template, template_path = load_html_template
|
|
|
|
return nil unless template
|
2013-02-21 13:16:24 +08:00
|
|
|
|
2013-03-26 02:13:42 +08:00
|
|
|
# Add the attribute 'inner_html' with the value of inner_html into the _binding
|
2020-01-28 06:00:23 +08:00
|
|
|
@output_buffer = ActionView::OutputBuffer.new
|
2018-04-11 02:09:37 +08:00
|
|
|
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
|
2013-03-26 02:13:42 +08:00
|
|
|
setter.call(inner_html)
|
2013-02-21 13:16:24 +08:00
|
|
|
|
|
|
|
layout_path = Canvas::MessageHelper.find_message_path('_layout.email.html.erb')
|
2020-01-28 06:00:23 +08:00
|
|
|
@output_buffer = ActionView::OutputBuffer.new
|
2018-04-11 02:09:37 +08:00
|
|
|
eval(ActionView::Template::Handlers::ERB::Erubi.new(File.read(layout_path)).src, binding, layout_path)
|
2013-07-12 00:36:36 +08:00
|
|
|
ensure
|
|
|
|
@i18n_scope = orig_i18n_scope
|
2013-04-20 04:30:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def load_html_template
|
|
|
|
html_file = template_filename('email.html')
|
|
|
|
html_path = Canvas::MessageHelper.find_message_path(html_file)
|
2018-04-11 02:09:37 +08:00
|
|
|
[File.read(html_path), html_path] if File.exist?(html_path)
|
2013-02-21 13:16:24 +08:00
|
|
|
end
|
|
|
|
|
2013-02-21 03:10:53 +08:00
|
|
|
# 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
|
2018-04-11 02:09:37 +08:00
|
|
|
def populate_body(message_body_template, path_type, binding, filename)
|
2013-02-21 03:10:53 +08:00
|
|
|
# Build the body content based on the path type
|
2019-09-20 05:39:37 +08:00
|
|
|
self.body = eval(Erubi::Engine.new(message_body_template, bufvar: '@output_buffer').src, binding, filename)
|
2018-04-11 02:09:37 +08:00
|
|
|
self.html_body = apply_html_template(binding) if path_type == 'email'
|
2013-02-21 03:10:53 +08:00
|
|
|
|
|
|
|
# Append a footer to the body if the path type is email
|
|
|
|
if path_type == 'email'
|
2018-04-11 02:09:37 +08:00
|
|
|
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)
|
2020-09-22 00:46:20 +08:00
|
|
|
# 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
|
2020-09-23 03:39:58 +08:00
|
|
|
if footer_message.present? && !self.notification&.registration?
|
2013-02-21 13:16:24 +08:00
|
|
|
self.body = <<-END.strip_heredoc
|
|
|
|
#{self.body}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
________________________________________
|
|
|
|
|
|
|
|
#{footer_message}
|
|
|
|
END
|
|
|
|
end
|
2013-02-21 03:10:53 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
self.body
|
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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.
|
2021-04-21 12:39:54 +08:00
|
|
|
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
|
2013-02-21 03:10:53 +08:00
|
|
|
|
2021-04-21 12:39:54 +08:00
|
|
|
# set @root_account using our pre_loaded_account, because link_root_account
|
|
|
|
# is called many times.
|
|
|
|
link_root_account(pre_loaded_account: root_account)
|
2013-02-21 03:10:53 +08:00
|
|
|
# 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"
|
2015-11-25 05:15:59 +08:00
|
|
|
user_time_zone = self.user.try(:time_zone) || root_account_time_zone || original_time_zone
|
2013-02-21 13:16:24 +08:00
|
|
|
Time.zone = user_time_zone
|
2013-02-21 03:10:53 +08:00
|
|
|
|
2015-10-22 02:45:51 +08:00
|
|
|
# (temporarily) override course name with user's nickname for the course
|
|
|
|
hacked_course = apply_course_nickname_to_asset(self.context, self.user)
|
|
|
|
|
2017-07-26 03:44:08 +08:00
|
|
|
path_type ||= communication_channel.try(:path_type) || 'email'
|
2013-02-21 13:16:24 +08:00
|
|
|
|
2013-02-21 03:10:53 +08:00
|
|
|
# Determine the message template file to be used in the message
|
2019-09-20 05:39:37 +08:00
|
|
|
filename = template_filename(path_type)
|
2013-02-21 03:10:53 +08:00
|
|
|
message_body_template = get_template(filename)
|
2019-09-20 05:39:37 +08:00
|
|
|
if !message_body_template && path_type == 'slack'
|
|
|
|
filename = template_filename('sms')
|
|
|
|
message_body_template = get_template(filename)
|
|
|
|
end
|
2013-02-21 03:10:53 +08:00
|
|
|
|
2017-07-26 06:54:57 +08:00
|
|
|
context, asset, user, delayed_messages, data = [self.context,
|
|
|
|
self.context, self.user, @delayed_messages, @data]
|
2013-02-21 03:10:53 +08:00
|
|
|
|
2014-03-14 01:44:09 +08:00
|
|
|
link_root_account.shard.activate do
|
2017-07-26 03:44:08 +08:00
|
|
|
if message_body_template.present?
|
2014-03-14 01:44:09 +08:00
|
|
|
populate_body(message_body_template, path_type, binding, filename)
|
2013-02-21 03:10:53 +08:00
|
|
|
|
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
|
2018-04-11 02:09:37 +08:00
|
|
|
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)
|
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
|
2013-02-21 03:10:53 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
self.body
|
2011-05-07 02:44:34 +08:00
|
|
|
ensure
|
2013-02-21 03:10:53 +08:00
|
|
|
# Set the timezone back to what it originally was
|
|
|
|
Time.zone = original_time_zone if original_time_zone.present?
|
|
|
|
|
2015-10-22 02:45:51 +08:00
|
|
|
hacked_course.apply_nickname_for!(nil) if hacked_course
|
|
|
|
|
2011-05-07 02:44:34 +08:00
|
|
|
@i18n_scope = nil
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2011-04-07 02:46:06 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Public: Deliver this message.
|
|
|
|
#
|
|
|
|
# Returns nothing.
|
2011-02-01 09:57:29 +08:00
|
|
|
def deliver
|
2013-02-14 01:13:44 +08:00
|
|
|
# don't dispatch canceled or already-sent messages.
|
|
|
|
return nil unless dispatch
|
2012-02-24 01:13:22 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
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
|
|
|
|
|
2019-07-07 01:18:20 +08:00
|
|
|
if path_type == 'slack' && !context_root_account.settings[:encrypted_slack_key]
|
|
|
|
logger.warn('Could not send slack message without configured key')
|
2013-02-14 01:13:44 +08:00
|
|
|
return nil
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2012-02-24 01:13:22 +08:00
|
|
|
|
2020-05-02 04:40:41 +08:00
|
|
|
check_acct = root_account || user&.account || Account.site_admin
|
2021-01-16 04:42:56 +08:00
|
|
|
if path_type == 'sms'
|
2020-05-02 04:40:41 +08:00
|
|
|
if Notification.types_to_send_in_sms(check_acct).exclude?(notification_name)
|
2021-02-10 14:53:52 +08:00
|
|
|
return skip_and_cancel
|
2020-03-04 05:45:29 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-03-06 11:35:36 +08:00
|
|
|
if path_type == "push"
|
|
|
|
if Notification.types_to_send_in_push.exclude?(notification_name) || !check_acct.enable_push_notifications?
|
2021-02-10 14:53:52 +08:00
|
|
|
return skip_and_cancel
|
2020-11-17 05:34:50 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-11-20 08:37:08 +08:00
|
|
|
InstStatsd::Statsd.increment("message.deliver.#{path_type}.#{notification_name}",
|
|
|
|
short_stat: 'message.deliver',
|
|
|
|
tags: {path_type: path_type, notification_name: notification_name})
|
|
|
|
|
2020-04-07 00:29:49 +08:00
|
|
|
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})
|
|
|
|
|
2019-01-21 23:29:11 +08:00
|
|
|
if check_acct.feature_enabled?(:notification_service)
|
2016-01-15 01:01:09 +08:00
|
|
|
enqueue_to_sqs
|
|
|
|
else
|
2019-07-07 01:18:20 +08:00
|
|
|
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
|
2016-01-15 01:01:09 +08:00
|
|
|
send(delivery_method)
|
2015-11-21 05:30:54 +08:00
|
|
|
end
|
2016-01-15 01:01:09 +08:00
|
|
|
end
|
|
|
|
|
2021-02-10 14:53:52 +08:00
|
|
|
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
|
|
|
|
|
2016-01-16 05:52:12 +08:00
|
|
|
# Public: Enqueues a message to the notification_service's sqs queue
|
|
|
|
#
|
|
|
|
# Returns nothing
|
2016-01-15 01:01:09 +08:00
|
|
|
def enqueue_to_sqs
|
2018-02-23 04:15:54 +08:00
|
|
|
targets = notification_targets
|
|
|
|
if targets.empty?
|
2020-07-03 03:12:27 +08:00
|
|
|
# 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})
|
|
|
|
|
2018-02-23 04:15:54 +08:00
|
|
|
self.transmission_errors = "No notification targets specified"
|
|
|
|
self.set_transmission_error
|
2020-07-03 03:12:27 +08:00
|
|
|
else
|
2018-02-23 04:15:54 +08:00
|
|
|
targets.each do |target|
|
|
|
|
Services::NotificationService.process(
|
2018-11-02 00:09:25 +08:00
|
|
|
notification_service_id,
|
2018-02-23 04:15:54 +08:00
|
|
|
notification_message,
|
|
|
|
path_type,
|
2020-03-24 02:11:51 +08:00
|
|
|
target,
|
2020-04-03 05:04:54 +08:00
|
|
|
self.notification&.priority?
|
2018-02-23 04:15:54 +08:00
|
|
|
)
|
|
|
|
end
|
2016-04-23 00:23:32 +08:00
|
|
|
complete_dispatch
|
2016-01-16 05:52:12 +08:00
|
|
|
end
|
2016-10-18 03:00:39 +08:00
|
|
|
rescue Aws::SQS::Errors::ServiceError => e
|
2016-01-15 01:01:09 +08:00
|
|
|
Canvas::Errors.capture(
|
|
|
|
e,
|
|
|
|
message: 'Message delivery failed',
|
|
|
|
to: to,
|
|
|
|
object: inspect.to_s
|
|
|
|
)
|
2016-04-23 00:23:32 +08:00
|
|
|
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
|
2012-02-24 01:13:22 +08:00
|
|
|
|
2016-01-16 05:52:12 +08:00
|
|
|
# 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
|
2016-01-23 07:26:44 +08:00
|
|
|
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}"
|
2016-01-16 05:52:12 +08:00
|
|
|
else
|
2019-07-07 01:18:20 +08:00
|
|
|
if to =~ /^\+[0-9]+$/ || path_type == 'slack'
|
2016-08-16 04:33:22 +08:00
|
|
|
body
|
|
|
|
else
|
|
|
|
Mailer.create_message(self).to_s
|
|
|
|
end
|
2016-01-16 05:52:12 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Public: Returns all notification_service targets to send to
|
|
|
|
#
|
|
|
|
# Returns the targets in which to send the notification to
|
|
|
|
def notification_targets
|
2016-01-23 07:26:44 +08:00
|
|
|
case path_type
|
|
|
|
when "push"
|
2020-02-26 03:10:21 +08:00
|
|
|
self.user.notification_endpoints.pluck(:arn)
|
2016-01-23 07:26:44 +08:00
|
|
|
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
|
|
|
|
]
|
2019-07-07 01:18:20 +08:00
|
|
|
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')
|
|
|
|
]
|
2016-01-16 05:52:12 +08:00
|
|
|
else
|
|
|
|
[to]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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)
|
2013-02-14 01:13:44 +08:00
|
|
|
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
|
2013-02-14 01:13:44 +08:00
|
|
|
|
|
|
|
hash
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2013-02-14 01:13:44 +08:00
|
|
|
|
|
|
|
# not sure what this is even doing?
|
2014-03-18 04:54:26 +08:00
|
|
|
message_types.to_a.sort_by { |m| m[0] == 'Other' ? CanvasSort::Last : m[0] }
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
2021-04-08 06:36:51 +08:00
|
|
|
# Public: Message to use if the message is unavailable to send.
|
|
|
|
#
|
|
|
|
# Returns a string
|
|
|
|
def self.unavailable_message
|
|
|
|
I18n.t('message preview unavailable')
|
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Public: Get the root account of this message's context.
|
|
|
|
#
|
|
|
|
# Returns an account.
|
2012-10-25 06:35:57 +08:00
|
|
|
def context_root_account
|
2020-02-22 08:52:41 +08:00
|
|
|
if context.is_a?(AccountNotification)
|
|
|
|
return context.account.root_account
|
|
|
|
end
|
|
|
|
|
2012-10-25 06:35:57 +08:00
|
|
|
unbounded_loop_paranoia_counter = 10
|
2013-02-14 01:13:44 +08:00
|
|
|
current_context = context
|
|
|
|
|
2015-04-29 00:44:44 +08:00
|
|
|
until current_context.respond_to?(:root_account)
|
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)
|
2013-02-14 01:13:44 +08:00
|
|
|
current_context = current_context.context
|
2012-10-25 06:35:57 +08:00
|
|
|
unbounded_loop_paranoia_counter -= 1
|
|
|
|
end
|
2013-02-14 01:13:44 +08:00
|
|
|
|
|
|
|
current_context.root_account
|
2012-10-25 06:35:57 +08:00
|
|
|
end
|
|
|
|
|
2017-07-26 03:44:08 +08:00
|
|
|
# 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
|
|
|
|
|
2021-03-18 00:00:21 +08:00
|
|
|
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
|
|
|
|
|
2018-11-02 00:09:25 +08:00
|
|
|
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
|
|
|
|
|
2015-02-19 04:04:55 +08:00
|
|
|
def custom_logo
|
|
|
|
context_root_account && context_root_account.settings[:email_logo]
|
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Internal: Set default values before save.
|
|
|
|
#
|
|
|
|
# Returns true.
|
2011-02-01 09:57:29 +08:00
|
|
|
def infer_defaults
|
2013-02-14 01:13:44 +08:00
|
|
|
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)
|
2012-10-25 06:35:57 +08:00
|
|
|
|
2013-03-13 23:11:14 +08:00
|
|
|
root_account = context_root_account
|
|
|
|
self.root_account_id ||= root_account.try(:id)
|
|
|
|
|
2014-05-22 06:06:13 +08:00
|
|
|
self.from_name = infer_from_name
|
2017-07-26 03:18:35 +08:00
|
|
|
self.reply_to_name = name_helper.reply_to_name
|
2012-10-25 06:35:57 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
true
|
|
|
|
end
|
2011-05-07 02:44:34 +08:00
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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.
|
2015-10-30 01:03:25 +08:00
|
|
|
def translate(*args)
|
|
|
|
key, options = I18nliner::CallHelpers.infer_arguments(args)
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Add scope if it's present in the model and missing from the key.
|
2015-10-30 01:03:25 +08:00
|
|
|
if !options[:i18nliner_inferred_key] && @i18n_scope && key !~ /\A#/
|
2013-02-14 01:13:44 +08:00
|
|
|
key = "##{@i18n_scope}.#{key}"
|
|
|
|
end
|
|
|
|
|
2015-10-30 01:03:25 +08:00
|
|
|
super(key, options)
|
2011-05-07 02:44:34 +08:00
|
|
|
end
|
|
|
|
alias :t :translate
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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.
|
2012-03-30 13:20:15 +08:00
|
|
|
def data=(values_hash)
|
|
|
|
@data = OpenStruct.new(values_hash)
|
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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
|
2012-12-07 03:19:08 +08:00
|
|
|
|
2021-04-08 06:36:51 +08:00
|
|
|
# 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
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# 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
|
|
|
|
|
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 = {})
|
2017-07-25 03:15:26 +08:00
|
|
|
super(:only => [:id, :created_at, :sent_at, :workflow_state, :from, :from_name, :to, :reply_to, :subject, :body, :html_body])['message']
|
2013-03-13 23:11:14 +08:00
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
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.
|
2012-12-07 03:19:08 +08:00
|
|
|
def deliver_via_email
|
|
|
|
res = nil
|
2013-02-14 01:13:44 +08:00
|
|
|
logger.info "Delivering mail: #{self.inspect}"
|
2012-12-07 03:19:08 +08:00
|
|
|
begin
|
2017-03-15 01:48:55 +08:00
|
|
|
res = Mailer.create_message(self).deliver_now
|
2012-12-07 03:19:08 +08:00
|
|
|
rescue Net::SMTPServerBusy => e
|
|
|
|
@exception = e
|
|
|
|
logger.error "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
2013-02-14 01:13:44 +08:00
|
|
|
cancel if e.message.try(:match, /Bad recipient/)
|
2013-02-26 08:04:06 +08:00
|
|
|
rescue StandardError, Timeout::Error => e
|
2012-12-07 03:19:08 +08:00
|
|
|
@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
|
2012-12-07 03:19:08 +08:00
|
|
|
if res
|
2011-02-10 07:50:14 +08:00
|
|
|
complete_dispatch
|
2012-12-07 03:19:08 +08:00
|
|
|
elsif @exception
|
2013-01-06 00:22:55 +08:00
|
|
|
raise_error = @exception.to_s !~ /^450/
|
|
|
|
log_error = raise_error && !@exception.is_a?(Timeout::Error)
|
|
|
|
if log_error
|
2015-04-05 10:39:49 +08:00
|
|
|
Canvas::Errors.capture(
|
|
|
|
@exception,
|
|
|
|
message: 'Message delivery failed',
|
|
|
|
to: to,
|
|
|
|
object: inspect.to_s
|
|
|
|
)
|
2012-12-07 03:19:08 +08:00
|
|
|
end
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2012-12-07 03:19:08 +08:00
|
|
|
self.errored_dispatch
|
2013-01-06 00:22:55 +08:00
|
|
|
if raise_error
|
|
|
|
raise @exception
|
|
|
|
else
|
|
|
|
return false
|
|
|
|
end
|
2011-02-10 07:50:14 +08:00
|
|
|
end
|
2013-02-14 01:13:44 +08:00
|
|
|
|
2012-12-07 03:19:08 +08:00
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2013-02-14 01:13:44 +08:00
|
|
|
# Internal: Deliver the message through Twitter.
|
|
|
|
#
|
2013-04-23 02:48:29 +08:00
|
|
|
# The template should define the content for :link and not place into the body of the template itself
|
|
|
|
#
|
2013-02-14 01:13:44 +08:00
|
|
|
# Returns nothing.
|
2012-12-07 03:19:08 +08:00
|
|
|
def deliver_via_twitter
|
2014-09-12 03:44:34 +08:00
|
|
|
twitter_service = user.user_services.where(service: 'twitter').first
|
2017-07-26 06:54:57 +08:00
|
|
|
host = HostUrl.context_host(link_root_account)
|
2014-04-15 01:30:13 +08:00
|
|
|
msg_id = AssetSignature.generate(self)
|
|
|
|
Twitter::Messenger.new(self, twitter_service, host, msg_id).deliver
|
2012-12-07 03:19:08 +08:00
|
|
|
complete_dispatch
|
|
|
|
end
|
|
|
|
|
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.
|
2013-02-14 01:13:44 +08:00
|
|
|
#
|
|
|
|
# Returns nothing.
|
2012-12-07 03:19:08 +08:00
|
|
|
def deliver_via_sms
|
2015-08-18 03:26:39 +08:00
|
|
|
if to =~ /^\+[0-9]+$/
|
|
|
|
begin
|
2020-07-08 02:31:32 +08:00
|
|
|
unless user.account.feature_enabled?(:international_sms)
|
2015-08-18 03:26:39 +08:00
|
|
|
raise "International SMS is currently disabled for this user's account"
|
|
|
|
end
|
2015-11-03 05:19:22 +08:00
|
|
|
if Canvas::Twilio.enabled?
|
|
|
|
Canvas::Twilio.deliver(
|
|
|
|
to,
|
|
|
|
body,
|
2016-03-03 04:52:41 +08:00
|
|
|
from_recipient_country: true
|
2015-11-03 05:19:22 +08:00
|
|
|
)
|
|
|
|
end
|
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
|
2012-12-07 03:19:08 +08:00
|
|
|
end
|
2014-05-22 06:06:13 +08:00
|
|
|
|
2015-01-14 06:41:12 +08:00
|
|
|
# Internal: Deliver the message using AWS SNS.
|
|
|
|
#
|
|
|
|
# Returns nothing.
|
|
|
|
def deliver_via_push
|
|
|
|
begin
|
2015-07-25 00:01:44 +08:00
|
|
|
self.user.notification_endpoints.each do |notification_endpoint|
|
2015-01-14 06:41:12 +08:00
|
|
|
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
|
|
|
|
|
2014-05-22 06:06:13 +08:00
|
|
|
private
|
|
|
|
def infer_from_name
|
2017-07-26 03:44:08 +08:00
|
|
|
return name_helper.from_name if name_helper.from_name.present?
|
2018-04-21 02:15:30 +08:00
|
|
|
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
|
2017-07-26 03:44:08 +08:00
|
|
|
return context_context.nickname_for(user) if can_use_name_for_from?(context_context)
|
2014-05-22 06:06:13 +08:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2017-07-26 03:44:08 +08:00
|
|
|
def can_use_name_for_from?(c)
|
|
|
|
c && !c.is_a?(Account) && notification&.dashboard? &&
|
|
|
|
c.respond_to?(:name) && c.name.present?
|
2014-05-22 06:06:13 +08:00
|
|
|
end
|
|
|
|
|
2017-07-26 03:18:35 +08:00
|
|
|
def name_helper
|
2018-11-28 00:02:15 +08:00
|
|
|
@name_helper ||= Messages::NameHelper.new(
|
|
|
|
asset: context,
|
|
|
|
message_recipient: self.user,
|
|
|
|
notification_name: notification_name
|
|
|
|
)
|
2014-05-22 06:06:13 +08:00
|
|
|
end
|
|
|
|
|
2015-10-22 02:45:51 +08:00
|
|
|
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
|