Chandima Prabhath commited on
Commit
bd4dd6a
Β·
1 Parent(s): 71b0af4

Refactor app.py: enhance BotConfig and BotClient classes, streamline environment variable validation, and improve message handling for better clarity and functionality.

Browse files
Files changed (1) hide show
  1. app.py +202 -286
app.py CHANGED
@@ -7,424 +7,348 @@ import re
7
  import json
8
  import time
9
  import random
 
10
  from fastapi import FastAPI, Request, HTTPException
11
  from fastapi.responses import PlainTextResponse, JSONResponse
12
  from FLUX import generate_image
13
  from VoiceReply import generate_voice_reply
14
  from polLLM import generate_llm
15
 
16
- # Configure logging
17
- logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- # Env vars
20
- GREEN_API_URL = os.getenv("GREEN_API_URL")
21
- GREEN_API_MEDIA_URL = os.getenv("GREEN_API_MEDIA_URL", "https://api.green-api.com")
22
- GREEN_API_TOKEN = os.getenv("GREEN_API_TOKEN")
23
- GREEN_API_ID_INSTANCE = os.getenv("GREEN_API_ID_INSTANCE")
24
- WEBHOOK_AUTH_TOKEN = os.getenv("WEBHOOK_AUTH_TOKEN")
25
- BOT_GROUP_CHAT = "[email protected]" # Chat ID for system messages
26
- image_dir = "/tmp/images"
27
- audio_dir = "/tmp/audio"
28
-
29
- if not all([GREEN_API_URL, GREEN_API_TOKEN, GREEN_API_ID_INSTANCE, WEBHOOK_AUTH_TOKEN]):
30
- raise ValueError("Environment variables are not set properly")
31
-
32
- # Queues & in‑memory stores
33
  task_queue = queue.Queue()
34
- trivia_store = {} # chat_id β†’ {"question":…, "answer":…}
35
- polls = {} # chat_id β†’ {"question":…, "options": […], "votes": {1: 0, ...}, "voters": {jid: opt}}
36
 
37
- app = FastAPI()
38
-
39
- # Global inactivity tracker
40
  last_message_time = time.time()
41
 
42
- # --- Inactivity Monitor ---
43
  def inactivity_monitor():
44
  global last_message_time
45
  while True:
46
- time.sleep(60) # check every minute
47
- if time.time() - last_message_time >= 300: # 5 minutes inactivity
48
- if BOT_GROUP_CHAT:
49
- reminder = "⏰ I haven't heard from you in a while! I'm still here if you need anything."
50
- send_message("inactivity", BOT_GROUP_CHAT, reminder)
51
  last_message_time = time.time()
52
 
53
  threading.Thread(target=inactivity_monitor, daemon=True).start()
54
 
55
- # --- Background Worker ---
 
 
56
  def worker():
57
  while True:
58
  task = task_queue.get()
59
  try:
60
- typ = task["type"]
61
- mid = task["message_id"]
62
- cid = task["chat_id"]
63
- if typ == "image":
64
- handle_image_generation(mid, cid, task["prompt"])
65
- elif typ == "audio":
66
- response_audio(mid, cid, task["prompt"])
67
  except Exception as e:
68
- logging.error(f"Error processing {task}: {e}")
69
  finally:
70
  task_queue.task_done()
71
 
72
- threading.Thread(target=worker, daemon=True).start()
 
73
 
