ch-bot / app.py
xenon4646's picture
Upload 4 files
d3499fa verified
import os
import time
import requests
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
# ===== 設定(環境変数から取得)=====
CHANNEL_ID = os.getenv("CHANNEL_ID", "")
GROUP_CHAT_ID = os.getenv("GROUP_CHAT_ID", "")
X_ACCOUNT_TOKEN = os.getenv("X_ACCOUNT_TOKEN", "")
JST = ZoneInfo("Asia/Tokyo")
BASE_URL = f"https://desk-api.channel.io/desk/channels/{CHANNEL_ID}"
GET_MESSAGES_URL = f"{BASE_URL}/groups/{GROUP_CHAT_ID}/messages"
POST_MESSAGE_URL = GET_MESSAGES_URL
HEADERS_GET = {
"accept": "application/json",
"accept-language": "ja",
"x-account": X_ACCOUNT_TOKEN,
}
HEADERS_POST = {
"accept": "application/json",
"accept-language": "ja",
"content-type": "application/json",
"x-account": X_ACCOUNT_TOKEN,
}
# ===== 設定値 =====
RANKING_KEYWORD = "あけおめ"
MAX_PARTICIPANTS = 6
MONITOR_DURATION_MINUTES = 10
# ===== ユーティリティ関数 =====
def parse_updated_at(value):
"""メッセージのupdatedAtをパースしてdatetimeを返す"""
if isinstance(value, (int, float)):
return datetime.fromtimestamp(value / 1000, tz=JST)
elif isinstance(value, str):
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
return dt.astimezone(JST)
return None
def get_messages():
"""グループチャットのメッセージを取得"""
params = {
"sortOrder": "asc",
"limit": 100,
"logFolded": "false",
}
try:
res = requests.get(
GET_MESSAGES_URL,
headers=HEADERS_GET,
params=params,
timeout=30,
)
res.raise_for_status()
return res.json().get("messages", [])
except Exception as e:
print(f"メッセージ取得エラー: {e}")
return []
def post_message(text):
"""グループチャットにメッセージを送信"""
payload = {
"requestId": f"desk-web-{int(time.time() * 1000)}",
"blocks": [
{"type": "text", "value": text}
],
}
try:
res = requests.post(
POST_MESSAGE_URL,
headers=HEADERS_POST,
json=payload,
timeout=30,
)
res.raise_for_status()
print("メッセージ送信成功")
return True
except Exception as e:
print(f"メッセージ送信エラー: {e}")
return False
def create_ranking_message(participants):
"""ランキングメッセージを作成"""
if not participants:
return "🏆 あけおめランキング 🏆\n\n参加者はいませんでした。"
lines = ["🏆 あけおめランキング 🏆\n"]
medals = ["🥇", "🥈", "🥉"]
for i, p in enumerate(participants):
rank = i + 1
medal = medals[rank - 1] if rank <= 3 else f"{rank}位"
time_str = p["time"].strftime("%H:%M:%S")
name = p.get("name", "不明")
lines.append(f"{medal} {name} ({time_str})")
lines.append(f"\n参加人数: {len(participants)}人")
return "\n".join(lines)
def get_user_name(msg):
"""メッセージからユーザー名を取得"""
# Channel.ioのメッセージ構造からユーザー名を抽出
# personName または name フィールドを使用
name = msg.get("personName") or msg.get("name") or "不明"
return name
# ===== メイン処理 =====
def wait_until_midnight():
"""次の0:00まで待機"""
now = datetime.now(JST)
tomorrow = now + timedelta(days=1)
midnight = tomorrow.replace(hour=0, minute=0, second=0, microsecond=0)
wait_seconds = (midnight - now).total_seconds()
if wait_seconds > 0:
print(f"次の0:00まで {int(wait_seconds)}秒 待機します...")
time.sleep(wait_seconds)
def monitor_and_rank():
"""0:00から10分間監視し、あけおめランキングを作成"""
print("あけおめ監視を開始します...")
participants = []
processed_ids = set()
start_time = datetime.now(JST)
end_time = start_time + timedelta(minutes=MONITOR_DURATION_MINUTES)
print(f"監視期間: {start_time.strftime('%Y-%m-%d %H:%M:%S')}{end_time.strftime('%H:%M:%S')}")
while datetime.now(JST) < end_time:
# 現在時刻が0:00以降かつ監視期間内かチェック
now = datetime.now(JST)
# メッセージを取得
messages = get_messages()
for msg in messages:
msg_id = msg.get("id")
if msg_id in processed_ids:
continue
plain_text = msg.get("plainText", "")
updated_at = msg.get("updatedAt")
if not plain_text or not updated_at:
continue
# メッセージ時刻をパース
msg_time = parse_updated_at(updated_at)
if not msg_time:
continue
# 0:00以降のメッセージかチェック
midnight_today = now.replace(hour=0, minute=0, second=0, microsecond=0)
if msg_time < midnight_today:
processed_ids.add(msg_id)
continue
# 「あけおめ」を含むかチェック
if RANKING_KEYWORD in plain_text:
user_name = get_user_name(msg)
# 既に参加者に含まれていないかチェック
if not any(p["id"] == msg_id for p in participants):
participants.append({
"id": msg_id,
"name": user_name,
"time": msg_time,
})
print(f"参加者を検知: {user_name} ({msg_time.strftime('%H:%M:%S')})")
processed_ids.add(msg_id)
else:
processed_ids.add(msg_id)
# 6人達成チェック
if len(participants) >= MAX_PARTICIPANTS:
print(f"{MAX_PARTICIPANTS}人達成!ランキングを発表します。")
break
# 少し待機
time.sleep(5)
# ランキング発表
ranking_msg = create_ranking_message(participants)
post_message(ranking_msg)
print("ランキング発表完了")
def check_config():
"""設定が正しいかチェック"""
missing = []
if not CHANNEL_ID:
missing.append("CHANNEL_ID")
if not GROUP_CHAT_ID:
missing.append("GROUP_CHAT_ID")
if not X_ACCOUNT_TOKEN:
missing.append("X_ACCOUNT_TOKEN")
if missing:
print("エラー: 以下の環境変数が設定されていません:")
for var in missing:
print(f" - {var}")
return False
return True
def main():
"""メインループ"""
print("あけおめランキングボットを起動します...")
if not check_config():
print("設定エラーのため終了します。")
return
print("設定確認OK。毎日0:00からの監視を開始します。")
while True:
try:
# 0:00まで待機
wait_until_midnight()
# 監視とランキング作成
monitor_and_rank()
# 少し待機してから次のループへ
time.sleep(60)
except KeyboardInterrupt:
print("ボットを停止します。")
break
except Exception as e:
print(f"エラーが発生しました: {e}")
time.sleep(10)
if __name__ == "__main__":
main()