|
| 1 | +import logging |
| 2 | +from datetime import timedelta |
| 3 | +from typing import Any |
| 4 | + |
| 5 | +from django.utils import timezone |
| 6 | + |
| 7 | +from sentry.integrations.tasks.create_comment import create_comment as create_external_comment |
| 8 | +from sentry.integrations.tasks.update_comment import update_comment as update_external_comment |
| 9 | +from sentry.issues.action_log import GroupActionActor, action_context_scope, publish_action |
| 10 | +from sentry.issues.action_log.types import CommentDeleteAction, CommentEditAction, SentryActorRef |
| 11 | +from sentry.models.activity import Activity |
| 12 | +from sentry.models.group import Group |
| 13 | +from sentry.models.grouplink import GroupLink |
| 14 | +from sentry.models.groupsubscription import GroupSubscription |
| 15 | +from sentry.notifications.types import GroupSubscriptionReason |
| 16 | +from sentry.signals import comment_created, comment_deleted, comment_updated |
| 17 | +from sentry.types.activity import ActivityType |
| 18 | +from sentry.users.models.user import User |
| 19 | +from sentry.users.services.user.model import RpcUser |
| 20 | + |
| 21 | +logger = logging.getLogger(__name__) |
| 22 | + |
| 23 | + |
| 24 | +def _webhook_data(note: Activity) -> dict[str, Any]: |
| 25 | + return { |
| 26 | + "comment_id": note.id, |
| 27 | + "timestamp": note.datetime, |
| 28 | + "comment": note.data.get("text"), |
| 29 | + "project_slug": note.project.slug, |
| 30 | + } |
| 31 | + |
| 32 | + |
| 33 | +def _sync_comment(note: Activity, user_id: int, *, created: bool) -> None: |
| 34 | + assert note.group is not None |
| 35 | + task = create_external_comment if created else update_external_comment |
| 36 | + for external_issue_id in GroupLink.objects.filter( |
| 37 | + project_id=note.group.project_id, |
| 38 | + group_id=note.group_id, |
| 39 | + linked_type=GroupLink.LinkedType.issue, |
| 40 | + ).values_list("linked_id", flat=True): |
| 41 | + task.apply_async( |
| 42 | + kwargs={ |
| 43 | + "external_issue_id": external_issue_id, |
| 44 | + "group_note_id": note.id, |
| 45 | + "user_id": user_id, |
| 46 | + } |
| 47 | + ) |
| 48 | + |
| 49 | + |
| 50 | +def create_comment( |
| 51 | + *, |
| 52 | + group: Group, |
| 53 | + user: User | RpcUser, |
| 54 | + data: dict[str, Any], |
| 55 | + source: str, |
| 56 | + from_email: bool = False, |
| 57 | +) -> Activity | None: |
| 58 | + """ |
| 59 | + Create a comment, preserving the existing inbound-email policy. |
| 60 | +
|
| 61 | + Email deduplicates within the project without a time limit and skips subscriptions, |
| 62 | + integration sync, and the comment_created signal. Activity notifications still run. |
| 63 | + """ |
| 64 | + duplicates = Activity.objects.filter( |
| 65 | + group=group, type=ActivityType.NOTE.value, user_id=user.id, data=data |
| 66 | + ) |
| 67 | + if from_email: |
| 68 | + duplicates = duplicates.filter(project_id=group.project_id) |
| 69 | + else: |
| 70 | + duplicates = duplicates.filter(datetime__gte=timezone.now() - timedelta(hours=1)) |
| 71 | + if duplicates.exists(): |
| 72 | + return None |
| 73 | + |
| 74 | + if not from_email: |
| 75 | + GroupSubscription.objects.subscribe( |
| 76 | + group=group, subscriber=user, reason=GroupSubscriptionReason.comment |
| 77 | + ) |
| 78 | + |
| 79 | + with action_context_scope(source=source, actor=GroupActionActor.user(user.id)): |
| 80 | + note = Activity.objects.create_group_activity( |
| 81 | + group=group, type=ActivityType.NOTE, user_id=user.id, data=data |
| 82 | + ) |
| 83 | + if not from_email: |
| 84 | + _sync_comment(note, user.id, created=True) |
| 85 | + |
| 86 | + if not from_email: |
| 87 | + comment_created.send_robust( |
| 88 | + project=group.project, user=user, group=group, data=_webhook_data(note), sender="post" |
| 89 | + ) |
| 90 | + return note |
| 91 | + |
| 92 | + |
| 93 | +def update_comment( |
| 94 | + note: Activity, |
| 95 | + *, |
| 96 | + user: User | RpcUser, |
| 97 | + data: dict[str, Any], |
| 98 | + mentions: list[SentryActorRef], |
| 99 | + source: str, |
| 100 | + original_action_id: int | None, |
| 101 | +) -> Activity: |
| 102 | + assert note.group is not None |
| 103 | + note.data.update(data) |
| 104 | + note.save() |
| 105 | + if original_action_id is not None: |
| 106 | + publish_action( |
| 107 | + CommentEditAction( |
| 108 | + comment_id=original_action_id, text=data.get("text"), mentions=mentions |
| 109 | + ), |
| 110 | + source=source, |
| 111 | + group_id=note.group_id, |
| 112 | + project=note.group.project, |
| 113 | + actor=GroupActionActor.user(user.id), |
| 114 | + ) |
| 115 | + else: |
| 116 | + logger.info("group_notes.groupactionlogentry.not_found", extra={"group_id": note.group_id}) |
| 117 | + |
| 118 | + if note.data.get("external_id"): |
| 119 | + _sync_comment(note, user.id, created=False) |
| 120 | + comment_updated.send_robust( |
| 121 | + project=note.group.project, |
| 122 | + user=user, |
| 123 | + group=note.group, |
| 124 | + data=_webhook_data(note), |
| 125 | + sender="put", |
| 126 | + ) |
| 127 | + return note |
| 128 | + |
| 129 | + |
| 130 | +def delete_comment( |
| 131 | + note: Activity, *, user: User | RpcUser, source: str, original_action_id: int | None |
| 132 | +) -> None: |
| 133 | + group = note.group |
| 134 | + assert group is not None |
| 135 | + notes_by_user = Activity.objects.filter( |
| 136 | + group=group, type=ActivityType.NOTE.value, user_id=user.id |
| 137 | + ) |
| 138 | + last_comment = notes_by_user.count() == 1 |
| 139 | + webhook_data = _webhook_data(note) |
| 140 | + note.delete() |
| 141 | + if original_action_id is not None: |
| 142 | + publish_action( |
| 143 | + CommentDeleteAction(comment_id=original_action_id), |
| 144 | + source=source, |
| 145 | + group_id=group.id, |
| 146 | + project=group.project, |
| 147 | + actor=GroupActionActor.user(user.id), |
| 148 | + ) |
| 149 | + else: |
| 150 | + logger.info("group_notes.groupactionlogentry.not_found", extra={"group_id": group.id}) |
| 151 | + comment_deleted.send_robust( |
| 152 | + project=group.project, user=user, group=group, data=webhook_data, sender="delete" |
| 153 | + ) |
| 154 | + if last_comment: |
| 155 | + GroupSubscription.objects.filter( |
| 156 | + user_id=user.id, |
| 157 | + group=group, |
| 158 | + project=group.project, |
| 159 | + reason=GroupSubscriptionReason.comment, |
| 160 | + ).delete() |
0 commit comments