Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1 | # Copyright 2017 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Helper functions for deciding who to notify and why..""" |
| 6 | from __future__ import print_function |
| 7 | from __future__ import division |
| 8 | from __future__ import absolute_import |
| 9 | |
| 10 | import collections |
| 11 | import logging |
| 12 | |
| 13 | import settings |
| 14 | from features import filterrules_helpers |
| 15 | from features import savedqueries_helpers |
| 16 | from framework import authdata |
| 17 | from framework import framework_bizobj |
| 18 | from framework import framework_constants |
| 19 | from framework import framework_helpers |
| 20 | from framework import framework_views |
| 21 | from framework import permissions |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 22 | from mrproto import tracker_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 23 | from search import query2ast |
| 24 | from search import searchpipeline |
| 25 | from tracker import component_helpers |
| 26 | from tracker import tracker_bizobj |
| 27 | |
| 28 | # When sending change notification emails, choose the reply-to header and |
| 29 | # footer message based on three levels of the recipient's permissions |
| 30 | # for that issue. |
| 31 | REPLY_NOT_ALLOWED = 'REPLY_NOT_ALLOWED' |
| 32 | REPLY_MAY_COMMENT = 'REPLY_MAY_COMMENT' |
| 33 | REPLY_MAY_UPDATE = 'REPLY_MAY_UPDATE' |
| 34 | |
| 35 | # These are strings describing the various reasons that we send notifications. |
| 36 | REASON_REPORTER = 'You reported this issue' |
| 37 | REASON_OWNER = 'You are the owner of the issue' |
| 38 | REASON_OLD_OWNER = 'You were the issue owner before this change' |
| 39 | REASON_DEFAULT_OWNER = 'A rule made you owner of the issue' |
| 40 | REASON_CCD = 'You were specifically CC\'d on the issue' |
| 41 | REASON_DEFAULT_CCD = 'A rule CC\'d you on the issue' |
| 42 | # TODO(crbug.com/monorail/2857): separate reasons for notification to group |
| 43 | # members resulting from component and rules derived ccs. |
| 44 | REASON_GROUP_CCD = ( |
| 45 | 'A group you\'re a member of was specifically CC\'d on the issue') |
| 46 | REASON_STARRER = 'You starred the issue' |
| 47 | REASON_SUBSCRIBER = 'Your saved query matched the issue' |
| 48 | REASON_ALSO_NOTIFY = 'A rule was set up to notify you' |
| 49 | REASON_ALL_NOTIFICATIONS = ( |
| 50 | 'The project was configured to send all issue notifications ' |
| 51 | 'to this address') |
| 52 | REASON_LINKED_ACCOUNT = 'Your linked account would have been notified' |
| 53 | |
| 54 | # An AddrPerm is how we represent our decision to notify a given |
| 55 | # email address, which version of the email body to send to them, and |
| 56 | # whether to offer them the option to reply to the notification. Many |
| 57 | # of the functions in this file pass around AddrPerm lists (an "APL"). |
| 58 | # is_member is a boolean |
| 59 | # address is a string email address |
| 60 | # user is a User PB, including built-in user preference fields. |
| 61 | # reply_perm is one of REPLY_NOT_ALLOWED, REPLY_MAY_COMMENT, |
| 62 | # REPLY_MAY_UPDATE. |
| 63 | # user_prefs is a UserPrefs object with string->string user prefs. |
| 64 | AddrPerm = collections.namedtuple( |
| 65 | 'AddrPerm', 'is_member, address, user, reply_perm, user_prefs') |
| 66 | |
| 67 | |
| 68 | |
| 69 | def ComputeIssueChangeAddressPermList( |
| 70 | cnxn, ids_to_consider, project, issue, services, omit_addrs, |
| 71 | users_by_id, pref_check_function=lambda u: u.notify_issue_change): |
| 72 | """Return a list of user email addresses to notify of an issue change. |
| 73 | |
| 74 | User email addresses are determined by looking up the given user IDs |
| 75 | in the given users_by_id dict. |
| 76 | |
| 77 | Args: |
| 78 | cnxn: connection to SQL database. |
| 79 | ids_to_consider: list of user IDs for users interested in this issue. |
| 80 | project: Project PB for the project containing this issue. |
| 81 | issue: Issue PB for the issue that was updated. |
| 82 | services: Services. |
| 83 | omit_addrs: set of strings for email addresses to not notify because |
| 84 | they already know. |
| 85 | users_by_id: dict {user_id: user_view} user info. |
| 86 | pref_check_function: optional function to use to check if a certain |
| 87 | User PB has a preference set to receive the email being sent. It |
| 88 | defaults to "If I am in the issue's owner or cc field", but it |
| 89 | can be set to check "If I starred the issue." |
| 90 | |
| 91 | Returns: |
| 92 | A list of AddrPerm objects. |
| 93 | """ |
| 94 | memb_addr_perm_list = [] |
| 95 | logging.info('Considering %r ', ids_to_consider) |
| 96 | all_user_prefs = services.user.GetUsersPrefs(cnxn, ids_to_consider) |
| 97 | for user_id in ids_to_consider: |
| 98 | if user_id == framework_constants.NO_USER_SPECIFIED: |
| 99 | continue |
| 100 | user = services.user.GetUser(cnxn, user_id) |
| 101 | # Notify people who have a pref set, or if they have no User PB |
| 102 | # because the pref defaults to True. |
| 103 | if user and not pref_check_function(user): |
| 104 | logging.info('Not notifying %r: user preference', user.email) |
| 105 | continue |
| 106 | # TODO(jrobbins): doing a bulk operation would reduce DB load. |
| 107 | auth = authdata.AuthData.FromUserID(cnxn, user_id, services) |
| 108 | perms = permissions.GetPermissions(user, auth.effective_ids, project) |
| 109 | config = services.config.GetProjectConfig(cnxn, project.project_id) |
| 110 | granted_perms = tracker_bizobj.GetGrantedPerms( |
| 111 | issue, auth.effective_ids, config) |
| 112 | |
| 113 | if not permissions.CanViewIssue( |
| 114 | auth.effective_ids, perms, project, issue, |
| 115 | granted_perms=granted_perms): |
| 116 | logging.info('Not notifying %r: user cannot view issue', user.email) |
| 117 | continue |
| 118 | |
| 119 | addr = users_by_id[user_id].email |
| 120 | if addr in omit_addrs: |
| 121 | logging.info('Not notifying %r: user already knows', user.email) |
| 122 | continue |
| 123 | |
| 124 | recipient_is_member = bool(framework_bizobj.UserIsInProject( |
| 125 | project, auth.effective_ids)) |
| 126 | |
| 127 | reply_perm = REPLY_NOT_ALLOWED |
| 128 | if project.process_inbound_email: |
| 129 | if permissions.CanEditIssue(auth.effective_ids, perms, project, issue): |
| 130 | reply_perm = REPLY_MAY_UPDATE |
| 131 | elif permissions.CanCommentIssue( |
| 132 | auth.effective_ids, perms, project, issue): |
| 133 | reply_perm = REPLY_MAY_COMMENT |
| 134 | |
| 135 | memb_addr_perm_list.append( |
| 136 | AddrPerm(recipient_is_member, addr, user, reply_perm, |
| 137 | all_user_prefs[user_id])) |
| 138 | |
| 139 | logging.info('For %s %s, will notify: %r', |
| 140 | project.project_name, issue.local_id, |
| 141 | [ap.address for ap in memb_addr_perm_list]) |
| 142 | |
| 143 | return memb_addr_perm_list |
| 144 | |
| 145 | |
| 146 | def ComputeProjectNotificationAddrList( |
| 147 | cnxn, services, project, contributor_could_view, omit_addrs): |
| 148 | """Return a list of non-user addresses to notify of an issue change. |
| 149 | |
| 150 | The non-user addresses are specified by email address strings, not |
| 151 | user IDs. One such address can be specified in the project PB. |
| 152 | It is not assumed to have permission to see all issues. |
| 153 | |
| 154 | Args: |
| 155 | cnxn: connection to SQL database. |
| 156 | services: A Services object. |
| 157 | project: Project PB containing the issue that was updated. |
| 158 | contributor_could_view: True if any project contributor should be able to |
| 159 | see the notification email, e.g., in a mailing list archive or feed. |
| 160 | omit_addrs: set of strings for email addresses to not notify because |
| 161 | they already know. |
| 162 | |
| 163 | Returns: |
| 164 | A list of tuples: [(False, email_addr, None, reply_permission_level), ...], |
| 165 | where reply_permission_level is always REPLY_NOT_ALLOWED for now. |
| 166 | """ |
| 167 | memb_addr_perm_list = [] |
| 168 | if contributor_could_view: |
| 169 | ml_addr = project.issue_notify_address |
| 170 | ml_user_prefs = services.user.GetUserPrefsByEmail(cnxn, ml_addr) |
| 171 | |
| 172 | if ml_addr and ml_addr not in omit_addrs: |
| 173 | memb_addr_perm_list.append( |
| 174 | AddrPerm(False, ml_addr, None, REPLY_NOT_ALLOWED, ml_user_prefs)) |
| 175 | |
| 176 | return memb_addr_perm_list |
| 177 | |
| 178 | |
| 179 | def ComputeIssueNotificationAddrList(cnxn, services, issue, omit_addrs): |
| 180 | """Return a list of non-user addresses to notify of an issue change. |
| 181 | |
| 182 | The non-user addresses are specified by email address strings, not |
| 183 | user IDs. They can be set by filter rules with the "Also notify" action. |
| 184 | "Also notify" addresses are assumed to have permission to see any issue, |
| 185 | even a restricted one. |
| 186 | |
| 187 | Args: |
| 188 | cnxn: connection to SQL database. |
| 189 | services: A Services object. |
| 190 | issue: Issue PB for the issue that was updated. |
| 191 | omit_addrs: set of strings for email addresses to not notify because |
| 192 | they already know. |
| 193 | |
| 194 | Returns: |
| 195 | A list of tuples: [(False, email_addr, None, reply_permission_level), ...], |
| 196 | where reply_permission_level is always REPLY_NOT_ALLOWED for now. |
| 197 | """ |
| 198 | addr_perm_list = [] |
| 199 | for addr in issue.derived_notify_addrs: |
| 200 | if addr not in omit_addrs: |
| 201 | notify_user_prefs = services.user.GetUserPrefsByEmail(cnxn, addr) |
| 202 | addr_perm_list.append( |
| 203 | AddrPerm(False, addr, None, REPLY_NOT_ALLOWED, notify_user_prefs)) |
| 204 | |
| 205 | return addr_perm_list |
| 206 | |
| 207 | |
| 208 | def _GetSubscribersAddrPermList( |
| 209 | cnxn, services, issue, project, config, omit_addrs, users_by_id): |
| 210 | """Lookup subscribers, evaluate their saved queries, and decide to notify.""" |
| 211 | users_to_queries = GetNonOmittedSubscriptions( |
| 212 | cnxn, services, [project.project_id], omit_addrs) |
| 213 | # TODO(jrobbins): need to pass through the user_id to use for "me". |
| 214 | subscribers_to_notify = EvaluateSubscriptions( |
| 215 | cnxn, issue, users_to_queries, services, config) |
| 216 | # TODO(jrobbins): expand any subscribers that are user groups. |
| 217 | subs_needing_user_views = [ |
| 218 | uid for uid in subscribers_to_notify if uid not in users_by_id] |
| 219 | users_by_id.update(framework_views.MakeAllUserViews( |
| 220 | cnxn, services.user, subs_needing_user_views)) |
| 221 | sub_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 222 | cnxn, subscribers_to_notify, project, issue, services, omit_addrs, |
| 223 | users_by_id, pref_check_function=lambda *args: True) |
| 224 | |
| 225 | return sub_addr_perm_list |
| 226 | |
| 227 | |
| 228 | def EvaluateSubscriptions( |
| 229 | cnxn, issue, users_to_queries, services, config): |
| 230 | """Determine subscribers who have subs that match the given issue.""" |
| 231 | # Note: unlike filter rule, subscriptions see explicit & derived values. |
| 232 | lower_labels = [lab.lower() for lab in tracker_bizobj.GetLabels(issue)] |
| 233 | label_set = set(lower_labels) |
| 234 | |
| 235 | subscribers_to_notify = [] |
| 236 | for uid, saved_queries in users_to_queries.items(): |
| 237 | for sq in saved_queries: |
| 238 | if sq.subscription_mode != 'immediate': |
| 239 | continue |
| 240 | if issue.project_id not in sq.executes_in_project_ids: |
| 241 | continue |
| 242 | cond = savedqueries_helpers.SavedQueryToCond(sq) |
| 243 | # TODO(jrobbins): Support linked accounts me_user_ids. |
| 244 | cond, _warnings = searchpipeline.ReplaceKeywordsWithUserIDs([uid], cond) |
| 245 | cond_ast = query2ast.ParseUserQuery( |
| 246 | cond, '', query2ast.BUILTIN_ISSUE_FIELDS, config) |
| 247 | |
| 248 | if filterrules_helpers.EvalPredicate( |
| 249 | cnxn, services, cond_ast, issue, label_set, config, |
| 250 | tracker_bizobj.GetOwnerId(issue), tracker_bizobj.GetCcIds(issue), |
| 251 | tracker_bizobj.GetStatus(issue)): |
| 252 | subscribers_to_notify.append(uid) |
| 253 | break # Don't bother looking at the user's other saved quereies. |
| 254 | |
| 255 | return subscribers_to_notify |
| 256 | |
| 257 | |
| 258 | def GetNonOmittedSubscriptions(cnxn, services, project_ids, omit_addrs): |
| 259 | """Get a dict of users w/ subscriptions in those projects.""" |
| 260 | users_to_queries = services.features.GetSubscriptionsInProjects( |
| 261 | cnxn, project_ids) |
| 262 | user_emails = services.user.LookupUserEmails( |
| 263 | cnxn, list(users_to_queries.keys())) |
| 264 | for user_id, email in user_emails.items(): |
| 265 | if email in omit_addrs: |
| 266 | del users_to_queries[user_id] |
| 267 | return users_to_queries |
| 268 | |
| 269 | |
| 270 | def ComputeCustomFieldAddrPerms( |
| 271 | cnxn, config, issue, project, services, omit_addrs, users_by_id): |
| 272 | """Check the reasons to notify users named in custom fields.""" |
| 273 | group_reason_list = [] |
| 274 | for fd in config.field_defs: |
| 275 | (direct_named_ids, |
| 276 | transitive_named_ids) = services.usergroup.ExpandAnyGroupEmailRecipients( |
| 277 | cnxn, ComputeNamedUserIDsToNotify(issue.field_values, fd)) |
| 278 | named_user_ids = direct_named_ids + transitive_named_ids |
| 279 | if named_user_ids: |
| 280 | named_addr_perms = ComputeIssueChangeAddressPermList( |
| 281 | cnxn, named_user_ids, project, issue, services, omit_addrs, |
| 282 | users_by_id, pref_check_function=lambda u: True) |
| 283 | group_reason_list.append( |
| 284 | (named_addr_perms, 'You are named in the %s field' % fd.field_name)) |
| 285 | |
| 286 | return group_reason_list |
| 287 | |
| 288 | |
| 289 | def ComputeNamedUserIDsToNotify(field_values, fd): |
| 290 | """Give a list of user IDs to notify because they're in a field.""" |
| 291 | if (fd.field_type == tracker_pb2.FieldTypes.USER_TYPE and |
| 292 | fd.notify_on == tracker_pb2.NotifyTriggers.ANY_COMMENT): |
| 293 | return [fv.user_id for fv in field_values |
| 294 | if fv.field_id == fd.field_id] |
| 295 | |
| 296 | return [] |
| 297 | |
| 298 | |
| 299 | def ComputeComponentFieldAddrPerms( |
| 300 | cnxn, config, issue, project, services, omit_addrs, users_by_id): |
| 301 | """Return [(addr_perm_list, reason),...] for users auto-cc'd by components.""" |
| 302 | component_ids = set(issue.component_ids) |
| 303 | group_reason_list = [] |
| 304 | for cd in config.component_defs: |
| 305 | if cd.component_id in component_ids: |
| 306 | (direct_ccs, |
| 307 | transitive_ccs) = services.usergroup.ExpandAnyGroupEmailRecipients( |
| 308 | cnxn, component_helpers.GetCcIDsForComponentAndAncestors(config, cd)) |
| 309 | cc_ids = direct_ccs + transitive_ccs |
| 310 | comp_addr_perms = ComputeIssueChangeAddressPermList( |
| 311 | cnxn, cc_ids, project, issue, services, omit_addrs, |
| 312 | users_by_id, pref_check_function=lambda u: True) |
| 313 | group_reason_list.append( |
| 314 | (comp_addr_perms, |
| 315 | 'You are auto-CC\'d on all issues in component %s' % cd.path)) |
| 316 | |
| 317 | return group_reason_list |
| 318 | |
| 319 | |
| 320 | def ComputeGroupReasonList( |
| 321 | cnxn, services, project, issue, config, users_by_id, omit_addrs, |
| 322 | contributor_could_view, starrer_ids=None, noisy=False, |
| 323 | old_owner_id=None, commenter_in_project=True, include_subscribers=True, |
| 324 | include_notify_all=True, |
| 325 | starrer_pref_check_function=lambda u: u.notify_starred_issue_change): |
| 326 | """Return a list [(addr_perm_list, reason),...] of addrs to notify.""" |
| 327 | # Get the transitive set of owners and Cc'd users, and their UserViews. |
| 328 | starrer_ids = starrer_ids or [] |
| 329 | reporter = [issue.reporter_id] if issue.reporter_id in starrer_ids else [] |
| 330 | if old_owner_id: |
| 331 | old_direct_owners, old_transitive_owners = ( |
| 332 | services.usergroup.ExpandAnyGroupEmailRecipients(cnxn, [old_owner_id])) |
| 333 | else: |
| 334 | old_direct_owners, old_transitive_owners = [], [] |
| 335 | |
| 336 | direct_owners, transitive_owners = ( |
| 337 | services.usergroup.ExpandAnyGroupEmailRecipients(cnxn, [issue.owner_id])) |
| 338 | der_direct_owners, der_transitive_owners = ( |
| 339 | services.usergroup.ExpandAnyGroupEmailRecipients( |
| 340 | cnxn, [issue.derived_owner_id])) |
| 341 | direct_comp, trans_comp = services.usergroup.ExpandAnyGroupEmailRecipients( |
| 342 | cnxn, component_helpers.GetComponentCcIDs(issue, config)) |
| 343 | direct_ccs, transitive_ccs = services.usergroup.ExpandAnyGroupEmailRecipients( |
| 344 | cnxn, list(issue.cc_ids)) |
| 345 | der_direct_ccs, der_transitive_ccs = ( |
| 346 | services.usergroup.ExpandAnyGroupEmailRecipients( |
| 347 | cnxn, list(issue.derived_cc_ids))) |
| 348 | # Remove cc's derived from components, which are grouped into their own |
| 349 | # notify-reason-group in ComputeComponentFieldAddrPerms(). |
| 350 | # This means that an exact email cc'd by both a component and a rule will |
| 351 | # get an email that says they are only being notified because of the |
| 352 | # component. |
| 353 | # Note that a user directly cc'd due to a rule who is also part of a |
| 354 | # group cc'd due to a component, will get a message saying they're cc'd for |
| 355 | # both the rule and the component. |
| 356 | der_direct_ccs = list(set(der_direct_ccs).difference(set(direct_comp))) |
| 357 | der_transitive_ccs = list(set(der_transitive_ccs).difference(set(trans_comp))) |
| 358 | |
| 359 | users_by_id.update(framework_views.MakeAllUserViews( |
| 360 | cnxn, services.user, transitive_owners, der_transitive_owners, |
| 361 | direct_comp, trans_comp, transitive_ccs, der_transitive_ccs)) |
| 362 | |
| 363 | # Notify interested people according to the reason for their interest: |
| 364 | # owners, component auto-cc'd users, cc'd users, starrers, and |
| 365 | # other notification addresses. |
| 366 | reporter_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 367 | cnxn, reporter, project, issue, services, omit_addrs, users_by_id) |
| 368 | owner_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 369 | cnxn, direct_owners + transitive_owners, project, issue, |
| 370 | services, omit_addrs, users_by_id) |
| 371 | old_owner_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 372 | cnxn, old_direct_owners + old_transitive_owners, project, issue, |
| 373 | services, omit_addrs, users_by_id) |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 374 | old_owner_addr_perm_list = [ |
| 375 | ap for ap in old_owner_addr_perm_list if ap not in owner_addr_perm_list |
| 376 | ] |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 377 | der_owner_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 378 | cnxn, der_direct_owners + der_transitive_owners, project, issue, |
| 379 | services, omit_addrs, users_by_id) |
| 380 | cc_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 381 | cnxn, direct_ccs, project, issue, services, omit_addrs, users_by_id) |
| 382 | transitive_cc_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 383 | cnxn, transitive_ccs, project, issue, services, omit_addrs, users_by_id) |
| 384 | der_cc_addr_perm_list = ComputeIssueChangeAddressPermList( |
| 385 | cnxn, der_direct_ccs + der_transitive_ccs, project, issue, |
| 386 | services, omit_addrs, users_by_id) |
| 387 | |
| 388 | starrer_addr_perm_list = [] |
| 389 | sub_addr_perm_list = [] |
| 390 | if not noisy or commenter_in_project: |
| 391 | # Avoid an OOM by only notifying a number of starrers that we can handle. |
| 392 | # And, we really should limit the number of emails that we send anyway. |
| 393 | max_starrers = settings.max_starrers_to_notify |
| 394 | starrer_ids = starrer_ids[-max_starrers:] |
| 395 | # Note: starrers can never be user groups. |
| 396 | starrer_addr_perm_list = ( |
| 397 | ComputeIssueChangeAddressPermList( |
| 398 | cnxn, starrer_ids, project, issue, |
| 399 | services, omit_addrs, users_by_id, |
| 400 | pref_check_function=starrer_pref_check_function)) |
| 401 | |
| 402 | if include_subscribers: |
| 403 | sub_addr_perm_list = _GetSubscribersAddrPermList( |
| 404 | cnxn, services, issue, project, config, omit_addrs, |
| 405 | users_by_id) |
| 406 | |
| 407 | # Get the list of addresses to notify based on filter rules. |
| 408 | issue_notify_addr_list = ComputeIssueNotificationAddrList( |
| 409 | cnxn, services, issue, omit_addrs) |
| 410 | # Get the list of addresses to notify based on project settings. |
| 411 | proj_notify_addr_list = [] |
| 412 | if include_notify_all: |
| 413 | proj_notify_addr_list = ComputeProjectNotificationAddrList( |
| 414 | cnxn, services, project, contributor_could_view, omit_addrs) |
| 415 | |
| 416 | group_reason_list = [ |
| 417 | (reporter_addr_perm_list, REASON_REPORTER), |
| 418 | (owner_addr_perm_list, REASON_OWNER), |
| 419 | (old_owner_addr_perm_list, REASON_OLD_OWNER), |
| 420 | (der_owner_addr_perm_list, REASON_DEFAULT_OWNER), |
| 421 | (cc_addr_perm_list, REASON_CCD), |
| 422 | (transitive_cc_addr_perm_list, REASON_GROUP_CCD), |
| 423 | (der_cc_addr_perm_list, REASON_DEFAULT_CCD), |
| 424 | ] |
| 425 | group_reason_list.extend(ComputeComponentFieldAddrPerms( |
| 426 | cnxn, config, issue, project, services, omit_addrs, |
| 427 | users_by_id)) |
| 428 | group_reason_list.extend(ComputeCustomFieldAddrPerms( |
| 429 | cnxn, config, issue, project, services, omit_addrs, |
| 430 | users_by_id)) |
| 431 | group_reason_list.extend([ |
| 432 | (starrer_addr_perm_list, REASON_STARRER), |
| 433 | (sub_addr_perm_list, REASON_SUBSCRIBER), |
| 434 | (issue_notify_addr_list, REASON_ALSO_NOTIFY), |
| 435 | (proj_notify_addr_list, REASON_ALL_NOTIFICATIONS), |
| 436 | ]) |
| 437 | return group_reason_list |