코딩하는 문과생

[Python] slack 봇 알리미 본문

프로그래밍/Python

[Python] slack 봇 알리미

코딩하는 문과생 2021. 7. 21. 21:41

[서론]

스프링 부트 개발을 중단하기로 결정하고 나서, 파이썬으로 작성한 데이터 분석배치가 붕 떠버렸다. 배치는 이미 개발이 완료된 상태라 그냥 두기엔 아깝기도 해서, 알림봇을 하나 만들어 사용해보려 한다. 매일 새벽에 배치작업이 마무리되고 나서 슬랙 봇으로 알림이 오는 아키텍처를 구상했고, 파이썬 slackclient 패키지를 이용해 손쉽게 구현할 수 있었다.

 

[구현]

구현은 1. 슬랙 워크스페이스와 채널을 생성 후, 2. 알림봇 앱을 만들어 생성된 앱을 채널에 추가한다. 이후 3. 파이썬 배치가 동작하면서, slack api로부터 발급받은 토큰을 이용해 배치 결과를 4. 슬랙으로 전송해 준다.

 

- Slack 에서 해야할 일

1. 워크스페이스 생성

우선 알림을 받을 워크 스페이스를 먼저 생성한다. 워크스페이스 생성 시 채널이름을 입력하는 부분도 있으니 나중에 만들기 귀찮으면 워크스페이스 생성 시 만들면 된다. (워크스페이스명: Timing, 채널명: notification)

워크스페이스를 생성한다.

 

워크스페이스 생성 완료

 

2. Bot App생성

slack api에 접속하여 사용할 bot app을 생성한다. slack api 홈페이지에 들어가면 메인화면에 'create an app' 버튼이 있는데 클릭해준다.

create an app클릭

 

이후 앱 이름과 앱이 설치될 워크스페이스를 지정한다. (앱명: myTimingBot, 설치워크스페이스: Timing)

 

App이름과 알림을 보낼 워크스페이스 선택

 

이후 App 형태를 지정한다. 단순히 알림봇을 구현할 예정이므로 Bots를 선택한다,

Bots 클릭

 

선택 후 나오는 화면 왼쪽 탬에서 oAuth & Permission을 선택한다. 해당 탭에서는 봇의 권한을 부여하는데, 단순한 알림을 구현할 것이므로 'chat:write'를 선택하고 'Install to Workspace'를 클릭한다.

Oauth & Permission 선택 후

 

Scopes에 'chat:write' 를 설정한다.

 

Install to Workspace를 클릭하여 봇 앱을 워크스페이스에 생성한다.

 

'Install to Workspace'를 클릭하면 봇앱이 생성되며, 토큰을 파이썬 배치에서 사용해야 하므로 복사해서 저장해둔다.

토큰 복사

 

3. 알림 받을 채널에 생성된 봇 앱 추가

워크스페이스 내 myTimingBot을 선택하여 알림을 받을 채널(notification)에 생성된 봇앱을 추가한다.

생성된 채널에 myTimingBot App 추가
notification 채널에 myTimingBot 앱이 추가되었다. 

 

- 파이썬 코드 작성

1. slackclient 설치

pip 패키지 관리자를 이용하여 slackclient를 설치한다.

$ pip install slackclient

 

2. 코드작성

2021년 2월까지만 해도 slack = Slack()이라는  객체를 선언하여 봇에 메세지를 전달할 수 있었지만, 이후 슬랙 정책이 바뀌면서 requests 또는 슬랙에서 제공하는 webClient를 사용해야지만 봇에 메세지를 전달할 수 있다.

from slack import WebClient
from slack.errors import SlackApiError
import json

# ...생략

