From 343ecf3b467d927efdeccc5375d025d946ec8df8 Mon Sep 17 00:00:00 2001 From: KWON Date: Tue, 22 Jul 2025 14:39:20 +0900 Subject: [PATCH] =?UTF-8?q?mattermost=EB=A1=9C=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=B3=B4=EB=82=B4=EA=B8=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env_sample | 24 +++++++++++ .gitignore | 3 ++ README.md | 100 ++++++++++++++++++++++++++++++++++++++++++++ app/__init__.py | 0 app/app.py | 36 ++++++++++++++++ lib/common.py | 0 lib/config.py | 63 ++++++++++++++++++++++++++++ lib/send_message.py | 83 ++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ run.py | 31 ++++++++++++++ 10 files changed, 343 insertions(+) create mode 100644 .env_sample create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/app.py create mode 100644 lib/common.py create mode 100644 lib/config.py create mode 100644 lib/send_message.py create mode 100644 requirements.txt create mode 100644 run.py diff --git a/.env_sample b/.env_sample new file mode 100644 index 0000000..71d401c --- /dev/null +++ b/.env_sample @@ -0,0 +1,24 @@ +# 서버 도메인 (필수) +SERVER_DOMAIN=example.com + +# 서버 프로토콜 (기본값: http) +#SERVER_PROTOCOL=https + +# 웹훅 서버 설정 +#WEBHOOK_SERVER_HOST=0.0.0.0 +#WEBHOOK_SERVER_PORT=5000 + +# NOTION +NOTION_API_SECRET=abc123 +#NOTION_WEBHOOK_SECRET=abc1234 + +# Mattermost +MATTERMOST_WEBHOOK_URL=https://mattermost.example.com/hooks/abc123 +#MATTERMOST_BOT_TOKEN=abc123 +#MATTERMOST_CH_ID= + +# 디버그 모드 (기본값: false) +#DEBUG=false + +# 로그 레벨 (기본값: INFO) +#LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c1fa45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +.vscode/ +**/__pycache__/** diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0c3427 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Notion Notification System + +노션(Notion)의 페이지 및 데이터베이스 변경 사항을 감지하고, 이를 Mattermost로 전송하는 웹훅 기반 알림 시스템입니다. + +--- + +## 🔧 주요 기능 + +- Notion Webhook 수신 서버 구현 (Flask 기반) +- 변경 이벤트를 파싱하여 메시지 생성 +- Mattermost에 메시지를 전송 (웹훅 또는 봇 API 방식 모두 지원) +- .env 환경 설정 기반 유연한 구성 +- 향후 다양한 웹훅 소스 확장을 고려한 구조 + +--- + +## 📁 프로젝트 구조 + +```bash +notion_noti/ +├── app.py # Flask 진입점 +├── config.py # .env 기반 설정 로드 및 검증 +├── views.py # 웹훅 라우트 정의 +├── lib/ +│ ├── notion_api.py # 노션 이벤트 파싱/처리 +│ └── send_message.py # Mattermost 메시지 전송 기능 +├── .env # 환경 설정 파일 +├── requirements.txt # 의존 패키지 목록 +``` + + +--- + +## ⚙️ 설치 및 실행 + +### 1. 의존 패키지 설치 + +```bash +python -m venv .venv +source .venv/bin/activate # (Windows: .venv\Scripts\activate) +pip install -r requirements.txt +``` + +### 2. 환경 설정 +`.env` 파일을 아래와 같은 형식으로 작성합니다. + +```env +# 서버 설정 +SERVER_DOMAIN=example.com +SERVER_PROTOCOL=https +WEBHOOK_SERVER_HOST=0.0.0.0 +WEBHOOK_SERVER_PORT=5000 + +# Notion API +NOTION_API_SECRET=secret_xxxxx +#NOTION_WEBHOOK_SECRET=your_webhook_secret # (선택사항) + +# Mattermost 설정 (둘 중 하나 이상) +MATTERMOST_WEBHOOK_URL=https://mattermost.example.com/hooks/abc123 +# 또는 +MATTERMOST_BOT_TOKEN=your_token +MATTERMOST_CH_ID=channel_id_here + +# 기타 +DEBUG=true +LOG_LEVEL=INFO +``` + +### 3. 실행 +```bash +python app.py +``` + +Flask 서버가 다음과 같이 실행됩니다. +```csharp + * Running on http://0.0.0.0:5000 +``` + +## 🧪 테스트 +### curl로 웹훅 테스트 +```bash +curl -X POST http://localhost:5000/webhook/notion \ + -H "Content-Type: application/json" \ + -d '{ + "event": "test_event", + "page_id": "abc123", + "action": "created" + }' +``` + +### Postman 또는 테스트 스크립트도 사용 가능 + +## 📌 확장 계획 + +- Mattermost 슬래시 명령어 처리 +- 다른 서비스(GitHub, Jira 등)의 웹훅 수신 처리 +- 메시지 템플릿 설정 지원 +- Docker 이미지 제공 +- Synology NAS 환경에서 안정화 운영 지원 + diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..c1a6cae --- /dev/null +++ b/app/app.py @@ -0,0 +1,36 @@ +import os +import sys + +# 📌 프로젝트 루트 경로 추가 +sys.path.append(os.path.abspath(os.path.dirname(__file__))) + +from flask import Flask + +from views import webhook_bp # views.py에 정의된 Blueprint 객체 +from lib.config import Config + +import logging + +def create_app(): + app = Flask(__name__) + + # 로그 레벨 설정 + logging.basicConfig( + level=Config.LOG_LEVEL, + format="%(asctime)s [%(levelname)s] %(message)s" + ) + + # 웹훅 라우트 등록 + app.register_blueprint(webhook_bp) + + return app + + +if __name__ == "__main__": + Config.validate() + app = create_app() + app.run( + host=Config.WEBHOOK_SERVER_HOST, + port=Config.WEBHOOK_SERVER_PORT, + debug=Config.DEBUG + ) diff --git a/lib/common.py b/lib/common.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/config.py b/lib/config.py new file mode 100644 index 0000000..42f0880 --- /dev/null +++ b/lib/config.py @@ -0,0 +1,63 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # 서버 설정 + SERVER_DOMAIN = os.getenv('SERVER_DOMAIN', 'localhost') + SERVER_PROTOCOL = os.getenv('SERVER_PROTOCOL', 'http') + WEBHOOK_SERVER_HOST = os.getenv('WEBHOOK_SERVER_HOST', '0.0.0.0') + WEBHOOK_SERVER_PORT = int(os.getenv('WEBHOOK_SERVER_PORT', 5000)) + + # 노션 API + NOTION_API_SECRET = os.getenv('NOTION_API_SECRET') + + # 웹훅 서명 검증용 비밀키 (옵션) + NOTION_WEBHOOK_SECRET = os.getenv('NOTION_WEBHOOK_SECRET', None) + + # Mattermost 관련 + MATTERMOST_SERVER_URL = os.getenv('MATTERMOST_SERVER_URL', '').rstrip('/') + MATTERMOST_WEBHOOK_URL = os.getenv('MATTERMOST_WEBHOOK_URL') + MATTERMOST_BOT_TOKEN = os.getenv('MATTERMOST_BOT_TOKEN') + MATTERMOST_CH_ID = os.getenv('MATTERMOST_CH_ID') + + # 기타 설정 + DEBUG = os.getenv('DEBUG', 'false').lower() in ['true', '1', 'yes'] + LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + + @classmethod + def mattermost_post_api_url(cls): + if cls.MATTERMOST_SERVER_URL: + return f"{cls.MATTERMOST_SERVER_URL}/api/v4/posts" + return None + + @classmethod + def validate(cls): + missing = [] + + # NOTION_API_SECRET 필수 확인 + if not cls.NOTION_API_SECRET: + missing.append('NOTION_API_SECRET') + + # Mattermost는 둘 중 하나 이상 설정 필수 + if not cls.MATTERMOST_WEBHOOK_URL and not (cls.MATTERMOST_BOT_TOKEN and cls.MATTERMOST_CH_ID): + if not cls.MATTERMOST_WEBHOOK_URL: + missing.append('MATTERMOST_WEBHOOK_URL') + if not cls.MATTERMOST_BOT_TOKEN: + missing.append('MATTERMOST_BOT_TOKEN') + if not cls.MATTERMOST_CH_ID: + missing.append('MATTERMOST_CH_ID') + + if missing: + err_msg = ( + f"환경변수 설정 오류: 아래 필수 항목이 누락되었습니다:\n" + + "\n".join(f" - {var}" for var in missing) + + "\n설정 후 다시 실행해주세요." + ) + raise EnvironmentError(err_msg) + else: + print("필수 항목 검증 완료") + +# 초기 검증 실행 +Config.validate() diff --git a/lib/send_message.py b/lib/send_message.py new file mode 100644 index 0000000..6f6c3b4 --- /dev/null +++ b/lib/send_message.py @@ -0,0 +1,83 @@ +import os, sys + +# 현재 파일 기준으로 프로젝트 루트 경로를 sys.path 에 추가 +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import requests +import logging +from lib.config import Config + +def send_message_to_mattermost(message: str) -> bool: + """ + 설정된 방식에 따라 Mattermost로 메시지를 전송합니다. + - 웹훅 URL이 있으면 웹훅으로 전송 + - 봇 토큰과 채널 ID가 있으면 API로 전송 + - 둘 다 있으면 두 방식 모두 시도 + """ + success = True + + if Config.MATTERMOST_WEBHOOK_URL: + result = _send_via_webhook(message) + if not result: + logging.error("웹훅 전송 실패") + success = False + + if Config.MATTERMOST_BOT_TOKEN and Config.MATTERMOST_CH_ID: + result = _send_via_bot_api(message) + if not result: + logging.error("봇 API 전송 실패") + success = False + + if not Config.MATTERMOST_WEBHOOK_URL and not (Config.MATTERMOST_BOT_TOKEN and Config.MATTERMOST_CH_ID): + logging.error("Mattermost 전송 실패: 환경변수 설정이 없음") + success = False + + return success + + + +def _send_via_webhook(message: str) -> bool: + try: + resp = requests.post( + Config.MATTERMOST_WEBHOOK_URL, + json={"text": message}, + timeout=5 + ) + if resp.status_code == 200: + return True + logging.error(f"[WEBHOOK ERROR] {resp.status_code}: {resp.text}") + return False + except Exception as e: + logging.exception("[WEBHOOK EXCEPTION]") + return False + + +def _send_via_bot_api(message: str) -> bool: + url = Config.mattermost_post_api_url() + if not url: + logging.error("MATTERMOST_SERVER_URL 설정이 필요합니다.") + return False + + headers = { + "Authorization": f"Bearer {Config.MATTERMOST_BOT_TOKEN}", + "Content-Type": "application/json", + } + payload = { + "channel_id": Config.MATTERMOST_CH_ID, + "message": message, + } + try: + resp = requests.post(url, headers=headers, json=payload, timeout=5) + if resp.status_code == 201: + return True + logging.error(f"[BOT API ERROR] {resp.status_code}: {resp.text}") + return False + except Exception as e: + logging.exception("[BOT API EXCEPTION]") + return False + + +if __name__ == "__main__": + test_msg = "✅ 테스트 메시지입니다: 시스템 점검 완료" + success = send_message_to_mattermost(test_msg) + print("[RESULT]", "전송 성공" if success else "전송 실패") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3cc7fb9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask>=2.2.0 +requests>=2.31.0 +python-dotenv>=1.0.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000..b5ccc79 --- /dev/null +++ b/run.py @@ -0,0 +1,31 @@ +import os +import sys + +# 📌 프로젝트 루트 경로 추가 +sys.path.append(os.path.abspath(os.path.dirname(__file__))) + +from lib.config import Config +from app.app import create_app + +import logging + +def main(): + # 설정 검증 + Config.validate() + + # 로그 포맷 설정 + logging.basicConfig( + level=Config.LOG_LEVEL, + format="%(asctime)s [%(levelname)s] %(message)s" + ) + + # Flask 앱 생성 및 실행 + app = create_app() + app.run( + host=Config.WEBHOOK_SERVER_HOST, + port=Config.WEBHOOK_SERVER_PORT, + debug=Config.DEBUG + ) + +if __name__ == "__main__": + main()