74
- # --- send helpers ---
75
- def send_message_to_chat(to_number, message, retries=3):
76
- chat_id = to_number if to_number.endswith("@g.us") else to_number
77
- url = f"{GREEN_API_URL}/waInstance{GREEN_API_ID_INSTANCE}/sendMessage/{GREEN_API_TOKEN}"
78
- payload = {"chatId": chat_id, "message": message}
79
- for i in range(retries):
80
- try:
81
- r = requests.post(url, json=payload)
82
- r.raise_for_status()
83
- return r.json()
84
- except requests.RequestException as e:
85
- if i == retries - 1:
86
- logging.error("send_message_to_chat failed: %s", str(e))
87
- return {"error": str(e)}
88
-
89
- def send_message(message_id, to_number, message, retries=3):
90
- chat_id = to_number if to_number.endswith("@g.us") else to_number
91
- url = f"{GREEN_API_URL}/waInstance{GREEN_API_ID_INSTANCE}/sendMessage/{GREEN_API_TOKEN}"
92
- payload = {"chatId": chat_id, "message": message, "quotedMessageId": message_id}
93
- for i in range(retries):
94
- try:
95
- r = requests.post(url, json=payload)
96
- r.raise_for_status()
97
- return r.json()
98
- except requests.RequestException as e:
99
- if i == retries - 1:
100
- return {"error": str(e)}
101
-
102
- def send_image(message_id, to_number, image_path, caption="Here you go!", retries=3):
103
- chat_id = to_number if to_number.endswith("@g.us") else to_number
104
- url = f"{GREEN_API_MEDIA_URL}/waInstance{GREEN_API_ID_INSTANCE}/sendFileByUpload/{GREEN_API_TOKEN}"
105
- payload = {"chatId": chat_id, "caption": caption, "quotedMessageId": message_id}
106
- files = [("file", ("image.jpg", open(image_path, "rb"), "image/jpeg"))]
107
- for i in range(retries):
108
- try:
109
- r = requests.post(url, data=payload, files=files)
110
- r.raise_for_status()
111
- return r.json()
112
- except requests.RequestException as e:
113
- if i == retries - 1:
114
- return {"error": str(e)}
115
-
116
- def send_audio(message_id, to_number, audio_path, retries=3):
117
- logging.debug("send_audio")
118
- chat_id = to_number if to_number.endswith("@g.us") else to_number
119
- if not os.path.exists(audio_path):
120
- logging.debug(f"Missing audio: {audio_path}")
121
- url = f"{GREEN_API_MEDIA_URL}/waInstance{GREEN_API_ID_INSTANCE}/sendFileByUpload/{GREEN_API_TOKEN}"
122
- payload = {"chatId": chat_id, "caption": "Here is your voice reply!", "quotedMessageId": message_id}
123
- try:
124
- with open(audio_path, "rb") as f:
125
- files = [("file", ("audio.mp3", f, "audio/mpeg"))]
126
- for i in range(retries):
127
- try:
128
- r = requests.post(url, data=payload, files=files)
129
- r.raise_for_status()
130
- return r.json()
131
- except requests.RequestException as e:
132
- if i == retries - 1:
133
- return {"error": str(e)}
134
- except Exception as e:
135
- return {"error": str(e)}
136
 
137
- # --- core response functions ---
138
  def response_text(message_id, chat_id, prompt):
139
  try:
140
  msg = generate_llm(prompt)
141
- send_message(message_id, chat_id, msg)
142
- except Exception:
143
- send_message(message_id, chat_id, "Error processing your request.")
 
144
 
145
  def response_audio(message_id, chat_id, prompt):
146
- logging.debug("response_audio prompt=%s", prompt)
147
  try:
148
- result = generate_voice_reply(prompt, model="openai-audio", voice="coral", audio_dir=audio_dir)
149
  if result and result[0]:
150
  audio_path, _ = result
151
- send_audio(message_id, chat_id, audio_path)
152
- if os.path.exists(audio_path):
153
- os.remove(audio_path)
154
  else:
155
  response_text(message_id, chat_id, prompt)
156
  except Exception as e:
157
- logging.debug("audio error: %s", e)
158
- send_message(message_id, chat_id, "Error generating audio. Try again later.")
159
 
160
  def handle_image_generation(message_id, chat_id, prompt):
161
- for i in range(4):
 
162
  try:
163
- img, path, ret_prompt, url = generate_image(prompt, message_id, message_id, image_dir)
164
  if img:
165
- formatted_ret_prompt = "\n\n".join(
166
- f"_{paragraph.strip()}_" for paragraph in ret_prompt.split("\n\n") if paragraph.strip()
167
- )
168
- send_image(
169
- message_id,
170
- chat_id,
171
- path,
172
- caption=f"✨ Image ready: {url}\n>{chr(8203)} {formatted_ret_prompt}"
173
- )
174
  else:
175
- send_message(message_id, chat_id, "Image generation failed.")
176
  except Exception as e:
177
- logging.error("Error in handle_image_generation: %s", e)
178
- send_message(message_id, chat_id, "Error generating image.")
 
 
 
 
 
179
 
180
- # --- Startup Message ---
181
  def send_startup_message():
182
- if BOT_GROUP_CHAT:
183
- startup_msg = "🌟 Hi! I'm Eve, your friendly AI assistant. I'm now live and ready to help with images, voice replies, and more!"
184
- resp = send_message_to_chat(BOT_GROUP_CHAT, startup_msg)
185
- if "error" in resp:
186
- logging.error("Startup message failed: %s", resp["error"])
187
- else:
188
- logging.warning("BOT_GROUP_CHAT is not set; startup message not sent.")
 
189
 
