Spaces:
Sleeping
Sleeping
| import hmac | |
| import base64 | |
| import hashlib | |
| import os | |
| from dotenv import load_dotenv | |
| import requests | |
| from typing import Optional | |
| from fastapi import FastAPI, Request, Header, HTTPException | |
| import datetime | |
| import json | |
| import pytz | |
| from google.oauth2 import service_account | |
| from googleapiclient.discovery import build | |
| from google.auth.transport.requests import Request as GoogleRequest | |
| from google.oauth2.credentials import Credentials | |
| from google_auth_oauthlib.flow import InstalledAppFlow | |
| import google.generativeai as genai | |
| from vertexai.generative_models import GenerativeModel | |
| # .env ํ์ผ์์ ํ๊ฒฝ ๋ณ์๋ฅผ ๋ถ๋ฌ์ต๋๋ค. | |
| load_dotenv() | |
| app = FastAPI() | |
| # โ ๏ธ Chatwork ์นํ ์ค์ ํ์ด์ง์์ ๋ฐ์ '์นํ ํ ํฐ' | |
| CHATWORK_WEBHOOK_TOKEN = os.getenv("CHATWORK_WEBHOOK_TOKEN") | |
| # โ ๏ธ 1๋จ๊ณ์์ ๋ฐ๊ธ๋ฐ์ 'API ํ ํฐ' | |
| CHATWORK_API_TOKEN = os.getenv("CHATWORK_API_TOKEN") | |
| base_path = os.path.dirname(__file__) | |
| credentials_path = os.path.join(base_path, 'new_credential.json') | |
| token_path = os.path.join(base_path, 'new_token.json') | |
| if not os.path.exists(credentials_path): | |
| print(f"'{credentials_path}' ํ์ผ์ด ์์ด ์๋ก ์์ฑํฉ๋๋ค.") | |
| credentials_str = os.getenv('GOOGLE_CREDENTIALS_JSON') | |
| if credentials_str: | |
| with open(credentials_path, 'w', encoding='utf-8') as f: | |
| f.write(credentials_str) | |
| else: | |
| print("๊ฒฝ๊ณ : .env ํ์ผ์ 'GOOGLE_CREDENTIALS_JSON' ๋ณ์๊ฐ ์์ต๋๋ค.") | |
| if not os.path.exists(token_path): | |
| print(f"'{token_path}' ํ์ผ์ด ์์ด ์๋ก ์์ฑํฉ๋๋ค.") | |
| token_str = os.getenv('GOOGLE_TOKEN_JSON') | |
| if token_str: | |
| with open(token_path, 'w', encoding='utf-8') as f: | |
| f.write(token_str) | |
| CREDENTIALS_FILENAME = credentials_path | |
| TOKEN_FILENAME = token_path | |
| # Google Calendar API ์ค์ฝํ (์ฝ๊ธฐ ์ ์ฉ) | |
| SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] | |
| # ๊ธฐ์ค ์๊ฐ๋ (GAS์ Session.getScriptTimeZone()์ ํด๋น) | |
| # ํ์์ ๋ฐ๋ผ 'Asia/Tokyo', 'America/New_York' ๋ฑ์ผ๋ก ๋ณ๊ฒฝํ์ธ์. | |
| TIMEZONE = 'Asia/Tokyo' | |
| rooms = [ | |
| {"capacity": 6, "name": "1F2", "email": os.getenv("GOOGLE_CALENDAR_1F2")}, | |
| {"capacity": 6, "name": "1F3", "email": os.getenv("GOOGLE_CALENDAR_1F3")}, | |
| {"capacity": 12, "name": "3F1", "email": os.getenv("GOOGLE_CALENDAR_3F1")}, | |
| {"capacity": 4, "name": "3F2", "email": os.getenv("GOOGLE_CALENDAR_3F2")}, | |
| {"capacity": 4, "name": "3F3", "email": os.getenv("GOOGLE_CALENDAR_3F3")}, | |
| {"capacity": 4, "name": "3F5", "email": os.getenv("GOOGLE_CALENDAR_3F5")}, | |
| {"capacity": 4, "name": "3F6", "email": os.getenv("GOOGLE_CALENDAR_3F6")}, | |
| {"capacity": 12, "name": "6F1", "email": os.getenv("GOOGLE_CALENDAR_6F1")}, | |
| {"capacity": 4, "name": "6F2", "email": os.getenv("GOOGLE_CALENDAR_6F2")}, | |
| {"capacity": 4, "name": "6F3", "email": os.getenv("GOOGLE_CALENDAR_6F3")}, | |
| {"capacity": 2, "name": "6F5", "email": os.getenv("GOOGLE_CALENDAR_6F5")}, | |
| {"capacity": 2, "name": "6F6", "email": os.getenv("GOOGLE_CALENDAR_6F6")}, | |
| {"capacity": 2, "name": "6F7", "email": os.getenv("GOOGLE_CALENDAR_6F7")}, | |
| {"capacity": 4, "name": "7F2", "email": os.getenv("GOOGLE_CALENDAR_7F2")}, | |
| {"capacity": 4, "name": "7F3", "email": os.getenv("GOOGLE_CALENDAR_7F3")}, | |
| {"capacity": 4, "name": "7F5", "email": os.getenv("GOOGLE_CALENDAR_7F5") }, | |
| {"capacity": 4, "name": "7F6", "email": os.getenv("GOOGLE_CALENDAR_7F6") } | |
| ] | |
| # def get_credentials(): | |
| # creds = None | |
| # if os.path.exists(TOKEN_FILENAME): | |
| # creds = Credentials.from_authorized_user_file(TOKEN_FILENAME, SCOPES) | |
| # # ํฌ๋ฆฌ๋ด์ ์ด ์๊ฑฐ๋ ์ ํจํ์ง ์์ผ๋ฉด ์ฌ์ฉ์ ๋ก๊ทธ์ธ ํ๋ก์ฐ๋ฅผ ์คํํฉ๋๋ค. | |
| # if not creds or not creds.valid: | |
| # if creds and creds.expired and creds.refresh_token: | |
| # creds.refresh(GoogleRequest()) | |
| # else: | |
| # flow = InstalledAppFlow.from_client_secrets_file( | |
| # CREDENTIALS_FILENAME, SCOPES) | |
| # creds = flow.run_local_server(port=0) | |
| # # ๋ค์ ์คํ์ ์ํด ํ ํฐ์ ์ ์ฅํฉ๋๋ค. | |
| # with open(TOKEN_FILENAME, 'w') as token: | |
| # token.write(creds.to_json()) | |
| # return creds | |
| def get_oauth_credentials(): | |
| """ | |
| OAuth 2.0 ์ฌ์ฉ์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๊ณ ์ ํจํ Credentials ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค. | |
| - token.json์ด ์์ผ๋ฉด ์ฌ์ฉํ๊ณ , ์๊ฑฐ๋ ๋ง๋ฃ๋์์ผ๋ฉด ์๋ก ์ธ์ฆํฉ๋๋ค. | |
| """ | |
| creds = None | |
| print(f"log1") | |
| # token.json ํ์ผ์ด ์ด๋ฏธ ์กด์ฌํ๋ฉด, ์ ์ฅ๋ ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ต๋๋ค. | |
| if os.path.exists(TOKEN_FILENAME): | |
| print(f"log2") | |
| creds = Credentials.from_authorized_user_file(TOKEN_FILENAME, SCOPES) | |
| print(f"log3") | |
| # ์ธ์ฆ ์ ๋ณด๊ฐ ์๊ฑฐ๋ ์ ํจํ์ง ์์ผ๋ฉด, ์ฌ์ฉ์์๊ฒ ๋ก๊ทธ์ธ์ ์์ฒญํฉ๋๋ค. | |
| if not creds or not creds.valid: | |
| print(f"log4") | |
| if creds and creds.expired and creds.refresh_token: | |
| print(f"log5") | |
| # ํ ํฐ์ด ๋ง๋ฃ๋์์ผ๋ฉด, ๋ฆฌํ๋ ์ ํ ํฐ์ ์ฌ์ฉํด ๊ฐฑ์ ํฉ๋๋ค. | |
| creds.refresh(GoogleRequest()) | |
| else: | |
| print(f"log6") | |
| # ํ ํฐ์ด ์์ผ๋ฉด, credentials.json์ ์ด์ฉํด ์๋ก์ด ์ธ์ฆ ์ ์ฐจ๋ฅผ ์์ํฉ๋๋ค. | |
| flow = InstalledAppFlow.from_client_secrets_file( | |
| CREDENTIALS_FILENAME, SCOPES) | |
| print(f"log7") | |
| # ์๋ฒ ํ๊ฒฝ์ ์ํด ๋ก์ปฌ ์๋ฒ ๋์ ์ฝ์ ๊ธฐ๋ฐ ์ธ์ฆ์ ์คํํฉ๋๋ค. | |
| creds = flow.run_console() | |
| # ๋ค์ ์คํ์ ์ํด ์๋ก ๋ฐ๊ธ๋ฐ๊ฑฐ๋ ๊ฐฑ์ ๋ ์ธ์ฆ ์ ๋ณด๋ฅผ token.json ํ์ผ์ ์ ์ฅํฉ๋๋ค. | |
| print(f"log8") | |
| with open(TOKEN_FILENAME, 'w') as token: | |
| print(f"log9") | |
| token.write(creds.to_json()) | |
| print(f"log10") | |
| return creds | |
| def check_room_free_slots_over_1h(date_str=None): | |
| # ํ์์ค ์ ๋ณด | |
| output_lines = [] | |
| # --- ์ธ์ฆ ๋ฐ ์๋น์ค ๋น๋ --- | |
| try: | |
| creds = get_oauth_credentials() | |
| service = build('calendar', 'v3', credentials=creds) | |
| except Exception as e: | |
| print(f"่ช่จผใจใฉใผ: {e}") | |
| return | |
| # --- ์กฐํ ์๊ฐ ๋ฒ์ ์ค์ --- | |
| # --- ์กฐํ ์๊ฐ ๋ฒ์ ์ค์ --- | |
| tz = pytz.timezone(TIMEZONE) | |
| now = datetime.datetime.now(tz) | |
| if date_str: | |
| target_date = datetime.datetime.strptime(date_str, "%Y-%m-%d").date() | |
| start_time = tz.localize(datetime.datetime.combine(target_date, datetime.time(10, 0))) | |
| end_time = tz.localize(datetime.datetime.combine(target_date, datetime.time(19, 0))) | |
| output_lines.append(f"็ งไผๅฏพ่ฑก: {start_time.strftime('%Y/%m/%d')}") | |
| else: | |
| today = now.date() | |
| start_hour = max(10, now.hour) | |
| start_time = tz.localize(datetime.datetime.combine(today, datetime.time(start_hour, 0))) | |
| end_time = tz.localize(datetime.datetime.combine(today, datetime.time(19, 0))) | |
| output_lines.append(f"็ งไผๅฏพ่ฑก: {start_time.strftime('%Y/%m/%d')}") | |
| if start_time >= end_time: | |
| output_lines.append("็ งไผๅฏ่ฝใชๆ้ใใใใพใใ") | |
| return "\n".join(output_lines) | |
| # --- FreeBusy API๋ก ๋ชจ๋ ํ์์ค์ ๋ฐ์ ์๊ฐ ํ ๋ฒ์ ์กฐํ --- | |
| body = { | |
| "timeMin": start_time.isoformat(), | |
| "timeMax": end_time.isoformat(), | |
| "timeZone": TIMEZONE, | |
| "items": [{"id": room["email"]} for room in rooms] | |
| } | |
| print(f"freebusy 1") | |
| try: | |
| freebusy_result = service.freebusy().query(body=body).execute() | |
| calendars_busy_info = freebusy_result.get('calendars', {}) | |
| print(f"freebusy 2") | |
| except Exception as e: | |
| print(f"freebusy 3") | |
| return f"FreeBusy API ๅคฑๆ: {e}" | |
| output_lines.append("-" * 30) | |
| print(f"freebusy 4") | |
| # --- ๊ฐ ํ์์ค๋ณ๋ก ๋น ์๊ฐ ๊ณ์ฐ ๋ฐ ์ถ๋ ฅ --- | |
| for room in rooms: | |
| room_email = room['email'] | |
| busy_info = calendars_busy_info.get(room_email, {}) | |
| if busy_info.get('errors'): | |
| reason = busy_info['errors'][0].get('reason', 'unknown') | |
| output_lines.append(f"[{room['name']}] ใซใฌใณใใผ็ งไผๅคฑๆ: {reason}") | |
| continue | |
| busy_slots = busy_info.get('busy', []) | |
| cursor = start_time | |
| free_slots = [] | |
| for busy in busy_slots: | |
| busy_start = datetime.datetime.fromisoformat(busy['start']) | |
| busy_end = datetime.datetime.fromisoformat(busy['end']) | |
| if busy_start > cursor: | |
| free_slots.append({'start': cursor, 'end': busy_start}) | |
| if busy_end > cursor: | |
| cursor = busy_end | |
| if cursor < end_time: | |
| free_slots.append({'start': cursor, 'end': end_time}) | |
| long_free_slots = [ | |
| slot for slot in free_slots | |
| if (slot['end'] - slot['start']) >= datetime.timedelta(hours=1) | |
| ] | |
| if long_free_slots: | |
| output_lines.append(f"[{room['name']}] (ๅฎๅก {room['capacity']}ๅ)") | |
| for slot in long_free_slots: | |
| output_lines.append(f" {slot['start'].strftime('%H:%M')} ~ {slot['end'].strftime('%H:%M')}") | |
| return "\n".join(output_lines) | |
| def search_calendar(calendar_data , order: str) -> str: | |
| genai.configure(api_key=os.getenv("GEMINI_API_KEY")) | |
| model = genai.GenerativeModel("gemini-1.5-pro") | |
| message = f"""ไปฅไธใ็ฉบใๅฎคใฎ็ฉบใใฆใใๆ้ๆ ๅ ฑใงใใ\n {calendar_data} ใใฎใใผใฟใ่ฆใฆ""" | |
| prompt = "{message}{order}".format(message=message, order=order) | |
| token_info = model.count_tokens(prompt) | |
| print(f"Prompt Token Count: {token_info.total_tokens}") | |
| response = model.generate_content(prompt) | |
| return response.text if response.text else "็ๆๅคฑๆ" | |
| def calendar_main(input_string: str): | |
| try: | |
| parts = input_string.split() | |
| if len(parts) < 3 or parts[0].lower() != 'ไผ่ญฐๅฎค': | |
| print("\n โโโโโโโ ไผ่ญฐๅฎคใใใชใใฎใงRETURN \n") | |
| return | |
| # ๋ ์ง ํ์ฑ ๋ฐ ํฌ๋งทํ | |
| date_input = parts[1] | |
| formatted_date= "" | |
| if date_input == "ใใ": | |
| formatted_date = "" | |
| elif len(date_input) != 8 or not date_input.isdigit(): | |
| print("ๆฅไปใฎใใฉใผใใใใๆญฃใใใใใพใใ YYYYMMDD ใซใใฆใใ ใใใ") | |
| return | |
| else: | |
| formatted_date = f"{date_input[:4]}-{date_input[4:6]}-{date_input[6:]}" | |
| # ์ธ์์ ํ์ฑ | |
| capacity_input = parts[2] | |
| if not capacity_input.isdigit(): | |
| print("ไบบๆฐใฏๆฐๅญใฎใฟๅ ฅๅใใฆใใ ใใใ") | |
| return | |
| min_capacity = int(capacity_input) | |
| # 4๋ฒ์งธ ์ธ์๊ฐ ์์ผ๋ฉด ์ทจ๋ | |
| optional_arg = "" | |
| if len(parts) > 3: | |
| optional_arg = " ".join(parts[3:]) | |
| print(f"่ฟฝๅ ใชใใทใงใณ: '{optional_arg}'") | |
| except Exception as e: | |
| print(f"ใจใฉใผ็บ็: {e}") | |
| return | |
| free_rooms = check_room_free_slots_over_1h(formatted_date) | |
| print(f"\n โโโโโโโ response {free_rooms} \n") | |
| if free_rooms: | |
| response = search_calendar(free_rooms, f"{formatted_date}ใซ{min_capacity}ๅใๅ ฅใใไผ่ญฐๅฎคใๆใใฆใใ ใใ{optional_arg}ใๅ็ญใฏใทใณใใซใซ[ไผ่ญฐๅฎคๅ] ๅฎๅก ็ฉบใๆ้ใฎใใฉใผใใใใงใใฆใใ ใใ") | |
| return response | |
| else: | |
| return | |
| async def home(): | |
| return "Hello, FastAPI! ์ด๊ฑด ๋ฃจํธ ๊ฒฝ๋ก์ ๋๋ค." | |
| async def handle_chatwork_webhook( | |
| request: Request, | |
| # ๐ str | None ๋์ Optional[str]์ ์ฌ์ฉํ๋๋ก ์์ ํฉ๋๋ค. | |
| signature: Optional[str] = Header(None, alias="X-ChatWorkWebhookSignature") | |
| ): | |
| """ | |
| Chatwork ์นํ ์ ์์ ํ๊ณ ์๋ช ์ ๊ฒ์ฆํ ๋ค ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. | |
| """ | |
| if not signature: | |
| raise HTTPException(status_code=400, detail="Signature header is missing.") | |
| # ์์ฒญ์ ์๋ณธ ๋ณธ๋ฌธ(raw body)์ ๊ฐ์ ธ์ต๋๋ค. | |
| raw_body = await request.body() | |
| # --- ์๋ช ๊ฒ์ฆ ๋ก์ง --- | |
| # 1. ์นํ ํ ํฐ์ base64๋ก ๋์ฝ๋ฉํฉ๋๋ค. | |
| secret_key = base64.b64decode(CHATWORK_WEBHOOK_TOKEN) | |
| # 2. HMAC-SHA256 ํด์๋ฅผ ์์ฑํฉ๋๋ค. | |
| digest = hmac.new(secret_key, raw_body, hashlib.sha256).digest() | |
| # 3. ๊ณ์ฐ๋ ํด์๋ฅผ base64๋ก ์ธ์ฝ๋ฉํฉ๋๋ค. | |
| computed_signature = base64.b64encode(digest).decode() | |
| # 4. ์์ ํ ์๋ช ๊ณผ ๊ณ์ฐ๋ ์๋ช ์ด ์ผ์นํ๋์ง ํ์ธํฉ๋๋ค. | |
| if not hmac.compare_digest(computed_signature, signature): | |
| raise HTTPException(status_code=401, detail="Invalid signature.") | |
| # ์๋ช ์ด ์ ํจํ๋ฉด, ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ํ์ฑํ์ฌ ์ฒ๋ฆฌํฉ๋๋ค. | |
| data = await request.json() | |
| # ๋ก๊ทธ์ ์์ ๋ ๋ฐ์ดํฐ ์ถ๋ ฅ (์๋ฒ ๋ก๊ทธ์์ ํ์ธ ๊ฐ๋ฅ) | |
| print("โ Webhook received and verified:") | |
| print(data) | |
| # --- ๐ ๋ต์ฅ ๋ฉ์์ง ์ ์ก ๋ก์ง ์ถ๊ฐ --- | |
| try: | |
| # ์นํ ํ์ด๋ก๋์์ ํ์ํ ์ ๋ณด ์ถ์ถ | |
| event = data.get("webhook_event", {}) | |
| room_id = event.get("room_id") | |
| from_account_id = event.get("account_id") | |
| message_id = event.get("message_id") | |
| message_body = event.get("body") | |
| calendar_data = "" | |
| if message_body: | |
| # strip()์ ์ฌ์ฉํ์ฌ ์๋ค ๊ณต๋ฐฑ ๋ฐ ์ค๋ฐ๊ฟ ๋ฌธ์๋ฅผ ์ ๊ฑฐํ๊ณ ์ถ๋ ฅํฉ๋๋ค. | |
| cleaned_message = message_body.strip() | |
| if cleaned_message[0] != 'ไผ': | |
| print("ไผ่ญฐๅฎคใฎๆค็ดขใณใใณใใงใฏใใใพใใใ") | |
| print(cleaned_message[0]) | |
| return | |
| if cleaned_message: | |
| calendar_data = calendar_main(cleaned_message) | |
| if not calendar_data: | |
| return | |
| if room_id and from_account_id and message_id: | |
| # Chatwork API ์๋ํฌ์ธํธ | |
| url = f"https://api.chatwork.com/v2/rooms/{room_id}/messages" | |
| # API ์์ฒญ ํค๋ | |
| headers = {"X-ChatWorkToken": CHATWORK_API_TOKEN} | |
| # ๋ณด๋ผ ๋ฉ์์ง ๋ณธ๋ฌธ (์์ ํ์ธ ๋ต์ฅ) | |
| # [rp] ํ๊ทธ๋ฅผ ์ฌ์ฉํ๋ฉด ํน์ ๋ฉ์์ง์ ๋ํ ๋ต์ฅ ํ์์ผ๋ก ๋ณด๋ผ ์ ์์ต๋๋ค. | |
| reply_body = ( | |
| f"[rp aid={from_account_id} to={room_id}-{message_id}]" | |
| f"\nโ {calendar_data}" | |
| ) | |
| payload = {"body": reply_body} | |
| # Chatwork API๋ก ๋ฉ์์ง ์ ์ก ์์ฒญ | |
| response = requests.post(url, headers=headers, data=payload) | |
| response.raise_for_status() # ์์ฒญ์ด ์คํจํ๋ฉด ์์ธ ๋ฐ์ | |
| print(f"โ Replied to room {room_id} successfully.") | |
| except Exception as e: | |
| print(f"โ Failed to send reply message: {e}") | |
| # ๋ต์ฅ ์ ์ก์ ์คํจํ๋๋ผ๋ ์นํ ์์ ์์ฒด๋ ์ฑ๊ณตํ์ผ๋ฏ๋ก ์ค๋ฅ๋ฅผ ๋ฐํํ์ง ์์ ์ ์์ต๋๋ค. | |
| # Chatwork์ ์ฑ๊ณต์ ์ผ๋ก ์์ ํ์์ ์๋ฆผ | |
| return {"status": "success"} | |
| get_oauth_credentials() |