From 23b0b261abc45463834c7f2e3422f8c2bdc4ddee Mon Sep 17 00:00:00 2001 From: Max Dominik Weber Date: Thu, 4 Apr 2013 04:09:27 +0000 Subject: [PATCH 01/25] Formatted readme to markdown also fixed minor errors in the readme --- README | 107 ------------------------------------------------------ README.md | 85 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 107 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 9013b20..0000000 --- a/README +++ /dev/null @@ -1,107 +0,0 @@ -ircbotframe.py is an IRC bot framework written in python. - -Eventually usage will be to import the ircBot class from ircbotframe.py - ------------------ -1. Example usage: ------------------ - -First, create the bot object with the constructor: - - bot = ircBot(, , , ) - -Next, bind functions to the various IRC message types. These functions will be called when the IRC bot receives messages with those message types. The function to be called should be of the form: - - funcname(sender, header, message) - -These can then be bound using: - - bot.bind(, ) - -Once functions have been bound, connect the irc bot to the server using: - - bot.connect() - -Then join a / some channel(s) using: - - bot.joinchan() - -where begins with the '#' character. Finally, make the bot start listening to messages with: - - bot.run() - ---------------- -2. Bot methods: ---------------- - -ban (banMask, channel, reason): - - Bans and kicks users satistfying the ban mask from with a given (optional). Only works if the bot has operator privileges in . - -bind (msgtype, callback) - - This command binds a particular message type (passed as a string) to a given function. If the bot hears a message with this message type, the corresponding function will be called. The function MUST be of the form: - - funcname(sender, headers, message) - - where is the nick of the entity that sent the message, is a list of any additional message headers before the message content and is the actual message content. Bind can be used to listen for both server and user message types (bear in mind that server message types are usually numeric strings). Bind only allows a single function to be bound to each message type. - -connect () - - Makes the bot connect to the specified irc server. - -debug (state) - - Sets the bot's debug state to which is a boolean value. - -disconnect (qMessage) - - Makes the bot disconnect from the irc server with the quit message (for no quit message, put an empty string) - -identify (nick, callbackApproved, approvedParameters, callbackDenied, deniedParameters) - - where and are of the form: - - funcname(parameter 1, parameter 2, ...) - - Used for checking whether or not a user with the nickname is who they say they are by checking their WHOIS info from the server. If they are verified, the function will be called with the parameters (which will fill , , etc. in order). If they cannot be verified or are not registered, then is called with . Parameter lists and are tuples with the tuple items matching the and function parameters in order from the second parameter. For example: - - identify(nick, approved, (string, integer1), denied, (integer2)) - - If the nick is verified the following function will be called: - - approved(string, integer1) - - Otherwise the other function will be called: - - denied(integer2) - -joinchan (channel) - - Makes the bot join a given channel. MUST start with a '#' character. - -kick (nick, channel, reason) - - If the bot has operator privileges in the channel , the user with will be kicked from it with the given reason . - -reconnect () - - Disconnects from the server then reconnects. It disconnects with the quit message "Reconnecting". - -run () - - Tells the bot to start listening to the messages it receives and to act upon the input using functions connected to the bindings using the command bind(). The bot starts listening on a new thread and so is not a blocking call. Any bot functions you wish to call must be called by functions connected to bindings (using the command bind()). - -say (recipient, message) - - The bot says the given message to the recipient . The recipient can be a channel (and should start with a '#' character if this is the case). - -send (string) - - Sends a raw IRC message given by to the server. Can have dire consequences if your message is not formatted correctly. It is advised that you consult the IRC specification in RFC 1459. - -stop () - - Stops the IRC bot from running. You should disconnect from the server first. The bot's thread will terminate naturally. Should you wish to use the bot's thread join() function, stop() should be called first. - - diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c8516d --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +ircbotframe.py is an IRC bot framework written in python. + +To use this, import the `ircBot` class from ircbotframe.py + +Example usage +============= + +First, create the bot object with the constructor: + + bot = ircBot(, , , ) + +Next, bind functions to the various IRC message types. These functions will be called when the IRC bot receives messages with those message types. The function to be called should be of the form: + + funcname(sender, header, message) + +These can then be bound using: + + bot.bind(, ) + +Once functions have been bound, connect the irc bot to the server using: + + bot.connect() + +Then join a / some channel(s) using: + + bot.joinchan() + +where `channel name` begins with the `#` character. Finally, make the bot start listening to messages with: + + bot.run() + +Bot methods +=========== + + ban (banMask, channel, reason) + +Bans and kicks users satistfying the ban mask `banMask` from `channel` with a given `reason` (optional). Only works if the bot has operator privileges in `channel`. + + bind (msgtype, callback) + +This command binds a particular message type `msgtype` (passed as a string) to a given function. If the bot hears a message with this message type, the corresponding function will be called. The `callback` function MUST be of the form `funcname(sender, headers, message)` where `sender` is the nick of the entity that sent the message, `headers` is a list of any additional message headers before the message content and `message` is the actual message content. Bind can be used to listen for both server and user message types (bear in mind that server message types are usually numeric strings). Bind only allows a single function to be bound to each message type. + + connect () + +Makes the bot connect to the specified irc server. + + debug (state) + +Sets the bot's debug state to `state` which is a boolean value. + + disconnect (qMessage) + +Makes the bot disconnect from the irc server with the quit message `qMessage` (for no quit message, put an empty string) + + identify (nick, callbackApproved, approvedParameters, callbackDenied, deniedParameters) + +where `callbackApproved` and `callbackDenied` are of the form `funcname(parameter 1, parameter 2, ...)`. Used for checking whether or not a user with the nickname `nick` is who they say they are by checking their `WHOIS` info from the server. If they are verified, the function `callbackApproved` will be called with the parameters `approvedParameters` (which will fill `parameter 1`, `parameter 2`, etc. in order). If they cannot be verified or are not registered, then `callbackDenied` is called with `deniedParameters`. Parameter lists `approvedParameters` and `deniedParameters` are tuples with the tuple items matching the `callbackApproved` and `callbackDenied` function parameters in order from the second parameter. For example, `identify(nick, approved, (string, integer1), denied, (integer2))`. If the nick is verified the following function will be called: `approved(string, integer1)`. Otherwise the other function will be called: `denied(integer2)` + + joinchan (channel) + +Makes the bot join a given channel. `channel` *must* start with a `#` character. + + kick (nick, channel, reason) + +If the bot has operator privileges in the channel `channel`, the user with `nick` will be kicked from it with the given reason . + + reconnect () + +Disconnects from the server then reconnects. It disconnects with the quit message "Reconnecting". + + run () + +Tells the bot to start listening to the messages it receives and to act upon the input using functions connected to the bindings using the command `bind()`. The bot starts listening on a new thread and so is not a blocking call. Any bot functions you wish to call must be called by functions connected to bindings (using the command `bind()`). + + say (recipient, message) + +The bot says the given message `message` to the recipient `recipient`. The recipient can be a channel (and should start with a `#` character if this is the case). + + send (string) + +Sends a raw IRC message given by `string` to the server. Can have dire consequences if your message is not formatted correctly. It is advised that you consult the IRC specification in RFC 1459. + + stop () + +Stops the IRC bot from running. You should disconnect from the server first. The bot's thread will terminate naturally. Should you wish to use the bot's thread `join()` function, `stop()` should be called first. From b3c835cf1f7c398ac9150cdfc4c49a06eeeb4da6 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Thu, 4 Apr 2013 04:43:21 +0000 Subject: [PATCH 02/25] Run python files through 2to3 This also updates the readme to reflect this. --- README.md | 2 +- examplebot.py | 8 ++++---- ircbotframe.py | 22 +++++++++++----------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5c8516d..8ed0bcb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ircbotframe.py is an IRC bot framework written in python. +ircbotframe.py is an IRC bot framework written in Python 3. To use this, import the `ircBot` class from ircbotframe.py diff --git a/examplebot.py b/examplebot.py index 1246f40..61e378a 100644 --- a/examplebot.py +++ b/examplebot.py @@ -45,10 +45,10 @@ def privmsg(sender, headers, message): if sender == owner: bot.identify(sender, kickSuccess, (message[6:firstSpace], message[firstSpace+1:secondSpace], message[secondSpace+1:]), authFailure, (headers[0], sender)) else: - print "PRIVMSG: \"" + message + "\"" + print("PRIVMSG: \"" + message + "\"") def actionmsg(sender, headers, message): - print "An ACTION message was sent by " + sender + " with the headers " + str(headers) + ". It says: \"" + sender + " " + message + "\"" + print("An ACTION message was sent by " + sender + " with the headers " + str(headers) + ". It says: \"" + sender + " " + message + "\"") def endMOTD(sender, headers, message): bot.joinchan(chanName) @@ -76,10 +76,10 @@ def endMOTD(sender, headers, message): bot.start() inputStr = "" while inputStr != "stop": - inputStr = raw_input() + inputStr = input() bot.stop() bot.join() else: - print "Usage: python examplebot.py " + print("Usage: python examplebot.py ") diff --git a/ircbotframe.py b/ircbotframe.py index 9c259ea..bcde8c4 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -35,10 +35,10 @@ def sendImmediately(self, string): if not self.error: try: self.irc.send(bytes(string) + b"\r\n") - except socket.error, msg: + except socket.error as msg: self.error = True - print "Output error", msg - print "Was sending \"" + string + "\"" + print("Output error", msg) + print("Was sending \"" + string + "\"") def isInError(self): return self.error @@ -54,8 +54,8 @@ def __recv(self): # Last (incomplete) line is kept for buffer purposes. try: data = self.buffer + self.irc.recv(4096) - except socket.error, msg: - raise socket.error, msg + except socket.error as msg: + raise socket.error(msg) self.lines += data.split(b"\r\n") self.buffer = self.lines[len(self.lines) - 1] self.lines = self.lines[:len(self.lines) - 1] @@ -65,8 +65,8 @@ def getLine(self): while len(self.lines) == 0: try: self.__recv() - except socket.error, msg: - raise socket.error, msg + except socket.error as msg: + raise socket.error(msg) time.sleep(1); line = self.lines[0] self.lines = self.lines[1:] @@ -160,7 +160,7 @@ def __processLine(self, line): def __debugPrint(self, s): if self.debug: - print s + print(s) # PUBLIC FUNCTIONS def ban(self, banMask, channel, reason): self.__debugPrint("Banning " + banMask + "...") @@ -169,7 +169,7 @@ def ban(self, banMask, channel, reason): def bind(self, msgtype, callback): # Check if the msgtype already exists - for i in xrange(0, len(self.binds)): + for i in range(0, len(self.binds)): # Remove msgtype if it has already been "binded" to if self.binds[i][0] == msgtype: self.binds.remove(i) @@ -214,8 +214,8 @@ def run(self): while len(line) == 0: try: line = self.inBuf.getLine() - except socket.error, msg: - print "Input error", msg + except socket.error as msg: + print("Input error", msg) self.reconnect() if line.startswith("PING"): self.outBuf.sendImmediately("PONG " + line.split()[1]) From 217035d26e7fd9bd0dff7135acbdf151a7aab88b Mon Sep 17 00:00:00 2001 From: Max Dominik Weber Date: Thu, 4 Apr 2013 05:27:06 +0000 Subject: [PATCH 03/25] Fixes #1 --- ircbotframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ircbotframe.py b/ircbotframe.py index bcde8c4..3bec468 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -34,7 +34,7 @@ def sendImmediately(self, string): # Sends the given string without buffering. if not self.error: try: - self.irc.send(bytes(string) + b"\r\n") + self.irc.send(string.encode("utf-8") + b"\r\n") except socket.error as msg: self.error = True print("Output error", msg) From be39fc12727c9bcde9e835e16d6eeb4c155dd926 Mon Sep 17 00:00:00 2001 From: Max Dominik Weber Date: Thu, 4 Apr 2013 06:30:33 +0000 Subject: [PATCH 04/25] Fixes #2 --- ircbotframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ircbotframe.py b/ircbotframe.py index 3bec468..ff6f197 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -46,7 +46,7 @@ class ircInputBuffer: # Keeps a record of the last line fragment received by the socket which is usually not a complete line. # It is prepended onto the next block of data to make a complete line. def __init__(self, irc): - self.buffer = "" + self.buffer = b"" self.irc = irc self.lines = [] def __recv(self): From 87bdfb56663fcc74342cc22fba06bc60f66b5f3e Mon Sep 17 00:00:00 2001 From: Fenhl Date: Thu, 4 Apr 2013 20:47:45 +0000 Subject: [PATCH 05/25] Ditch byte strings, some refactoring --- ircbotframe.py | 65 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index ff6f197..881d988 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -11,6 +11,7 @@ def __init__(self, irc): self.irc = irc self.queue = [] self.error = False + def __pop(self): if len(self.queue) == 0: self.waiting = False @@ -18,9 +19,11 @@ def __pop(self): self.sendImmediately(self.queue[0]) self.queue = self.queue[1:] self.__startPopTimer() + def __startPopTimer(self): self.timer = threading.Timer(1, self.__pop) self.timer.start() + def sendBuffered(self, string): # Sends the given string after the rest of the messages in the buffer. # There is a 1 second gap between each message. @@ -30,15 +33,17 @@ def sendBuffered(self, string): self.waiting = True self.sendImmediately(string) self.__startPopTimer() + def sendImmediately(self, string): # Sends the given string without buffering. if not self.error: try: - self.irc.send(string.encode("utf-8") + b"\r\n") + self.irc.send((string + "\r\n").encode("utf-8")) except socket.error as msg: self.error = True print("Output error", msg) print("Was sending \"" + string + "\"") + def isInError(self): return self.error @@ -46,22 +51,25 @@ class ircInputBuffer: # Keeps a record of the last line fragment received by the socket which is usually not a complete line. # It is prepended onto the next block of data to make a complete line. def __init__(self, irc): - self.buffer = b"" + self.buffer = "" self.irc = irc self.lines = [] + def __recv(self): # Receives new data from the socket and splits it into lines. - # Last (incomplete) line is kept for buffer purposes. try: - data = self.buffer + self.irc.recv(4096) + data = self.buffer + self.irc.recv(4096).decode("utf-8") except socket.error as msg: raise socket.error(msg) - self.lines += data.split(b"\r\n") - self.buffer = self.lines[len(self.lines) - 1] - self.lines = self.lines[:len(self.lines) - 1] + self.lines += data.split("\r\n") + # Last (incomplete) line is kept for buffer purposes. + self.buffer = self.lines[-1] + self.lines = self.lines[:-1] + def getLine(self): # Returns the next line of IRC received by the socket. - # Converts the received string to standard string format before returning. + # This should already be in the standard string format. + # If no lines are buffered, this blocks until a line is received. while len(self.lines) == 0: try: self.__recv() @@ -70,7 +78,7 @@ def getLine(self): time.sleep(1); line = self.lines[0] self.lines = self.lines[1:] - return str(line) + return line class ircBot(threading.Thread): def __init__(self, network, port, name, description): @@ -84,6 +92,7 @@ def __init__(self, network, port, name, description): self.identifyLock = False self.binds = [] self.debug = False + # PRIVATE FUNCTIONS def __identAccept(self, nick): """ Executes all the callbacks that have been approved for this nick @@ -96,6 +105,7 @@ def __identAccept(self, nick): self.identifyNickCommands.pop(i) else: i += 1 + def __identReject(self, nick): # Calls the given "denied" callback for all functions called by that nick. i = 0 @@ -106,11 +116,13 @@ def __identReject(self, nick): self.identifyNickCommands.pop(i) else: i += 1 + def __callBind(self, msgtype, sender, headers, message): # Calls the function associated with the given msgtype. for (messageType, callback) in self.binds: if (messageType == msgtype): callback(sender, headers, message) + def __processLine(self, line): # If a message comes from another user, it will have an @ symbol if "@" in line: @@ -121,7 +133,7 @@ def __processLine(self, line): lastColon = line[gap+1:].find(":") + 2 + gap else: lastColon = line[1:].find(":") + 1 - + # Does most of the parsing of the line received from the IRC network. # if there is no message to the line. ie. only one colon at the start of line if ":" not in line[1:]: @@ -131,7 +143,7 @@ def __processLine(self, line): # Split everything up to the lastColon (ie. the headers) headers = line[1:lastColon-1].strip().split(" ") message = line[lastColon:] - + sender = headers[0] if len(headers) < 2: self.__debugPrint("Unhelpful number of messages in message: \"" + line + "\"") @@ -157,24 +169,27 @@ def __processLine(self, line): else: self.outBuf.sendBuffered("WHOIS " + self.identifyNickCommands[0][0]) self.__callBind(headers[1], sender, headers[2:], message) - + def __debugPrint(self, s): if self.debug: print(s) + # PUBLIC FUNCTIONS def ban(self, banMask, channel, reason): + # only bans, no kick. self.__debugPrint("Banning " + banMask + "...") self.outBuf.sendBuffered("MODE +b " + channel + " " + banMask) - self.kick(nick, channel, reason) - + # TODO get nick + #self.kick(nick, channel, reason) + def bind(self, msgtype, callback): # Check if the msgtype already exists for i in range(0, len(self.binds)): - # Remove msgtype if it has already been "binded" to + # Remove msgtype if it has already been "bound" to if self.binds[i][0] == msgtype: self.binds.remove(i) self.binds.append((msgtype, callback)) - + def connect(self): self.__debugPrint("Connecting...") self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -182,30 +197,39 @@ def connect(self): self.inBuf = ircInputBuffer(self.irc) self.outBuf = ircOutputBuffer(self.irc) self.outBuf.sendBuffered("NICK " + self.name) - self.outBuf.sendBuffered("USER " + self.name + " " + self.name + " " + self.name + " :" + self.desc) + self.outBuf.sendBuffered("USER " + self.name + " 0 * :" + self.desc) + def debugging(self, state): self.debug = state + def disconnect(self, qMessage): self.__debugPrint("Disconnecting...") + # TODO make the following block until the message is sent self.outBuf.sendBuffered("QUIT :" + qMessage) self.irc.close() + def identify(self, nick, approvedFunc, approvedParams, deniedFunc, deniedParams): self.__debugPrint("Verifying " + nick + "...") self.identifyNickCommands += [(nick, approvedFunc, approvedParams, deniedFunc, deniedParams)] + # TODO this doesn't seem right if not self.identifyLock: self.outBuf.sendBuffered("WHOIS " + nick) self.identifyLock = True + def joinchan(self, channel): self.__debugPrint("Joining " + channel + "...") self.outBuf.sendBuffered("JOIN " + channel) + def kick(self, nick, channel, reason): self.__debugPrint("Kicking " + nick + "...") self.outBuf.sendBuffered("KICK " + channel + " " + nick + " :" + reason) + def reconnect(self): self.disconnect("Reconnecting") self.__debugPrint("Pausing before reconnecting...") time.sleep(5) self.connect() + def run(self): self.__debugPrint("Bot is now running.") self.connect() @@ -223,13 +247,16 @@ def run(self): self.__processLine(line) if self.outBuf.isInError(): self.reconnect() + def say(self, recipient, message): - self.outBuf.sendBuffered("PRIVMSG " + recipient + " :" + message) + self.outBuf.sendBuffered("PRIVMSG " + recipient + " :" + message) + def send(self, string): self.outBuf.sendBuffered(string) + def stop(self): self.keepGoing = False + def unban(self, banMask, channel): self.__debugPrint("Unbanning " + banMask + "...") self.outBuf.sendBuffered("MODE -b " + channel + " " + banMask) - From 100008634ad8e74e2d1ec2255800d2e984e2059a Mon Sep 17 00:00:00 2001 From: Max Dominik Weber Date: Wed, 24 Jul 2013 17:23:43 +0000 Subject: [PATCH 06/25] Fix a bug with bind() --- ircbotframe.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index 881d988..f6b32b4 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -90,7 +90,7 @@ def __init__(self, network, port, name, description): self.port = port self.identifyNickCommands = [] self.identifyLock = False - self.binds = [] + self.binds = {} self.debug = False # PRIVATE FUNCTIONS @@ -119,9 +119,9 @@ def __identReject(self, nick): def __callBind(self, msgtype, sender, headers, message): # Calls the function associated with the given msgtype. - for (messageType, callback) in self.binds: - if (messageType == msgtype): - callback(sender, headers, message) + callback = self.binds.get(msgtype) + if callback: + callback(sender, headers, message) def __processLine(self, line): # If a message comes from another user, it will have an @ symbol @@ -183,12 +183,7 @@ def ban(self, banMask, channel, reason): #self.kick(nick, channel, reason) def bind(self, msgtype, callback): - # Check if the msgtype already exists - for i in range(0, len(self.binds)): - # Remove msgtype if it has already been "bound" to - if self.binds[i][0] == msgtype: - self.binds.remove(i) - self.binds.append((msgtype, callback)) + self.binds[msgtype] = callback def connect(self): self.__debugPrint("Connecting...") From 3096607f1c7a5631d7895ffbd67a94267e06be66 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sat, 27 Jul 2013 10:04:26 +0000 Subject: [PATCH 07/25] Add pycache to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file From 52697b7d1cfb0f2b8d9e29dd22ecd853883833d3 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sun, 18 Aug 2013 05:38:54 +0000 Subject: [PATCH 08/25] Add optional server password --- README.md | 2 +- ircbotframe.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ed0bcb..4f748c1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Example usage First, create the bot object with the constructor: - bot = ircBot(, , , ) + bot = ircBot(, , , [, password=]) Next, bind functions to the various IRC message types. These functions will be called when the IRC bot receives messages with those message types. The function to be called should be of the form: diff --git a/ircbotframe.py b/ircbotframe.py index f6b32b4..1f1e375 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -81,11 +81,12 @@ def getLine(self): return line class ircBot(threading.Thread): - def __init__(self, network, port, name, description): + def __init__(self, network, port, name, description, password=None): threading.Thread.__init__(self) self.keepGoing = True self.name = name self.desc = description + self.password = password self.network = network self.port = port self.identifyNickCommands = [] @@ -191,6 +192,8 @@ def connect(self): self.irc.connect((self.network, self.port)) self.inBuf = ircInputBuffer(self.irc) self.outBuf = ircOutputBuffer(self.irc) + if self.password is not None: + self.outBut.sendBuffered("PASS " + self.password) self.outBuf.sendBuffered("NICK " + self.name) self.outBuf.sendBuffered("USER " + self.name + " 0 * :" + self.desc) From e3ea56f14899cf848703b416b194b381bee4030e Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sun, 18 Aug 2013 05:54:17 +0000 Subject: [PATCH 09/25] Typo --- ircbotframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ircbotframe.py b/ircbotframe.py index 1f1e375..f77fc8b 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -193,7 +193,7 @@ def connect(self): self.inBuf = ircInputBuffer(self.irc) self.outBuf = ircOutputBuffer(self.irc) if self.password is not None: - self.outBut.sendBuffered("PASS " + self.password) + self.outBuf.sendBuffered("PASS " + self.password) self.outBuf.sendBuffered("NICK " + self.name) self.outBuf.sendBuffered("USER " + self.name + " 0 * :" + self.desc) From a21b373310db5b7d8c9bdf660d48d216e971adef Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sun, 18 Aug 2013 06:08:44 +0000 Subject: [PATCH 10/25] Add SSL --- README.md | 2 +- ircbotframe.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f748c1..64fffb3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Example usage First, create the bot object with the constructor: - bot = ircBot(, , , [, password=]) + bot = ircBot(, , , [, password=][, ssl=True]) Next, bind functions to the various IRC message types. These functions will be called when the IRC bot receives messages with those message types. The function to be called should be of the form: diff --git a/ircbotframe.py b/ircbotframe.py index f77fc8b..d079385 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -1,6 +1,7 @@ import socket import threading import re +import ssl import time class ircOutputBuffer: @@ -81,7 +82,7 @@ def getLine(self): return line class ircBot(threading.Thread): - def __init__(self, network, port, name, description, password=None): + def __init__(self, network, port, name, description, password=None, ssl=False): threading.Thread.__init__(self) self.keepGoing = True self.name = name @@ -89,6 +90,7 @@ def __init__(self, network, port, name, description, password=None): self.password = password self.network = network self.port = port + self.ssl = ssl self.identifyNickCommands = [] self.identifyLock = False self.binds = {} @@ -189,6 +191,8 @@ def bind(self, msgtype, callback): def connect(self): self.__debugPrint("Connecting...") self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if self.ssl: + self.irc = ssl.wrap_socket(self.irc) self.irc.connect((self.network, self.port)) self.inBuf = ircInputBuffer(self.irc) self.outBuf = ircOutputBuffer(self.irc) From f261dc346efe428c95bc005f4276fa818dd0f754 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Mon, 19 Aug 2013 05:42:38 +0000 Subject: [PATCH 11/25] Replace invisible char with escape sequence --- ircbotframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ircbotframe.py b/ircbotframe.py index d079385..8c00434 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -156,7 +156,7 @@ def __processLine(self, line): if cut != -1: sender = sender[:cut] msgtype = headers[1] - if msgtype == "PRIVMSG" and message.startswith("ACTION ") and message.endswith(""): + if msgtype == "PRIVMSG" and message.startswith("\001ACTION ") and message.endswith("\001"): msgtype = "ACTION" message = message[8:-1] self.__callBind(msgtype, sender, headers[2:], message) From 49b2a1e5ec8a592214691b59f84225e385a44063 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sun, 13 Oct 2013 06:33:30 +0000 Subject: [PATCH 12/25] Basic logging Messages to a channel are saved in a variable, not to a file. --- ircbotframe.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index 8c00434..bfe1c26 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -62,6 +62,8 @@ def __recv(self): data = self.buffer + self.irc.recv(4096).decode("utf-8") except socket.error as msg: raise socket.error(msg) + except UnicodeDecodeError: + data = '' self.lines += data.split("\r\n") # Last (incomplete) line is kept for buffer purposes. self.buffer = self.lines[-1] @@ -95,6 +97,8 @@ def __init__(self, network, port, name, description, password=None, ssl=False): self.identifyLock = False self.binds = {} self.debug = False + self.default_log_length = 200 + self.channel_data = {} # PRIVATE FUNCTIONS def __identAccept(self, nick): @@ -156,22 +160,27 @@ def __processLine(self, line): if cut != -1: sender = sender[:cut] msgtype = headers[1] - if msgtype == "PRIVMSG" and message.startswith("\001ACTION ") and message.endswith("\001"): - msgtype = "ACTION" - message = message[8:-1] - self.__callBind(msgtype, sender, headers[2:], message) + if msgtype == 'PRIVMSG': + if message.startswith('\001ACTION ') and message.endswith('\001'): + msgtype = 'ACTION' + message = message[8:-1] + if headers[2].startswith('#'): + self.channel_data[headers[2]]['log'].append((msgtype, sender, headers[2:], message)) # log PRIVMSG and ACTION only for now + if len(self.channel_data[headers[2]]['log']) > self.channel_data[headers[2]]['log_length']: + self.channel_data[headers[2]]['log'] = self.channel_data[headers[2]]['log'][-self.channel_data[headers[2]]['log_length']:] # trim log to log length if necessary else: - self.__debugPrint("[" + headers[1] + "] " + message) - if (headers[1] == "307" or headers[1] == "330") and len(headers) >= 4: + msgtype = headers[1] + self.__debugPrint('[' + msgtype + '] ' + message) + if msgtype in ['307', '330'] and len(headers) >= 4: self.__identAccept(headers[3]) - if headers[1] == "318" and len(headers) >= 4: + if msgtype == '318' and len(headers) >= 4: self.__identReject(headers[3]) #identifies the next user in the nick commands list if len(self.identifyNickCommands) == 0: self.identifyLock = False else: self.outBuf.sendBuffered("WHOIS " + self.identifyNickCommands[0][0]) - self.__callBind(headers[1], sender, headers[2:], message) + self.__callBind(msgtype, sender, headers[2:], message) def __debugPrint(self, s): if self.debug: @@ -220,6 +229,10 @@ def identify(self, nick, approvedFunc, approvedParams, deniedFunc, deniedParams) def joinchan(self, channel): self.__debugPrint("Joining " + channel + "...") + self.channel_data[channel] = { + 'log': [], + 'log_length': self.default_log_length + } self.outBuf.sendBuffered("JOIN " + channel) def kick(self, nick, channel, reason): From f61844f221dc6628d9a261aff57ad47887bf59c4 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sun, 13 Oct 2013 21:35:13 +0000 Subject: [PATCH 13/25] Log messages sent by the bot --- ircbotframe.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index bfe1c26..dd3f96d 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -98,6 +98,7 @@ def __init__(self, network, port, name, description, password=None, ssl=False): self.binds = {} self.debug = False self.default_log_length = 200 + self.log_own_messages = True self.channel_data = {} # PRIVATE FUNCTIONS @@ -165,9 +166,7 @@ def __processLine(self, line): msgtype = 'ACTION' message = message[8:-1] if headers[2].startswith('#'): - self.channel_data[headers[2]]['log'].append((msgtype, sender, headers[2:], message)) # log PRIVMSG and ACTION only for now - if len(self.channel_data[headers[2]]['log']) > self.channel_data[headers[2]]['log_length']: - self.channel_data[headers[2]]['log'] = self.channel_data[headers[2]]['log'][-self.channel_data[headers[2]]['log_length']:] # trim log to log length if necessary + self.__log(headers[2], msgtype, sender, headers[2:], message) # log PRIVMSG and ACTION only for now else: msgtype = headers[1] self.__debugPrint('[' + msgtype + '] ' + message) @@ -186,6 +185,12 @@ def __debugPrint(self, s): if self.debug: print(s) + def __log(self, channel, msgtype, sender, headers, message): + if channel in self.channel_data: + self.channel_data[channel]['log'].append((msgtype, sender, headers, message)) + if len(self.channel_data[channel]['log']) > self.channel_data[channel]['log_length']: + self.channel_data[channel]['log'] = self.channel_data[channel]['log'][-self.channel_data[channel]['log_length']:] # trim log to log length if necessary + # PUBLIC FUNCTIONS def ban(self, banMask, channel, reason): # only bans, no kick. @@ -264,6 +269,8 @@ def run(self): self.reconnect() def say(self, recipient, message): + if self.log_own_messages: + self.__log(recipient, 'PRIVMSG', self.name, [recipient], message) self.outBuf.sendBuffered("PRIVMSG " + recipient + " :" + message) def send(self, string): From 335ba79db5c643118283e942aedf0adcce621074 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Tue, 15 Oct 2013 23:26:25 +0000 Subject: [PATCH 14/25] Rename __log to log --- ircbotframe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index dd3f96d..fbf124f 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -166,7 +166,7 @@ def __processLine(self, line): msgtype = 'ACTION' message = message[8:-1] if headers[2].startswith('#'): - self.__log(headers[2], msgtype, sender, headers[2:], message) # log PRIVMSG and ACTION only for now + self.log(headers[2], msgtype, sender, headers[2:], message) # log PRIVMSG and ACTION only for now else: msgtype = headers[1] self.__debugPrint('[' + msgtype + '] ' + message) @@ -185,7 +185,7 @@ def __debugPrint(self, s): if self.debug: print(s) - def __log(self, channel, msgtype, sender, headers, message): + def log(self, channel, msgtype, sender, headers, message): if channel in self.channel_data: self.channel_data[channel]['log'].append((msgtype, sender, headers, message)) if len(self.channel_data[channel]['log']) > self.channel_data[channel]['log_length']: @@ -270,7 +270,7 @@ def run(self): def say(self, recipient, message): if self.log_own_messages: - self.__log(recipient, 'PRIVMSG', self.name, [recipient], message) + self.log(recipient, 'PRIVMSG', self.name, [recipient], message) self.outBuf.sendBuffered("PRIVMSG " + recipient + " :" + message) def send(self, string): From 753082e6609edd4f0f706f881ef1135046c4af58 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Fri, 18 Oct 2013 04:57:40 +0000 Subject: [PATCH 15/25] Add topic command --- ircbotframe.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index fbf124f..8f558c7 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -279,6 +279,9 @@ def send(self, string): def stop(self): self.keepGoing = False + def topic(self, channel, message): + self.send('TOPIC ' + channel + ' :' + message) + def unban(self, banMask, channel): - self.__debugPrint("Unbanning " + banMask + "...") - self.outBuf.sendBuffered("MODE -b " + channel + " " + banMask) + self.__debugPrint('Unbanning ' + banMask + '...') + self.outBuf.sendBuffered('MODE -b ' + channel + ' ' + banMask) From 58ccbf52492935f989f40bb108c37717973f1498 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sat, 19 Oct 2013 09:39:01 +0000 Subject: [PATCH 16/25] Update readme with better intro and documentation of additional methods --- README.md | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 64fffb3..bfbd52b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -ircbotframe.py is an IRC bot framework written in Python 3. +This is an **IRC bot framework written in Python 3**. -To use this, import the `ircBot` class from ircbotframe.py +To use it, import the `ircBot` class from [`ircbotframe.py`](ircbotframe.py). Example usage ============= @@ -32,54 +32,62 @@ where `channel name` begins with the `#` character. Finally, make the bot start Bot methods =========== - ban (banMask, channel, reason) + ban(banMask, channel, reason) Bans and kicks users satistfying the ban mask `banMask` from `channel` with a given `reason` (optional). Only works if the bot has operator privileges in `channel`. - bind (msgtype, callback) + bind(msgtype, callback) This command binds a particular message type `msgtype` (passed as a string) to a given function. If the bot hears a message with this message type, the corresponding function will be called. The `callback` function MUST be of the form `funcname(sender, headers, message)` where `sender` is the nick of the entity that sent the message, `headers` is a list of any additional message headers before the message content and `message` is the actual message content. Bind can be used to listen for both server and user message types (bear in mind that server message types are usually numeric strings). Bind only allows a single function to be bound to each message type. - connect () + connect() Makes the bot connect to the specified irc server. - - debug (state) + + debug(state) Sets the bot's debug state to `state` which is a boolean value. - disconnect (qMessage) + disconnect(qMessage) Makes the bot disconnect from the irc server with the quit message `qMessage` (for no quit message, put an empty string) - - identify (nick, callbackApproved, approvedParameters, callbackDenied, deniedParameters) + + identify(nick, callbackApproved, approvedParameters, callbackDenied, deniedParameters) where `callbackApproved` and `callbackDenied` are of the form `funcname(parameter 1, parameter 2, ...)`. Used for checking whether or not a user with the nickname `nick` is who they say they are by checking their `WHOIS` info from the server. If they are verified, the function `callbackApproved` will be called with the parameters `approvedParameters` (which will fill `parameter 1`, `parameter 2`, etc. in order). If they cannot be verified or are not registered, then `callbackDenied` is called with `deniedParameters`. Parameter lists `approvedParameters` and `deniedParameters` are tuples with the tuple items matching the `callbackApproved` and `callbackDenied` function parameters in order from the second parameter. For example, `identify(nick, approved, (string, integer1), denied, (integer2))`. If the nick is verified the following function will be called: `approved(string, integer1)`. Otherwise the other function will be called: `denied(integer2)` - joinchan (channel) + joinchan(channel) Makes the bot join a given channel. `channel` *must* start with a `#` character. - - kick (nick, channel, reason) + + kick(nick, channel, reason) If the bot has operator privileges in the channel `channel`, the user with `nick` will be kicked from it with the given reason . - reconnect () + reconnect() Disconnects from the server then reconnects. It disconnects with the quit message "Reconnecting". - run () + run() Tells the bot to start listening to the messages it receives and to act upon the input using functions connected to the bindings using the command `bind()`. The bot starts listening on a new thread and so is not a blocking call. Any bot functions you wish to call must be called by functions connected to bindings (using the command `bind()`). - - say (recipient, message) + + say(recipient, message) The bot says the given message `message` to the recipient `recipient`. The recipient can be a channel (and should start with a `#` character if this is the case). - send (string) + send(string) Sends a raw IRC message given by `string` to the server. Can have dire consequences if your message is not formatted correctly. It is advised that you consult the IRC specification in RFC 1459. - - stop () + + stop() Stops the IRC bot from running. You should disconnect from the server first. The bot's thread will terminate naturally. Should you wish to use the bot's thread `join()` function, `stop()` should be called first. + + topic(channel, message) + +The bot sets the topic in `channel` to `message`, if it has sufficient privileges. + + unban(banMask, channel) + +Unbans previously banned users satistfying the ban mask `banMask` from `channel`. Only works if the bot has operator privileges in `channel`. From 53e36978aac5700bd5c9642b61d0c067d81e5f53 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Sun, 6 Mar 2016 01:20:08 +0000 Subject: [PATCH 17/25] Add IPv6 support, fixes #4 --- README.md | 14 ++++---- ircbotframe.py | 91 +++++++++++++++++++++++++++++++------------------- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index bfbd52b..f10247c 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,24 @@ Example usage First, create the bot object with the constructor: - bot = ircBot(, , , [, password=][, ssl=True]) + bot = ircBot(, , , [, password=][, ssl=True][, ip_ver=<4 or 6>]) Next, bind functions to the various IRC message types. These functions will be called when the IRC bot receives messages with those message types. The function to be called should be of the form: funcname(sender, header, message) - + These can then be bound using: - bot.bind(, ) - + bot.bind(, ) + Once functions have been bound, connect the irc bot to the server using: bot.connect() - + Then join a / some channel(s) using: bot.joinchan() - + where `channel name` begins with the `#` character. Finally, make the bot start listening to messages with: bot.run() @@ -53,7 +53,7 @@ Sets the bot's debug state to `state` which is a boolean value. Makes the bot disconnect from the irc server with the quit message `qMessage` (for no quit message, put an empty string) identify(nick, callbackApproved, approvedParameters, callbackDenied, deniedParameters) - + where `callbackApproved` and `callbackDenied` are of the form `funcname(parameter 1, parameter 2, ...)`. Used for checking whether or not a user with the nickname `nick` is who they say they are by checking their `WHOIS` info from the server. If they are verified, the function `callbackApproved` will be called with the parameters `approvedParameters` (which will fill `parameter 1`, `parameter 2`, etc. in order). If they cannot be verified or are not registered, then `callbackDenied` is called with `deniedParameters`. Parameter lists `approvedParameters` and `deniedParameters` are tuples with the tuple items matching the `callbackApproved` and `callbackDenied` function parameters in order from the second parameter. For example, `identify(nick, approved, (string, integer1), denied, (integer2))`. If the nick is verified the following function will be called: `approved(string, integer1)`. Otherwise the other function will be called: `denied(integer2)` joinchan(channel) diff --git a/ircbotframe.py b/ircbotframe.py index 8f558c7..b08230a 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -1,7 +1,8 @@ -import socket -import threading +import ipaddress import re +import socket import ssl +import threading import time class ircOutputBuffer: @@ -12,7 +13,7 @@ def __init__(self, irc): self.irc = irc self.queue = [] self.error = False - + def __pop(self): if len(self.queue) == 0: self.waiting = False @@ -20,11 +21,11 @@ def __pop(self): self.sendImmediately(self.queue[0]) self.queue = self.queue[1:] self.__startPopTimer() - + def __startPopTimer(self): self.timer = threading.Timer(1, self.__pop) self.timer.start() - + def sendBuffered(self, string): # Sends the given string after the rest of the messages in the buffer. # There is a 1 second gap between each message. @@ -34,7 +35,7 @@ def sendBuffered(self, string): self.waiting = True self.sendImmediately(string) self.__startPopTimer() - + def sendImmediately(self, string): # Sends the given string without buffering. if not self.error: @@ -44,7 +45,7 @@ def sendImmediately(self, string): self.error = True print("Output error", msg) print("Was sending \"" + string + "\"") - + def isInError(self): return self.error @@ -55,7 +56,7 @@ def __init__(self, irc): self.buffer = "" self.irc = irc self.lines = [] - + def __recv(self): # Receives new data from the socket and splits it into lines. try: @@ -68,7 +69,7 @@ def __recv(self): # Last (incomplete) line is kept for buffer purposes. self.buffer = self.lines[-1] self.lines = self.lines[:-1] - + def getLine(self): # Returns the next line of IRC received by the socket. # This should already be in the standard string format. @@ -84,7 +85,7 @@ def getLine(self): return line class ircBot(threading.Thread): - def __init__(self, network, port, name, description, password=None, ssl=False): + def __init__(self, network, port, name, description, password=None, ssl=False, ip_ver=None): threading.Thread.__init__(self) self.keepGoing = True self.name = name @@ -100,12 +101,32 @@ def __init__(self, network, port, name, description, password=None, ssl=False): self.default_log_length = 200 self.log_own_messages = True self.channel_data = {} - + if ip_ver == 4: + self.socket_family = socket.AF_INET + elif ip_ver == 6: + self.socket_family = socket.AF_INET6 + elif ip_ver is None: + try: + address = ipaddress.ip_address(network) + except: + for family, _, _, _, _ in socket.getaddrinfo(network, port, proto=socket.IPPROTO_TCP): + if family == socket.AF_INET6: + self.socket_family = socket.AF_INET6 + else: + self.socket_family = socket.AF_INET + else: + self.socket_family = { + 4: socket.AF_INET, + 6: socket.AF_INET6 + }[address.version] + else: + raise ValueError('Invalid IP version: {!r}'.format(ip_ver)) + # PRIVATE FUNCTIONS def __identAccept(self, nick): """ Executes all the callbacks that have been approved for this nick """ - i = 0 + i = 0 while i < len(self.identifyNickCommands): (nickName, accept, acceptParams, reject, rejectParams) = self.identifyNickCommands[i] if nick == nickName: @@ -113,7 +134,7 @@ def __identAccept(self, nick): self.identifyNickCommands.pop(i) else: i += 1 - + def __identReject(self, nick): # Calls the given "denied" callback for all functions called by that nick. i = 0 @@ -124,13 +145,13 @@ def __identReject(self, nick): self.identifyNickCommands.pop(i) else: i += 1 - + def __callBind(self, msgtype, sender, headers, message): # Calls the function associated with the given msgtype. callback = self.binds.get(msgtype) if callback: callback(sender, headers, message) - + def __processLine(self, line): # If a message comes from another user, it will have an @ symbol if "@" in line: @@ -141,7 +162,7 @@ def __processLine(self, line): lastColon = line[gap+1:].find(":") + 2 + gap else: lastColon = line[1:].find(":") + 1 - + # Does most of the parsing of the line received from the IRC network. # if there is no message to the line. ie. only one colon at the start of line if ":" not in line[1:]: @@ -151,7 +172,7 @@ def __processLine(self, line): # Split everything up to the lastColon (ie. the headers) headers = line[1:lastColon-1].strip().split(" ") message = line[lastColon:] - + sender = headers[0] if len(headers) < 2: self.__debugPrint("Unhelpful number of messages in message: \"" + line + "\"") @@ -180,17 +201,17 @@ def __processLine(self, line): else: self.outBuf.sendBuffered("WHOIS " + self.identifyNickCommands[0][0]) self.__callBind(msgtype, sender, headers[2:], message) - + def __debugPrint(self, s): if self.debug: print(s) - + def log(self, channel, msgtype, sender, headers, message): if channel in self.channel_data: self.channel_data[channel]['log'].append((msgtype, sender, headers, message)) if len(self.channel_data[channel]['log']) > self.channel_data[channel]['log_length']: self.channel_data[channel]['log'] = self.channel_data[channel]['log'][-self.channel_data[channel]['log_length']:] # trim log to log length if necessary - + # PUBLIC FUNCTIONS def ban(self, banMask, channel, reason): # only bans, no kick. @@ -198,13 +219,13 @@ def ban(self, banMask, channel, reason): self.outBuf.sendBuffered("MODE +b " + channel + " " + banMask) # TODO get nick #self.kick(nick, channel, reason) - + def bind(self, msgtype, callback): self.binds[msgtype] = callback - + def connect(self): self.__debugPrint("Connecting...") - self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.irc = socket.socket(self.socket_family, socket.SOCK_STREAM) if self.ssl: self.irc = ssl.wrap_socket(self.irc) self.irc.connect((self.network, self.port)) @@ -214,16 +235,16 @@ def connect(self): self.outBuf.sendBuffered("PASS " + self.password) self.outBuf.sendBuffered("NICK " + self.name) self.outBuf.sendBuffered("USER " + self.name + " 0 * :" + self.desc) - + def debugging(self, state): self.debug = state - + def disconnect(self, qMessage): self.__debugPrint("Disconnecting...") # TODO make the following block until the message is sent self.outBuf.sendBuffered("QUIT :" + qMessage) self.irc.close() - + def identify(self, nick, approvedFunc, approvedParams, deniedFunc, deniedParams): self.__debugPrint("Verifying " + nick + "...") self.identifyNickCommands += [(nick, approvedFunc, approvedParams, deniedFunc, deniedParams)] @@ -231,7 +252,7 @@ def identify(self, nick, approvedFunc, approvedParams, deniedFunc, deniedParams) if not self.identifyLock: self.outBuf.sendBuffered("WHOIS " + nick) self.identifyLock = True - + def joinchan(self, channel): self.__debugPrint("Joining " + channel + "...") self.channel_data[channel] = { @@ -239,17 +260,17 @@ def joinchan(self, channel): 'log_length': self.default_log_length } self.outBuf.sendBuffered("JOIN " + channel) - + def kick(self, nick, channel, reason): self.__debugPrint("Kicking " + nick + "...") self.outBuf.sendBuffered("KICK " + channel + " " + nick + " :" + reason) - + def reconnect(self): self.disconnect("Reconnecting") self.__debugPrint("Pausing before reconnecting...") time.sleep(5) self.connect() - + def run(self): self.__debugPrint("Bot is now running.") self.connect() @@ -267,21 +288,21 @@ def run(self): self.__processLine(line) if self.outBuf.isInError(): self.reconnect() - + def say(self, recipient, message): if self.log_own_messages: self.log(recipient, 'PRIVMSG', self.name, [recipient], message) self.outBuf.sendBuffered("PRIVMSG " + recipient + " :" + message) - + def send(self, string): self.outBuf.sendBuffered(string) - + def stop(self): self.keepGoing = False - + def topic(self, channel, message): self.send('TOPIC ' + channel + ' :' + message) - + def unban(self, banMask, channel): self.__debugPrint('Unbanning ' + banMask + '...') self.outBuf.sendBuffered('MODE -b ' + channel + ' ' + banMask) From 2c35543050379cc4f0f4f2b043795d3a4ab54070 Mon Sep 17 00:00:00 2001 From: lemoer Date: Sat, 4 Jun 2016 23:49:58 +0200 Subject: [PATCH 18/25] Fix: IPv6 is now used if there is an aaaa record Because of the missing break statement, the address family was set to IPv4 though we have an aaaa record in the dns response. --- ircbotframe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ircbotframe.py b/ircbotframe.py index b08230a..d8e2d4e 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -112,6 +112,7 @@ def __init__(self, network, port, name, description, password=None, ssl=False, i for family, _, _, _, _ in socket.getaddrinfo(network, port, proto=socket.IPPROTO_TCP): if family == socket.AF_INET6: self.socket_family = socket.AF_INET6 + break else: self.socket_family = socket.AF_INET else: From 22f22fa79b85ba2cf7a3ef917e77f20eed420325 Mon Sep 17 00:00:00 2001 From: lemoer Date: Sat, 4 Jun 2016 23:57:44 +0200 Subject: [PATCH 19/25] Use timeout while receiving on the socket Before this patch our thread was blocking as long as the server does not send a message. Now we can do additional stuff directly in this thread without waiting for the server. --- ircbotframe.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index d8e2d4e..3a1d8e7 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -61,8 +61,6 @@ def __recv(self): # Receives new data from the socket and splits it into lines. try: data = self.buffer + self.irc.recv(4096).decode("utf-8") - except socket.error as msg: - raise socket.error(msg) except UnicodeDecodeError: data = '' self.lines += data.split("\r\n") @@ -71,15 +69,18 @@ def __recv(self): self.lines = self.lines[:-1] def getLine(self): - # Returns the next line of IRC received by the socket. + # Returns the next line of IRC received by the socket or None. # This should already be in the standard string format. - # If no lines are buffered, this blocks until a line is received. + # If no lines are buffered, this blocks until a line is received + # or we reach the socket timeout. When the timeout is + # reached, the function returns None. + while len(self.lines) == 0: try: self.__recv() - except socket.error as msg: - raise socket.error(msg) - time.sleep(1); + except socket.timeout: + return None + line = self.lines[0] self.lines = self.lines[1:] return line @@ -227,6 +228,8 @@ def bind(self, msgtype, callback): def connect(self): self.__debugPrint("Connecting...") self.irc = socket.socket(self.socket_family, socket.SOCK_STREAM) + self.irc.settimeout(1.0) + if self.ssl: self.irc = ssl.wrap_socket(self.irc) self.irc.connect((self.network, self.port)) @@ -276,13 +279,16 @@ def run(self): self.__debugPrint("Bot is now running.") self.connect() while self.keepGoing: - line = "" - while len(line) == 0: - try: - line = self.inBuf.getLine() - except socket.error as msg: - print("Input error", msg) - self.reconnect() + line = None + + try: + line = self.inBuf.getLine() + except socket.error as msg: + print("Input error", msg) + self.reconnect() + + if line is None: + continue if line.startswith("PING"): self.outBuf.sendImmediately("PONG " + line.split()[1]) else: From c6770c29cf8e4852d2fb0b97645706d8020ce916 Mon Sep 17 00:00:00 2001 From: lemoer Date: Sat, 4 Jun 2016 23:58:06 +0200 Subject: [PATCH 20/25] Format: added some whitespace --- ircbotframe.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ircbotframe.py b/ircbotframe.py index 3a1d8e7..8cc28d5 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -232,11 +232,14 @@ def connect(self): if self.ssl: self.irc = ssl.wrap_socket(self.irc) + self.irc.connect((self.network, self.port)) self.inBuf = ircInputBuffer(self.irc) self.outBuf = ircOutputBuffer(self.irc) + if self.password is not None: self.outBuf.sendBuffered("PASS " + self.password) + self.outBuf.sendBuffered("NICK " + self.name) self.outBuf.sendBuffered("USER " + self.name + " 0 * :" + self.desc) @@ -293,6 +296,7 @@ def run(self): self.outBuf.sendImmediately("PONG " + line.split()[1]) else: self.__processLine(line) + if self.outBuf.isInError(): self.reconnect() From 56e0161ab7dc55da9b7e5a197e4a325a23a0bb33 Mon Sep 17 00:00:00 2001 From: lemoer Date: Sun, 5 Jun 2016 00:17:48 +0200 Subject: [PATCH 21/25] Differentiate between gracefully reconnect and non gracefully At some points it does not make sense to send the QUIT signal before reconnecting. --- ircbotframe.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index 8cc28d5..447fb74 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -246,11 +246,16 @@ def connect(self): def debugging(self, state): self.debug = state + def close(self): + self.outBuf = None + self.inBuf = None + self.irc.close() + def disconnect(self, qMessage): self.__debugPrint("Disconnecting...") # TODO make the following block until the message is sent self.outBuf.sendBuffered("QUIT :" + qMessage) - self.irc.close() + self.close() def identify(self, nick, approvedFunc, approvedParams, deniedFunc, deniedParams): self.__debugPrint("Verifying " + nick + "...") @@ -272,8 +277,12 @@ def kick(self, nick, channel, reason): self.__debugPrint("Kicking " + nick + "...") self.outBuf.sendBuffered("KICK " + channel + " " + nick + " :" + reason) - def reconnect(self): - self.disconnect("Reconnecting") + def reconnect(self, gracefully=True): + if gracefully: + self.disconnect("Reconnecting") + else: + self.close() + self.__debugPrint("Pausing before reconnecting...") time.sleep(5) self.connect() @@ -281,6 +290,7 @@ def reconnect(self): def run(self): self.__debugPrint("Bot is now running.") self.connect() + while self.keepGoing: line = None @@ -288,7 +298,7 @@ def run(self): line = self.inBuf.getLine() except socket.error as msg: print("Input error", msg) - self.reconnect() + self.reconnect(gracefully=False) if line is None: continue @@ -298,7 +308,7 @@ def run(self): self.__processLine(line) if self.outBuf.isInError(): - self.reconnect() + self.reconnect(gracefully=False) def say(self, recipient, message): if self.log_own_messages: From 80f3d1043e81eb9c8dce67a3f6d67a5f54b0a8f4 Mon Sep 17 00:00:00 2001 From: lemoer Date: Sun, 5 Jun 2016 00:37:29 +0200 Subject: [PATCH 22/25] Disconnect after bot.keepGoing is set to False --- ircbotframe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ircbotframe.py b/ircbotframe.py index 447fb74..5bca303 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -310,6 +310,8 @@ def run(self): if self.outBuf.isInError(): self.reconnect(gracefully=False) + self.disconnect() + def say(self, recipient, message): if self.log_own_messages: self.log(recipient, 'PRIVMSG', self.name, [recipient], message) From dbadc640e62b6f3d881036a42d7d9f0a269b3820 Mon Sep 17 00:00:00 2001 From: lemoer Date: Sun, 5 Jun 2016 03:42:21 +0200 Subject: [PATCH 23/25] Introduce better connect behavior This commit causes several changes to the internals of this library. We are now using the python builtin modules "sched" and "queue" to send and receive data. The connect function is now blocking until we reach the connect_timeout or the server sends the end of the motd. There are two new configuration variables: * connect_timeout * reconnect_interval --- README.md | 2 +- ircbotframe.py | 160 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 109 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index f10247c..fc506d7 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ This command binds a particular message type `msgtype` (passed as a string) to a connect() -Makes the bot connect to the specified irc server. +Makes the bot connect to the specified irc server. This function is blocking until the server sends the end of the motd or the connect_timeout is reached. The return value indicates which case you have (`True` means successful). debug(state) diff --git a/ircbotframe.py b/ircbotframe.py index 5bca303..a37b726 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -4,50 +4,41 @@ import ssl import threading import time +import sched +import queue + class ircOutputBuffer: - # Delays consecutive messages by at least 1 second. - # This prevents the bot spamming the IRC server. + # This class provides buffered and unbuffered sending to a socket def __init__(self, irc): - self.waiting = False self.irc = irc - self.queue = [] - self.error = False - - def __pop(self): - if len(self.queue) == 0: - self.waiting = False - else: - self.sendImmediately(self.queue[0]) - self.queue = self.queue[1:] - self.__startPopTimer() - - def __startPopTimer(self): - self.timer = threading.Timer(1, self.__pop) - self.timer.start() + self.queue = queue.Queue() def sendBuffered(self, string): # Sends the given string after the rest of the messages in the buffer. - # There is a 1 second gap between each message. - if self.waiting: - self.queue.append(string) - else: - self.waiting = True - self.sendImmediately(string) - self.__startPopTimer() + self.queue.put_nowait(string) + return True + + def sendFromQueue(self): + # Send the oldest message in the buffer if there is one + try: + string = self.queue.get_nowait() + result = self.sendImmediately(string) + self.queue.task_done() + return result + except queue.Empty: + return True def sendImmediately(self, string): # Sends the given string without buffering. - if not self.error: - try: - self.irc.send((string + "\r\n").encode("utf-8")) - except socket.error as msg: - self.error = True - print("Output error", msg) - print("Was sending \"" + string + "\"") + try: + self.irc.send((string + "\r\n").encode("utf-8")) + return True + except socket.error as msg: + print("Output error", msg) + print("Was sending \"" + string + "\"") + return False - def isInError(self): - return self.error class ircInputBuffer: # Keeps a record of the last line fragment received by the socket which is usually not a complete line. @@ -85,6 +76,7 @@ def getLine(self): self.lines = self.lines[1:] return line + class ircBot(threading.Thread): def __init__(self, network, port, name, description, password=None, ssl=False, ip_ver=None): threading.Thread.__init__(self) @@ -102,6 +94,14 @@ def __init__(self, network, port, name, description, password=None, ssl=False, i self.default_log_length = 200 self.log_own_messages = True self.channel_data = {} + self.irc = None + self.outBuf = None + self.inBuf = None + self.connected = False + self.connect_timeout = 30 + self.reconnect_interval = 30 + self.__sched = sched.scheduler() + if ip_ver == 4: self.socket_family = socket.AF_INET elif ip_ver == 6: @@ -193,6 +193,8 @@ def __processLine(self, line): else: msgtype = headers[1] self.__debugPrint('[' + msgtype + '] ' + message) + if msgtype == '376': + self.connected = True if msgtype in ['307', '330'] and len(headers) >= 4: self.__identAccept(headers[3]) if msgtype == '318' and len(headers) >= 4: @@ -208,6 +210,40 @@ def __debugPrint(self, s): if self.debug: print(s) + def __periodicSend(self): + if not self.irc: + return + + if not self.outBuf.sendFromQueue(): + self.close() + return + + # Delays consecutive messages by at least 1 second. + # This prevents the bot spamming the IRC server. + self.__sched.enter(1, priority=10, action=self.__periodicSend) + + def __periodicRecv(self): + if not self.irc: + return + + try: + line = self.inBuf.getLine() + except socket.error as msg: + self.__debugPrint("Input error", msg) + self.close() + return + + if line is not None: + if line.startswith("PING"): + if not self.outBuf.sendImmediately("PONG " + line.split()[1]): + self.close() + return + else: + self.__processLine(line) + + # next recv should be directly but with verly low priority + self.__sched.enter(0.01, priority=1, action=self.__periodicRecv) + def log(self, channel, msgtype, sender, headers, message): if channel in self.channel_data: self.channel_data[channel]['log'].append((msgtype, sender, headers, message)) @@ -218,22 +254,34 @@ def log(self, channel, msgtype, sender, headers, message): def ban(self, banMask, channel, reason): # only bans, no kick. self.__debugPrint("Banning " + banMask + "...") - self.outBuf.sendBuffered("MODE +b " + channel + " " + banMask) + self.send("MODE +b " + channel + " " + banMask) # TODO get nick #self.kick(nick, channel, reason) def bind(self, msgtype, callback): self.binds[msgtype] = callback + def __handleConnectingTimeout(self): + if not self.connected: + self.close() + def connect(self): self.__debugPrint("Connecting...") self.irc = socket.socket(self.socket_family, socket.SOCK_STREAM) - self.irc.settimeout(1.0) + self.irc.settimeout(self.connect_timeout) if self.ssl: self.irc = ssl.wrap_socket(self.irc) - self.irc.connect((self.network, self.port)) + try: + self.irc.connect((self.network, self.port)) + except socket.error as msg: + self.__debugPrint("Connection failed: %s" % msg) + self.close() + return False + + self.irc.settimeout(1.0) + self.inBuf = ircInputBuffer(self.irc) self.outBuf = ircOutputBuffer(self.irc) @@ -243,6 +291,22 @@ def connect(self): self.outBuf.sendBuffered("NICK " + self.name) self.outBuf.sendBuffered("USER " + self.name + " 0 * :" + self.desc) + self.connected = False + + self.__periodicSend() + self.__periodicRecv() + self.__sched.enter(self.connect_timeout, priority=20, action=self.__handleConnectingTimeout) + + while True: + if self.connected: + self.__debugPrint("Connection was successful!") + return True + + if self.irc is None: + return False + + self.__sched.run(blocking=False) + def debugging(self, state): self.debug = state @@ -250,6 +314,8 @@ def close(self): self.outBuf = None self.inBuf = None self.irc.close() + self.irc = None + self.connected = False def disconnect(self, qMessage): self.__debugPrint("Disconnecting...") @@ -284,7 +350,7 @@ def reconnect(self, gracefully=True): self.close() self.__debugPrint("Pausing before reconnecting...") - time.sleep(5) + time.sleep(self.reconnect_interval) self.connect() def run(self): @@ -292,23 +358,13 @@ def run(self): self.connect() while self.keepGoing: - line = None - - try: - line = self.inBuf.getLine() - except socket.error as msg: - print("Input error", msg) - self.reconnect(gracefully=False) - - if line is None: + if self.irc is None: + self.__debugPrint("Pausing before reconnecting...") + time.sleep(self.reconnect_interval) + self.connect() continue - if line.startswith("PING"): - self.outBuf.sendImmediately("PONG " + line.split()[1]) - else: - self.__processLine(line) - if self.outBuf.isInError(): - self.reconnect(gracefully=False) + self.__sched.run(blocking=False) self.disconnect() From 5777691943f56dd5b70ae5be758a3e693e25678c Mon Sep 17 00:00:00 2001 From: lemoer Date: Sun, 5 Jun 2016 03:46:10 +0200 Subject: [PATCH 24/25] Sending without an active connection now results in a warning The previous behavior was an exception because self.outBuf is None in case of no connection. --- ircbotframe.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ircbotframe.py b/ircbotframe.py index a37b726..1599c10 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -320,7 +320,7 @@ def close(self): def disconnect(self, qMessage): self.__debugPrint("Disconnecting...") # TODO make the following block until the message is sent - self.outBuf.sendBuffered("QUIT :" + qMessage) + self.send("QUIT :" + qMessage) self.close() def identify(self, nick, approvedFunc, approvedParams, deniedFunc, deniedParams): @@ -328,7 +328,7 @@ def identify(self, nick, approvedFunc, approvedParams, deniedFunc, deniedParams) self.identifyNickCommands += [(nick, approvedFunc, approvedParams, deniedFunc, deniedParams)] # TODO this doesn't seem right if not self.identifyLock: - self.outBuf.sendBuffered("WHOIS " + nick) + self.send("WHOIS " + nick) self.identifyLock = True def joinchan(self, channel): @@ -337,11 +337,11 @@ def joinchan(self, channel): 'log': [], 'log_length': self.default_log_length } - self.outBuf.sendBuffered("JOIN " + channel) + self.send("JOIN " + channel) def kick(self, nick, channel, reason): self.__debugPrint("Kicking " + nick + "...") - self.outBuf.sendBuffered("KICK " + channel + " " + nick + " :" + reason) + self.send("KICK " + channel + " " + nick + " :" + reason) def reconnect(self, gracefully=True): if gracefully: @@ -371,9 +371,13 @@ def run(self): def say(self, recipient, message): if self.log_own_messages: self.log(recipient, 'PRIVMSG', self.name, [recipient], message) - self.outBuf.sendBuffered("PRIVMSG " + recipient + " :" + message) + self.send("PRIVMSG " + recipient + " :" + message) def send(self, string): + if not self.connected: + self.__debugPrint("WARNING: you are trying to send without being connected - \"", string, "\"") + return + self.outBuf.sendBuffered(string) def stop(self): @@ -384,4 +388,4 @@ def topic(self, channel, message): def unban(self, banMask, channel): self.__debugPrint('Unbanning ' + banMask + '...') - self.outBuf.sendBuffered('MODE -b ' + channel + ' ' + banMask) + self.send('MODE -b ' + channel + ' ' + banMask) From 95ce4740ee4e1ded431e435ffc17b3cf1968a7e9 Mon Sep 17 00:00:00 2001 From: lemoer Date: Sun, 5 Jun 2016 04:12:21 +0200 Subject: [PATCH 25/25] The client is now pinging the server regularly With this patch the client is able to detect if the connection to the server is lost. So he is able to try to reconnect. There are two new configuration variables: * ping_timeout * ping_interval --- ircbotframe.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ircbotframe.py b/ircbotframe.py index 1599c10..46be5ae 100644 --- a/ircbotframe.py +++ b/ircbotframe.py @@ -100,6 +100,11 @@ def __init__(self, network, port, name, description, password=None, ssl=False, i self.connected = False self.connect_timeout = 30 self.reconnect_interval = 30 + self.ping_timeout = 10 + self.ping_interval = 60 + + self.bind("PONG", self.__handlePong) + self.__unansweredPing = False self.__sched = sched.scheduler() if ip_ver == 4: @@ -244,6 +249,26 @@ def __periodicRecv(self): # next recv should be directly but with verly low priority self.__sched.enter(0.01, priority=1, action=self.__periodicRecv) + def __periodicPing(self): + self.ping() + self.__sched.enter(self.ping_interval, 1, self.__periodicPing) + + def __handlePong(self, sender, headers, message): + self.__unansweredPing = False + + def __handlePingTimeout(self): + if self.__unansweredPing: + self.__debugPrint("Ping timeout reached. Killing the connection.") + self.close() + + def ping(self): + if self.__unansweredPing: + return + + self.outBuf.sendImmediately('PING %s' % self.network) + self.__unansweredPing = True + self.__sched.enter(self.ping_timeout, 1, self.__handlePingTimeout) + def log(self, channel, msgtype, sender, headers, message): if channel in self.channel_data: self.channel_data[channel]['log'].append((msgtype, sender, headers, message)) @@ -357,6 +382,8 @@ def run(self): self.__debugPrint("Bot is now running.") self.connect() + self.__periodicPing() + while self.keepGoing: if self.irc is None: self.__debugPrint("Pausing before reconnecting...")