diff --git a/src/handler/bigchat/create_bigchat_sheet.py b/src/handler/bigchat/create_bigchat_sheet.py index a117b2b..8cbff85 100644 --- a/src/handler/bigchat/create_bigchat_sheet.py +++ b/src/handler/bigchat/create_bigchat_sheet.py @@ -1,3 +1,7 @@ +from implementation.slack_client import Reaction +from implementation.member_finder import MemberManager, MemberNotFound, MemberLackInfo + + class CreateBigchatSheet: def __init__(self, event, slack_client, gs_client): self.text = event["text"] @@ -9,6 +13,7 @@ def run(self): if "새로운 빅챗" not in self.text: return False + # TODO: REGEX로 더 깔끔하게 따올 수 있지 않을까? sheet_name = self.text.split("새로운 빅챗", maxsplit=1)[1].split("\n")[0].strip() if not sheet_name: self.slack_client.send_message(msg="시트 이름이 입력되지 않았어. 다시 입력해줘!", ts=self.ts) @@ -16,8 +21,33 @@ def run(self): worksheet_id = self.gs_client.create_bigchat_sheet(sheet_name) sheet_url = self.gs_client.get_url(worksheet_id) + self.slack_client.send_message( msg=f"새로운 빅챗, 등록 완료! <{sheet_url}|{sheet_name}> :google_spreadsheets:", ts=self.ts, ) + + # 빅챗 시트가 생성되기 이전에 등록을 시도한(GOGO 이모지를 누른) + # 인원들이 누락된 것에 대한 사후처리 + channel = self.slack_client.get_channel() + assert channel is not None + reaction = self.slack_client.get_emoji( + channel=channel, + timestamp=self.ts, + ) + if reaction is not None: + reaction: Reaction + for user in reaction.users: + error_message = None + try: + member = MemberManager.get_instance().find(user) + except MemberNotFound: + error_message = f"<@{user}>, 네 정보를 찾지 못했어. 운영진에게 연락해줘!" + except MemberLackInfo: + error_message = f"<@{user}>, 네 정보에 누락된 값이 있어. 운영진에게 연락해줘!" + else: + self.gs_client.append_row(worksheet_id, member.transform_for_spreadsheet()) + finally: + if error_message: + self.slack_client.send_message(msg=error_message, ts=self.ts) return True diff --git a/src/handler/bigchat/join_bigchat.py b/src/handler/bigchat/join_bigchat.py index 732cfef..2d3989b 100644 --- a/src/handler/bigchat/join_bigchat.py +++ b/src/handler/bigchat/join_bigchat.py @@ -1,9 +1,10 @@ from typing import List import re +import textwrap from implementation.member_finder import MemberNotFound, MemberLackInfo from implementation.slack_client import Message -from util.utils import strip_multiline + SPREADSHEET_PAT = re.compile( r"https://docs.google.com/spreadsheets/d/.*/edit#gid=(\d*)" @@ -60,7 +61,7 @@ def run(self): self.slack_client.send_message(msg=f"<@{self.user}>, 등록 완료!", ts=self.ts) self.slack_client.send_message_only_visible_to_user( - msg=strip_multiline( + msg=textwrap.dedent( f""" <@{self.user}> 네 신청 정보를 아래와 같이 등록했어. 바뀐 부분이 있다면 운영진에게 DM으로 알려줘! ``` @@ -69,7 +70,8 @@ def run(self): 이메일: {member.email} 학교/회사: {member.school_name_or_company_name} ``` - (참고로 이 메시지는 너만 볼 수 있어!)""" + (참고로 이 메시지는 너만 볼 수 있어!) + """ ), channel=self.channel, ts=self.ts, diff --git a/src/handler/controller.py b/src/handler/controller.py index 5e73dc7..058bd77 100644 --- a/src/handler/controller.py +++ b/src/handler/controller.py @@ -9,15 +9,6 @@ from implementation.member_finder import MemberManager from implementation.slack_client import SlackClient -MEMBER_MANAGER = None - - -def _get_member_manager(): # TODO(seonghyeok): we need better singleton - global MEMBER_MANAGER - if not MEMBER_MANAGER: - MEMBER_MANAGER = MemberManager(GoogleSpreadsheetClient()) - return MEMBER_MANAGER - # reaction_added event sample: # { @@ -40,7 +31,7 @@ def join_bigchat(event, say, client): envs.JOIN_BIGCHAT_EMOJI, SlackClient(say, client), GoogleSpreadsheetClient(), - _get_member_manager(), + MemberManager.get_instance(), ).run() @@ -52,7 +43,7 @@ def abandon_bigchat(event, say, client): envs.ANNA_ID, envs.JOIN_BIGCHAT_EMOJI, SlackClient(say, client), - _get_member_manager(), + MemberManager.get_instance(), GoogleSpreadsheetClient(), ).run() diff --git a/src/implementation/member_finder.py b/src/implementation/member_finder.py index d55ae18..319b3ea 100644 --- a/src/implementation/member_finder.py +++ b/src/implementation/member_finder.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import Dict, List @@ -35,6 +37,12 @@ class MemberLackInfo(Exception): class MemberManager: + @classmethod + def get_instance(cls) -> MemberManager: + if not hasattr(cls, "_instance"): + cls._instance = cls(GoogleSpreadsheetClient()) + return cls._instance + def __init__(self, gs_client: GoogleSpreadsheetClient): self.gs_client = gs_client self.members_worksheet_id = int(envs.MEMBERS_INFO_WORKSHEET_ID) diff --git a/src/implementation/slack_client.py b/src/implementation/slack_client.py index bece05e..26b2653 100644 --- a/src/implementation/slack_client.py +++ b/src/implementation/slack_client.py @@ -19,6 +19,12 @@ class Emoji(BaseModel): name: str +class Reaction(BaseModel): + name: str + users: List[str] + count: int + + class SlackClient: def __init__(self, say: Say, web_client: WebClient): self.say = say @@ -50,6 +56,10 @@ def _messages_to_members(messages, channel): for msg in messages ] + def get_channel(self) -> Optional[str]: + """현재 메시지가 발송된 채널을 반환한다.""" + return self.say.channel + def get_replies( self, channel: str, thread_ts: str = None, ts: str = None ) -> List[Message]: @@ -88,6 +98,27 @@ def add_emoji(self, channel, ts, emoji_name): return raise ex + def get_emoji(self, channel: str, ts: str, emoji_name: str) -> Optional[Reaction]: + """channel에 있는 ts 시간에 발송된 메시지에 사용자들이 남긴 반응 목록을 가져온다. + + 해당 반응이 존재하지 않는다면 None을 반환한다.""" + response = self.web_client.reactions_get( + channel=channel, + full=True, + timestamp=ts, + ) + assert response["ok"] + assert response["type"] == "message" + for reaction in response["message"]["reactions"]: + if reaction["name"] == emoji_name: + return Reaction( + name=reaction["name"], + users=reaction["users"], + count=reaction["count"], + ) + else: + return None + def remove_emoji(self, channel, ts, emoji_name): try: self.web_client.reactions_remove( diff --git a/test/handler/bigchat/test_create_bigchat_sheet.py b/test/handler/bigchat/test_create_bigchat_sheet.py index 67a7a96..c91bd85 100644 --- a/test/handler/bigchat/test_create_bigchat_sheet.py +++ b/test/handler/bigchat/test_create_bigchat_sheet.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock from handler.bigchat.create_bigchat_sheet import CreateBigchatSheet +from implementation.slack_client import Reaction +from implementation.member_finder import Member, MemberManager from test.handler.bigchat.sample_data import create_sample_app_mention_event @@ -44,3 +46,33 @@ def test_not_run_by_sheet_name_notfound(self): mock_slack_client.send_message.assert_called_once() assert result is False assert "시트 이름이 입력되지 않았어. 다시 입력해줘!" in mock_slack_client.send_message.call_args.kwargs["msg"] + + def test_gogo_pressed_while_building_spreadsheet(self): + """빅챗 시트 생성이 완료되기 이전에 등록을 시도한(GOGO 이모지를 누른) + 인원들이 누락되지 않고 빅챗에 등록되었는지 확인합니다. + """ + event = create_sample_app_mention_event("<@U01BN035Y6L> 새로운 빅챗 빅챗 24-08-01") + mock_slack_client = MagicMock() + mock_slack_client.get_emoji.return_value = Reaction( + name="gogo", + users=["U01BN035Y6L"], + count=1, + ) + mock_gs_client = MagicMock() + mock_member_manager = MagicMock() + mock_member_manager.find.return_value = Member( + kor_name="김동주", + eng_name="Kim Dongjoo", + email="email", + phone="phone", + school_name_or_company_name="school_name_or_company_name", + ) + MemberManager.get_instance = MagicMock(return_value=mock_member_manager) + + sut = CreateBigchatSheet(event, mock_slack_client, mock_gs_client) + + assert sut.run() + + mock_member_manager.find.assert_called_once() + mock_slack_client.get_emoji.assert_called_once() + mock_gs_client.append_row.assert_called_once()