From df21baf7f091e420f368ccfb10ee52c6dee7f59a Mon Sep 17 00:00:00 2001 From: Seok Won <alfex4936@gmail.com> Date: Sun, 6 Dec 2020 12:02:10 +0900 Subject: [PATCH] =?UTF-8?q?Slack=20+=20Kafka:=20=EC=95=84=EC=A3=BC?= =?UTF-8?q?=EB=8C=80=ED=95=99=EA=B5=90=20=EA=B3=B5=EC=A7=80=20=EB=B4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1시간마다 모든 공지를 불러 {"TITLE": "제목", "DATE": "올린 날", "LINK": "http 주소", "WRITER": "글쓴이"}를 json 형태로 저장한다. 이 json 파일이나 새로운 공지가 있으면 기존 json과 비교해서 새로운 데이터를 Consumer로 보내고, Consumer는 새로운 데이터를 받으면, Slack API를 이용해, "#아주대" 채널에 공지를 올려준다. 마지막 파싱 시간도 기록해 종료 후 다시 불러도 1시간이 지나지 않으면 파싱하지 않는다. 결과) Last parsing: 1972-12-01 07:00:00 Trying to parse new posts... Sending a new post...: 12179 ... Last parsing: 2020-12-04 19:11:42.839219 Trying to parse new posts... No new posts yet... Resting 1 hour... ... Last parsing: 2020-12-06 11:55:35.386262 Wait for 3494 seconds to sync new posts. --- img/slack_ajou.png | Bin 0 -> 27773 bytes python/README.md | 30 ++++++ python/src/AjouSlackConsumer.py | 89 ++++++++++++++++++ python/src/AjouSlackProducer.py | 157 +++++++++++++++++++++++++++++++ python/src/SlackKafkaConsumer.py | 7 +- python/src/SlackKafkaProducer.py | 4 +- python/src/config.py | 1 + 7 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 img/slack_ajou.png create mode 100644 python/src/AjouSlackConsumer.py create mode 100644 python/src/AjouSlackProducer.py diff --git a/img/slack_ajou.png b/img/slack_ajou.png new file mode 100644 index 0000000000000000000000000000000000000000..46307c275336b55279f9bfe61b26cad09d1a4af3 GIT binary patch literal 27773 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 TNEd%K16%3o>gTe~DWM4f3r03% literal 0 HcmV?d00001 diff --git a/python/README.md b/python/README.md index d9e159b..591b4bc 100644 --- a/python/README.md +++ b/python/README.md @@ -34,6 +34,36 @@ WIN10@DESKTOP:~$ pip install confluent-kafka ``` +## Slack API with Ajou University notices parser + +[Get Slack API here](https://api.slack.com/) + +First, invite your bot to your channel. (In this example, we set it as "#아주대") + +The [producer](https://github.com/Alfex4936/kafka-Studies/tree/main/python/src/AjouSlackProducer.py) will notify the [consumer](https://github.com/Alfex4936/kafka-Studies/tree/main/python/src/AjouSlackConsumer.py) whenever there are new notices. + +The *producer* checks new notices per an hour, saves latest 10 notices to json file, +and sees if there is/are a new notice/s. + +If there is a new notice, it sends {"TITLE": "title", "DATE": "post date", "LINK": "http address", "WRITER": "writer"} to the consumer. + +The *consumer* checks new datas every 5 seconds, if it gets a new data, +it consumes the data and leave a comment like this below image. + +<div align="center"> +<p> + <img width="480" src="https://github.com/Alfex4936/kafka-Studies/blob/main/img/slack_ajou.png"> +</p> +</div> + +:b: Run the server first to see the results. + +```console +WIN10@DESKTOP:~$ python AjouSlackProducer.py + +WIN10@DESKTOP:~$ python AjouSlackConsumer.py +``` + ## Slack API Producer Usage [Get Slack API](https://api.slack.com/) diff --git a/python/src/AjouSlackConsumer.py b/python/src/AjouSlackConsumer.py new file mode 100644 index 0000000..60f60d8 --- /dev/null +++ b/python/src/AjouSlackConsumer.py @@ -0,0 +1,89 @@ +import json +import time + +import os + +from config import Config +from confluent_kafka import Consumer, KafkaError +from slack import WebClient +from slack.errors import SlackApiError + + +# Bot User OAuth Access Token +token = os.environ["SLACK_BOT_TOKEN"] + +sc = WebClient(token) + +# Set 'auto.offset.reset': 'smallest' if you want to consume all messages +# from the beginning of the topic +settings = { + "bootstrap.servers": Config.MY_SERVER, + "group.id": "ajou-notify", + "default.topic.config": {"auto.offset.reset": "largest"}, +} +c = Consumer(settings) + +# Topic = "AJOU-NOTIFY +c.subscribe([Config.AJOU_TOPIC_ID]) + +try: + while True: + msg = c.poll(0.1) + time.sleep(5) + if msg is None: + continue + elif not msg.error(): + print("Received a message: {0}".format(msg.value())) + if msg.value() is None: + continue + + try: + app_msg = json.loads(msg.value().decode()) + except: + app_msg = json.loads(msg.value()) + + try: + title = app_msg["TITLE"] + date = app_msg["DATE"] + href = app_msg["LINK"] + writer = app_msg["WRITER"] + + channel = "아주대" + # TODO: 학사면 좀 더 중요하게? + text = ":star: `%s` 새로운 공지!\n>%s: %s\n>링크: <%s|공지 확인하기>" % ( + date, + writer, + title, + href, + ) + print('\nSending message "%s" to channel %s' % (text, channel)) + except SlackApiError as e: + print("Failed to get channel/text from message.") + print(e.response["error"]) + channel = "kafka" + text = msg.value() + + try: + sc_response = sc.chat_postMessage( + channel=channel, text=text, as_user=True, username="아주대 공지 봇" + ) # as_user은 new slack app에서 작동 안 함 + + except SlackApiError as e: + assert e.response["ok"] is False + print("\t** FAILED: %s" % e.response["error"]) + elif msg.error().code() == KafkaError._PARTITION_EOF: + print( + "End of partition reached {0}/{1}".format(msg.topic(), msg.partition()) + ) + else: + print("Error occured: {0}".format(msg.error().str())) + +except Exception as e: + print(type(e)) + print(dir(e)) + +except KeyboardInterrupt: + print("Pressed CTRL+C...") + +finally: + c.close() diff --git a/python/src/AjouSlackProducer.py b/python/src/AjouSlackProducer.py new file mode 100644 index 0000000..e55943b --- /dev/null +++ b/python/src/AjouSlackProducer.py @@ -0,0 +1,157 @@ +import datetime +import json +import os +import time +from contextlib import contextmanager +from pathlib import Path + +import requests +from bs4 import BeautifulSoup +from config import Config +from confluent_kafka import Producer +from slack import WebClient +from slack.errors import SlackApiError + +# Producer callback function +def acked(err, msg): + if err is not None: + print("Failed to deliver message: {0}: {1}".format(msg.value(), err.str())) + else: + print("Message produced: {0}".format(msg.value())) # binary + + +# Make data into dictionary format +def makeJson(postId, postTitle, postDate, postLink, postWriter): + return { + postId: { + "TITLE": postTitle, + "DATE": postDate, + "LINK": ADDRESS + postLink, + "WRITER": postWriter, + } + } + + +# Ajou notices parser +def parser(): + req = requests.get(f"{ADDRESS}?mode=list&&articleLimit=10&article.offset=0") + req.encoding = "utf-8" + html = req.text + soup = BeautifulSoup(html, "html.parser") + ids = soup.select("table > tbody > tr > td.b-num-box") + posts = soup.select("table > tbody > tr > td.b-td-left > div > a") + dates = soup.select("table > tbody > tr > td.b-td-left > div > div > span.b-date") + writers = soup.select( + "table > tbody > tr > td.b-td-left > div > div.b-m-con > span.b-writer" + ) + return ids, posts, dates, writers + + +ADDRESS = "https://www.ajou.ac.kr/kr/ajou/notice.do" +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +JSON_PATH = os.path.join(BASE_DIR, "already_read.json") +LENGTH = 10 +PRODUCED = 0 + +# Bot User OAuth Access Token +# used scopes: channels:history, channels:read, chat:write, im:history, mpim:history, users:read +token = os.environ["SLACK_BOT_TOKEN"] + +read = None +# 공지 Parser +if not Path(JSON_PATH).is_file(): # 파일 없으면 기본 형식 만듬 + base_data = {"POSTS": {}, "LAST_PARSED": "1972-12-01 07:00:00.000000"} + + with open(JSON_PATH, "a+") as f: + f.write(json.dumps(base_data)) + +# json read +with open(JSON_PATH, "r+") as f_read: + read = json.load(f_read) + +# Set last parsed time to rest 1 hour well +LAST_PARSED = datetime.datetime.strptime(read["LAST_PARSED"], "%Y-%m-%d %H:%M:%S.%f") + +# init parser +ids, posts, dates, writers = parser() + +# Slack API 초기화 +sc = WebClient(token) +channel = "C01G2CR5MEE" # 아주대 + +# Kafka Producer 만들기 "localhost:9092" +settings = {"bootstrap.servers": Config.MY_SERVER} +p = Producer(settings) + +try: + while True: # 1시간마다 무한 반복 + PRODUCED = 0 + LAST_PARSED = datetime.datetime.strptime( + read["LAST_PARSED"], "%Y-%m-%d %H:%M:%S.%f" + ) + + try: + now = datetime.datetime.now() + diff = (now - LAST_PARSED).seconds + print("Last parsing:", LAST_PARSED) + if diff / 3600 < 1: # 업데이트 후 1시간이 안 지났음, 대기 + print(f"Wait for {3600 - diff} seconds to sync new posts.") + time.sleep(3600 - diff) + + read["LAST_PARSED"] = now.strftime("%Y-%m-%d %H:%M:%S.%f") + + print("Trying to parse new posts...") + ids, posts, dates, writers = parser() # 다시 파싱 + for i in range(LENGTH): + postId = ids[i].text.strip() + postLink = posts[i].get("href") + postTitle = posts[i].text.strip() + # postTitle = posts[i].get("title") + postDate = dates[i].text.strip() + postWriter = writers[i].text + + data = makeJson(postId, postTitle, postDate, postLink, postWriter) + # {'10000': {'TITLE': '설문조사', 'DATE': '20.12.04', 'LINK': 'https', 'WRITER': '입학처'}} + + if postId not in read["POSTS"]: + print("Sending a new post...:", postId) + read["POSTS"].update(data) + + PRODUCED += 1 + p.produce( + Config.AJOU_TOPIC_ID, + value=json.dumps(data[postId]), + callback=acked, + ) + p.poll(0.5) # 데이터 Kafka에게 전송 + else: + continue + if PRODUCED: + print(f"Sent {PRODUCED} posts...") + else: + print("No new posts yet...") + + except SlackApiError as e: + assert e.response["ok"] is False + print("\t** FAILED: %s" % e.response["error"]) + + with open(JSON_PATH, "w+") as f: + f.write(json.dumps(read)) + with open(JSON_PATH, "r+") as f: + read = json.load(f) + + print("Resting 1 hour...") + + time.sleep(3600) + + +except Exception as e: + print(type(e)) + print(dir(e)) + +except KeyboardInterrupt: + print("Pressed CTRL+C...") + +finally: + print("Exiting...") + p.flush(100) diff --git a/python/src/SlackKafkaConsumer.py b/python/src/SlackKafkaConsumer.py index 61eb482..74981df 100644 --- a/python/src/SlackKafkaConsumer.py +++ b/python/src/SlackKafkaConsumer.py @@ -1,6 +1,9 @@ import json import time + +import os + from confluent_kafka import Consumer, KafkaError from slack import WebClient from slack.errors import SlackApiError @@ -8,7 +11,7 @@ from slack.errors import SlackApiError # Bot User OAuth Access Token # Scope = chat:write -token = "" +token = os.environ["SLACK_BOT_TOKEN"] sc = WebClient(token) @@ -26,7 +29,7 @@ c.subscribe(["SLACK-KAFKA"]) try: while True: - msg = c.poll(0.1) + msg = c.poll(0.1) # read data time.sleep(5) if msg is None: continue diff --git a/python/src/SlackKafkaProducer.py b/python/src/SlackKafkaProducer.py index ddd438f..8ec7f86 100644 --- a/python/src/SlackKafkaProducer.py +++ b/python/src/SlackKafkaProducer.py @@ -1,6 +1,8 @@ import json import time +import os + from config import Config from confluent_kafka import Producer from slack import WebClient @@ -9,7 +11,7 @@ from slack.errors import SlackApiError # Bot User OAuth Access Token # used scopes: channels:history, channels:read, chat:write, im:history, mpim:history, users:read -token = "" +token = os.environ["SLACK_BOT_TOKEN"] # Slack API 초기화 sc = WebClient(token) diff --git a/python/src/config.py b/python/src/config.py index 9df9e10..f4648f5 100644 --- a/python/src/config.py +++ b/python/src/config.py @@ -2,6 +2,7 @@ class Config: MY_SERVER = "localhost:9092" TOPIC_ID = "first-topic" SLACK_TOPID_ID = "SLACK-KAFKA" + AJOU_TOPIC_ID = "AJOU-NOTIFY" GROUP_ID = "group-one" CLIENT_ID = "client-1" -- GitLab