├── .gitignore ├── README.md ├── app.py ├── config.example.json └── readme-img ├── mirage.png └── title.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | # Edit at https://www.gitignore.io/?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Environments 97 | .env 98 | .venv 99 | env/ 100 | venv/ 101 | ENV/ 102 | env.bak/ 103 | venv.bak/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | .dmypy.json 118 | dmypy.json 119 | 120 | # Pyre type checker 121 | .pyre/ 122 | 123 | ### Python Patch ### 124 | .venv/ 125 | 126 | # config file 127 | config.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 복무봇 (Bokmu-Bot) 2 | 3 |
4 | 5 | ![](./readme-img/title.png) 6 | 7 | ![](./readme-img/mirage.png) 8 | 9 |
10 | 11 | 복무봇은 제가 군대를 가고 나서도 Slack 을 통해 저와 제 지인들이 제 군복무율을 확인할 수 있도록 (입대 5일전에) 만든 프로젝트 입니다. 12 | 13 | 하루에 한번, **정해진 시간마다 복무일자와 복무율**을 알려줍니다. 만약 아직 입대하지 않았다면, **입대까지 남은 날**을 보여줍니다. 14 | 15 | ## Dependencies 16 | 17 | 아래 모듈을 설치 해야합니다. 18 | 19 | - SlackClient 20 | - dateutil 21 | - schedule 22 | 23 | ## Get Started 24 | 25 | ### Create Your Slack App 26 | 27 | Slack 에서 여러분의 **[App을 생성](https://api.slack.com/apps)** 해주세요. 28 | 29 | ### Add Bot User into Slack 30 | 31 | Basic Information > Add features and functionality 에서 `Bots` 를 클릭합니다. 32 | 33 | 그 다음 `Add a Bot User` 를 클릭하고, Display Name 과 Default username 을 채워 넣고, `Add Bot User` 를 클릭해 여러분 Slack 에 봇 유저를 생성합니다. 34 | 35 | ### Fill config.json 36 | 37 | App 과 Bot User를 생성하셨다면, `config.json` 파일을 `app.py` 와 같은 경로에 생성해줍니다. 그리고 `config.example.json` 파일의 내용을 복사하여 붙여 넣어줍니다. `config.example.json` 의 내용은 다음과 같습니다. 38 | 39 | ```json 40 | { 41 | "slack": { 42 | "apiToken": "Slack App API Token (Bot User OAuth Access Token)", 43 | "clientId": "Slack App Client ID", 44 | "clientSecret": "Slack App Client Secret", 45 | "verificationToken": "Slack App Verification Token", 46 | "channelId": "Slack Channel Id" 47 | }, 48 | "bot": { 49 | "yourName": "사용자 이름", 50 | "notifyAt": "슬랙에 매일 메세지를 보낼 시각. 24시간제 시간제로 기입 (오전 7시는 7, 오후 1시는 13)", 51 | "startDate": "yyyy/mm/dd (입대일자)", 52 | "endDate": "yyyy/mm/dd (제대일자)" 53 | } 54 | } 55 | ``` 56 | 57 | ### API 정보 58 | 59 | 그 다음 `config.json` 파일을 아래 설명을 읽고 채워 넣어주세요. 60 | 61 | - **clientId, clientSecret, verificationToken** 62 | 63 | Basic Information > App Credentials 64 | 65 | 에서 찾으실 수 있습니다. 해당하는 정보를 입력해줍니다. 66 | 67 | - **apiToken** 68 | 69 | Install App > Install App to Your Team 70 | 71 | 에서 `Install App to Workspace` 를 클릭하고, `Authorize` 를 클릭합니다. 72 | 73 | 그 뒤 **xoxb-** 로 시작하는 `Bot User OAuth Access Token` 의 내용으로 입력해주시면 됩니다. 74 | 75 | - **channelId** 76 | 77 | 복무봇의 메세지가 올라올 채널을 설정합니다. 78 | 79 | **[이 곳](https://www.wikihow.com/Find-a-Channel-ID-on-Slack-on-PC-or-Mac)** 을 클릭하여 원하는 채널의 ID를 알아내세요. 80 | 81 | ### 봇 설정 82 | 83 | - **yourName** : 여러분의 이름을 입력하세요. 84 | - **notifyAt** : 하루에 한번 메세지를 보내는 시각을 설정합니다. 24 시간제로 입력해주세요. 85 | - **startDate** : 군복무 시작일 입니다. 86 | - **endDate** : 전역일 입니다. 87 | 88 | ## Usage 89 | 90 | ``` 91 | python app.py 92 | ``` 93 | 94 | 명령을 통해 `app.py` 를 실행합니다. 95 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from slackclient import SlackClient 4 | from dateutil import parser 5 | from datetime import datetime 6 | import schedule 7 | 8 | # load configurations 9 | config = json.loads(open("./config.json").read()) 10 | 11 | API_TOKEN = config["slack"]["apiToken"] 12 | CLIENT_ID = config["slack"]["clientId"] 13 | CLIENT_SECRET = config["slack"]["clientSecret"] 14 | VERIFICATION_TOKEN = config["slack"]["verificationToken"] 15 | CHANNEL_ID = config["slack"]["channelId"] 16 | 17 | YOUR_NAME = config["bot"]["yourName"] 18 | NOTIFY_AT = config["bot"]["notifyAt"] 19 | START_DATE = parser.parse(config["bot"]["startDate"]) 20 | END_DATE = parser.parse(config["bot"]["endDate"]) 21 | 22 | START_IMG = "https://i.imgur.com/PkbKJY1.jpg" # 시작 메세지 이미지 23 | DAILY_BEFORE_ENLISTMENT_IMG = "https://i.imgur.com/uM1dyrM.png" # 군입대 전 메세지 이미지 24 | DAILY_AFTER_ENLISTMENT_IMG = "https://i.imgur.com/U059WfE.png" # 군입대 후 메세지 이미지 25 | 26 | 27 | # init slack client 28 | sc = SlackClient(API_TOKEN) 29 | 30 | 31 | def _format_date(date): 32 | return date.strftime("%Y년 %m월 %d일") 33 | 34 | 35 | def _calc_total_days(): 36 | # 총 군복무 일 수 37 | total_days = (END_DATE - START_DATE).days 38 | return total_days 39 | 40 | 41 | def _calc_remaining_days(): 42 | # 남은 군복무 일 수 43 | remaining_days = (END_DATE - datetime.now()).days 44 | return remaining_days 45 | 46 | 47 | def _calc_after_enlistment_days(): 48 | # 군 입대후 지난 일 수. 음수라면 입대 전 49 | after_enlistment_days = (datetime.now() - START_DATE).days 50 | return after_enlistment_days 51 | 52 | 53 | def _calc_percentage(): 54 | # 복무율 55 | percentage = round( 56 | (_calc_after_enlistment_days() / _calc_total_days()) * 100, 2) 57 | return percentage 58 | 59 | 60 | def _render_progress_bar(): 61 | # 텍스트 프로그레스바 62 | total_squares = 20 63 | filled_squares = round(_calc_percentage() / (100 / total_squares)) 64 | unfilled_squares = total_squares - filled_squares 65 | 66 | progress_bar = "" 67 | 68 | for i in range(filled_squares): 69 | progress_bar = progress_bar + "█ " 70 | 71 | for i in range(unfilled_squares): 72 | progress_bar = progress_bar + "▒ " 73 | 74 | return progress_bar 75 | 76 | 77 | def send_start_message(): 78 | formatted_start_date = _format_date(START_DATE) 79 | formatted_end_date = _format_date(END_DATE) 80 | 81 | sc.api_call( 82 | "chat.postMessage", 83 | channel=CHANNEL_ID, 84 | blocks=[ 85 | { 86 | "type": "section", 87 | "text": { 88 | "type": "mrkdwn", 89 | "text": "복무봇이 시작되었습니다. :sob:" 90 | } 91 | }, 92 | { 93 | "type": "divider" 94 | }, 95 | { 96 | "type": "section", 97 | "fields": [ 98 | { 99 | "type": "mrkdwn", 100 | "text": "*장병이름*\n{}".format(YOUR_NAME) 101 | }, 102 | { 103 | "type": "mrkdwn", 104 | "text": "*알림시각*\n{}시".format(NOTIFY_AT) 105 | }, 106 | { 107 | "type": "mrkdwn", 108 | "text": "*복무 시작일*\n{}".format(formatted_start_date) 109 | }, 110 | { 111 | "type": "mrkdwn", 112 | "text": "*복무 종료일*\n{}".format(formatted_end_date) 113 | } 114 | ], 115 | "accessory": { 116 | "type": "image", 117 | "image_url": START_IMG, 118 | "alt_text": "plants" 119 | } 120 | }, 121 | { 122 | "type": "context", 123 | "elements": [ 124 | { 125 | "type": "plain_text", 126 | "text": "Developed by Hudi (https://hudi.kr)", 127 | } 128 | ] 129 | } 130 | ] 131 | ) 132 | 133 | 134 | def _send_daily_message_before_enlistment(): 135 | formatted_today_date = _format_date(datetime.now()) 136 | formatted_start_date = _format_date(START_DATE) 137 | formatted_end_date = _format_date(END_DATE) 138 | 139 | total_days = _calc_total_days() 140 | after_enlistment_days = _calc_after_enlistment_days() 141 | 142 | sc.api_call( 143 | "chat.postMessage", 144 | channel=CHANNEL_ID, 145 | blocks=[ 146 | { 147 | "type": "section", 148 | "text": { 149 | "type": "mrkdwn", 150 | "text": "`{}` 오늘, *{}* 님은 입대까지 {}일 남았습니다.\n\n".format(formatted_today_date, YOUR_NAME, -after_enlistment_days) 151 | } 152 | }, 153 | { 154 | "type": "divider" 155 | }, 156 | { 157 | "type": "section", 158 | "fields": [ 159 | { 160 | "type": "mrkdwn", 161 | "text": "*복무 시작일*\n{}".format(formatted_start_date) 162 | }, 163 | { 164 | "type": "mrkdwn", 165 | "text": "*복무 종료일*\n{}".format(formatted_end_date) 166 | }, 167 | { 168 | "type": "mrkdwn", 169 | "text": "*총 복무일*\n{}일".format(total_days) 170 | }, 171 | { 172 | "type": "mrkdwn", 173 | "text": "*입대까지*\n{}일".format(-after_enlistment_days) 174 | } 175 | ], 176 | "accessory": { 177 | "type": "image", 178 | "image_url": DAILY_BEFORE_ENLISTMENT_IMG, 179 | "alt_text": "plants" 180 | } 181 | }, 182 | { 183 | "type": "context", 184 | "elements": [ 185 | { 186 | "type": "plain_text", 187 | "text": "Developed by Hudi (https://hudi.kr)", 188 | "emoji": True 189 | } 190 | ] 191 | } 192 | ] 193 | ) 194 | 195 | 196 | def _send_daily_message_after_enlistment(): 197 | formatted_today_date = _format_date(datetime.now()) 198 | formatted_start_date = _format_date(START_DATE) 199 | formatted_end_date = _format_date(END_DATE) 200 | 201 | percentage = _calc_percentage() 202 | progress_bar = _render_progress_bar() 203 | total_days = _calc_total_days() 204 | after_enlistment_days = _calc_after_enlistment_days() 205 | remaining_days = _calc_remaining_days() 206 | 207 | sc.api_call( 208 | "chat.postMessage", 209 | channel=CHANNEL_ID, 210 | blocks=[ 211 | { 212 | "type": "section", 213 | "text": { 214 | "type": "mrkdwn", 215 | "text": "`{}` 오늘, *{}* 님의 복무율 \n\n 복무율 *{}%* | {} | *{}일 / {}일*".format(formatted_today_date, YOUR_NAME, percentage, progress_bar, after_enlistment_days, total_days) 216 | } 217 | }, 218 | { 219 | "type": "divider" 220 | }, 221 | { 222 | "type": "section", 223 | "fields": [ 224 | { 225 | "type": "mrkdwn", 226 | "text": "*복무 시작일*\n{}".format(formatted_start_date) 227 | }, 228 | { 229 | "type": "mrkdwn", 230 | "text": "*복무 종료일*\n{}".format(formatted_end_date) 231 | }, 232 | { 233 | "type": "mrkdwn", 234 | "text": "*총 복무일*\n{}일".format(total_days) 235 | }, 236 | { 237 | "type": "mrkdwn", 238 | "text": "*현재 복무일*\n{}일".format(after_enlistment_days) 239 | }, 240 | { 241 | "type": "mrkdwn", 242 | "text": "*남은 복무일*\n{}일".format(remaining_days) 243 | } 244 | ], 245 | "accessory": { 246 | "type": "image", 247 | "image_url": DAILY_AFTER_ENLISTMENT_IMG, 248 | "alt_text": "plants" 249 | } 250 | }, 251 | { 252 | "type": "context", 253 | "elements": [ 254 | { 255 | "type": "plain_text", 256 | "text": "Developed by Hudi (https://hudi.kr)", 257 | "emoji": True 258 | } 259 | ] 260 | } 261 | ] 262 | ) 263 | 264 | 265 | def send_daily_message(): 266 | after_enlistment_days = _calc_after_enlistment_days() 267 | if (after_enlistment_days < 0): 268 | _send_daily_message_before_enlistment() 269 | else: 270 | _send_daily_message_after_enlistment() 271 | 272 | print("[복무봇] 현재 복무율 전송") 273 | 274 | 275 | if __name__ == '__main__': 276 | notify_at = "{}:00".format(str(NOTIFY_AT).zfill(2)) 277 | 278 | print("[복무봇] 실행 준비중...") 279 | 280 | send_start_message() 281 | print("[복무봇] 실행 메세지 전송") 282 | 283 | send_daily_message() 284 | print("[복무봇] 현재 복무율 최초 전송") 285 | # 봇 실행시 최초 실행 286 | 287 | # 스케줄 시작 288 | schedule.every().day.at(notify_at).do(send_daily_message) 289 | print("[복무봇] 복무봇 스케줄 설정: {} 마다 실행".format(notify_at)) 290 | 291 | print("[복무봇] 스케줄링 시작!") 292 | while True: 293 | schedule.run_pending() 294 | time.sleep(1) 295 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "slack": { 3 | "apiToken": "Slack App API Token (Bot User OAuth Access Token)", 4 | "clientId": "Slack App Client ID", 5 | "clientSecret": "Slack App Client Secret", 6 | "verificationToken": "Slack App Verification Token", 7 | "channelId": "Slack Channel Id" 8 | }, 9 | "bot": { 10 | "yourName": "사용자 이름", 11 | "notifyAt": "슬랙에 매일 메세지를 보낼 시각. 24시간제 시간제로 기입 (오전 7시는 7, 오후 1시는 13)", 12 | "startDate": "yyyy/mm/dd (입대일자)", 13 | "endDate": "yyyy/mm/dd (제대일자)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /readme-img/mirage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devHudi/bokmu-bot/784acac8cf04d18ab53cd0d028885ed8f238e0df/readme-img/mirage.png -------------------------------------------------------------------------------- /readme-img/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devHudi/bokmu-bot/784acac8cf04d18ab53cd0d028885ed8f238e0df/readme-img/title.png --------------------------------------------------------------------------------