Build a SlackBot | Daily Indicator

eeeclipse·2020년 6월 10일
1

이 문서는 매일 아침 데일리 인디케이터를 자동으로 메세징하는 슬랙봇을 제작하는 과정을 담은 문서입니다.

개요

바야흐로 자동화의 시대. 매일 아침, 인디케이터는 30분 - 1시간 정도의 시간이 소요됩니다. 이 시간을 조금 더 줄여서, 비즈니스 인사이트를 도출하는 시간을 확보해봅시다. 이제, 인간이 직접 하던 작업을 컴퓨터에게 시켜봅시다.

개발환경

  • jupyter notebook python 3.x
  • 패키지
    • PyMySQL
    • slacker / slackclient
    • pandas

작업

01. python으로 mysql 접속하고 자료를 받아오기

일자별, OS 별 회원가입 인원수를 알아보자

## PyMySQL 패키지 설치
## https://pymysql.readthedocs.io/en/latest/user/installation.html

pip install PyMySQL
import pymysql
import pandas as pd 

# 접속
db = pymysql.connect(host='localhost', port=3306, user='user', passwd='password', charset='utf8')
curs = db.cursor()

try:
    # SELECT
    with db.cursor() as curs:
        sql = "select date, os_cd, count FROM tbl where date group by date"
        curs.execute(sql)
        rs = curs.fetchall()
        df01 = pd.DataFrame(data=rs)
        print(df01)
finally:
    db.close()

dataFrame으로 다음 이시간에 이쁘게 표로 만들어주도록 합시다.

02. 슬랙봇 만들기

SLACK API에 접속하면 봇을 만들 수 있습니다.

아직은 테스트중이니 테스트라는 접미사를 붙여주고 토큰을 받아오도록 합시다.
토큰은 소중하므로 저만 소중하게 알고 있으려구요.

03. 가상환경 구축하기

이미 컴퓨터는 너무나도 많은 프로그램이 산재하여 있습니다(컴퓨터 : 죽여줘). 의존성관리나 패키지 관리를 하기 매우 어려우니 가상환경을 만들어보도록 봅시다.

우리는 python 3.x를 이용할 것으므로 virtualenv 를 사용합시다.

virtualenv test

test라는 폴더를 만들어서, 이 안에 가상환경을 구축했습니다.. activate 해주도록 하겠습니다.

그러면 앞에 (test)가 붙은 새로운 커맨드창이 열립니다. pip로 slackclient를 설치할 예정이지만 안부도 물을 겸 pip가 잘 지내는지 확인해봅시다.

잘 지내는 것 같습니다.

그러면 통신을 할 수 있도록 해줍시다.

slack bot과 통신하기 위해 slack bot의 ID를 얻어와야 합니다. 아래와 같은 방식으로 얻어올 수 있습니다. 각자의 서버에 맞도록 설정해야 하는 부분이 있습니다.. token은 위에서 얻은 slack bot의 token을 사용하면 됩니다.

(test) $ curl https://NAME.slack.com/api/auth.test?token=xoxb-어쩌구 저쩌구

결과는 json 형태로 출력됩니다.

04 코드 작성하기

slacker 를 이용해서, 위의 쿼리로 받아온 내용을 날려보내는 코드를 작성해보도록 합시다.

콘솔에서 slacker를 pip로 설치해줍시다.

다른 멤버들도 사용이 편리하도록 콘솔에 도움말을 출력해봅시다.


################################################################################
import os
import sys
import signal
import getopt
import logging
import logging.handlers
import traceback
import json
from termcolor import colored
from slacker import Slacker


################################################################################
logger = None


################################################################################
def get_logger(projname, root_folder='/opt/FutureServer',
               logsize=500*1024, logbackup_count=4):
    logdir = root_folder  # '%s/%s' % (root_folder, projname)
    if not os.path.exists(logdir):
        # noinspection PyBroadException
        try:
            os.makedirs(logdir)
        except:
            logdir = '/tmp/%s' % projname
            if not os.path.exists(logdir):
                os.makedirs(logdir)
    logfile='%s/%s.log' % (logdir, projname)
    loglevel = logging.INFO
    _logger = logging.getLogger(projname)
    _logger.setLevel(loglevel)
    if _logger.handlers is not None and len(_logger.handlers) >= 0:
        for handler in _logger.handlers:
            _logger.removeHandler(handler)
        _logger.handlers = []
    loghandler = logging.handlers.RotatingFileHandler(
        logfile, maxBytes=logsize, backupCount=logbackup_count)
    formatter = logging.Formatter(
        '%(asctime)s-%(name)s-%(levelname)s-%(message)s')
    loghandler.setFormatter(formatter)
    _logger.addHandler(loghandler)
    return _logger


################################################################################
def receive_signal(signum, _):
    """시그널 핸들러
    """
    if signum in [signal.SIGTERM, signal.SIGHUP, signal.SIGINT]:
        logger.info('Caught signal %s, exiting.' % (str(signum)))
        sys.exit()
    else:
        logger.info('Caught signal %s, ignoring.' % (str(signum)))