# slack봇에 출력할 메세지를 입력
# 단순 메세지가 아닌 일정한 형식을 부여하기 위해 리스트와 딕셔너리 구조를 사용하였다.
# 슬랙에서는 해당 내용을 json 형식으로 받아 처리한다.
message =  [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": "추천 종목 - " + notify_sell_summary['analysis_date'].values[0]
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "text": "[매수 추천 종목]",
                        "type": "mrkdwn"
                    },
                    "fields": [
                        {
                            "type": "mrkdwn",
                            "text": "*종목명*"
                        },
                        {
                            "type": "mrkdwn",
                            "text": "*매수의견*"
                        },
                    ]
                },
                {
                    "type": "section",
                    "text": {
                        "text": "[매도 추천 종목]",
                        "type": "mrkdwn"
                    },
                    "fields": [
                        {
                            "type": "mrkdwn",
                            "text": "*종목명*"
                        },
                        {
                            "type": "mrkdwn",
                            "text": "*매도의견*"
                        },
                    ]
                }
                # ...
            ]


# token정보가 timing_db_info에 저장되어 있다.
client = WebClient(token=timing_db_info.slack_token) 


try:
    # channel은 봇앱이 초대된 채널명을 작성하면 된다. 
    # 본 예시에서는 #notification 이라는 채널명이 timing_db_info에 저장되어 있다.
    response = client.chat_postMessage(
        channel=timing_db_info.channel_name,
        blocks=json.dumps(message))
    print(response)
    assert response["ok"] == True
except SlackApiError as e:
    # You will get a SlackApiError if "ok" is False
    assert e.response["ok"] is False
    assert e.response["error"]  # str like 'invalid_auth', 'channel_not_found'
    print(f"Got an error: {e.response['error']}")
    
# ...생략

 

3. 배치 구동 후 조회되는 메세지

배치를 구동시켜 슬랙에 메세지를 전송한다.

배치 구동 후 추천 종목이 슬랙봇을 통해 전달된다.

 

+ 4. 메세지 형식 변경

주식종목이 10개가 넘어가는 순간 알림봇에서 메세지를 전송할 수 없다는 에러가 발생했다. 확인해보니 message에 fields형식을 사용하는 방식은 MAX 10개까지만 데이터 추가가 가능하다. 즉, 위 사진을 예시로 들면 2개의 열에 5개의 행까지만 구성이 가능하다.

 

일정 조건을 만족하는 종목은 전부 알림으로 보내기를 원했기 때문에 문자열을 적절히 가공하여 보내는 방식으로 변경하였다. 또한 주가를 바로 확인하기 위해 네이버 증권으로 바로 이동이 가능한 link를 추가하였다.

# ...생략
    message = [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": "추천 종목 - " + str(nowDate)
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "text": "*매수 추천 종목*",
                        "type": "mrkdwn"
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "text": "",
                        "type": "mrkdwn"
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "text": "*매도 추천 종목*",
                        "type": "mrkdwn"
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "text": "",
                        "type": "mrkdwn"
                    }
                }
            ]

        temp_buy_sum = []
        if len(notify_buy_summary) == 0:
            nothing = "없음"
            temp_buy_sum.append(f"{nothing:<10}{nothing:<10}\n")
        else:
            for row in notify_buy_summary.itertuples():
                buy_stock = f"[{row[2]}] {row[4]}({str(row[3])})"
                link_buy_stock = f"<https://finance.naver.com/item/main.nhn?code={str(row[3])}|{buy_stock}>"
                buy_opinion = str(row[5])
                temp_buy_sum.append(f"{link_buy_stock} *[{buy_opinion}]*\n")
        message[2]["text"]["text"] += ''.join(temp_buy_sum)

        temp_sell_sum = []
        if len(notify_sell_summary) == 0:
            nothing = "없음"
            temp_sell_sum.append(f"{nothing:<10}{nothing:<10}\n")
        else:
            for row in notify_sell_summary.itertuples():
                sell_stock = f"[{row[2]}] {row[4]}({str(row[3])})"
                link_sell_stock = f"<https://finance.naver.com/item/main.nhn?code={str(row[3])}|{sell_stock}>"
                sell_opinion = str(row[5])
                temp_sell_sum.append(f"{link_sell_stock} *[{sell_opinion}]*\n")
        message[4]["text"]["text"] += ''.join(temp_sell_sum)
 # ...생략

 

변경된 형식, 링크 추가