190
  help_text = (
191
- "πŸ€– *Hi there, I'm Eve!* Here are the commands you can use:\n\n"
192
- "β€’ */help* – _Show this help message._\n"
193
- "β€’ */summarize <text>* – _Get a quick summary of your text._\n"
194
- "β€’ */translate <language>|<text>* – _Translate text to your chosen language._\n"
195
- "β€’ */joke* – _Enjoy a random, funny joke._\n"
196
- "β€’ */weather <location>* – _Get the current weather for a location._\n"
197
- "β€’ */inspire* – _Receive a short inspirational quote._\n"
198
- "β€’ */trivia* – _Start a new trivia question._\n"
199
- "β€’ */answer [your answer]* – _Reveal the trivia answer or check your answer if provided._\n"
200
- "β€’ */meme <text>* – _Generate a fun meme image._\n"
201
- "β€’ */poll <Question>|<Option1>|<Option2>|…* – _Create a poll._\n"
202
- "β€’ */results* – _See current poll results._\n"
203
- "β€’ */endpoll* – _End the poll and show final results._\n"
204
- "β€’ */gen <prompt>* – _Generate an image from your prompt._\n\n"
205
- "Send any other text and I'll reply with a voice message. I'm here to help, so don't hesitate to ask!"
206
- )
207
 
208
- # --- Webhook ---
209
  @app.post("/whatsapp")
210
  async def whatsapp_webhook(request: Request):
211
  global last_message_time
212
  last_message_time = time.time()
213
 
214
- auth = request.headers.get("Authorization", "").strip()
215
- if auth != f"Bearer {WEBHOOK_AUTH_TOKEN}":
 
216
  raise HTTPException(403, "Unauthorized")
217
- try:
218
- data = await request.json()
219
- chat_id = data["senderData"]["chatId"]
220
- print(f"New message from chat ID: {chat_id}")
221
- except:
222
- return JSONResponse({"error": "Invalid JSON"}, status_code=400)
223
- if data.get("typeWebhook") != "incomingMessageReceived":
224
  return {"success": True}
225
 
226
- logging.debug("recv: %s", data)
227
- sd = data["senderData"]
228
- chat = sd["chatId"]
229
- mid = data["idMessage"]
230
- sender_jid = sd.get("sender")
231
 
232
  md = data.get("messageData", {})
233
- if md.get("typeMessage") == "quotedMessage" or "quotedMessage" in md:
234
- logging.debug("skip native quotedMessage")
235
- return {"success": True}
236
 
 
237
  if "textMessageData" in md:
238
- body = md["textMessageData"].get("textMessage", "").strip()
239
- ctx = md["textMessageData"].get("contextInfo", {})
240
  elif "extendedTextMessageData" in md:
241
- body = md["extendedTextMessageData"].get("text", "").strip()
242
- ctx = md["extendedTextMessageData"].get("contextInfo", {})
243
  else:
244
  return {"success": True}
245
 
246
- if ctx.get("mentionedJid") or ctx.get("mentionedJidList"):
247
- return {"success": True}
248
- if chat.endswith("@g.us") and re.search(r"@\d+", body):
249
- return {"success": True}
250
-
251
  low = body.lower()
252
 
253
- # --- New Commands ---
254
  if low == "/help":
255
- send_message(mid, chat, help_text)
256
  return {"success": True}
257
 
258
  if low.startswith("/summarize "):
259
  txt = body[len("/summarize "):].strip()
260
  summary = generate_llm(f"Summarize this text in one short paragraph:\n\n{txt}")
261
- send_message(mid, chat, summary)
262
  return {"success": True}
263
 
264
  if low.startswith("/translate "):
265
  part = body[len("/translate "):]
266
  if "|" not in part:
267
- send_message(mid, chat, "Please use `/translate <language>|<text>`")
268
  else:
269
  lang, txt = part.split("|", 1)
270
  resp = generate_llm(f"Translate the following into {lang.strip()}:\n\n{txt.strip()}")
271
- send_message(mid, chat, resp)
272
  return {"success": True}
273
 
274
  if low == "/joke":
275
  try:
276
  joke = requests.get("https://official-joke-api.appspot.com/random_joke", timeout=5).json()
277
- send_message(mid, chat, f"{joke['setup']}\n\n{joke['punchline']}")
278
  except:
279
- send_message(mid, chat, generate_llm("Tell me a short, funny joke."))
280
  return {"success": True}
281
 
282
  if low.startswith("/weather "):
283
  loc = body[len("/weather "):].strip().replace(" ", "+")
284
  try:
285
  w = requests.get(f"http://sl.wttr.in/{loc}?format=4", timeout=5).text
286
- send_message(mid, chat, w)
287
  except:
288
- send_message(mid, chat, "Could not fetch weather.")
289
  return {"success": True}
290
 
291
  if low == "/inspire":
292
  quote = generate_llm("Give me a short inspirational quote.")
293
- send_message(mid, chat, f"✨ {quote}")
294
  return {"success": True}
295
 
