44
55import google .generativeai as genai
66import structlog
7+ from anyio import Event
78from google .api_core .exceptions import InvalidArgument , NotFound
89
910from flare_ai_social .ai import BaseAIProvider , GeminiProvider
1011from flare_ai_social .prompts import FEW_SHOT_PROMPT
1112from flare_ai_social .settings import settings
1213from flare_ai_social .telegram import TelegramBot
13- from flare_ai_social .twitter import TwitterBot
14+ from flare_ai_social .twitter import TwitterBot , TwitterConfig
1415
1516logger = structlog .get_logger (__name__ )
1617
18+ # Error messages
19+ ERR_AI_PROVIDER_NOT_INITIALIZED = "AI provider must be initialized"
20+
1721
1822class BotManager :
1923 """Manager class for handling multiple social media bots."""
@@ -25,6 +29,7 @@ def __init__(self) -> None:
2529 self .twitter_thread : threading .Thread | None = None
2630 self .active_bots : list [str ] = []
2731 self .running = False
32+ self ._telegram_polling_task : asyncio .Task | None = None
2833
2934 def initialize_ai_provider (self ) -> None :
3035 """Initialize the AI provider with either tuned model or default model."""
@@ -42,41 +47,46 @@ def initialize_ai_provider(self) -> None:
4247 model_info = genai .get_tuned_model (
4348 name = f"tunedModels/{ tuned_model_id } "
4449 )
45- logger .info ("Tuned model info" , model_info = model_info )
46-
4750 # Initialize AI provider with tuned model
4851 self .ai_provider = GeminiProvider (
4952 settings .gemini_api_key ,
5053 model_name = f"tunedModels/{ tuned_model_id } " ,
5154 system_instruction = FEW_SHOT_PROMPT ,
5255 )
53- logger .info (f"Using tuned model: tunedModels/ { tuned_model_id } " )
54- return
55- except ( InvalidArgument , NotFound ) as e :
56- logger . warning ( f"Failed to load tuned model: { e } " )
56+ logger .info ("Tuned model info" , model_info = model_info )
57+ except ( InvalidArgument , NotFound ):
58+ logger . warning ( "Failed to load tuned model." )
59+ self . _initialize_default_model ( )
5760 else :
5861 logger .warning (
59- f "Tuned model ' { tuned_model_id } ' not found in available models. Using default model."
62+ "Tuned model not found in available models. Using default model."
6063 )
61- except Exception as e :
62- logger .exception (f"Error accessing tuned models: { e } " )
64+ self ._initialize_default_model ()
65+ except Exception :
66+ logger .exception ("Error accessing tuned models" )
67+ self ._initialize_default_model ()
6368
64- # Fall back to default model
69+ def _initialize_default_model (self ) -> None :
70+ """Initialize the default Gemini model."""
6571 logger .info ("Using default Gemini Flash model with few-shot prompting" )
6672 self .ai_provider = GeminiProvider (
6773 settings .gemini_api_key ,
6874 model_name = "gemini-1.5-flash" ,
6975 system_instruction = FEW_SHOT_PROMPT ,
7076 )
7177
78+ def _check_ai_provider_initialized (self ) -> BaseAIProvider :
79+ """Check if AI provider is initialized and raise error if not."""
80+ if self .ai_provider is None :
81+ raise RuntimeError (ERR_AI_PROVIDER_NOT_INITIALIZED )
82+ return self .ai_provider
83+
7284 def start_twitter_bot (self ) -> bool :
7385 """Initialize and start the Twitter bot in a separate thread."""
74- # Check if Twitter is enabled in settings
7586 if not settings .enable_twitter :
7687 logger .info ("Twitter bot disabled in settings" )
7788 return False
7889
79- # Check if required Twitter credentials are configured
8090 if not all (
8191 [
8292 settings .x_api_key ,
@@ -92,10 +102,9 @@ def start_twitter_bot(self) -> bool:
92102 return False
93103
94104 try :
95- # Ensure AI provider is initialized
96- assert self .ai_provider is not None , "AI provider must be initialized"
97- twitter_bot = TwitterBot (
98- ai_provider = self .ai_provider ,
105+ ai_provider = self ._check_ai_provider_initialized ()
106+
107+ config = TwitterConfig (
99108 bearer_token = settings .x_bearer_token ,
100109 api_key = settings .x_api_key ,
101110 api_secret = settings .x_api_key_secret ,
@@ -107,25 +116,29 @@ def start_twitter_bot(self) -> bool:
107116 polling_interval = settings .twitter_polling_interval ,
108117 )
109118
110- # Start the Twitter bot in a separate thread
119+ twitter_bot = TwitterBot (
120+ ai_provider = ai_provider ,
121+ config = config ,
122+ )
123+
111124 self .twitter_thread = threading .Thread (
112125 target = twitter_bot .start , daemon = True , name = "TwitterBotThread"
113126 )
114127 self .twitter_thread .start ()
115128 logger .info ("Twitter bot started in background thread" )
116129 self .active_bots .append ("Twitter" )
117- return True
118130
119- except ValueError as e :
120- logger .exception (f "Failed to start Twitter bot: { e } " )
131+ except ValueError :
132+ logger .exception ("Failed to start Twitter bot" )
121133 return False
122- except Exception as e :
123- logger .error ( f "Unexpected error starting Twitter bot: { e } " , exc_info = True )
134+ except Exception :
135+ logger .exception ( "Unexpected error starting Twitter bot" )
124136 return False
137+ else :
138+ return True
125139
126140 async def start_telegram_bot (self ) -> bool :
127141 """Initialize and start the Telegram bot."""
128- # Check if Telegram is enabled in settings
129142 if not settings .enable_telegram :
130143 logger .info ("Telegram bot disabled in settings" )
131144 return False
@@ -135,79 +148,108 @@ async def start_telegram_bot(self) -> bool:
135148 return False
136149
137150 try :
138- # Parse allowed users if provided
139- allowed_users : list [int ] = []
140- if settings .telegram_allowed_users :
141- try :
142- # Convert comma-separated string to list of integers
143- allowed_users = [
144- int (user_id .strip ())
145- for user_id in settings .telegram_allowed_users .split ("," )
146- if user_id .strip ().isdigit ()
147- ]
148- except Exception as e :
149- logger .warning (f"Error parsing telegram_allowed_users: { e } " )
150-
151- # Ensure AI provider is initialized
152- assert self .ai_provider is not None , "AI provider must be initialized"
153-
154- # Create and start Telegram bot
151+ allowed_users = self ._parse_allowed_users ()
152+ ai_provider = self ._check_ai_provider_initialized ()
153+
155154 self .telegram_bot = TelegramBot (
156- ai_provider = self . ai_provider ,
155+ ai_provider = ai_provider ,
157156 api_token = settings .telegram_api_token ,
158157 allowed_user_ids = allowed_users ,
159158 polling_interval = settings .telegram_polling_interval ,
160159 )
161160
162- # Properly initialize and start polling
163161 await self .telegram_bot .initialize ()
164- await self .telegram_bot .start_polling ()
162+ self ._telegram_polling_task = asyncio .create_task (
163+ self .telegram_bot .start_polling ()
164+ )
165165 self .active_bots .append ("Telegram" )
166- return True
167166
168- except Exception as e :
169- logger .error (f"Failed to start Telegram bot: { e } " , exc_info = True )
167+ except Exception :
168+ logger .exception ("Failed to start Telegram bot" )
169+ if self .telegram_bot :
170+ await self .telegram_bot .shutdown ()
170171 return False
172+ else :
173+ return True
174+
175+ def _parse_allowed_users (self ) -> list [int ]:
176+ """Parse the allowed users from settings."""
177+ allowed_users : list [int ] = []
178+ if settings .telegram_allowed_users :
179+ try :
180+ allowed_users = [
181+ int (user_id .strip ())
182+ for user_id in settings .telegram_allowed_users .split ("," )
183+ if user_id .strip ().isdigit ()
184+ ]
185+ except ValueError :
186+ logger .warning ("Error parsing telegram_allowed_users" )
187+ return allowed_users
188+
189+ async def _check_telegram_status (self ) -> None :
190+ """Check and handle Telegram bot status."""
191+ if not (
192+ self .telegram_bot
193+ and self .telegram_bot .application
194+ and self .telegram_bot .application .updater
195+ and self .telegram_bot .application .updater .running
196+ ):
197+ logger .error ("Telegram bot stopped responding" )
198+ try :
199+ # Store telegram_bot in a local variable to help type checker
200+ telegram_bot = self .telegram_bot
201+ if telegram_bot is not None : # Add explicit None check
202+ await telegram_bot .shutdown ()
203+ if await self .start_telegram_bot ():
204+ logger .info ("Telegram bot restarted successfully" )
205+ else :
206+ logger .error ("Failed to restart Telegram bot" )
207+ self .active_bots .remove ("Telegram" )
208+ except Exception :
209+ logger .exception ("Error restarting Telegram bot" )
210+ self .active_bots .remove ("Telegram" )
211+
212+ def _check_twitter_status (self ) -> None :
213+ """Check and handle Twitter bot status."""
214+ if self .twitter_thread and not self .twitter_thread .is_alive ():
215+ logger .error ("Twitter bot thread terminated unexpectedly" )
216+ self .active_bots .remove ("Twitter" )
217+ if self .start_twitter_bot ():
218+ logger .info ("Twitter bot restarted successfully" )
171219
172220 async def monitor_bots (self ) -> None :
173221 """Monitor active bots and handle unexpected terminations."""
174222 self .running = True
223+
175224 try :
176225 while self .running and self .active_bots :
177- # Check Twitter bot status
178- if "Twitter" in self .active_bots and self .twitter_thread :
179- if not self .twitter_thread .is_alive ():
180- logger .error ("Twitter bot thread terminated unexpectedly" )
181- self .active_bots .remove ("Twitter" )
182- # Attempt to restart Twitter if auto-restart is enabled
183- if getattr (settings , "auto_restart_bots" , False ):
184- logger .info ("Attempting to restart Twitter bot" )
185- if self .start_twitter_bot ():
186- logger .info ("Twitter bot restarted successfully" )
187-
188- # Exit if no bots are active anymore
226+ if "Telegram" in self .active_bots and self .telegram_bot :
227+ await self ._check_telegram_status ()
228+
229+ if "Twitter" in self .active_bots :
230+ self ._check_twitter_status ()
231+
189232 if not self .active_bots :
190233 logger .error ("No active bots remaining" )
191234 break
192235
193236 await asyncio .sleep (5 )
194237
195- except Exception as e :
196- logger .error ( f "Error in bot monitoring loop: { e } " , exc_info = True )
238+ except Exception :
239+ logger .exception ( "Error in bot monitoring loop" )
197240 finally :
198241 self .running = False
199242
200243 async def shutdown (self ) -> None :
201244 """Gracefully shutdown all active bots."""
202245 self .running = False
203246
204- # Shutdown Telegram bot if active
205247 if self .telegram_bot :
206248 try :
207249 logger .info ("Shutting down Telegram bot" )
208250 await self .telegram_bot .shutdown ()
209- except Exception as e :
210- logger .exception (f "Error shutting down Telegram bot: { e } " )
251+ except Exception :
252+ logger .exception ("Error shutting down Telegram bot" )
211253
212254 if "Twitter" in self .active_bots :
213255 logger .info ("Twitter bot daemon thread will terminate with main process" )
@@ -220,32 +262,26 @@ async def async_start() -> None:
220262 bot_manager = BotManager ()
221263
222264 try :
223- # Initialize AI provider
224265 bot_manager .initialize_ai_provider ()
225266 if not bot_manager .ai_provider :
226267 logger .error ("Failed to initialize AI provider" )
227268 return
228269
229- # Start Twitter bot (if enabled and configured)
230270 bot_manager .start_twitter_bot ()
231-
232- # Start Telegram bot (if enabled and configured)
233271 await bot_manager .start_telegram_bot ()
234272
235273 if bot_manager .active_bots :
236- logger .info (f "Active bots: { ', ' .join (bot_manager .active_bots )} " )
274+ logger .info ("Active bots: %s" , ", " .join (bot_manager .active_bots ))
237275 monitor_task = asyncio .create_task (bot_manager .monitor_bots ())
238276
239277 try :
240- while bot_manager .active_bots :
241- await asyncio .sleep (1 )
278+ await Event ().wait ()
242279 except asyncio .CancelledError :
243280 logger .info ("Main task cancelled" )
244281 finally :
245282 monitor_task .cancel ()
246283 with contextlib .suppress (asyncio .CancelledError ):
247284 await monitor_task
248-
249285 await bot_manager .shutdown ()
250286 else :
251287 logger .info (
@@ -255,8 +291,8 @@ async def async_start() -> None:
255291 except KeyboardInterrupt :
256292 logger .info ("Application stopped by user" )
257293 await bot_manager .shutdown ()
258- except Exception as e :
259- logger .error ( f "Fatal error in async_start: { e } " , exc_info = True )
294+ except Exception :
295+ logger .exception ( "Fatal error in async_start" )
260296 await bot_manager .shutdown ()
261297
262298
@@ -266,5 +302,5 @@ def start_bot_manager() -> None:
266302 asyncio .run (async_start ())
267303 except KeyboardInterrupt :
268304 logger .info ("Application stopped by user" )
269- except Exception as e :
270- logger .error ( f "Fatal error in start: { e } " , exc_info = True )
305+ except Exception :
306+ logger .exception ( "Fatal error in start" )
0 commit comments