import threading import time import channel import gameloop nickserv_pass = None irc_chan = None game_channel = None def chunk(l, n): assert 0 < n chunked = [] item = [] for i in l: if len(item) >= n: chunked.append(item) item = [] item.append(i) if len(item) > 0: chunked.append(item) return chunked class GameLoop(threading.Thread): def __init__(self, irc, chan, irc_chan): self.irc = irc self.chan = chan self.irc_chan = irc_chan threading.Thread.__init__(self) def send(self, message): message_parts = message.encode().split(b' ') line = [] line_len = 0 for part in message_parts: if len(part) + line_len > 440: self.irc.bot_response(self.irc_chan, b' '.join(line)) line = [] line_len = 0 line.append(part) line_len += len(part) + 1 if len(line) > 0: self.irc.bot_response(self.irc_chan, b' '.join(line)) def notice(self, recipient, message): recipient = recipient.encode() message_parts = message.encode().split(b' ') line = [] line_len = 0 for part in message_parts: if len(part) + line_len > 440: self.irc.send_raw(b'NOTICE %s :%s %s' % (recipient, self.irc_chan, b' '.join(line))) line = [] line_len = 0 line.append(part) line_len += len(part) + 1 if len(line) > 0: self.irc.send_raw(b'NOTICE %s :[%s] %s' % (recipient, self.irc_chan, b' '.join(line))) def get_event(self): event = self.chan.recv() return event def voice(self, nicks): if type(nicks) == str: nicks = [nicks] for nicks in chunk(nicks, 4): self.irc.send_raw(b'MODE %s +%s %s' % (self.irc_chan, b'v'*len(nicks), b' '.join(i.encode() for i in nicks))) def devoice(self, nicks): if type(nicks) == str: nicks = [nicks] for nicks in chunk(nicks, 4): self.irc.send_raw(b'MODE %s -%s %s' % (self.irc_chan, b'v'*len(nicks), b' '.join(i.encode() for i in nicks))) def run(self): try: gameloop.game(self.send, self.notice, self.voice, self.devoice, self.get_event) except Exception as err: self.send('Crash! (%s, %s)' % (type(err), repr(err))) finally: self.chan.close() def start_gameloop(irc): global game_channel, irc_chan if game_channel is not None: return chan = channel.Channel() GameLoop(irc, chan, irc_chan).start() game_channel = chan def stop_gameloop(): global game_channel if game_channel is None: return game_channel.send((gameloop.events.quit,)) game_channel = None def send_event(event): global game_channel game_channel.send(event) def parse_command(message, nick, irc): def send(m): global irc_chan irc.bot_response(irc_chan, m) def args(num, index = 1): nonlocal message if type(num) == int: num = [num] if len(message) - index not in num: command = ' '.join(message[:index]) if len(num) == 1: if num[0] == 1: send('%s needs 1 argument' % command) else: send('%s needs %i arguments' % (command, num[0])) else: send('%s needs either %s arguments' % (command, ' or '.join(num))) return None return message[index:] events = gameloop.events message = message.split() if len(message) == 0: return c = message[0] if c == '!status': if args(0) is not None: send_event((events.status,)) elif c == '!start': arg = args([0, 1]) if arg is not None: if len(arg) == 0: send_event((events.start, nick)) else: send_event((events.start, nick, arg[0])) elif c == '!ready': if args(0) is not None: send_event((events.ready, nick)) elif c == '!unready': if args(0) is not None: send_event((events.unready, nick)) elif c == '!kill': if args(0) is not None: send_event((events.kill,)) elif c == '!join': if args(0) is not None: send_event((events.join, nick)) elif c == '!leave': if args(0) is not None: send_event((events.leave, nick)) elif c == '!players': if args(0) is not None: send_event((events.players,)) elif c == '!kick': arg = args(1) if arg is not None: kickee, = arg send_event((events.kick, nick, kickee)) elif c == '!deck': if len(message) < 2: send('Subcommands: !deck add | remove | list') return subc = message[1] if subc == 'add': arg = args(1, 2) if arg is not None: code, = arg if code == 'random': send_event((events.deck_add_random,)) else: send_event((events.deck_add, code)) elif subc == 'remove': arg = args(1, 2) if arg is not None: code, = arg send_event((events.deck_remove, code)) elif subc == 'list': if args(0, 2) is not None: send_event((events.deck_list,)) else: send('Subcommands: !deck add | remove | list') elif c == '!bot': if len(message) < 2: send('Subcommands: !bot add | remove') return subc = message[1] if subc == 'add': arg = args(2, 2) if arg is not None: bot_type, name = arg if bot_type == 'rando': send_event((events.bot_add_rando, name)) else: send('Allowed bot types: rando') elif subc == 'remove': arg = args(1, 2) if arg is not None: name, = arg send_event((events.bot_remove, name)) else: send('Subcommands: !bot add | remove') elif c == '!limit': arg = args([0, 1, 2]) if arg is None: return if len(arg) == 0: send_event((events.limit,)) else: num = arg[0] if not num.isdecimal(): send('Usage: !limit [ []]') return num = int(num) if len(arg) == 2: limit_type = arg[1] if limit_type == 'p' or limit_type == 'points': send_event((events.limit, gameloop.limit_types.points, num)) elif limit_type == 'r' or limit_type == 'rounds': send_event((events.limit, gameloop.limit_types.rounds, num)) else: send('Allowed limit types: p(oints), r(ounds)') else: send_event((events.limit, gameloop.limit_types.points, num)) elif c == '!card' or all(i.isdecimal() for i in message): if c == '!card': arg = message[1:] else: arg = message if not all(i.isdecimal() for i in arg): send('Usage: [!card] ...') return choices = [int(i) for i in arg] send_event((events.card, nick, choices)) elif c == '!cards': if args(0) is not None: send_event((events.cards, nick)) elif c == '!origins': if args(0) is not None: send_event((events.origins, nick)) elif c == '!help': arg = args([0, 1, 2]) if arg is not None: if len(arg) > 0: if arg[0][0] == '!': arg[0] = arg[0][1:] if len(arg) == 0: send('!status !start !ready !unready !kill !join !leave !players !kick !deck !limit !card !cards !origins') elif len(arg) == 1: if arg[0] in ('status', 'ready', 'unready', 'kill', 'join', 'leave', 'players', 'cards', 'origins'): send('Usage: !%s' % (arg[0])) elif arg[0] == 'start': send('Usage: !start []') elif arg[0] == 'kick': send('Usage: !kick ') elif arg[0] == 'card': send('Usage: [!card] ...') elif arg[0] == 'deck': send('Subcommands: !deck add | remove | list') elif arg[0] == 'bot': send('Subcommands: !bot add | remove') elif arg[0] == 'limit': send('Usage: !limit [ []]') else: send('No such command !%s' % (arg[0])) elif len(arg) == 2: if arg[0] == 'deck': if arg[1] == 'add': send('Usage: !deck add | random') elif arg[1] == 'remove': send('Usage: !deck remove ') elif arg[1] == 'list': send('Usage: !deck list') else: send('No such subcommand !%s %s' % (arg[0], arg[1])) elif arg[0] == 'bot': if arg[1] == 'add': send('Usage: !bot add ') elif arg[1] == 'remove': send('Usage: !bot remove ') else: send('No such subcommand !%s %s' % (arg[0], arg[1])) else: send('No such subcommand !%s %s' % (arg[0], arg[1])) else: send('Uh, how did we get %i args?' % len(arg)) # initialize(*, config) # Called to initialize the IRC bot # Runs before even logger is brought up, and blocks further bringup until it's done # config is a configpatser.ConfigParser object containig contents of bot.conf def initialize(*, config): global nickserv_pass, irc_chan nickserv_pass = config['nickserv']['password'] irc_chan = config['server']['channels'].split()[0].encode() # on_connect(*, irc) # Called after IRC bot has connected and sent the USER/NICk commands but not yet attempted anything else # Called for every reconnect # Blocks the bot until it's done, including PING/PONG handling # irc is the IRC API object def on_connect(*, irc): global nickserv_pass if nickserv_pass != '': irc.msg(b'nickserv', b'IDENTIFY ' + nickserv_pass.encode()) time.sleep(30) # One day I will do this correctly. Today is not the day stop_gameloop() start_gameloop(irc) # on_quit(*, irc) # Called just before IRC bot sends QUIT # Blocks the bot until it's done, including PING/PONG handling # irc is the IRC API object def on_quit(*, irc): stop_gameloop() # handle_message(*, prefix, message, nick, channel, irc) # Called for PRIVMSGs. # prefix is the prefix at the start of the message, without the leading ':' # message is the contents of the message # nick is who sent the message # channel is where you should send the response (note: in queries nick == channel) # irc is the IRC API object # All strings are bytestrings def handle_message(*, prefix, message, nick, channel, irc): global irc_chan if channel == irc_chan: parse_command(message.decode(), nick.decode(), irc) # handle_nonmessage(*, prefix, command, arguments, irc) # Called for all other commands than PINGs and PRIVMSGs. # prefix is the prefix at the start of the message, without the leading ':' # command is the command or number code # arguments is rest of the arguments of the command, represented as a list. ':'-arguments are handled automatically # irc is the IRC API object # All strings are bytestrings def handle_nonmessage(*, prefix, command, arguments, irc): if command == b'NICK': old = prefix.split(b'!')[0].decode() new = arguments[0].decode() send_event((gameloop.events.nick_change, old, new)) elif command == b'PART' or command == b'QUIT': nick = prefix.split(b'!')[0].decode() send_event((gameloop.events.leave, nick)) elif command == b'KICK': nick = arguments[1].decode() send_event((gameloop.events.leave, nick))