2013-03-13 17:46:53 +08:00
|
|
|
#
|
2017-04-28 04:05:04 +08:00
|
|
|
# Copyright (C) 2013 - present Instructure, Inc.
|
2013-03-13 17:46:53 +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.
|
|
|
|
#
|
2017-04-28 04:05:04 +08:00
|
|
|
# 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/>.
|
2013-03-13 17:46:53 +08:00
|
|
|
#
|
|
|
|
|
|
|
|
class NotificationMessageCreator
|
|
|
|
include LocaleSelection
|
|
|
|
|
2017-07-26 06:54:57 +08:00
|
|
|
attr_accessor :notification, :asset, :to_users, :to_channels, :message_data
|
2013-03-13 17:46:53 +08:00
|
|
|
|
|
|
|
# Options can include:
|
|
|
|
# :to_list - A list of Users, User IDs, and CommunicationChannels to send to
|
2017-07-26 06:54:57 +08:00
|
|
|
# :data - Options merged with Message options
|
2013-03-13 17:46:53 +08:00
|
|
|
def initialize(notification, asset, options={})
|
|
|
|
@notification = notification
|
|
|
|
@asset = asset
|
|
|
|
@to_users = []
|
|
|
|
@to_channels = []
|
|
|
|
if options[:to_list]
|
|
|
|
@to_users = users_from_to_list(options[:to_list])
|
|
|
|
@to_channels = communication_channels_from_to_list(options[:to_list])
|
|
|
|
end
|
|
|
|
@message_data = options.delete(:data)
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
# Public: create (and dispatch, and queue delayed) a message
|
|
|
|
# for this notication, associated with the given asset, sent to the given recipients
|
|
|
|
#
|
|
|
|
# asset - what the message applies to. An assignment, a discussion, etc.
|
|
|
|
# to_list - a list of who to send the message to. the list can contain Users, User ids, or CommunicationChannels
|
|
|
|
# options - a hash of extra options to merge with the options used to build the Message
|
|
|
|
#
|
|
|
|
# Returns a list of the messages dispatched immediately
|
|
|
|
def create_message
|
|
|
|
@user_counts = {}
|
|
|
|
|
|
|
|
to_user_channels = Hash.new([])
|
|
|
|
@to_users.each do |user|
|
|
|
|
to_user_channels[user] += [user.email_channel]
|
|
|
|
end
|
|
|
|
@to_channels.each do |channel|
|
|
|
|
to_user_channels[channel.user] += [channel]
|
|
|
|
end
|
|
|
|
to_user_channels.each_value{ |channels| channels.uniq! }
|
|
|
|
|
|
|
|
dashboard_messages = []
|
|
|
|
immediate_messages = []
|
|
|
|
delayed_messages = []
|
|
|
|
|
|
|
|
# Looping on users and channels might be a bad thing. If you had a User and their CommunicationChannel in
|
|
|
|
# the to_list (which currently never happens, I think), duplicate messages could be sent.
|
|
|
|
to_user_channels.each do |user, channels|
|
|
|
|
next unless asset_filtered_by_user(user)
|
2018-03-26 22:37:26 +08:00
|
|
|
user_locale = infer_locale(
|
|
|
|
:user => user,
|
|
|
|
:context => user_asset_context(asset_filtered_by_user(user)),
|
|
|
|
:ignore_browser_locale => true
|
|
|
|
)
|
2013-03-13 17:46:53 +08:00
|
|
|
I18n.with_locale(user_locale) do
|
|
|
|
channels.each do |default_channel|
|
|
|
|
if @notification.registration?
|
|
|
|
registration_channels = if default_channel then
|
|
|
|
[default_channel]
|
|
|
|
else
|
|
|
|
immediate_channels_for(user)
|
2014-05-22 04:10:10 +08:00
|
|
|
end
|
2013-03-13 17:46:53 +08:00
|
|
|
immediate_messages += build_immediate_messages_for(user, registration_channels)
|
|
|
|
else
|
|
|
|
if @notification.summarizable?
|
|
|
|
delayed_messages += build_summaries_for(user, default_channel)
|
|
|
|
end
|
2015-01-23 11:45:26 +08:00
|
|
|
end
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2015-01-23 11:45:26 +08:00
|
|
|
unless @notification.registration?
|
2018-03-08 00:33:41 +08:00
|
|
|
if @notification.summarizable? && no_daily_messages_in(delayed_messages) && too_many_messages_for?(user)
|
2016-08-04 06:03:33 +08:00
|
|
|
fallback = build_fallback_for(user)
|
|
|
|
delayed_messages << fallback if fallback
|
2015-01-23 11:45:26 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
unless user.pre_registered?
|
|
|
|
immediate_messages += build_immediate_messages_for(user)
|
|
|
|
dashboard_messages << build_dashboard_message_for(user) if @notification.dashboard? && @notification.show_in_feed?
|
2013-03-13 17:46:53 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
delayed_messages.each{ |message| message.save! }
|
|
|
|
dispatch_dashboard_messages(dashboard_messages)
|
|
|
|
dispatch_immediate_messages(immediate_messages)
|
|
|
|
|
|
|
|
@user_counts.each{|user_id, cnt| recent_messages_for_user(user_id, cnt) }
|
|
|
|
|
|
|
|
return immediate_messages + dashboard_messages
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def no_daily_messages_in(delayed_messages)
|
2015-04-24 05:31:38 +08:00
|
|
|
!delayed_messages.any?{ |message| message.frequency == 'daily' }
|
2013-03-13 17:46:53 +08:00
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def build_fallback_for(user)
|
2016-08-04 06:03:33 +08:00
|
|
|
fallback_channel = immediate_channels_for(user).find{ |cc| cc.path_type == 'email'}
|
|
|
|
return unless fallback_channel
|
2014-11-07 00:51:40 +08:00
|
|
|
fallback_policy = nil
|
|
|
|
NotificationPolicy.unique_constraint_retry do
|
|
|
|
fallback_policy = fallback_channel.notification_policies.by('daily').where(:notification_id => nil).first
|
|
|
|
fallback_policy ||= fallback_channel.notification_policies.create!(frequency: 'daily')
|
|
|
|
end
|
2013-03-13 17:46:53 +08:00
|
|
|
|
|
|
|
build_summary_for(user, fallback_policy)
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def build_summaries_for(user, channel=user.email_channel)
|
|
|
|
delayed_policies_for(user, channel).map{ |policy| build_summary_for(user, policy) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_summary_for(user, policy)
|
2016-06-09 22:15:47 +08:00
|
|
|
user.shard.activate do
|
|
|
|
message = user.messages.build(message_options_for(user))
|
|
|
|
message.parse!('summary')
|
|
|
|
delayed_message = policy.delayed_messages.build(:notification => @notification,
|
|
|
|
:frequency => policy.frequency,
|
|
|
|
:communication_channel_id => policy.communication_channel_id,
|
|
|
|
:root_account_id => message.context_root_account.try(:id),
|
|
|
|
:name_of_topic => message.subject,
|
|
|
|
:link => message.url,
|
|
|
|
:summary => message.body)
|
|
|
|
delayed_message.context = @asset
|
|
|
|
delayed_message.save! if Rails.env.test?
|
|
|
|
delayed_message
|
|
|
|
end
|
2013-03-13 17:46:53 +08:00
|
|
|
end
|
|
|
|
|
2015-01-23 11:45:26 +08:00
|
|
|
def build_immediate_messages_for(user, channels=immediate_channels_for(user).reject(&:unconfirmed?))
|
2013-03-13 17:46:53 +08:00
|
|
|
return [] unless asset_filtered_by_user(user)
|
|
|
|
messages = []
|
|
|
|
message_options = message_options_for(user)
|
2018-03-08 00:33:41 +08:00
|
|
|
channels.reject!{ |channel| ['email', 'sms'].include?(channel.path_type) } if @notification.summarizable? && too_many_messages_for?(user)
|
2014-08-29 09:45:03 +08:00
|
|
|
channels.reject!(&:bouncing?)
|
2013-03-13 17:46:53 +08:00
|
|
|
channels.each do |channel|
|
|
|
|
messages << user.messages.build(message_options.merge(:communication_channel => channel,
|
|
|
|
:to => channel.path))
|
|
|
|
increment_user_counts(user) if ['email', 'sms'].include?(channel.path_type)
|
|
|
|
end
|
|
|
|
messages.each(&:parse!)
|
|
|
|
messages
|
|
|
|
end
|
|
|
|
|
|
|
|
def dispatch_immediate_messages(messages)
|
|
|
|
Message.transaction do
|
|
|
|
# Cancel any that haven't been sent out for the same purpose
|
|
|
|
cancel_pending_duplicate_messages
|
|
|
|
messages.each do |message|
|
|
|
|
message.stage_without_dispatch!
|
|
|
|
message.save!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
MessageDispatcher.batch_dispatch(messages)
|
|
|
|
|
|
|
|
messages
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_dashboard_message_for(user)
|
|
|
|
message = user.messages.build(message_options_for(user).merge(:to => 'dashboard'))
|
|
|
|
message.parse!
|
|
|
|
message
|
|
|
|
end
|
|
|
|
|
|
|
|
def dispatch_dashboard_messages(messages)
|
|
|
|
messages.each do |message|
|
|
|
|
message.infer_defaults
|
|
|
|
message.create_stream_items
|
|
|
|
end
|
|
|
|
messages
|
|
|
|
end
|
|
|
|
|
2014-10-08 06:40:01 +08:00
|
|
|
def unretired_policies_for(user)
|
2019-03-16 12:14:17 +08:00
|
|
|
user.communication_channels.select { |cc| !cc.retired? }.map(&:notification_policies).flatten
|
2014-10-08 06:40:01 +08:00
|
|
|
end
|
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def delayed_policies_for(user, channel=user.email_channel)
|
|
|
|
# This condition is weird. Why would not throttling stop sending notifications?
|
|
|
|
# Why could an inactive email channel stop us here? We handle that later! And could still send
|
|
|
|
# notifications without it!
|
2018-03-08 00:33:41 +08:00
|
|
|
return [] if channel && !channel.active? && !too_many_messages_for?(user)
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
# If any channel has a policy, even policy-less channels don't get the notification based on the
|
|
|
|
# notification default frequency. Is that right?
|
2019-03-16 12:14:17 +08:00
|
|
|
policies = unretired_policies_for(user).select { |np| np.notification_id == @notification.id }
|
|
|
|
if !policies.empty?
|
|
|
|
policies = policies.select { |np| ['daily', 'weekly'].include?(np.frequency) && np.communication_channel.path_type == 'email' }
|
2013-03-13 17:46:53 +08:00
|
|
|
elsif channel &&
|
2016-08-04 06:03:33 +08:00
|
|
|
channel.active? &&
|
2019-03-06 23:22:27 +08:00
|
|
|
channel.path_type == 'email'
|
|
|
|
frequency = @notification.default_frequency(user)
|
|
|
|
if ['daily', 'weekly'].include?(frequency)
|
|
|
|
policies << channel.notification_policies.create!(:notification => @notification, :frequency => frequency)
|
|
|
|
end
|
2013-03-13 17:46:53 +08:00
|
|
|
end
|
|
|
|
policies
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def users_from_to_list(to_list)
|
|
|
|
to_list = [to_list] unless to_list.is_a? Enumerable
|
|
|
|
|
|
|
|
to_users = []
|
|
|
|
to_users += User.find(to_list.select{ |to| to.is_a? Numeric }.uniq)
|
|
|
|
to_users += to_list.select{ |to| to.is_a? User }
|
|
|
|
to_users.uniq!
|
|
|
|
|
|
|
|
to_users
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def communication_channels_from_to_list(to_list)
|
2014-05-22 04:10:10 +08:00
|
|
|
to_list = [to_list] unless to_list.is_a? Enumerable
|
2013-03-13 17:46:53 +08:00
|
|
|
to_list.select{ |to| to.is_a? CommunicationChannel }.uniq
|
|
|
|
end
|
|
|
|
|
|
|
|
def asset_filtered_by_user(user)
|
2019-03-16 12:14:17 +08:00
|
|
|
if asset.respond_to?(:filter_asset_by_recipient)
|
2013-03-13 17:46:53 +08:00
|
|
|
asset.filter_asset_by_recipient(@notification, user)
|
|
|
|
else
|
|
|
|
asset
|
|
|
|
end
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def message_options_for(user)
|
|
|
|
user_asset = asset_filtered_by_user(user)
|
2014-02-27 22:26:14 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
message_options = {
|
|
|
|
:subject => @notification.subject,
|
|
|
|
:notification => @notification,
|
|
|
|
:notification_name => @notification.name,
|
|
|
|
:user => user,
|
|
|
|
:context => user_asset,
|
|
|
|
}
|
|
|
|
# can't just merge these because nil values need to be overwritten in a later merge
|
|
|
|
message_options[:delay_for] = @notification.delay_for if @notification.delay_for
|
|
|
|
message_options[:data] = @message_data if @message_data
|
|
|
|
message_options
|
|
|
|
end
|
|
|
|
|
|
|
|
def increment_user_counts(user_id, count=1)
|
|
|
|
@user_counts[user_id] ||= 0
|
|
|
|
@user_counts[user_id] += count
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def user_asset_context(user_asset)
|
|
|
|
if user_asset.is_a?(Context)
|
|
|
|
user_asset
|
|
|
|
elsif user_asset.respond_to?(:context)
|
|
|
|
user_asset.context
|
|
|
|
end
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
# Finds channels for a user that should get this notification immediately
|
|
|
|
#
|
2016-04-27 04:37:18 +08:00
|
|
|
# If the user doesn't have a policy for this notification on a non-push
|
|
|
|
# channel and the default frequency is immediate, the user should get the
|
|
|
|
# notification by email.
|
2013-03-13 17:46:53 +08:00
|
|
|
# Unregistered users don't get notifications. (registration notifications
|
|
|
|
# are a special case handled elsewhere)
|
|
|
|
def immediate_channels_for(user)
|
|
|
|
return [] unless user.registered?
|
|
|
|
|
2019-03-16 12:14:17 +08:00
|
|
|
active_channel_scope = user.communication_channels.select { |cc| cc.active? && cc.notification_policies.find { |np| np.notification_id == @notification.id } }
|
2019-04-23 00:12:29 +08:00
|
|
|
immediate_channel_scope = active_channel_scope.select { |cc| cc.notification_policies.find { |np| np.notification_id == @notification.id && np.frequency == 'immediately' } }
|
2016-04-27 04:37:18 +08:00
|
|
|
|
2019-03-16 12:14:17 +08:00
|
|
|
user_has_a_policy = active_channel_scope.find { |cc| cc.path_type != 'push' }
|
2019-03-06 23:22:27 +08:00
|
|
|
if !user_has_a_policy && @notification.default_frequency(user) == 'immediately'
|
2019-03-16 12:14:17 +08:00
|
|
|
return [user.email_channel, *immediate_channel_scope.select { |cc| cc.path_type == 'push' }].compact
|
2016-04-27 04:37:18 +08:00
|
|
|
end
|
|
|
|
immediate_channel_scope
|
2013-03-13 17:46:53 +08:00
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def cancel_pending_duplicate_messages
|
|
|
|
# doesn't include dashboard messages. should it?
|
2013-07-30 03:36:17 +08:00
|
|
|
Message.where(:notification_id => @notification).
|
|
|
|
for(@asset).
|
2013-03-13 17:46:53 +08:00
|
|
|
by_name(@notification.name).
|
|
|
|
for_user(@to_users + @to_channels).
|
|
|
|
cancellable.
|
2019-01-21 22:56:41 +08:00
|
|
|
where("created_at BETWEEN ? AND ?", Setting.get("pending_duplicate_message_window_hours", "6").to_i.hours.ago, Time.now.utc).
|
2013-03-13 17:46:53 +08:00
|
|
|
update_all(:workflow_state => 'cancelled')
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
def too_many_messages_for?(user)
|
|
|
|
all_messages = recent_messages_for_user(user.id) || 0
|
|
|
|
@user_counts[user.id] = all_messages
|
|
|
|
all_messages >= user.max_messages_per_day
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
|
2013-03-13 17:46:53 +08:00
|
|
|
# Cache the count for number of messages sent to a user/user-with-category,
|
|
|
|
# it can also be manually re-set to reflect new rows added... this cache
|
|
|
|
# data can get out of sync if messages are cancelled for being repeats...
|
|
|
|
# not sure if we care about that...
|
|
|
|
def recent_messages_for_user(id, messages=nil)
|
|
|
|
if !id
|
|
|
|
nil
|
|
|
|
elsif messages
|
|
|
|
Rails.cache.write(['recent_messages_for', id].cache_key, messages, :expires_in => 1.hour)
|
|
|
|
else
|
|
|
|
user_id = id
|
|
|
|
messages = Rails.cache.fetch(['recent_messages_for', id].cache_key, :expires_in => 1.hour) do
|
2019-01-22 21:57:56 +08:00
|
|
|
Shackles.activate(:slave) do
|
|
|
|
Message.where("dispatch_at>? AND created_at>? AND user_id=? AND to_email=?", 24.hours.ago, 24.hours.ago, user_id, true).count
|
|
|
|
end
|
2013-03-13 17:46:53 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2014-05-22 04:10:10 +08:00
|
|
|
end
|