summaryrefslogtreecommitdiff
path: root/app/services/notification_recipient_service.rb
blob: 44ae23fad18109807352daa045278710a875f5db (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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
#
# Used by NotificationService to determine who should receive notification
#
class NotificationRecipientService
  attr_reader :project
  
  def initialize(project)
    @project = project
  end

  def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
    custom_action = build_custom_key(action, target)

    recipients = target.participants(current_user)

    unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
      recipients = add_project_watchers(recipients)
    end

    recipients = add_custom_notifications(recipients, custom_action)
    recipients = reject_mention_users(recipients)

    # Re-assign is considered as a mention of the new assignee so we add the
    # new assignee to the list of recipients after we rejected users with
    # the "on mention" notification level
    if [:reassign_merge_request, :reassign_issue].include?(custom_action)
      recipients << previous_assignee if previous_assignee
      recipients << target.assignee
    end

    recipients = reject_muted_users(recipients)
    recipients = add_subscribed_users(recipients, target)

    if [:new_issue, :new_merge_request].include?(custom_action)
      recipients = add_labels_subscribers(recipients, target)
    end

    recipients = reject_unsubscribed_users(recipients, target)
    recipients = reject_users_without_access(recipients, target)

    recipients.delete(current_user) if skip_current_user

    recipients.uniq
  end

  def build_relabeled_recipients(target, current_user, labels:)
    recipients = add_labels_subscribers([], target, labels: labels)
    recipients = reject_unsubscribed_users(recipients, target)
    recipients = reject_users_without_access(recipients, target)
    recipients.delete(current_user)
    recipients.uniq
  end

  def build_new_note_recipients(note)
    target = note.noteable

    ability, subject = if note.for_personal_snippet?
                         [:read_personal_snippet, note.noteable]
                       else
                         [:read_project, note.project]
                       end

    mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }

    # Add all users participating in the thread (author, assignee, comment authors)
    recipients =
      if target.respond_to?(:participants)
        target.participants(note.author)
      else
        mentioned_users
      end

    unless note.for_personal_snippet?
      # Merge project watchers
      recipients = add_project_watchers(recipients)

      # Merge project with custom notification
      recipients = add_custom_notifications(recipients, :new_note)
    end

    # Reject users with Mention notification level, except those mentioned in _this_ note.
    recipients = reject_mention_users(recipients - mentioned_users)
    recipients = recipients + mentioned_users

    recipients = reject_muted_users(recipients)

    recipients = add_subscribed_users(recipients, note.noteable)
    recipients = reject_unsubscribed_users(recipients, note.noteable)
    recipients = reject_users_without_access(recipients, note.noteable)

    recipients.delete(note.author)
    recipients.uniq
  end

  # Remove users with disabled notifications from array
  # Also remove duplications and nil recipients
  def reject_muted_users(users)
    reject_users(users, :disabled)
  end

  protected

  # Get project/group users with CUSTOM notification level
  def add_custom_notifications(recipients, action)
    user_ids = []

    # Users with a notification setting on group or project
    user_ids += user_ids_notifiable_on(project, :custom, action)
    user_ids += user_ids_notifiable_on(project.group, :custom, action)

    # Users with global level custom
    user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
    user_ids_with_group_level_global   = user_ids_notifiable_on(project.group, :global)

    global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
    user_ids += user_ids_with_global_level_custom(global_users_ids, action)

    recipients.concat(User.find(user_ids))
  end

  def add_project_watchers(recipients)
    recipients.concat(project_watchers).compact
  end

  # Get project users with WATCH notification level
  def project_watchers
    project_members_ids = user_ids_notifiable_on(project)

    user_ids_with_project_global = user_ids_notifiable_on(project, :global)
    user_ids_with_group_global   = user_ids_notifiable_on(project.group, :global)

    user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)

    user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
    user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)

    User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
  end

  # Remove users with notification level 'Mentioned'
  def reject_mention_users(users)
    reject_users(users, :mention)
  end

  def add_subscribed_users(recipients, target)
    return recipients unless target.respond_to? :subscribers

    recipients + target.subscribers(project)
  end

  def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
    return [] unless resource

    if notification_level
      settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
      settings = settings.select { |setting| setting.events[action] } if action.present?
      settings.map(&:user_id)
    else
      resource.notification_settings.pluck(:user_id)
    end
  end

  # Build a list of user_ids based on project notification settings
  def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
    user_ids = user_ids_notifiable_on(project, :watch)

    # If project setting is global, add to watch list if global setting is watch
    global_setting.each do |user_id|
      if user_ids_global_level_watch.include?(user_id)
        user_ids << user_id
      end
    end

    user_ids
  end

  # Build a list of user_ids based on group notification settings
  def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
    uids = user_ids_notifiable_on(group, :watch)

    # Group setting is watch, add to user_ids list if user is not project member
    user_ids = []
    uids.each do |user_id|
      if project_members.exclude?(user_id)
        user_ids << user_id
      end
    end

    # Group setting is global, add to user_ids list if global setting is watch
    global_setting.each do |user_id|
      if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
        user_ids << user_id
      end
    end

    user_ids
  end

  def user_ids_with_global_level_watch(ids)
    settings_with_global_level_of(:watch, ids).pluck(:user_id)
  end

  def user_ids_with_global_level_custom(ids, action)
    settings = settings_with_global_level_of(:custom, ids)
    settings = settings.select { |setting| setting.events[action] }
    settings.map(&:user_id)
  end

  def settings_with_global_level_of(level, ids)
    NotificationSetting.where(
      user_id: ids,
      source_type: nil,
      level: NotificationSetting.levels[level]
    )
  end

  # Reject users which has certain notification level
  #
  # Example:
  #   reject_users(users, :watch, project)
  #
  def reject_users(users, level)
    level = level.to_s

    unless NotificationSetting.levels.keys.include?(level)
      raise 'Invalid notification level'
    end

    users = users.to_a.compact.uniq
    users = users.select { |u| u.can?(:receive_notifications) }

    users.reject do |user|
      global_notification_setting = user.global_notification_setting

      next global_notification_setting.level == level unless project

      setting = user.notification_settings_for(project)

      if project.group && (setting.nil? || setting.global?)
        setting = user.notification_settings_for(project.group)
      end

      # reject users who globally set mention notification and has no setting per project/group
      next global_notification_setting.level == level unless setting

      # reject users who set mention notification in project
      next true if setting.level == level

      # reject users who have mention level in project and disabled in global settings
      setting.global? && global_notification_setting.level == level
    end
  end

  def reject_unsubscribed_users(recipients, target)
    return recipients unless target.respond_to? :subscriptions

    recipients.reject do |user|
      subscription = target.subscriptions.find_by_user_id(user.id)
      subscription && !subscription.subscribed
    end
  end

  def reject_users_without_access(recipients, target)
    ability = case target
              when Issuable
                :"read_#{target.to_ability_name}"
              when Ci::Pipeline
                :read_build # We have build trace in pipeline emails
              end

    return recipients unless ability

    recipients.select do |user|
      user.can?(ability, target)
    end
  end

  def add_labels_subscribers(recipients, target, labels: nil)
    return recipients unless target.respond_to? :labels

    (labels || target.labels).each do |label|
      recipients += label.subscribers(project)
    end

    recipients
  end

  # Build event key to search on custom notification level
  # Check NotificationSetting::EMAIL_EVENTS
  def build_custom_key(action, object)
    "#{action}_#{object.class.model_name.name.underscore}".to_sym
  end
end