296
- # TRIVIA
297
  if low == "/trivia":
298
- randomSeed = random.randint(0, 9999999)
299
  raw = generate_llm(
300
- (f"Generate a unique and random trivia question and answer based on this random seed {randomSeed} to make it unique in JSON format. "
301
- "The output should strictly follow this example format without extra text:\n\n"
302
- "{\"question\": \"What is the capital of France?\", \"answer\": \"Paris\"}")
303
  )
304
- def extract_json(text):
305
- import re
306
- match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
307
- if match:
308
- return match.group(1)
309
- return text
310
  try:
311
- json_text = extract_json(raw)
312
- obj = json.loads(json_text)
313
- if "question" in obj and "answer" in obj:
314
- trivia_store[chat] = obj
315
- send_message(mid, chat, f"❓ {obj['question']}\nReply with `/answer` followed by your answer (if you want to check it) or just `/answer` to reveal the correct answer.")
316
- else:
317
- raise ValueError("Missing expected keys.")
318
- except Exception as e:
319
- logging.error("Trivia JSON parse error: %s, raw response: %s", e, raw)
320
- send_message(mid, chat, "Failed to generate trivia. Please try again.")
321
  return {"success": True}
322
 
323
- # ANSWER: Accept any message starting with /answer. If additional text is provided, check it.
324
  if low.startswith("/answer"):
325
- # Remove command and any extra spaces
326
- user_response = body[len("/answer"):].strip()
327
- if chat in trivia_store:
328
- correct_answer = trivia_store[chat]["answer"]
329
- question = trivia_store[chat]["question"]
330
- # If user provided an answer, evaluate it via LLM; otherwise, just reveal the answer.
331
- if user_response:
332
- eval_prompt = (
333
- f"Question: {question}\n"
334
- f"Correct Answer: {correct_answer}\n"
335
- f"User Answer: {user_response}\n"
336
- "Is the user's answer correct? Respond with 'Correct' if yes, or 'Incorrect' if not, and explain briefly."
337
  )
338
- verdict = generate_llm(eval_prompt)
339
- send_message(mid, chat, f"πŸ’‘ {verdict}")
340
  else:
341
- send_message(mid, chat, f"πŸ’‘ Answer: {correct_answer}")
342
- trivia_store.pop(chat, None)
343
  else:
344
- send_message(mid, chat, "No active trivia. Send `/trivia` to start one.")
345
  return {"success": True}
346
 
 
347
  if low.startswith("/meme "):
348
  txt = body[len("/meme "):].strip()
349
- send_message(mid, chat, "🎨 Generating your meme...")
350
- task_queue.put({
351
- "type": "image",
352
- "message_id": mid,
353
- "chat_id": chat,
354
- "prompt": f"meme template with text: {txt}"
355
- })
356
  return {"success": True}
357
 
 
358
  if low.startswith("/poll "):
359
  parts = body[len("/poll "):].split("|")
360
  if len(parts) < 3:
361
- send_message(mid, chat, "Please use `/poll Question|Option1|Option2|...`")
362
  else:
363
- q = parts[0].strip()
364
- opts = [p.strip() for p in parts[1:]]
365
- votes = {i+1: 0 for i in range(len(opts))}
366
- polls[chat] = {"question": q, "options": opts, "votes": votes, "voters": {}}
367
- txt = f"πŸ“Š *Poll:* {q}\n" + "\n".join(
368
- f"{i+1}. {opt}" for i, opt in enumerate(opts)
369
- ) + "\n\nReply with the *option number* to vote."
370
- send_message(mid, chat, txt)
371
  return {"success": True}
372
 
373
- if chat in polls and body.isdigit():
374
- n = int(body)
375
- p = polls[chat]
376
- if 1 <= n <= len(p["options"]):
377
- prev = p["voters"].get(sender_jid)
378
  if prev:
379
- p["votes"][prev] -= 1
380
- p["votes"][n] += 1
381
- p["voters"][sender_jid] = n
382
- send_message(mid, chat, f"βœ… Vote recorded: {p['options'][n-1]}")
383
  return {"success": True}
384
 
385
  if low == "/results":
386
- if chat in polls:
387
- p = polls[chat]
388
- txt = f"πŸ“Š *Results:* {p['question']}\n" + "\n".join(
389
- f"{i}. {opt}: {p['votes'][i]}" for i, opt in enumerate([""] + p["options"]) if i > 0
390
- )
391
- send_message(mid, chat, txt)
392
  else:
393
- send_message(mid, chat, "No active poll.")
394
  return {"success": True}
395
 
396
  if low == "/endpoll":
