Changelog - Version 4.5 (2024-07-11)
New Features
- Tipping Support: Added support for tipping via StreamElements and StreamLabs.
- Subscribed to StreamElements tip events and established WebSocket connection.
- Subscribed to StreamLabs donation events and established WebSocket connection.
Code Enhancements
- New Import: Added
import uuid. - Tipping Services Connection: Introduced a function
connect_to_tipping_servicesto handle connections to StreamElements and StreamLabs by fetching tokens from the database and establishing WebSocket connections.async def connect_to_tipping_services(): global streamelements_token, streamlabs_token sqldb = await get_mysql_connection() try: async with sqldb.cursor(aiomysql.DictCursor) as cursor: await cursor.execute("SELECT StreamElements, StreamLabs FROM tipping_settings LIMIT 1") result = await cursor.fetchone() streamelements_token = result['StreamElements'] streamlabs_token = result['StreamLabs'] tasks = [] if streamelements_token: tasks.append(connect_to_streamelements()) if streamlabs_token: tasks.append(connect_to_streamlabs()) if tasks: await asyncio.gather(*tasks) else: bot_logger.error("No valid token found for either StreamElements or StreamLabs.") except aiomysql.MySQLError as err: bot_logger.error(f"Database error: {err}") finally: await sqldb.ensure_closed() - StreamElements Connection: Added a function
connect_to_streamelementsto subscribe to StreamElements tip events and listen for messages via WebSocket.async def connect_to_streamelements(): global streamelements_token uri = "wss://astro.streamelements.com" try: async with websockets.connect(uri) as streamelements_websocket: # Send the authentication message nonce = str(uuid.uuid4()) await streamelements_websocket.send(json.dumps({ 'type': 'subscribe', 'nonce': nonce, 'data': { 'topic': 'channel.tip', 'token': streamelements_token, 'token_type': 'jwt' } })) # Listen for messages while True: message = await streamelements_websocket.recv() await process_message(message, "StreamElements") except websockets.ConnectionClosed as e: bot_logger.error(f"StreamElements WebSocket connection closed: {e}") except Exception as e: bot_logger.error(f"StreamElements WebSocket error: {e}") - StreamLabs Connection: Added a function
connect_to_streamlabsto subscribe to StreamLabs donation events and listen for messages via WebSocket.async def connect_to_streamlabs(): global streamlabs_token uri = f"wss://sockets.streamlabs.com/socket.io/?token={streamlabs_token}&EIO=3&transport=websocket" try: async with websockets.connect(uri) as streamlabs_websocket: # Listen for messages while True: message = await streamlabs_websocket.recv() await process_message(message, "StreamLabs") except websockets.ConnectionClosed as e: bot_logger.error(f"StreamLabs WebSocket connection closed: {e}") except Exception as e: bot_logger.error(f"StreamLabs WebSocket error: {e}") - Process Messages: Introduced a function
process_messageto handle incoming messages from StreamElements and StreamLabs, and a functionprocess_tipping_messageto display tipping messages in the Twitch chat and save tipping data to the database.async def process_message(message, source): try: data = json.loads(message) if source == "StreamElements" and data.get('type') == 'response': # Handle the subscription response if 'error' in data: handle_streamelements_error(data['error'], data['data']['message']) else: bot_logger.info(f"StreamElements subscription success: {data['data']['message']}") else: await process_tipping_message(data, source) except Exception as e: bot_logger.error(f"Error processing message from {source}: {e}") def handle_streamelements_error(error, message): error_messages = { "err_internal_error": "An internal error occurred.", "err_bad_request": "The request was malformed or invalid.", "err_unauthorized": "The request lacked valid authentication credentials.", "rate_limit_exceeded": "The rate limit for the API has been exceeded.", "invalid_message": "The message was invalid or could not be processed." } error_message = error_messages.get(error, "Unknown error occurred.") bot_logger.error(f"StreamElements error: {error_message} - {message}") async def process_tipping_message(data, source): try: channel = bot.get_channel(CHANNEL_NAME) send_message = None if source == "StreamElements" and data.get('type') == 'tip': user = data['data']['username'] amount = data['data']['amount'] tip_message = data['data']['message'] send_message = f"{user} just tipped {amount}! Message: {tip_message}" elif source == "StreamLabs" and 'event' in data and data['event'] == 'donation': for donation in data['data']['donations']: user = donation['name'] amount = donation['amount'] tip_message = donation['message'] send_message = f"{user} just tipped {amount}! Message: {tip_message}" if send_message: await channel.send(send_message) # Save tipping data directly in this function sqldb = await get_mysql_connection() try: async with sqldb.cursor() as cursor: await cursor.execute( "INSERT INTO tipping (username, amount, message, source) VALUES (%s, %s, %s, %s)", (user, amount, tip_message, source) ) await sqldb.commit() except aiomysql.MySQLError as err: bot_logger.error(f"Database error: {err}") finally: await sqldb.ensure_closed() except Exception as e: bot_logger.error(f"Error processing tipping message: {e}") - AI Support: Added functionality to handle AI responses:
- If the bot is mentioned in the chat with no message, it responds with a greeting.
- If a message is provided, the bot uses the AI service at
ai.botofthespecter.comto generate a response. ```python if f’@{self.nick.lower()}’ in messageContent: user_message = message.content.replace(f’@{self.nick}’, ‘’).strip() if not user_message: await channel.send(f’Hello, {message.author.name}!’) else: ai_response = self.get_ai_response(user_message) await channel.send(ai_response)
async def get_ai_response(self, user_message): try: response = requests.post(‘https://ai.botofthespecter.com/’, json={“message”: user_message}) response.raise_for_status() # Notice bad responses ai_response = response.json().get(“text”, “Sorry, I could not understand your request.”) return ai_response except requests.RequestException as e: bot_logger.error(f”Error getting AI response: {e}”) return “Sorry, I could not understand your request.” ```
Database Changes
- New Tables:
- tipping_settings: Stores StreamElements and StreamLabs tokens.
- tipping: Records details of each tip, including username, amount, message, source, and timestamp.
'tipping_settings': ''' CREATE TABLE IF NOT EXISTS tipping_settings ( StreamElements VARCHAR(255) DEFAULT NULL, StreamLabs VARCHAR(255) DEFAULT NULL ) ENGINE=InnoDB ''', 'tipping': ''' CREATE TABLE IF NOT EXISTS tipping ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(255), amount DECIMAL(10, 2), message TEXT, source VARCHAR(255), timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB ''',
Other Improvements
- Message Parsing: Ensured the bot correctly parses messages when tagged with multiple mentions.
- Twitch WebSocket Update: Modified the Twitch WebSocket connection to allow simultaneous connections to StreamLabs and StreamElements WebSockets.
async with websockets.connect(twitch_websocket_uri) as twitch_websocket: