I want to build a Python bot that can watch and write messages to several Twitch channels at once. Threading works but uses too much CPU power. I heard Select() is better for performance.
My current code can read messages from multiple channels but I can’t figure out how to handle the writable connections properly. When select() returns writable sockets, how do I know which channel each socket belongs to?
Is there a way to use objects that contain both the socket and some kind of label? That way I could tell which writable connection goes with which channel.
I’m just coding as a hobby so this concept is pretty confusing for me. Any help would be great.
#!/usr/bin/env python3
import socket
import select
SERVER = "irc.chat.twitch.tv"
PORT_NUM = 6667
BOT_NAME = "mybotname"
AUTH_TOKEN = 'oauth:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'
DEFAULT_ROOM = "defaultroom"
def setup_connections(room_list):
connection_array = []
start_idx = 0
end_idx = len(room_list)
for i in range(start_idx, end_idx):
room_name = room_list[i]
conn = socket.socket()
conn.connect((SERVER, PORT_NUM))
conn.setblocking(False)
conn.send(bytes("PASS " + AUTH_TOKEN + "\r\n", "UTF-8"))
conn.send(bytes("NICK " + BOT_NAME + "\r\n", "UTF-8"))
conn.send(bytes("JOIN #" + room_name + " \r\n", "UTF-8"))
conn.send(bytes('CAP REQ :twitch.tv/membership\r\n'.encode('utf-8')))
conn.send(bytes('CAP REQ :twitch.tv/commands\r\n'.encode('utf-8')))
conn.send(bytes('CAP REQ :twitch.tv/tags\r\n'.encode('utf-8')))
connection_array.append(conn)
return connection_array
def run_bot():
keep_running = True
target_rooms = ['roomalpha', 'roombeta', 'roomgamma']
active_connections = setup_connections(target_rooms)
while keep_running:
ready_read, ready_write, error_sockets = select.select(active_connections, active_connections, [])
if len(ready_read) != 0:
for sock in ready_read:
print(str(sock.recv(1024), "utf-8"))
if __name__ == "__main__":
run_bot()
Use a dictionary to map sockets to channel names: socket_to_channel = {sock: channel_name} when you create connections. When select() returns writable sockets, just do channel = socket_to_channel[sock] to know which room you’re writing to. Works great for me.
Been working with Twitch IRC for two years and hit the same socket tracking issues. Cleanest fix I found: use a simple namedtuple to bundle socket objects with their metadata. Connection = namedtuple('Connection', ['socket', 'channel', 'last_ping']) works great. You can still pass [conn.socket for conn in connections] to select(), but when you get writable sockets back, just loop through your Connection objects and match the socket references. Gives you instant access to channel names without dictionary lookups and makes debugging way easier since everything’s in one place. Also - you’re not handling PING responses. Twitch will boot you after a few minutes without proper PONG replies, so catch those in your message parsing.
Had the same writable socket issue when I built my IRC bot. You’re passing the same connection list to both readable and writable in select(), but you don’t need writable checking unless you’ve got messages waiting to send. Most sockets stay writable anyway unless the network buffer’s full. Keep a separate outbound_queue dict - sockets as keys, message lists as values. Only add sockets to the writable list when they’ve got queued messages. When select returns a writable socket, grab the queued messages, send them, then stop monitoring that socket until new messages show up. Saves CPU cycles from checking writability when there’s nothing to send.
I’ve hit this socket management problem before. Skip the socket-to-channel mapping and wrap everything in a Connection class instead. Give it socket, channel_name, and maybe message_queue attributes. Your active_connections list holds these objects, not raw sockets. When select() returns ready sockets, loop through your Connection objects and check if obj.socket is in ready_write. This scales way better when you need per-connection state later - connection health, message buffering, whatever. Sure, dictionaries work for basic stuff, but classes keep your code cleaner as the bot gets more complex.
Just keep two separate lists - one for sockets, another for channel names in the same order. When select() returns ready sockets, use connections.index(writable_sock) to find the position, then grab channel_names[position] for the matching channel. It’s a bit hacky but works great for small bot projects without overcomplicating things.