izuemon commited on
Commit
505155d
·
verified ·
1 Parent(s): 23496b2

Update join.py

Browse files
Files changed (1) hide show
  1. join.py +370 -177
join.py CHANGED
@@ -1,215 +1,408 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
-
4
- import os
5
  import time
6
  import requests
7
- from typing import List, Dict, Any, Optional
8
-
9
- # ===== 設定 =====
10
- X_ACCOUNT = os.getenv("dmsendertoken")
11
- if not X_ACCOUNT:
12
- raise RuntimeError("環境変数 dmsendertoken が設定されていません")
13
-
14
- BASE = "https://desk-api.channel.io/desk/channels/240996"
15
- GROUP_ID = 574317
16
- CHECK_PERSON_ID = "655217"
17
-
18
- # accountIdベースのブラックリスト
19
- BLACKLIST_ACCOUNT_IDS = ["486092", "471246"]
20
 
21
  HEADERS = {
22
- "accept": "application/json",
23
  "accept-language": "ja",
24
- "content-type": "application/json",
25
- "x-account": X_ACCOUNT,
26
  }
27
 
28
- GET_PARAMS = {
29
- "sortOrder": "desc",
30
- "limit": 34,
31
- "logFolded": "false",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
- WELCOME_TEMPLATE = """
35
- こんにちは。ITRSAにようこそ。
36
- 自動でいくつかの部屋に招待しています。
37
- <b>アイコンや名前の設定方法:</b>
38
- 左下の自身のアイコン画像をクリックし、「個人設定」から変更できます。
39
- <b>使い方:</b>
40
- 左側のメニューから、部屋(グループ)を開くことができます。
41
- ・「あいてぃーあーるえすえい」は本部で、雑���などをする部屋です。
42
- ・「Youtubeダウンローダー」は、URLを送ると自動でYoutubeをダウンロードしてくれる部屋です。
43
- ・「チャッピーくん」は、ChatGPTと話せる部屋です。
44
- ・「幹部と裁判所からの報告」はここを管理する幹部や裁判所から通知が来る部屋です。この部屋は基本的に抜けないでください。
45
- """
46
-
47
- INVITE_GROUPS = [
48
- 583390,
49
- 574628,
50
- 574793,
51
- 574744,
52
- 574717,
53
- ]
54
-
55
- # ===== API =====
56
-
57
- def get_messages() -> List[Dict[str, Any]]:
58
- url = f"{BASE}/groups/{GROUP_ID}/messages"
59
- r = requests.get(url, headers=HEADERS, params=GET_PARAMS, timeout=20)
60
- r.raise_for_status()
61
- return r.json().get("messages", [])
62
 
 
 
63
 
64
- def get_group_titles() -> Dict[int, str]:
65
- url = f"{BASE}/groups?limit=1000"
66
- response = requests.get(url, headers=HEADERS, timeout=20)
67
- response.raise_for_status()
68
 
69
- groups = response.json().get("groups", [])
70
- return {
71
- group.get("id"): group.get("title")
72
- for group in groups
73
- if "id" in group and "title" in group
74
- }
75
 
 
76
 
77
- def get_manager_account_id(person_id: str) -> Optional[str]:
78
- """
79
- personId を使って managers API を取得し、
80
- managers[0].accountId を返す
81
- """
82
- url = f"{BASE}/managers"
83
- params = {
84
- "limit": 1,
85
- "since": person_id
86
- }
87
 
88
- r = requests.get(url, headers=HEADERS, params=params, timeout=20)
89
- r.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- data = r.json()
92
- managers = data.get("managers", [])
93
 
94
- if not managers:
95
- return None
 
 
 
 
96
 
97
- return str(managers[0].get("accountId"))
98
 
 
 
 
99
 
100
- def blacklist_manager(person_id: str) -> None:
101
- """
102
- personId を使って manager を削除
103
- """
104
- url = f"{BASE}/managers/{person_id}"
105
- requests.delete(url, headers=HEADERS, timeout=20).raise_for_status()
106
- payload = {
107
- "requestId": f"desk-web-{int(time.time() * 1000)}",
108
- "blocks": [
109
- {"type": "text", "value": "さようなら。"}
110
- ]
111
- }
112
- url = f"{BASE}/groups/{GROUP_ID}/messages"
113
- requests.post(url, headers=HEADERS, json=payload, timeout=20).raise_for_status()
114
- print(f"[INFO] personId={person_id} を削除しました")
115
 
 
116
 
117
- def post_welcome_message() -> None:
118
- TITLES = get_group_titles()
 
 
119
 
120
- welcome_text = WELCOME_TEMPLATE.format(
121
- hq=TITLES.get(532214, "本部")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  )
123
 
124
- url = f"{BASE}/groups/{GROUP_ID}/messages"
125
- payload = {
126
- "requestId": f"desk-web-{int(time.time() * 1000)}",
127
- "blocks": [
128
- {"type": "text", "value": welcome_text}
129
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
131
 
132
- requests.post(url, headers=HEADERS, json=payload, timeout=20).raise_for_status()
133
- print("[INFO] 歓迎メッセージを送信しました")
134
-
135
-
136
- def invite_person(group_id: int, person_id: str) -> None:
137
- url = f"{BASE}/groups/{group_id}/invite"
138
- params = {"managerIds": person_id}
139
- requests.post(url, headers=HEADERS, params=params, timeout=20).raise_for_status()
140
- print(f"[INFO] personId={person_id} を group {group_id} に招待しました")
141
-
142
-
143
- # ===== ロジック =====
144
 
145
- def process():
146
- messages = get_messages()
147
-
148
- # CHECK_PERSON_ID の最新発言時刻
149
- latest_check_person = max(
150
- (int(m.get("createdAt", 0))
151
- for m in messages
152
- if str(m.get("personId")) == CHECK_PERSON_ID),
153
- default=0
154
- )
155
-
156
- join_targets = []
157
-
158
- for m in messages:
159
- log = m.get("log") or {}
160
- if log.get("action") != "join":
161
- continue
162
-
163
- created_at = int(m.get("createdAt", 0))
164
- person_id = str(m.get("personId", ""))
165
 
166
- # ===== ここを追加 =====
167
- # CHECK_PERSON_ID の最新発言より前なら無視
168
- if created_at <= latest_check_person:
169
- continue
170
-
171
- # ===== ブラックリスト判定(accountIdベース)=====
172
  try:
173
- account_id = get_manager_account_id(person_id)
174
- except Exception as e:
175
- print(f"[ERROR] manager取得失敗 personId={person_id} : {e}")
176
- continue
177
-
178
- if account_id and account_id in BLACKLIST_ACCOUNT_IDS:
179
- try:
180
- blacklist_manager(person_id)
181
- except Exception as e:
182
- print(f"[ERROR] 削除失敗 personId={person_id} : {e}")
183
- continue
184
-
185
- # ===== 通常処理 =====
186
- join_targets.append(person_id)
187
- if not join_targets:
188
- return
189
 
190
- join_targets = list(set(join_targets))
 
 
 
 
 
191
 
192
- # 歓迎メッセージは1回だけ
193
- post_welcome_message()
 
 
 
 
194
 
195
- # 招待処理
196
- for pid in join_targets:
197
- for gid in INVITE_GROUPS:
198
- try:
199
- invite_person(gid, pid)
200
- except Exception as e:
201
- print(f"[ERROR] 招待失敗 personId={pid} group={gid} : {e}")
202
 
203
-
204
- def main():
205
- print("[INFO] Bot 起動(10秒間隔)")
206
- while True:
207
- try:
208
- process()
209
  except Exception as e:
210
- print(f"[ERROR] {e}")
211
- time.sleep(10)
212
 
213
 
214
  if __name__ == "__main__":
215
- main()
 
 
 
 
 
 
 
 
 
 
1
  import time
2
  import requests
3
+ import os
4
+ import json
5
+ from datetime import datetime, timedelta
6
+ from zoneinfo import ZoneInfo
7
+ import websocket
8
+ import threading
9
+ import rel
10
+
11
+ BASE_URL = "https://desk-api.channel.io/desk/channels/240996"
12
+ API_URL = "https://api.channel.works/desk/channels/240996"
13
+ TARGET_GROUP_CHAT_ID = "574628"
14
+ WEBSOCKET_URL = "wss://desk-ws.channel.io/socket.io/?platform=web&EIO=4&transport=websocket"
 
15
 
16
  HEADERS = {
 
17
  "accept-language": "ja",
18
+ "x-account": os.getenv("dmsendertoken")
 
19
  }
20
 
21
+ JST = ZoneInfo("Asia/Tokyo")
22
+
23
+ # WebSocket接続管理用
24
+ ws = None
25
+ ws_connected = False
26
+
27
+ # ---------------- 天気コード ----------------
28
+
29
+ WEATHER_CODE_MAP = {
30
+ 0: "雲の発達なしまたは観測なし",
31
+ 1: "雲が消散または弱まる",
32
+ 2: "変化なし",
33
+ 3: "雲が発生または発達",
34
+ 4: "煙による視程低下",
35
+ 5: "煙霧",
36
+ 6: "広範囲の砂塵",
37
+ 7: "風で砂や塵が舞う",
38
+ 8: "砂塵旋風",
39
+ 9: "砂嵐または塵嵐",
40
+ 10: "霧",
41
+ 11: "🌫️(斑状)",
42
+ 12: "🌫️(連続)",
43
+ 13: "⚡",
44
+ 14: "⛆",
45
+ 15: "⛆",
46
+ 16: "⛆",
47
+ 17: "⛈(降水なし)",
48
+ 18: "スコール",
49
+ 19: "漏斗雲",
50
+ 20: "霧雨または氷晶",
51
+ 21: "⛆",
52
+ 22: "❄",
53
+ 23: "雨と雪または氷あられ",
54
+ 24: "凍る雨または霧雨",
55
+ 25: "にわか雨",
56
+ 26: "にわか雪または雨雪",
57
+ 27: "にわか雹",
58
+ 28: "🌫️",
59
+ 29: "⛈",
60
+ 30: "弱い砂嵐(減少)",
61
+ 31: "弱い砂嵐(変化なし)",
62
+ 32: "弱い砂嵐(増加)",
63
+ 33: "激しい砂嵐(減少)",
64
+ 34: "激しい砂嵐(変化なし)",
65
+ 35: "激しい砂嵐(増加)",
66
+ 36: "弱い地吹雪(低)",
67
+ 37: "激しい地吹雪(低)",
68
+ 38: "弱い地吹雪(高)",
69
+ 39: "激しい地吹雪(高)",
70
+ 40: "遠方の霧",
71
+ 41: "斑状の霧",
72
+ 42: "霧(薄くなった・空見える)",
73
+ 43: "霧(薄くなった・空見えない)",
74
+ 44: "霧(変化なし・空見える)",
75
+ 45: "霧(変化なし・空見えない)",
76
+ 46: "霧(濃くなった・空見える)",
77
+ 47: "霧(濃くなった・空見えない)",
78
+ 48: "着氷性の霧(空見える)",
79
+ 49: "着氷性の霧(空見えない)",
80
+ 50: "弱い霧雨(断続)",
81
+ 51: "弱い霧雨(連続)",
82
+ 52: "中程度の霧雨(断続)",
83
+ 53: "中程度の霧雨(連続)",
84
+ 54: "強い霧雨(断続)",
85
+ 55: "強い霧雨(連続)",
86
+ 56: "凍る霧雨(弱い)",
87
+ 57: "凍る霧雨(強い)",
88
+ 58: "霧雨と雨(弱い)",
89
+ 59: "霧雨と雨(強い)",
90
+ 60: "弱い雨(断続)",
91
+ 61: "弱い雨(連続)",
92
+ 62: "中程度の雨(断続)",
93
+ 63: "中程度の雨(連続)",
94
+ 64: "強い雨(断続)",
95
+ 65: "強い雨(連続)",
96
+ 66: "凍る雨(弱い)",
97
+ 67: "凍る雨(強い)",
98
+ 68: "雨または霧雨と雪(弱い)",
99
+ 69: "雨または霧雨と雪(強い)",
100
+ 70: "弱い雪(断続)",
101
+ 71: "弱い雪(連続)",
102
+ 72: "中程度の雪(断続)",
103
+ 73: "中程度の雪(連続)",
104
+ 74: "強い雪(断続)",
105
+ 75: "強い雪(連続)",
106
+ 76: "ダイヤモンドダスト",
107
+ 77: "氷晶",
108
+ 78: "星形雪結晶",
109
+ 79: "氷あられ",
110
+ 80: "弱いにわか雨",
111
+ 81: "強いにわか雨",
112
+ 82: "猛烈なにわか雨",
113
+ 83: "弱い雨雪にわか",
114
+ 84: "強い雨雪にわか",
115
+ 85: "弱いにわか雪",
116
+ 86: "強いにわか雪",
117
+ 87: "弱い氷あられにわか",
118
+ 88: "強い氷あられにわか",
119
+ 89: "弱い雹にわか",
120
+ 90: "強い雹にわか",
121
+ 91: "弱い雨+直前雷雨",
122
+ 92: "強い雨+直前雷雨",
123
+ 93: "弱い雪等+直前雷雨",
124
+ 94: "強い雪等+直前雷雨",
125
+ 95: "雷雨(雹なし)",
126
+ 96: "雷雨(雹あり)",
127
+ 97: "激しい雷雨(雹なし)",
128
+ 98: "雷雨+砂嵐",
129
+ 99: "激しい雷雨(雹あり)"
130
  }
131
 
132
+ def format_weather_code(code: int) -> str:
133
+ if code < 10:
134
+ return WEATHER_CODE_MAP.get(code, str(code))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
+ main = code // 10
137
+ sub = code % 10
138
 
139
+ main_str = WEATHER_CODE_MAP.get(main)
140
+ sub_str = WEATHER_CODE_MAP.get(sub)
 
 
141
 
142
+ if main_str and sub_str:
143
+ return f"{main_str}、{sub_str}"
 
 
 
 
144
 
145
+ return str(code)
146
 
147
+ # ---------------- WebSocket処理 ----------------
 
 
 
 
 
 
 
 
 
148
 
149
+ def on_message(ws, message):
150
+ """WebSocketメッセージ受信時の処理"""
151
+ global ws_connected
152
+
153
+ try:
154
+ # Socket.IOプロトコルの処理
155
+ if message == "2":
156
+ # ping対応
157
+ ws.send("3")
158
+ return
159
+
160
+ if message == "3":
161
+ # pong
162
+ return
163
+
164
+ if message.startswith("0{"):
165
+ # 接続確立時の初期メッセージ
166
+ # 認証情報を送信
167
+ jwt_token = os.getenv("dmsendertoken")
168
+ auth_message = f'40/desk/channel,{{"channelId":"240996","jwt":"{jwt_token}"}}'
169
+ ws.send(auth_message)
170
+ ws_connected = True
171
+ print("WebSocket: 認証情報を送信しました")
172
+ return
173
+
174
+ if message.startswith("40"):
175
+ # 認証成功
176
+ print("WebSocket: 認証成功")
177
+ return
178
+
179
+ if message.startswith("42"):
180
+ # メッセージデータの処理
181
+ # "42"の後にJSON配列が続く
182
+ json_str = message[2:]
183
+ data = json.loads(json_str)
184
+
185
+ if isinstance(data, list) and len(data) >= 2:
186
+ event_type = data[0]
187
+ event_data = data[1]
188
+
189
+ if event_type == "push":
190
+ # メッセージ受信
191
+ handle_push_event(event_data)
192
+ elif event_type == "create":
193
+ # ユーザー参加
194
+ handle_create_event(event_data)
195
+
196
+ except Exception as e:
197
+ print(f"WebSocket メッセージ処理エラー: {e}")
198
+
199
+ def handle_push_event(event_data):
200
+ """pushイベントの処理"""
201
+ try:
202
+ entity = event_data.get("entity", {})
203
+ chat_id = entity.get("chatId")
204
+ chat_type = entity.get("chatType")
205
+ person_type = entity.get("personType")
206
+ message_text = entity.get("plainText", "")
207
+
208
+ if chat_type == "group" and chat_id == TARGET_GROUP_CHAT_ID:
209
+ print(f"グループメッセージ受信: {message_text}")
210
+ # ここで受信メッセージに対する処理を実装
211
+ # 例: コマンド処理など
212
+
213
+ except Exception as e:
214
+ print(f"push イベント処理エラー: {e}")
215
+
216
+ def handle_create_event(event_data):
217
+ """createイベントの処理(ユーザー参加など)"""
218
+ try:
219
+ entity = event_data.get("entity", {})
220
+ log = entity.get("log", {})
221
+ chat_id = entity.get("chatId")
222
+ person_id = entity.get("personId")
223
+
224
+ if log.get("action") == "join" and chat_id == TARGET_GROUP_CHAT_ID:
225
+ print(f"ユーザーが参加しました: {person_id}")
226
+ # 参加メッセージなどを送信する場合はここで処理
227
+
228
+ except Exception as e:
229
+ print(f"create イベント処理エラー: {e}")
230
+
231
+ def on_error(ws, error):
232
+ print(f"WebSocket エラー: {error}")
233
+
234
+ def on_close(ws, close_status_code, close_msg):
235
+ global ws_connected
236
+ ws_connected = False
237
+ print("WebSocket 接続が閉じられました")
238
+
239
+ def on_open(ws):
240
+ print("WebSocket 接続が開かれました")
241
+
242
+ def start_websocket():
243
+ """WebSocket接続を開始"""
244
+ websocket.enableTrace(False)
245
+ ws = websocket.WebSocketApp(
246
+ WEBSOCKET_URL,
247
+ on_open=on_open,
248
+ on_message=on_message,
249
+ on_error=on_error,
250
+ on_close=on_close
251
+ )
252
+
253
+ # 永続的に接続を維持
254
+ ws.run_forever(
255
+ ping_interval=30,
256
+ ping_timeout=10,
257
+ reconnect=5
258
+ )
259
 
260
+ # ---------------- メッセージ送信 ----------------
 
261
 
262
+ def post_group_message(chat_id, body):
263
+ """WebSocket APIを使用してグループメッセージを送信"""
264
+ url = f"{API_URL}/groups/{chat_id}/messages"
265
+ r = requests.post(url, headers=HEADERS, json=body)
266
+ r.raise_for_status()
267
+ return r.json()
268
 
269
+ # ---------------- 時報 ----------------
270
 
271
+ def create_time_signal_body():
272
+ now = datetime.now(JST)
273
+ hour = now.hour
274
 
275
+ if hour < 12:
276
+ period = "午前"
277
+ display_hour = hour if hour != 0 else 12
278
+ else:
279
+ period = "午後"
280
+ display_hour = hour - 12 if hour > 12 else 12
 
 
 
 
 
 
 
 
 
281
 
282
+ message = f"<b>時報</b>\n{period}{display_hour}時をお知らせします。"
283
 
284
+ return {
285
+ "requestId": f"desk-web-time-signal-{int(time.time())}",
286
+ "blocks": [{"type": "text", "value": message}],
287
+ }
288
 
289
+ # ---------------- 天気予報 ----------------
290
+
291
+ def load_todoufuken():
292
+ with open("todoufuken.json", encoding="utf-8") as f:
293
+ lines = []
294
+ for line in f:
295
+ stripped = line.lstrip()
296
+ if stripped.startswith("//"):
297
+ continue
298
+ lines.append(line)
299
+
300
+ json_str = "".join(lines)
301
+ return json.loads(json_str)
302
+
303
+
304
+ def fetch_weather(todoufuken):
305
+ lats = ",".join(str(p["lat"]) for p in todoufuken)
306
+ lons = ",".join(str(p["lon"]) for p in todoufuken)
307
+
308
+ url = (
309
+ "https://api.open-meteo.com/v1/forecast"
310
+ f"?latitude={lats}"
311
+ f"&longitude={lons}"
312
+ "&hourly=relative_humidity_2m,apparent_temperature,temperature_2m,"
313
+ "precipitation_probability,weather_code"
314
+ "&forecast_days=1"
315
+ "&timezone=Asia%2FTokyo"
316
  )
317
 
318
+ r = requests.get(url)
319
+ r.raise_for_status()
320
+ return r.json()
321
+
322
+
323
+ def create_weather_body():
324
+ now = datetime.now(JST)
325
+ today_7am = now.replace(hour=7, minute=0, second=0, microsecond=0)
326
+
327
+ todoufuken = load_todoufuken()
328
+ data = fetch_weather(todoufuken)
329
+
330
+ lines = ["<b>☀️ 本日の天気予報(7時以降・1時間ごと)</b>"]
331
+
332
+ for idx, pref in enumerate(todoufuken):
333
+ hourly = data[idx]["hourly"]
334
+ times = hourly["time"]
335
+
336
+ lines.append(f"\n<b>{pref['name']}:</b>")
337
+
338
+ TARGET_HOURS = {7, 10, 13, 16, 21}
339
+
340
+ for i, t in enumerate(times):
341
+ t_jst = (
342
+ datetime.fromisoformat(t)
343
+ .replace(tzinfo=ZoneInfo("UTC"))
344
+ .astimezone(JST)
345
+ )
346
+
347
+ if t_jst.hour not in TARGET_HOURS:
348
+ continue
349
+
350
+ wc = hourly["weather_code"][i]
351
+ at = hourly["apparent_temperature"][i]
352
+ temp = hourly["temperature_2m"][i]
353
+ pop = hourly["precipitation_probability"][i]
354
+ rh = hourly["relative_humidity_2m"][i]
355
+
356
+ wc_str = format_weather_code(wc)
357
+
358
+ line = (
359
+ f" {t_jst.strftime('%H時')}:"
360
+ f"{wc_str} "
361
+ f"{at}℃ {temp}℃ {pop}% {rh}%"
362
+ )
363
+ lines.append(line)
364
+ message2 = "☀️・・・晴れ、☁️・・・曇り、🌫️・・・霧・霧雨、🌧️・・・雨、🌨️・・・雪、🌦️・・・にわか雨、⛈️・・・雷雨"
365
+ return {
366
+ "requestId": f"desk-web-weather-{int(time.time())}",
367
+ "blocks": [{"type": "text", "value": "\n".join(lines)},{"type": "text", "value": message2}],
368
  }
369
 
370
+ # ---------------- メインループ ----------------
 
 
 
 
 
 
 
 
 
 
 
371
 
372
+ def main_loop():
373
+ last_time_signal = None
374
+ last_weather = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
+ while True:
 
 
 
 
 
377
  try:
378
+ now = datetime.now(JST)
379
+ current_date = now.date()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
+ # 毎正時の時報
382
+ if now.minute == 0 and last_time_signal != now.hour:
383
+ body = create_time_signal_body()
384
+ post_group_message(TARGET_GROUP_CHAT_ID, body)
385
+ last_time_signal = now.hour
386
+ print(f"時報を送信しました: {now.strftime('%H:%M')}")
387
 
388
+ # 午前11時30分に天気予報
389
+ if now.hour == 11 and now.minute == 30 and last_weather != current_date:
390
+ body = create_weather_body()
391
+ post_group_message(TARGET_GROUP_CHAT_ID, body)
392
+ last_weather = current_date
393
+ print("天気予報を送信しました")
394
 
395
+ time.sleep(30) # 30秒間隔でチェック
 
 
 
 
 
 
396
 
 
 
 
 
 
 
397
  except Exception as e:
398
+ print("Error:", e)
399
+ time.sleep(10)
400
 
401
 
402
  if __name__ == "__main__":
403
+ # WebSocket接続を別スレッドで開始
404
+ ws_thread = threading.Thread(target=start_websocket, daemon=True)
405
+ ws_thread.start()
406
+
407
+ # メインループを実行
408
+ main_loop()