2019-05-06 09:41:12 +00:00
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
|
|
|
|
import channel
|
|
|
|
|
|
|
|
import gameloop
|
|
|
|
|
|
|
|
nickserv_pass = None
|
|
|
|
irc_chan = None
|
|
|
|
|
|
|
|
game_channel = None
|
|
|
|
|
2019-05-06 13:00:55 +00:00
|
|
|
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
|
|
|
|
|
2019-05-06 09:41:12 +00:00
|
|
|
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):
|
2019-05-06 11:13:13 +00:00
|
|
|
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))
|
2019-05-06 09:41:12 +00:00
|
|
|
|
|
|
|
def notice(self, recipient, message):
|
2019-05-06 11:13:13 +00:00
|
|
|
recipient = recipient.encode()
|
|
|
|
|
|
|
|
message_parts = message.encode().split(b' ')
|
|
|
|
|
|
|
|
line = []
|
|
|
|
line_len = 0
|
|
|
|
for part in message_parts:
|
|
|
|
if len(part) + line_len > 440:
|
2019-05-09 07:35:34 +00:00
|
|
|
self.irc.send_raw(b'NOTICE %s :%s %s' % (recipient, self.irc_chan, b' '.join(line)))
|
2019-05-06 11:13:13 +00:00
|
|
|
line = []
|
|
|
|
line_len = 0
|
|
|
|
|
|
|
|
line.append(part)
|
|
|
|
line_len += len(part) + 1
|
|
|
|
|
|
|
|
if len(line) > 0:
|
2019-05-06 11:50:35 +00:00
|
|
|
self.irc.send_raw(b'NOTICE %s :[%s] %s' % (recipient, self.irc_chan, b' '.join(line)))
|
2019-05-06 09:41:12 +00:00
|
|
|
|
|
|
|
def get_event(self):
|
|
|
|
event = self.chan.recv()
|
|
|
|
return event
|
|
|
|
|
2019-05-06 13:00:55 +00:00
|
|
|
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)))
|
|
|
|
|
2019-05-06 09:41:12 +00:00
|
|
|
def run(self):
|
|
|
|
try:
|
2019-05-06 13:00:55 +00:00
|
|
|
gameloop.game(self.send, self.notice, self.voice, self.devoice, self.get_event)
|
2019-05-06 09:41:12 +00:00
|
|
|
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)
|
|
|
|
|
2019-05-06 10:50:27 +00:00
|
|
|
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:]
|
|
|
|
|
2019-05-06 09:41:12 +00:00
|
|
|
events = gameloop.events
|
|
|
|
|
2019-05-06 10:50:27 +00:00
|
|
|
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':
|
2019-05-06 11:29:34 +00:00
|
|
|
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]))
|
2019-05-06 10:50:27 +00:00
|
|
|
|
|
|
|
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,))
|
|
|
|
|
2019-05-07 09:05:59 +00:00
|
|
|
elif c == '!kick':
|
|
|
|
arg = args(1)
|
|
|
|
if arg is not None:
|
|
|
|
kickee, = arg
|
|
|
|
send_event((events.kick, nick, kickee))
|
|
|
|
|
2019-05-06 10:50:27 +00:00
|
|
|
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,))
|
2019-05-06 09:41:12 +00:00
|
|
|
|
2019-05-06 10:50:27 +00:00
|
|
|
else:
|
|
|
|
send('Subcommands: !deck add | remove | list')
|
|
|
|
|
|
|
|
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 [<number> [<type>]]')
|
|
|
|
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] <number> ...')
|
|
|
|
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))
|
|
|
|
|
2019-05-06 16:37:29 +00:00
|
|
|
elif c == '!origins':
|
|
|
|
if args(0) is not None:
|
|
|
|
send_event((events.origins, nick))
|
|
|
|
|
2019-05-06 10:50:27 +00:00
|
|
|
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:
|
2019-05-07 09:05:59 +00:00
|
|
|
send('!status !start !ready !unready !kill !join !leave !players !kick !deck !limit !card !cards !origins')
|
2019-05-06 10:50:27 +00:00
|
|
|
|
|
|
|
elif len(arg) == 1:
|
2019-05-06 16:37:29 +00:00
|
|
|
if arg[0] in ('status', 'ready', 'unready', 'kill', 'join', 'leave', 'players', 'cards', 'origins'):
|
2019-05-06 10:50:27 +00:00
|
|
|
send('Usage: !%s' % (arg[0]))
|
2019-05-06 11:29:34 +00:00
|
|
|
elif arg[0] == 'start':
|
2019-05-07 09:05:59 +00:00
|
|
|
send('Usage: !start [<preset>]')
|
|
|
|
elif arg[0] == 'kick':
|
|
|
|
send('Usage: !kick <nick>')
|
2019-05-06 10:50:27 +00:00
|
|
|
elif arg[0] == 'card':
|
|
|
|
send('Usage: [!card] <number> ...')
|
|
|
|
elif arg[0] == 'deck':
|
|
|
|
send('Subcommands: !deck add | remove | list')
|
|
|
|
elif arg[0] == 'limit':
|
|
|
|
send('Usage: !limit [<number> [<type>]]')
|
|
|
|
else:
|
|
|
|
send('No such command !%s' % (arg[0]))
|
|
|
|
elif len(arg) == 2:
|
|
|
|
if arg[0] == 'deck':
|
|
|
|
if arg[1] == 'add':
|
|
|
|
send('Usage: !deck add <code> | random')
|
|
|
|
elif arg[1] == 'remove':
|
|
|
|
send('Usage: !deck remove <code>')
|
|
|
|
elif arg[1] == 'list':
|
|
|
|
send('Usage: !deck list')
|
|
|
|
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))
|
2019-05-06 09:41:12 +00:00
|
|
|
|
2018-01-03 16:08:24 +00:00
|
|
|
# initialize(*, config)
|
2017-09-06 17:47:32 +00:00
|
|
|
# Called to initialize the IRC bot
|
|
|
|
# Runs before even logger is brought up, and blocks further bringup until it's done
|
2018-01-03 16:08:24 +00:00
|
|
|
# config is a configpatser.ConfigParser object containig contents of bot.conf
|
|
|
|
def initialize(*, config):
|
2019-05-06 09:41:12 +00:00
|
|
|
global nickserv_pass, irc_chan
|
|
|
|
nickserv_pass = config['nickserv']['password']
|
|
|
|
irc_chan = config['server']['channels'].split()[0].encode()
|
2017-09-06 17:47:32 +00:00
|
|
|
|
2018-01-02 16:31:23 +00:00
|
|
|
# on_connect(*, irc)
|
|
|
|
# Called after IRC bot has connected and sent the USER/NICk commands but not yet attempted anything else
|
2018-06-14 07:50:30 +00:00
|
|
|
# Called for every reconnect
|
2018-01-02 16:31:23 +00:00
|
|
|
# Blocks the bot until it's done, including PING/PONG handling
|
2018-01-03 16:08:24 +00:00
|
|
|
# irc is the IRC API object
|
2018-01-02 16:31:23 +00:00
|
|
|
def on_connect(*, irc):
|
2019-05-06 09:41:12 +00:00
|
|
|
global nickserv_pass
|
|
|
|
|
2019-05-06 12:10:54 +00:00
|
|
|
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
|
2019-05-06 09:41:12 +00:00
|
|
|
|
|
|
|
stop_gameloop()
|
|
|
|
start_gameloop(irc)
|
2018-01-02 16:31:23 +00:00
|
|
|
|
2018-01-20 14:54:13 +00:00
|
|
|
# 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):
|
2019-05-06 09:41:12 +00:00
|
|
|
stop_gameloop()
|
2018-01-20 14:54:13 +00:00
|
|
|
|
2017-09-06 17:47:32 +00:00
|
|
|
# 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
|
2018-06-14 07:50:30 +00:00
|
|
|
# All strings are bytestrings
|
2017-09-06 17:47:32 +00:00
|
|
|
def handle_message(*, prefix, message, nick, channel, irc):
|
2019-05-06 09:41:12 +00:00
|
|
|
global irc_chan
|
|
|
|
|
|
|
|
if channel == irc_chan:
|
2019-05-06 10:50:27 +00:00
|
|
|
parse_command(message.decode(), nick.decode(), irc)
|
2017-09-06 17:47:32 +00:00
|
|
|
|
|
|
|
# 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
|
2018-01-03 16:08:24 +00:00
|
|
|
# irc is the IRC API object
|
2018-06-14 07:50:30 +00:00
|
|
|
# All strings are bytestrings
|
2017-09-06 17:47:32 +00:00
|
|
|
def handle_nonmessage(*, prefix, command, arguments, irc):
|
2019-05-06 10:50:27 +00:00
|
|
|
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))
|