From 01e6b5b9a6354872cb5367769a0b9bb693020dbf Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 27 Jan 2013 23:18:12 +0100 Subject: [PATCH] IRC Bot ported to Twisted --- syncplay/ircBot.py | 387 ++++++++++++++++++++------------------------- syncplay/server.py | 36 ++--- 2 files changed, 185 insertions(+), 238 deletions(-) diff --git a/syncplay/ircBot.py b/syncplay/ircBot.py index dbc290c..befa806 100644 --- a/syncplay/ircBot.py +++ b/syncplay/ircBot.py @@ -1,231 +1,190 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# @author HarHar - -import socket -import threading +from twisted.words.protocols import irc +from twisted.internet import reactor, protocol from syncplay import utils -from time import sleep -import traceback +class ColorCode(object): + NORMAL = chr(15) + BOLD = chr(2) + RED = chr(3) + "5" + BLUE = chr(3) + "12" + +''' +@author Uriziel +@author HarHar +''' class Bot(object): - def __init__(self, server='irc.rizon.net', serverPassword='', port=6667, nick='SyncBot', nickservPass='', channel='', channelPassword='', functions=[]): - #Arguments - # server - IRC Server to connect to - # serverPassword - probably empty - # port - usually between 6660 - 6669, 7000 - # nick - duh - # nickservPass - if not given, NickServ identification string won't be sent - # channel - channel to autojoin and interact with - # channelPassword - if channel is +k - # functions - list/tuple of functions that can be used from the bot: - # * pause(setBy, state=bool) - # * getRooms() -> list - # * getPosition(room) -> int - # * setPosition(setBy, seconds) - # * getUsers(room) -> list of {'nick': str, 'file': str, 'length': int} - # * isPaused(room) -> bool + def __init__(self, server='irc.rizon.net', port=6667, nick='SyncBot', channel='', functions=[]): + ''' + functions - dict of functions that can be used from the bot: + functions = { + "pause": lambda setBy, state: None, + "getRooms": lambda: ["",], + "setRoomPosition": lambda setBy, seconds 0: None, + "getRoomPosition": lambda room: 0, + "getRoomUsers": lambda room: [{"nick": "", "file": "", "duration": 0},], + "isRoomPaused": lambda room: True, + } + ''' + self.factory = BotFactory(self, channel.encode("ascii", "replace"), nick.encode("ascii", "replace")) + self.proto = None + self.server = server + self.port = port + self._functions = functions + + def start(self): + reactor.connectTCP(self.server, self.port, self.factory) + + def registerProto(self, proto): + self.proto = proto - self.functions = functions - self.server = server - self.serverPassword = serverPassword - self.port = port - self.nick = nick - self.nickservPass = nickservPass - self.channel = channel - self.channelPassword = channelPassword + def __playpause(self, user): + rooms = self._functions["getRooms"]() + for room in rooms: + users = self._functions["getRoomUsers"](room) + for u in users: + if u['nick'] == user: + paused = self._functions["isRoomPaused"](room) + self._functions["pause"](user, not (paused)) + return "<{}> {} the room: `{}`".format(ColorCode.BOLD, user, ColorCode.NORMAL, "paused" if paused else "unpaused", room) + return "{}Error!{} Your nick was not found on the server.".format(ColorCode.RED, ColorCode.NORMAL) - #Connection/authentication routine - self.sock = socket.socket() - self.sock.connect((server, port)) + def __listRooms(self): + rooms = self._functions["getRooms"]() + if(len(rooms) >= 3): + v = ("`, `".join(rooms[:-1]), rooms[-1]) + return "Currently the Syncplay server hosts viewing sessions as follows: `{}` and ultimately `{}`.".format(*v) + elif(len(rooms) == 2): + return "Currently the Syncplay server hosts viewing sessions as follows: `{}` and `{}`.".format(rooms[0], rooms[1]) + elif(len(rooms) == 1): + return "Currently the Syncplay server hosts one viewing session called `{}`".format(rooms[0]) + else: + return "{}Notice:{} No rooms have been found on server".format(ColorCode.BLUE, ColorCode.NORMAL) - if serverPassword != '': - self.sockSend('PASS ' + serverPassword) - self.sockSend('NICK ' + nick) - self.sockSend('USER ' + nick + ' ' + nick + ' ' + nick + ' :SyncPlay Bot') #Don't ask me - self.sock.recv(4096) #Wait for authentication to finish + def __getListOfFiles(self, users): + files = [] + for u in users: + if [u["file"], u["duration"]] not in files: + files.append([u["file"], u["duration"]]) + return files - if nickservPass != '': - self.msg('NickServ', 'IDENTIFY ' + nickservPass) - self.sock.recv(4096) #We don't want to join if nickserv hasn't done its job (shouldn't really matter, but good for vHost) + def __getUserlist(self, room): + users = self._functions["getRoomUsers"](room) + position = self._functions["getRoomPosition"](room) + paused = "Paused" if self._functions["isRoomPaused"](room) else "Playing" + files = self.__getListOfFiles(users) + message = "" + for f in files: + if (f[0] == None): + message += "No file:\n" + else: + v = ( + ColorCode.BOLD, paused, ColorCode.NORMAL, + utils.formatTime(position), + utils.formatTime(f[1]), f[0] + ) + message += "{}<{}>{} [{}/{}] {}\n".format(*v) + u = [u['nick'] for u in users if f[0] == u['file'] and f[1] == u['duration']] + if (len(u) > 1): + message += "Played by: <{}> and <{}>.\n".format(">, <".join(u[:-1]), u[-1]) + else: + message += "Played by {} alone.\n".format(u[0]) + return message - if channel != '': - self.join(channel, channelPassword) + def __getRoomInfo(self, action): + if(action.startswith("rooms")): + room = action.replace("rooms", "", 1) + else: + room = action.replace("ri", "", 1) + room = room.strip() + if('' == room): + return "{}Usage:{} !roominfo [room]".format(ColorCode.BLUE, ColorCode.NORMAL) + rooms = self._functions["getRooms"]() + if(not room in rooms): + return "{}Error!{} Room does not exists.".format(ColorCode.RED, ColorCode.NORMAL) + message = self.__getUserlist(room) + return message + + def takeAction(self, action, user): + if(action == "help" or action == "h"): + v = (ColorCode.BOLD, ColorCode.NORMAL) + return "{}Available commands:{} !rooms / !roominfo [room] / !playpause (or aliases: !r, !ri [room], !p).".format(*v) + elif(action == "rooms" or action == "r"): + return self.__listRooms() + elif(action.startswith("roominfo") or action.startswith("ri")): + return self.__getRoomInfo(action) + elif(action == "playpause" or action == "p"): + return self.__playpause(user) + else: + return "{}Error!{} Unknown command".format(ColorCode.RED, ColorCode.NORMAL) + + def _sendChanMessage(self, msg): + if(self.proto): + self.proto.sendChanMessage(msg) - self.active = True - self.thread = threading.Thread(target=handlingThread, args=(self.sock, self)) - self.thread.setDaemon(True) - self.thread.start() + def sp_joined(self, who, room): + msg ="{}<{}>{} has joined the room: `{}`".format(ColorCode.BOLD, who, ColorCode.NORMAL, room) + self._sendChanMessage(msg) - def sp_joined(self, who, room): - self.msg(self.channel, chr(2) + '<' + who + '>'+ chr(15) +' has joined ' + room) - def sp_left(self, who, room): - self.msg(self.channel, chr(2) + '<' + who + '>'+ chr(15) +' has left ' + room) - def sp_unpaused(self, who, room): - self.msg(self.channel, chr(2) + '<' + who + '>'+ chr(15) +' has unpaused (room ' + room + ')') - def sp_paused(self, who, room): - self.msg(self.channel, chr(2) + '<' + who + '>'+ chr(15) +' has paused (room ' + room + ')') - def sp_fileplaying(self, who, filename, room): #for when syncplay knows what filename is being played - if filename == '': - return - self.msg(self.channel, chr(2) + '<' + who + '>'+ chr(15) +' is playing "' + filename + '" (room ' + room + ')') - def sp_seek(self, who, fromTime, toTime, room): - self.msg(self.channel, chr(2) + '<' + who + '>'+ chr(15) +' has jumped from ' + utils.formatTime(fromTime) + ' to ' + utils.formatTime(toTime) +' (room ' + room + ')') + def sp_left(self, who, room): + msg ="{}<{}>{} has left the room: `{}`".format(ColorCode.BOLD, who, ColorCode.NORMAL, room) + self._sendChanMessage(msg) - def sockSend(self, s): - try: - self.sock.send(s.encode('utf8') + '\r\n') - except socket.error, info: - self.active = False - print '\033[91mSocket error (bot disconnected)\033[0;0m ' + str(info) - - def msg(self, who, message): - self.sockSend('PRIVMSG ' + who + ' :' + message) - def join(self, channel, passw=''): - if passw != '': passw = ' ' + passw - self.sockSend('\r\nJOIN ' + channel + passw) - #Just to make sure we joined; doesn't hurt anyone - sleep(1) - self.sockSend('\r\nJOIN ' + channel + passw) - def part(self, channel, reason=''): - self.sockSend('PART ' + channel + ' :' + reason) - def quit(self, reason='Leaving'): - self.active = False - self.sockSend('QUIT :' + reason) - self.sock.close() - def nick(self, newnick): - self.sockSend('NICK ' + newnick) - self.nick = newnick - def irc_onMsg(self, nickFrom, host, to, msg): - sleep(0.5) - if to[0] == '#': #channel - split = msg.split(' ') + def sp_unpaused(self, who, room): + msg ="{}<{}>{} has unpaused (in room `{}`)".format(ColorCode.BOLD, who, ColorCode.NORMAL, room) + self._sendChanMessage(msg) + + def sp_paused(self, who, room): + msg ="{}<{}>{} has paused (in room `{}`)".format(ColorCode.BOLD, who, ColorCode.NORMAL, room) + self._sendChanMessage(msg) + + def sp_fileplaying(self, who, filename, room): + if filename: + msg ="{}<{}>{} is playing {} (in room `{}`)".format(ColorCode.BOLD, who, ColorCode.NORMAL, filename, room) + self._sendChanMessage(msg) + + def sp_seek(self, who, fromTime, toTime, room): + v = (ColorCode.BOLD, who, ColorCode.NORMAL, utils.formatTime(fromTime), utils.formatTime(toTime), room,) + msg ="{}<{}>{} has jumped from {} to {} (in room `{}`)".format(*v) + self._sendChanMessage(msg) + +class BotProto(irc.IRCClient): + def __init__(self, bot, nickname): + self.nickname = nickname + self.bot = bot + self.bot.registerProto(self) + + def signedOn(self): + self.join(self.factory.channel) - if split[0].lower() == '!rooms': - rooms = self.functions[1]() + def joined(self, channel): + self.msg(channel, "Syncplay IRC Bot - I'm all fired up!") - if len(rooms) == 0: - self.msg(to, chr(3) + '12Notice:' + chr(15) + ' No rooms found on server') - return + def privmsg(self, user, channel, msg): + user = user.split('!', 1)[0] + isActionMessage = channel == self.nickname or msg.startswith(self.nickname + ":") or msg.startswith("!") or msg.startswith(".") + if isActionMessage: + action = msg.replace(self.nickname + ":", "") + action = msg.lstrip(".!") + reply = self.bot.takeAction(action.lower(), user) + for line in reply.splitlines(): + self.msg(channel, line) - out = 'Currently the Syncplay server hosts viewing sessions as follows: ' - i = 0 - for room in rooms: - if i == len(rooms)-1: - out += chr(3) + '10' + room + chr(15) + '.' - elif i == len(rooms)-2: - out += chr(3) + '10' + room + chr(15) + ' and ultimately ' - else: - out += chr(3) + '10' + room + chr(15) + ', ' - i += 1 - self.msg(to, out) - elif split[0].lower() == '!roominfo': - if len(split) >= 2: - rooms = self.functions[1]() - room = '' - for r in split[1:]: - room += r + ' ' - room = room[:-1] - if (room in rooms) == False: - self.msg(to, chr(3) + '5Error!' + chr(15) + ' Room does not exists (' + room + ')') - else: - users = self.functions[4](room) - paused = self.functions[5](room) - out = chr(2) + '' + chr(15) if paused else chr(2) + '' + chr(15) - time = self.functions[2](room) - if time == None: time = 0 - for u in users: - if u['length'] == None: continue - out += ' [' + utils.formatTime(time) + '/' + utils.formatTime(u['length']) + '] ' - break - else: - out += ' ' + def sendChanMessage(self, msg): + self.msg(self.factory.channel, msg) - for u in users: - if u['file'] == None: continue - out += u['file'] - break - else: - out += '[no file]' - self.msg(to, out) - out = 'Users: ' - i = 0 - for user in users: - if i == len(users)-1: - out += chr(3) + '2' + user['nick'] + chr(15) + '.' - elif i == len(users)-2: - out += chr(3) + '2' + user['nick'] + chr(15) + ' and ' - else: - out += chr(3) + '2' + user['nick'] + chr(15) + ', ' - i += 1 - self.msg(to, out) - else: - self.msg(to, chr(2) + 'Usage:' + chr(15) + ' !roominfo [room]') - elif split[0].lower() == '!pause': - rooms = self.functions[1]() +class BotFactory(protocol.ClientFactory): + def __init__(self, bot, channel, nickname): + self.channel = channel + self.nickname = nickname + self.bot = bot + + def buildProtocol(self, addr): + p = BotProto(self.bot, self.nickname) + p.factory = self + return p - for room in rooms: - users = self.functions[4](room) - for u in users: - if u['nick'] == nickFrom: - self.functions[0](nickFrom, True) - return - self.msg(to, chr(3) + '5Error!' + chr(15) + ' Your nick was not found on the server') - elif split[0].lower() == '!play': - rooms = self.functions[1]() - - for room in rooms: - users = self.functions[4](room) - for u in users: - if u['nick'] == nickFrom: - self.functions[0](nickFrom, False) - return - self.msg(to, chr(3) + '5Error!' + chr(15) + ' Your nick was not found on the server') - elif split[0].lower() == '!help': - self.msg(to, chr(2) + 'Available commands:' + chr(15) + ' !rooms / !roominfo [room] / !pause / !play') + def clientConnectionLost(self, connector, reason): + connector.connect() -def handlingThread(sock, bot): - while bot.active: - try: - rcvd = sock.recv(4096).split('\n') - except socket.error, info: - bot.active = False - print '\033[91mSocket error (bot disconnected)\033[0;0m ' + str(info) - break - for line in rcvd: - try: - line = line.replace('\r', '') - - if line.split(' ')[0] == 'PING': - try: - sock.send('\r\nPONG ' + line.split(' ')[1].replace(':', '') + '\r\n') - except: #if we were fooled by the server :C - sock.send('\r\nPONG\r\n') - #\r\n on the beggining too because if we send two things too fast, the IRC server can discern - - lsplit = line.split(':') - if len(lsplit) >= 2: - if len(lsplit[1].split(' ')) >= 2: - if lsplit[1].split(' ')[1] == '404': - bot.join(bot.channel, bot.channelPassword) - - if 'PRIVMSG' in lsplit[1] or 'NOTICE' in lsplit[1]: - # ---BEGIN WTF BLOCK--- - lsplit = line.split(':') - addrnfrom = '' - if '~' in lsplit[1]: - addrnfrom = lsplit[1].split('~')[1].split(' ')[0] - nfrom = lsplit[1].split('!')[0] - else: - nfrom = lsplit[1].split('!')[0] - - if len(lsplit[1].split()) >= 3: - to = lsplit[1].split()[2] - msg = '' - for brks in lsplit[2:]: - msg += brks + ':' - msg = msg[:-1].lstrip() - # ---END WTF BLOCK- -- - bot.irc_onMsg(nfrom, addrnfrom, to, msg) - except: - print traceback.format_exc() \ No newline at end of file + def clientConnectionFailed(self, connector, reason): + print "IRC Bot connection failed, please check your configuration" diff --git a/syncplay/server.py b/syncplay/server.py index 7be007a..d3fb4bb 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -1,5 +1,4 @@ #coding:utf8 -#TODO: #12, #13, #8; import hashlib from twisted.internet import task, reactor from twisted.internet.protocol import Factory @@ -30,56 +29,45 @@ class SyncFactory(Factory): if(ircConnectionData): self.setupIRCBot(ircConnectionData) - def readIrcConfig(self, ircConfig): #TODO: + def readIrcConfig(self, ircConfig): if(ircConfig and os.path.isfile(ircConfig)): cfg = codecs.open(ircConfig, "r", "utf-8-sig").read() cfg = cfg.splitlines() ircConnectionData = { "server": "", - "serverPassword": "", "port": "", "nick": "", - "nickservPass": "", - "channelPassword": "", "channel": "" } for line in cfg: if("irc.server: " in line): ircConnectionData['server'] = line.split(": ")[1] - elif("irc.serverPassword: " in line): - ircConnectionData['serverPassword'] = line.split(": ")[1] elif("irc.serverPort: " in line): ircConnectionData['port'] = int(line.split(": ")[1]) elif("irc.botName: " in line): ircConnectionData['nick'] = line.split(": ")[1] - elif("irc.nickservPass: " in line): - ircConnectionData['nickservPass'] = line.split(": ")[1] - elif("irc.channelPassword: " in line): - ircConnectionData['channelPassword'] = line.split(": ")[1] elif("irc.channel: " in line): ircConnectionData['channel'] = line.split(": ")[1] return ircConnectionData def setupIRCBot(self, ircConnectionData): - botFunctions = [ - self.ircPauseRoom, - self.getRooms, - self.getRoomPosition, - self.ircSetRoomPosition, - self.getRoomUsernames, - self.isRoomPaused, - ] + botFunctions = { + "pause": self.ircPauseRoom, + "getRooms": self.getRooms, + "setRoomPosition": self.ircSetRoomPosition, + "getRoomPosition": self.getRoomPosition, + "getRoomUsers": self.getRoomUsernames, + "isRoomPaused": self.isRoomPaused, + } try: self.ircBot = IRCBot( ircConnectionData['server'], - ircConnectionData['serverPassword'], ircConnectionData['port'], ircConnectionData['nick'], - ircConnectionData['nickservPass'], ircConnectionData['channel'], - ircConnectionData['channelPassword'], botFunctions, ) + self.ircBot.start() except: print "IRC Bot could not be started, please check your configuration" @@ -339,9 +327,9 @@ class SyncFactory(Factory): if room in self._rooms: for user in self._rooms[room].itervalues(): if(user.file): - l.append({'nick': user.name, 'file': user.file['name'], "length": user.file['duration']}) + l.append({'nick': user.name, 'file': user.file['name'], "duration": user.file['duration']}) else: - l.append({'nick': user.name, 'file': None, "length": None}) + l.append({'nick': user.name, 'file': None, "duration": None}) return l def isRoomPaused(self, room):