Skip to content

Commit ae4a5af

Browse files
committed
수정된 메시지에는 반응하지 않는다
슬랙은 이미 올라온 메시지를 고쳐도 이벤트를 보낸다. app_mention 경로에는 이걸 걸러내는 곳이 없어서, 멘션 명령이 담긴 글을 고치면 같은 명령이 한 번 더 실행됐다 (리마인더 DM 재발송 등). 답글도 중복으로 달린다. 수정 사실이 이벤트마다 다른 자리에 실려오기 때문에 (message 는 subtype 'message_changed' + 본문이 event['message'] 아래로, app_mention 은 최상위 'edited') 판별을 util.slack_event.is_edited_message 로 모으고, 두 이벤트 핸들러 진입점에서 바로 끊는다. 핸들러 안이 아니라 진입점에서 막아야 loading 이모지도 붙었다 떨어지지 않는다. message 이벤트는 SubinLikeResponse 의 subtype 검사가 이미 막고 있었지만, 같은 판별을 한곳에 두고 진입점에서 함께 끊도록 했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pko9c11HWMkm9zEPfCAPGc
1 parent e7c598d commit ae4a5af

3 files changed

Lines changed: 102 additions & 0 deletions

File tree

src/anna.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
verify_calendar_token,
3434
)
3535
from util.daily_scheduler import DailyScheduler
36+
from util.slack_event import is_edited_message
3637

3738
init_logger()
3839

@@ -63,6 +64,12 @@ def handle_reaction_removed_event(ack, event, say, client):
6364
@app.event("app_mention")
6465
def handle_app_mention_event(ack, event, say, client):
6566
ack()
67+
# 이미 있던 멘션을 고쳐도 이벤트가 다시 온다 — 명령이 두 번 실행되지 않도록 여기서 끊는다
68+
if is_edited_message(event):
69+
logging.getLogger(__name__).info(
70+
"Ignoring app_mention from an edited message (ts=%s)", event.get("ts")
71+
)
72+
return
6673
mention_response(say=say, event=event, client=client)
6774

6875

@@ -75,6 +82,12 @@ def handle_channel_created_event(ack, event, say, client):
7582
@app.event("message")
7683
def handle_message_event(ack, event, say, client):
7784
ack()
85+
# 수정은 새 글이 아니다 (자동 답글 대상 채널은 수정이 잦아 DEBUG 로만 남긴다)
86+
if is_edited_message(event):
87+
logging.getLogger(__name__).debug(
88+
"Ignoring edited message event (ts=%s)", event.get("ts")
89+
)
90+
return
7891
subin_like_response(event=event, say=say, client=client)
7992

8093

src/util/slack_event.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""슬랙 이벤트 판별 헬퍼."""
2+
3+
4+
def is_edited_message(event) -> bool:
5+
"""이미 올라온 메시지를 고쳐서 온 이벤트인지.
6+
7+
슬랙은 메시지 '수정'에도 이벤트를 보내는데, 새 글과 똑같이 처리하면
8+
같은 멘션 명령이 한 번 더 실행되고(리마인더 재발송 등) 답글도 중복으로 달린다.
9+
수정 사실은 이벤트 종류마다 다른 자리에 실려온다:
10+
11+
- message: subtype 이 'message_changed' 이고, 본문은 event['message'] 아래로 내려간다
12+
- app_mention: 최상위에 'edited' 가 붙는다 (이미 있던 멘션을 고치면 다시 발생)
13+
"""
14+
if not isinstance(event, dict):
15+
return False
16+
if event.get("subtype") == "message_changed":
17+
return True
18+
if event.get("edited"):
19+
return True
20+
if (event.get("message") or {}).get("edited"):
21+
return True
22+
return False

test/util/test_slack_event.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import unittest
2+
3+
from util.slack_event import is_edited_message
4+
5+
# 새로 올라온 멘션 (실제 payload 형태)
6+
NEW_APP_MENTION = {
7+
"type": "app_mention",
8+
"text": "<@UANNA> 빅챗 리마인더 테스트 <@U0001>",
9+
"user": "UQJ8HQJG5",
10+
"ts": "1689403771.805849",
11+
"channel": "C03SZTDEDK3",
12+
"event_ts": "1689403771.805849",
13+
}
14+
15+
# 그 멘션을 고쳤을 때 다시 오는 이벤트 — 최상위에 edited 가 붙는다
16+
EDITED_APP_MENTION = {
17+
**NEW_APP_MENTION,
18+
"edited": {"user": "UQJ8HQJG5", "ts": "1689403800.000000"},
19+
}
20+
21+
# 메시지 수정 — 본문이 message 아래로 내려가고 subtype 이 붙는다
22+
MESSAGE_CHANGED = {
23+
"type": "message",
24+
"subtype": "message_changed",
25+
"channel": "C03SZTDEDK3",
26+
"ts": "1689403800.000200",
27+
"message": {
28+
"type": "message",
29+
"text": "고친 글",
30+
"user": "UQJ8HQJG5",
31+
"ts": "1689403771.805849",
32+
"edited": {"user": "UQJ8HQJG5", "ts": "1689403800.000000"},
33+
},
34+
"previous_message": {"text": "원래 글", "ts": "1689403771.805849"},
35+
}
36+
37+
NEW_MESSAGE = {
38+
"type": "message",
39+
"channel": "C03SZTDEDK3",
40+
"text": "새로 쓴 글",
41+
"user": "UQJ8HQJG5",
42+
"ts": "1689403771.805849",
43+
}
44+
45+
# 이미지 첨부 글은 subtype 이 있어도 수정이 아니다 (자동 답글 대상)
46+
FILE_SHARE_MESSAGE = {
47+
**NEW_MESSAGE,
48+
"subtype": "file_share",
49+
"files": [{"mimetype": "image/png", "url_private": "https://..."}],
50+
}
51+
52+
53+
class TestIsEditedMessage(unittest.TestCase):
54+
def test_edited_events(self):
55+
assert is_edited_message(EDITED_APP_MENTION)
56+
assert is_edited_message(MESSAGE_CHANGED)
57+
58+
def test_new_events(self):
59+
assert not is_edited_message(NEW_APP_MENTION)
60+
assert not is_edited_message(NEW_MESSAGE)
61+
assert not is_edited_message(FILE_SHARE_MESSAGE)
62+
63+
def test_ignores_falsy_and_malformed_payloads(self):
64+
assert not is_edited_message({})
65+
assert not is_edited_message(None)
66+
assert not is_edited_message({"edited": None})
67+
assert not is_edited_message({"message": None})

0 commit comments

Comments
 (0)