397
- if chat in polls:
398
- p = polls.pop(chat)
399
- txt = f"πŸ“Š *Final Results:* {p['question']}\n" + "\n".join(
400
- f"{i}. {opt}: {p['votes'][i]}" for i, opt in enumerate([""] + p["options"]) if i > 0
401
- )
402
- send_message(mid, chat, txt)
403
  else:
404
- send_message(mid, chat, "No active poll.")
405
  return {"success": True}
406
 
 
407
  if low.startswith("/gen"):
408
  prompt = body[len("/gen"):].strip()
409
  if not prompt:
410
- send_message(mid, chat, "Please use `/gen <prompt>` to generate an image.")
411
  else:
412
- send_message(mid, chat, "✨ Your image is being generated. Please wait...")
413
- task_queue.put({
414
- "type": "image",
415
- "message_id": mid,
416
- "chat_id": chat,
417
- "prompt": prompt
418
- })
419
  return {"success": True}
420
 
421
- # Fallback: voice reply for any other text
422
- task_queue.put({
423
- "type": "audio",
424
- "message_id": mid,
425
- "chat_id": chat,
426
- "prompt": body
427
- })
428
  return {"success": True}
429
 
430
  @app.get("/", response_class=PlainTextResponse)
@@ -432,13 +356,5 @@ def index():
432
  return "Server is running!"
433
 
434
  if __name__ == "__main__":
435
- # Send startup message on launch
436
- def send_startup_message():
437
- if BOT_GROUP_CHAT:
438
- startup_msg = "🌟 Hi! I'm Eve, your friendly AI assistant. I'm now live and ready to help with images, voice replies, and more!"
439
- send_message_to_chat(BOT_GROUP_CHAT, startup_msg)
440
- else:
441
- logging.warning("BOT_GROUP_CHAT is not set; startup message not sent.")
442
- send_startup_message()
443
  import uvicorn
444
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
7
  import json
8
  import time
9
  import random
10
+ from concurrent.futures import ThreadPoolExecutor
11
  from fastapi import FastAPI, Request, HTTPException
12
  from fastapi.responses import PlainTextResponse, JSONResponse
13
  from FLUX import generate_image
14
  from VoiceReply import generate_voice_reply
15
  from polLLM import generate_llm
16
 
17
+ # --- Configuration and Client Classes ---
18
+
19
+ class BotConfig:
20
+ GREEN_API_URL = os.getenv("GREEN_API_URL")
21
+ GREEN_API_MEDIA_URL = os.getenv("GREEN_API_MEDIA_URL", "https://api.green-api.com")
22
+ GREEN_API_TOKEN = os.getenv("GREEN_API_TOKEN")
23
+ GREEN_API_ID_INSTANCE = os.getenv("GREEN_API_ID_INSTANCE")
24
+ WEBHOOK_AUTH_TOKEN = os.getenv("WEBHOOK_AUTH_TOKEN")
25
+ BOT_GROUP_CHAT = "[email protected]"
26
+ IMAGE_DIR = "/tmp/images"
27
+ AUDIO_DIR = "/tmp/audio"
28
+
29
+ @classmethod
30
+ def validate(cls):
31
+ if not all([cls.GREEN_API_URL, cls.GREEN_API_TOKEN, cls.GREEN_API_ID_INSTANCE, cls.WEBHOOK_AUTH_TOKEN]):
32
+ raise ValueError("Environment variables are not set properly")
33
+
34
+ class BotClient:
35
+ def __init__(self, cfg: BotConfig):
36
+ self.cfg = cfg
37
+ self.session = requests.Session()
38
+ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s")
39
+
40
+ def send(self, endpoint: str, payload: dict, files=None, retries=3):
41
+ url = f"{self.cfg.GREEN_API_URL}/waInstance{self.cfg.GREEN_API_ID_INSTANCE}/{endpoint}/{self.cfg.GREEN_API_TOKEN}"
42
+ for attempt in range(1, retries + 1):
43
+ try:
44
+ resp = self.session.post(url, json=payload if files is None else None, data=None if files is None else payload, files=files)
45
+ resp.raise_for_status()
46
+ return resp.json()
47
+ except requests.RequestException as e:
48
+ logging.warning(f"Attempt {attempt}/{retries} failed for {endpoint}: {e}")
49
+ if attempt == retries:
50
+ logging.error(f"{endpoint} ultimately failed: {e}")
51
+ return {"error": str(e)}
52
+
53
+ def send_message(self, message_id: str, chat_id: str, text: str):
54
+ payload = {"chatId": chat_id, "message": text, "quotedMessageId": message_id}
55
+ return self.send(f"sendMessage", payload)
56
+
57
+ def send_message_to(self, chat_id: str, text: str):
58
+ payload = {"chatId": chat_id, "message": text}
59
+ return self.send(f"sendMessage", payload)
60
+
61
+ def send_media(self, message_id: str, chat_id: str, file_path: str, caption: str, media_type: str):
62
+ endpoint = "sendFileByUpload"
63
+ payload = {"chatId": chat_id, "caption": caption, "quotedMessageId": message_id}
64
+ with open(file_path, "rb") as f:
65
+ mime = "image/jpeg" if media_type == "image" else "audio/mpeg"
66
+ files = [("file", (os.path.basename(file_path), f, mime))]
67
+ return self.send(endpoint, payload, files=files)
68
+
69
+ # Validate env
70
+ BotConfig.validate()
71
+ client = BotClient(BotConfig)
72
+
73
+ # --- Queues, stores, and threading ---
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  task_queue = queue.Queue()
76
+ trivia_store = {}
77
+ polls = {}
78
 
 
 
 
79
  last_message_time = time.time()
80
 
81
+ # Inactivity monitor
82
  def inactivity_monitor():
83
  global last_message_time
84
  while True:
85
+ time.sleep(60)
86
+ if time.time() - last_message_time >= 300:
87
+ client.send_message_to(BotConfig.BOT_GROUP_CHAT,
88
+ "⏰ I haven't heard from you in a while! I'm still here if you need anything.")
 
89
  last_message_time = time.time()
90
 
91
  threading.Thread(target=inactivity_monitor, daemon=True).start()
92
 
93
+ # Worker pool
94
+ executor = ThreadPoolExecutor(max_workers=4)
95
+
96
  def worker():
97
  while True:
98
  task = task_queue.get()
99
  try:
100
+ if task["type"] == "image":
101
+ handle_image_generation(task["message_id"], task["chat_id"], task["prompt"])
102
+ elif task["type"] == "audio":
103
+ response_audio(task["message_id"], task["chat_id"], task["prompt"])
 
 
 
104
  except Exception as e:
105
+ logging.error(f"Error in worker for task {task}: {e}")
106
  finally:
107
  task_queue.task_done()
108
 
109
+ for _ in range(4):
110
+ threading.Thread(target=worker, daemon=True).start()
111
 
112
+ # --- Core response functions ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
 
114
  def response_text(message_id, chat_id, prompt):
115
  try:
116
  msg = generate_llm(prompt)
117
+ client.send_message(message_id, chat_id, msg)
118
+ except Exception as e:
119
+ logging.error(f"LLM error: {e}")
120
+ client.send_message(message_id, chat_id, "Error processing your request.")
121
 
122
  def response_audio(message_id, chat_id, prompt):
 
123
  try:
124
+ result = generate_voice_reply(prompt, model="openai-audio", voice="coral", audio_dir=BotConfig.AUDIO_DIR)
125
  if result and result[0]:
126
  audio_path, _ = result
127
+ client.send_media(message_id, chat_id, audio_path, "", media_type="audio")
128
+ os.remove(audio_path)
 
129
  else:
130
  response_text(message_id, chat_id, prompt)
131
  except Exception as e:
132
+ logging.error(f"Audio error: {e}")
133
+ client.send_message(message_id, chat_id, "Error generating audio. Try again later.")
134
 
135
  def handle_image_generation(message_id, chat_id, prompt):
136
+ attempts = 4
137
+ for attempt in range(1, attempts + 1):
138
  try:
139
+ img, path, ret_prompt, url = generate_image(prompt, message_id, message_id, BotConfig.IMAGE_DIR)
140
  if img:
141
+ formatted = "\n\n".join(f"_{p.strip()}_" for p in ret_prompt.split("\n\n") if p.strip())
142
+ caption = f"✨ Image ready: {url}\n>{chr(8203)} {formatted}"
143
+ client.send_media(message_id, chat_id, path, caption, media_type="image")
144
+ return
 
 
 
 
 
145
  else:
146
+ raise RuntimeError("generate_image returned no image")
147
  except Exception as e:
148
+ logging.warning(f"Image gen attempt {attempt}/{attempts} failed: {e}")
149
+ if attempt < attempts:
150
+ time.sleep(5)
151
+ else:
152
+ client.send_message(message_id, chat_id, "😒 Image generation failed after several attempts.")
153
+
154
+ # --- Startup ---
155
 
 
156
  def send_startup_message():
157
+ client.send_message_to(BotConfig.BOT_GROUP_CHAT,
158
+ "🌟 Hi! I'm Eve, your friendly AI assistant. I'm now live and ready to help!")
159
+
160
+ send_startup_message()
161
+
162
+ # --- FastAPI App & Webhook ---
163
+
164
+ app = FastAPI()
165
 
166
  help_text = (
167
+ "πŸ€– *Hi there, I'm Eve!* Here are the commands you can use:\n\n"
168
+ "β€’ */help* – _Show this help message._\n"
169
+ "β€’ */summarize <text>* – _Get a quick summary._\n"
170
+ "β€’ */translate <language>|<text>* – _Translate text._\n"
171
+ "β€’ */joke* – _Get a joke._\n"
172
+ "β€’ */weather <location>* – _Get weather._\n"
173
+ "β€’ */inspire* – _Get an inspirational quote._\n"
174
+ "β€’ */trivia* – _Start trivia._\n"
175
+ "β€’ */answer* – _Answer trivia._\n"
176
+ "β€’ */meme <text>* – _Generate a meme._\n"
177
+ "β€’ */poll Q|A|B…* – _Create a poll._\n"
178
+ "β€’ */results* – _Show poll results._\n"
179
+ "β€’ */endpoll* – _End the poll._\n"
180
+ "β€’ */gen <prompt>* – _Generate an image._\n\n"
181
+ "Any other text β†’ I'll send you a voice reply!"
182
+ )
183
 
 
184
  @app.post("/whatsapp")
185
  async def whatsapp_webhook(request: Request):
186
  global last_message_time
187
  last_message_time = time.time()
188
 
189
+ # Auth
190
+ auth = request.headers.get("Authorization", "")
191
+ if auth != f"Bearer {BotConfig.WEBHOOK_AUTH_TOKEN}":
192
  raise HTTPException(403, "Unauthorized")
193
+
194
+ data = await request.json()
195
+ chat_id = data.get("senderData", {}).get("chatId")
196
+ # Only respond in the group chat
197
+ if chat_id != BotConfig.BOT_GROUP_CHAT:
 
 
198
  return {"success": True}
199
 
200
+ if data.get("typeWebhook") != "incomingMessageReceived":
201
+ return {"success": True}
 
 
 
202
 
203
  md = data.get("messageData", {})
204
+ mid = data["idMessage"]
 
 
205
 
206
+ # Extract text
207
  if "textMessageData" in md:
208
+ body = md["textMessageData"]["textMessage"].strip()
 
209
  elif "extendedTextMessageData" in md:
210
+ body = md["extendedTextMessageData"]["text"].strip()
 
211
  else:
212
  return {"success": True}
213
 
 
 
 
 
 
214
  low = body.lower()
215
 
216
+ # Commands
217
  if low == "/help":
218
+ client.send_message(mid, chat_id, help_text)
219
  return {"success": True}
220
 
221
  if low.startswith("/summarize "):
222
  txt = body[len("/summarize "):].strip()
223
  summary = generate_llm(f"Summarize this text in one short paragraph:\n\n{txt}")
224
+ client.send_message(mid, chat_id, summary)
225
  return {"success": True}
226
 
227
  if low.startswith("/translate "):
228
  part = body[len("/translate "):]
229
  if "|" not in part:
230
+ client.send_message(mid, chat_id, "Please use `/translate <language>|<text>`")
231
  else:
232
  lang, txt = part.split("|", 1)
233
  resp = generate_llm(f"Translate the following into {lang.strip()}:\n\n{txt.strip()}")
234
+ client.send_message(mid, chat_id, resp)
235
  return {"success": True}
236
 
237
  if low == "/joke":
238
  try:
239
  joke = requests.get("https://official-joke-api.appspot.com/random_joke", timeout=5).json()
240
+ client.send_message(mid, chat_id, f"{joke['setup']}\n\n{joke['punchline']}")
241
  except:
242
+ client.send_message(mid, chat_id, generate_llm("Tell me a short, funny joke."))
243
  return {"success": True}
244
 
245
  if low.startswith("/weather "):
246
  loc = body[len("/weather "):].strip().replace(" ", "+")
247
  try:
248
  w = requests.get(f"http://sl.wttr.in/{loc}?format=4", timeout=5).text
249
+ client.send_message(mid, chat_id, w)
250
  except:
251
+ client.send_message(mid, chat_id, "Could not fetch weather.")
252
  return {"success": True}
253
 
254
  if low == "/inspire":
255
  quote = generate_llm("Give me a short inspirational quote.")
256
+ client.send_message(mid, chat_id, f"✨ {quote}")
257
  return {"success": True}
258
 
259
+ # Trivia
260
  if low == "/trivia":
261
+ seed = random.randint(0, 1_000_000)
262
  raw = generate_llm(
263
+ f"Generate a unique trivia Q&A in JSON based on seed {seed}:\n"
264
+ '{"question":"...","answer":"..."}'
 
265
  )
 
 
 
 
 
 
266
  try:
