summaryrefslogtreecommitdiff
path: root/app/models/notification_recipient.rb
blob: 481c1d963c67a095adfb540e063c2de59c220893 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# frozen_string_literal: true

class NotificationRecipient
  include Gitlab::Utils::StrongMemoize

  attr_reader :user, :type, :reason
  def initialize(user, type, **opts)
    unless NotificationSetting.levels.key?(type) || type == :subscription
      raise ArgumentError, "invalid type: #{type.inspect}"
    end

    @custom_action = opts[:custom_action]
    @acting_user = opts[:acting_user]
    @target = opts[:target]
    @project = opts[:project] || default_project
    @group = opts[:group] || @project&.group
    @user = user
    @type = type
    @reason = opts[:reason]
    @skip_read_ability = opts[:skip_read_ability]
  end

  def notification_setting
    @notification_setting ||= find_notification_setting
  end

  def notification_level
    @notification_level ||= notification_setting&.level&.to_sym
  end

  def notifiable?
    return false unless has_access?
    return false if own_activity?

    # even users with :disabled notifications receive manual subscriptions
    return !unsubscribed? if @type == :subscription

    return false unless suitable_notification_level?

    # check this last because it's expensive
    # nobody should receive notifications if they've specifically unsubscribed
    # except if they were mentioned.
    return false if @type != :mention && unsubscribed?

    true
  end

  def suitable_notification_level?
    case notification_level
    when :disabled, nil
      false
    when :custom
      custom_enabled? || %i[participating mention].include?(@type)
    when :watch, :participating
      !action_excluded?
    when :mention
      @type == :mention
    else
      false
    end
  end

  def custom_enabled?
    @custom_action && notification_setting&.event_enabled?(@custom_action)
  end

  def unsubscribed?
    return false unless @target
    return false unless @target.respond_to?(:subscriptions)

    subscription = @target.subscriptions.find { |subscription| subscription.user_id == @user.id }
    subscription && !subscription.subscribed
  end

  def own_activity?
    return false unless @acting_user

    if user == @acting_user
      # if activity was generated by the same user, change reason to :own_activity
      @reason = NotificationReason::OWN_ACTIVITY
      # If the user wants to be notified, we must return `false`
      !@acting_user.notified_of_own_activity?
    else
      false
    end
  end

  def has_access?
    DeclarativePolicy.subject_scope do
      break false unless user.can?(:receive_notifications)
      break true if @skip_read_ability

      break false if @target && !user.can?(:read_cross_project)
      break false if @project && !user.can?(:read_project, @project)

      break true unless read_ability
      break true unless DeclarativePolicy.has_policy?(@target)

      user.can?(read_ability, @target)
    end
  end

  def action_excluded?
    excluded_watcher_action? || excluded_participating_action?
  end

  def excluded_watcher_action?
    return false unless @custom_action && notification_level == :watch

    NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
  end

  def excluded_participating_action?
    return false unless @custom_action && notification_level == :participating

    NotificationSetting::EXCLUDED_PARTICIPATING_EVENTS.include?(@custom_action)
  end

  private

  def read_ability
    return nil if @skip_read_ability
    return @read_ability if instance_variable_defined?(:@read_ability)

    @read_ability =
      case @target
      when Issuable
        :"read_#{@target.to_ability_name}"
      when Ci::Pipeline
        :read_build # We have build trace in pipeline emails
      when ActiveRecord::Base
        :"read_#{@target.class.model_name.name.underscore}"
      else
        nil
      end
  end

  def default_project
    return nil if @target.nil?
    return @target if @target.is_a?(Project)
    return @target.project if @target.respond_to?(:project)
  end

  def find_notification_setting
    project_setting = @project && user.notification_settings_for(@project)

    return project_setting unless project_setting.nil? || project_setting.global?

    group_setting = closest_non_global_group_notification_settting

    return group_setting unless group_setting.nil?

    user.global_notification_setting
  end

  # Returns the notification_setting of the lowest group in hierarchy with non global level
  def closest_non_global_group_notification_settting
    return unless @group
    return if indexed_group_notification_settings.empty?

    notification_setting = nil

    @group.self_and_ancestors_ids.each do |id|
      notification_setting = indexed_group_notification_settings[id]
      break if notification_setting
    end

    notification_setting
  end

  def indexed_group_notification_settings
    strong_memoize(:indexed_group_notification_settings) do
      @group.notification_settings.where(user_id: user.id)
        .where.not(level: NotificationSetting.levels[:global])
        .index_by(&:source_id)
    end
  end
end