################################################################################
def sendmsg(**kwargs):
    token = kwargs['json']['token']
    channel = None
    for chandic in kwargs['json']['channels']:
        if chandic['name'] == kwargs['channel']:
            channel = chandic
            break
    if not channel:
        raise ReferenceError('Cannot find channel named <%s> from json config'
                             % kwargs['channel'])
    if not kwargs['icon_emoji']:
        kwargs['icon_emoji'] = channel['icon_emoji']
    slack = Slacker(token, incoming_webhook_url=channel['incoming_webhook_url'])
    # max length of text at slack may be 4,000
    txtlen = 3990 - len(kwargs['title'])
    if len(kwargs['text']) > txtlen:
        text = '*%s*' % kwargs['title']
        slack.chat.post_message(channel['name'], text,
                                username=kwargs['user_name'],
                                icon_emoji=kwargs['icon_emoji'],
                                )
        aggtxt = ""
        for line in kwargs['text'].split('\n'):
            if len(aggtxt) + len(line) > txtlen:
                slack.chat.post_message(channel['name'], '```%s```' % aggtxt,
                                        username=kwargs['user_name'],
                                        icon_emoji=kwargs['icon_emoji'],
                                        )
                aggtxt = ''
            aggtxt += line
        if len(aggtxt) > 0:
            slack.chat.post_message(channel['name'], '```%s```' % aggtxt,
                                    username=kwargs['user_name'],
                                    icon_emoji=kwargs['icon_emoji'],
                                    )
    else:
        text = '*%s*\n```%s```' \
               % (kwargs['title'], kwargs['text'])
        slack.chat.post_message(channel['name'], text,
                                username=kwargs['user_name'],
                                icon_emoji=kwargs['icon_emoji'],
                                )
    # 파일 업로드는 잘 안되었음
    # if kwargs['text_file'] and os.path.exists(kwargs['text_file']):
    #     # slack.files.upload(kwargs['text_file'],
    #     #                    channels=slack.channels.get_channel_id(channel['name']))
    #     r = slack.files.upload(kwargs['text_file'])
    #     pass


################################################################################
def usage(msg=None):
    """사용방법 출력
    """
    if msg:
        sys.stderr.write(colored('Error: %s!!!\n' % msg, 'red'))
    prog = sys.argv[0]
    sys.stderr.write(colored('''
Usage: {prog} [options]
  send a message to slack channel
options:
    -h, --help : print this help message
    -c, --config : set json config file
     (default is sendslack.json in which same folder of this program is located)
    -l, --channel : specify channel (do not skip)
    -t, --title : message title
    -x, --text : message body parameter
    -f, --text_file : message body
    -u, --user_name : user name
    -j, --icon_emoji : set icon imogi (eg. :cow: )
'''.format(prog=prog), 'green'))
    sys.exit(1)


################################################################################
if __name__ == '__main__':
    # signal handler
    # signal.signal(signal.SIGCHLD, signal.SIG_IGN)
    signal.signal(signal.SIGTERM, receive_signal)
    # signal.signal(signal.SIGHUP, receive_signal)
    signal.signal(signal.SIGINT, receive_signal)
    # parsing commmand lines
    kwargs = {
        'json': None,
        'config': None,
        'channel': None,
        'title': None,
        'text': None,
        'text_file': None,
        'user_name': None,
        'icon_emoji': None,
    }
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hc:l:t:x:f:u:j:",
                                   ["help", "config=",
                                    "channel=", "title=",
                                    "text=", 'text_file=', 'user_name=',
                                    "icon_emoji="])
        for o, a in opts:
            if o in ("-h", "--help"):
                usage()
            elif o in ("-c", "--config"):
                kwargs['config'] = a
            elif o in ("-l", "--channel"):
                kwargs['channel'] = a
            elif o in ("-t", "--title"):
                kwargs['title'] = a
            elif o in ("-x", "--text"):
                kwargs['text'] = a
            elif o in ("-f", "--text_file"):
                kwargs['text_file'] = a
            elif o in ("-u", "--user_name"):
                kwargs['user_name'] = a
            elif o in ("-j", "--icon_emoji"):
                kwargs['icon_emoji'] = a

        logger = get_logger('sendslack', root_folder='/tmp')
        if not kwargs['config']:
            sc_dir = os.path.dirname(os.path.realpath(__file__))
            kwargs['config'] = '%s/sendslack.json' % sc_dir
        if not os.path.exists(kwargs['config']):
            raise IOError('Cannot read config file <%s>' % kwargs['config'])
        with open(kwargs['config']) as ifp:
            kwargs['json'] = json.load(ifp)
        if not kwargs['channel']:
            raise ValueError('-l, --channel channel needed!')
        if not kwargs['title']:
            raise ValueError('-t, --title title needed!')
        if not (kwargs['text'] or kwargs['text_file']):
            raise ValueError('-x, --text or -f, --text_file needed!')
        if kwargs['text_file']:
            with open(kwargs['text_file']) as ifp:
                kwargs['text'] = ifp.read()

        sendmsg(**kwargs)
    except Exception as e:
        exc_info = sys.exc_info()
        out = traceback.format_exception(*exc_info)
        del exc_info
        logger.error("%s\n" % ''.join(out))
        logger.error('Error:%s'%str(e))
        usage(str(e))

이제 CLI로 접속이 가능해졌습니다. 날아오는 slack 메세지가 블록으로 이쁘게 되어있으면 기분이 좋으므로, json 형태로 이쁘게 만들어줍시다.

실행해보자

(test) > python sendslack.py -l "random" -t "test" -x "집에보내줘" -u "test_general""
(test) > python sendslack.py -l "general" -t "test" -x "집에보내줘" -u "test"

이렇게 나옵니다. 이모지를 바꿀 수도 있고 ... 아무튼 그렇습니다.

05. 스케줄러 작성하기

파일만 실행하면 메세지가 전해지다니!

그렇지만 현대인에게는 이러한 고난이도 클릭작업은 너무나도 과도한 피로와 스트레스를 유발합니다. 따라서 우리는 매일 아침 9시에 자동으로 메세지를 날려주도록 합시다.

다음 이 시간에....

profile
mathematician, researcher, Data Scientist ⊗ Engineer ⊗ Architect, DBA

0개의 댓글