267
+ obj = json.loads(raw.strip().strip("```json").strip("```"))
268
+ trivia_store[chat_id] = obj
269
+ client.send_message(mid, chat_id,
270
+ f"❓ {obj['question']}\nReply `/answer` or `/answer your guess`.")
271
+ except:
272
+ client.send_message(mid, chat_id, "Failed to generate trivia.")
 
 
 
 
273
  return {"success": True}
274
 
 
275
  if low.startswith("/answer"):
276
+ resp = body[len("/answer"):].strip()
277
+ if chat_id in trivia_store:
278
+ qa = trivia_store.pop(chat_id)
279
+ if resp:
280
+ verdict = generate_llm(
281
+ f"Q: {qa['question']}\nCorrect: {qa['answer']}\nUser: {resp}\nCorrect?"
 
 
 
 
 
 
282
  )
283
+ client.send_message(mid, chat_id, verdict)
 
284
  else:
285
+ client.send_message(mid, chat_id, f"πŸ’‘ Answer: {qa['answer']}")
 
286
  else:
287
+ client.send_message(mid, chat_id, "No active trivia. `/trivia` to start.")
288
  return {"success": True}
289
 
290
+ # Meme
291
  if low.startswith("/meme "):
292
  txt = body[len("/meme "):].strip()
293
+ client.send_message(mid, chat_id, "🎨 Generating your meme...")
294
+ task_queue.put({"type":"image","message_id":mid,"chat_id":chat_id,"prompt":f"meme: {txt}"})
 
 
 
 
 
295
  return {"success": True}
296
 
297
+ # Polls
298
  if low.startswith("/poll "):
299
  parts = body[len("/poll "):].split("|")
300
  if len(parts) < 3:
301
+ client.send_message(mid, chat_id, "Use `/poll Q|A|B`")
302
  else:
303
+ q, *opts = [p.strip() for p in parts]
304
+ votes = {i+1:0 for i in range(len(opts))}
305
+ polls[chat_id] = {"question":q,"options":opts,"votes":votes,"voters":{}}
306
+ text = f"πŸ“Š *Poll:* {q}\n" + "\n".join(f"{i+1}. {o}" for i,o in enumerate(opts))
307
+ client.send_message(mid, chat_id, text)
 
 
 
308
  return {"success": True}
309
 
310
+ if chat_id in polls and low.isdigit():
311
+ n = int(low)
312
+ poll = polls[chat_id]
313
+ if 1 <= n <= len(poll["options"]):
314
+ prev = poll["voters"].get(data["senderData"].get("sender"))
315
  if prev:
316
+ poll["votes"][prev] -= 1
317
+ poll["votes"][n] += 1
318
+ poll["voters"][data["senderData"].get("sender")] = n
319
+ client.send_message(mid, chat_id, f"βœ… Voted for {poll['options'][n-1]}")
320
  return {"success": True}
321
 
322
  if low == "/results":
323
+ if chat_id in polls:
324
+ p = polls[chat_id]
325
+ text = f"πŸ“Š *Results:* {p['question']}\n" + "\n".join(f"{i}. {o}: {p['votes'][i]}" for i,o in enumerate(p["options"],1))
326
+ client.send_message(mid, chat_id, text)
 
 
327
  else:
328
+ client.send_message(mid, chat_id, "No active poll.")
329
  return {"success": True}
330
 
331
  if low == "/endpoll":
332
+ if chat_id in polls:
333
+ p = polls.pop(chat_id)
334
+ text = f"πŸ“Š *Final Results:* {p['question']}\n" + "\n".join(f"{i}. {o}: {p['votes'][i]}" for i,o in enumerate(p["options"],1))
335
+ client.send_message(mid, chat_id, text)
 
 
336
  else:
337
+ client.send_message(mid, chat_id, "No active poll.")
338
  return {"success": True}
339
 
340
+ # Image gen
341
  if low.startswith("/gen"):
342
  prompt = body[len("/gen"):].strip()
343
  if not prompt:
344
+ client.send_message(mid, chat_id, "Use `/gen <prompt>`")
345
  else:
346
+ client.send_message(mid, chat_id, "✨ Generating image...")
347
+ task_queue.put({"type":"image","message_id":mid,"chat_id":chat_id,"prompt":prompt})
 
 
 
 
 
348
  return {"success": True}
349
 
350
+ # Fallback: voice reply
351
+ task_queue.put({"type":"audio","message_id":mid,"chat_id":chat_id,"prompt":body})
 
 
 
 
 
352
  return {"success": True}
353
 
354
  @app.get("/", response_class=PlainTextResponse)
 
356
  return "Server is running!"
357
 
358
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
359
  import uvicorn
360
  uvicorn.run(app, host="0.0.0.0", port=7860)