summaryrefslogtreecommitdiff
path: root/app/models/notification_recipient.rb
blob: 8e44e3d8e1731c10bbab066eb394458a75579da5 (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
# 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 emails_disabled?
    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 :mention
      @type == :mention
    when :participating
      @custom_action == :failed_pipeline || %i[participating mention].include?(@type)
    when :custom
      custom_enabled? || %i[participating mention].include?(@type)
    when :watch
      !excluded_watcher_action?
    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 excluded_watcher_action?
    return false unless @type == :watch
    return false unless @custom_action

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

  private

  # They are disabled if the project or group has disallowed it.
  # No need to check the group if there is already a project
  def emails_disabled?
    @project ? @project.emails_disabled? : @group&.emails_disabled?
  end

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

    @read_ability =
      if @target.is_a?(Ci::Pipeline)
        :read_build # We have build trace in pipeline emails
      elsif default_ability_for_target
        :"read_#{default_ability_for_target}"
      end
  end

  def default_ability_for_target
    @default_ability_for_target ||=
      if @target.respond_to?(:to_ability_name)
        @target.to_ability_name
      elsif @target.class.respond_to?(:model_name)
        @target.class.model_name.name.underscore
      end
  end

  def default_project
    return 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_setting

    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_setting
    return unless @group

    @group
      .notification_settings(hierarchy_order: :asc)
      .where(user: user)
      .where.not(level: NotificationSetting.levels[:global])
      .first
  end
end