| wiioa/bot.py |
| 1 | #!/usr/bin/python |
| 2 | import socket, os, string, sys, re, signal |
| 3 | |
| 4 | """ Creates an IRC bot that connects to an IRC server and parses received text. |
| 5 | You can delve as deep into the connection with the IRC server as you choose. |
| 6 | Subclass IRCBot and reimplement a high-level callback for specifc events such as text spoken in |
| 7 | a channel optionally reimplement IRCBot.act which passes raw received data from |
| 8 | IRC straight to you - it's your decision. See the online documentation at http://cerulean.pyresoft.com/docs/ircbot for a very simple sample and member reference. |
| 9 | Changelog: |
| 10 | - Sat Jan 8 2005: Added IRCBot.userEntered(user, channel) callback - called when a new user enters a channel, as well as fixing bugs with IRCBot.messageFromSender. """ |
| 11 | |
| 12 | """ The debug level. Set to False to not display debug output. """ |
| 13 | debug = True |
| 14 | |
| 15 | def dbg(s): |
| 16 | """ Write output for debugging purpose. """ |
| 17 | if debug: |
| 18 | print s |
| 19 | |
| 20 | class IRCBot: |
| 21 | """ The L{IRCBot} class provides a full IRC bot framework for connection to an IRC server. Once |
| 22 | connected, the bot parses received text and calls functions upon significant events, i.e a message has been received in a channel, or a private message has been sent by another user. |
| 23 | |
| 24 | One can subclass this class and reimplement a higher-level callback (L{text received in a channel<messageFromChannel>}) or can optionally reimplement a L{lower-level function<act>} which receives raw data from the IRC server. |
| 25 | A very simple example is as follows. It connects to an IRC server and joins two channels. If someone in either of those channels says a string beginning with 'hello' it responds with a greeting: |
| 26 | |
| 27 | >>> class HelloBot(IRCBot): |
| 28 | ... def messageFromChannel(self, channel, user, message): |
| 29 | ... if message.startswith("hello"): |
| 30 | ... self.sendMessageToChannel(channel, "Hello to you too " + user + "!") |
| 31 | |
| 32 | >>> bot = HelloBot("HelloBot", "irc.freenode.net", ["#channel1", "#channel2"]) |
| 33 | >>> bot.execute() ; bot.close() |
| 34 | |
| 35 | |
| 36 | """ |
| 37 | def __init__(self, nick, server, channels): |
| 38 | """ Constructor to initiate and store all the information needed for a connection to the IRC server. |
| 39 | @type nick: string |
| 40 | @param nick: The nickname of the bot. |
| 41 | @type server: string |
| 42 | @param server: The IRC server to connect to. |
| 43 | @type channels: list |
| 44 | @param channels: The IRC channels to join upon connection. The channels are joined in IRCBot.firstTime. """ |
| 45 | self.firstconnection = True |
| 46 | self.nick = nick |
| 47 | self.server = server |
| 48 | self.channels = channels |
| 49 | self.port = 6666 |
| 50 | self.name = "A hyper-intelligent shade of blue" |
| 51 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 52 | |
| 53 | def execute(self): |
| 54 | """ Runs the bot's main loop, reading data from the IRC server and acting accordingly. """ |
| 55 | self.startConnection() |
| 56 | olddata = "" |
| 57 | while 1: |
| 58 | # First read the data. |
| 59 | try: |
| 60 | data = string.strip(self.recv(1024)) |
| 61 | except socket.error, msg: |
| 62 | pass |
| 63 | # If we received \0 then exit, we're done. |
| 64 | if not data: |
| 65 | break |
| 66 | dbg("bot <- " + data) |
| 67 | |
| 68 | # If we're ping'd then pong back and return. |
| 69 | if data.startswith('PING '): |
| 70 | self.send("PONG :" + data[6:]) |
| 71 | else: |
| 72 | # Main parsing |
| 73 | self.baseAct(data) |
| 74 | |
| 75 | # If we just connected to IRC call the firsttime function. |
| 76 | if self.firstconnection: |
| 77 | self.firstTime(data) |
| 78 | self.firstconnection = False |
| 79 | |
| 80 | def close(self): |
| 81 | """ Closes the connection with the IRC server. """ |
| 82 | self.sock.close() |
| 83 | |
| 84 | def firstTime(self, data): |
| 85 | """ Callback called when the bot has just initiated the connection with the IRC server. |
| 86 | Reimplement to your liking. The default behavior is to join the channels you specified in the constructor. |
| 87 | @type data: string |
| 88 | @param data: The data received from the IRC server upon first connection. """ |
| 89 | for channel in self.channels: |
| 90 | self.send("JOIN " + channel) |
| 91 | |
| 92 | def sendMessageToChannel(self, channel, message): |
| 93 | """ Send a message to a channel. |
| 94 | @type channel: string |
| 95 | @param channel: The channel to send the message to. |
| 96 | @type message: string |
| 97 | @param message: The message to send to the channel. """ |
| 98 | self.send("PRIVMSG " + channel + " :" + message) |
| 99 | |
| 100 | #- Callbacks you may implement instead of delving into the raw data passed to IRCBot.act |
| 101 | def messageFromChannel(self, channel, user, message): |
| 102 | """ Callback called when a message is received from a channel the bot is residing in. |
| 103 | @type channel: string |
| 104 | @param channel: The channel from which a message has been received. |
| 105 | @type user: string |
| 106 | @param user: The user who sent the message in the channel. |
| 107 | @type message: string |
| 108 | @param message: The message received. """ |
| 109 | dbg("Received message from '" + str(user) + "' in channel '" + channel + "':\n" + message) |
| 110 | |
| 111 | def sendMessageToChannel(self, channel, message): |
| 112 | """ Sends a message to a channel. |
| 113 | @type channel: string |
| 114 | @param channel: The channel to send the message to. |
| 115 | @type message: string |
| 116 | @param message: The message to be sent to the channel. """ |
| 117 | self.send("PRIVMSG " + channel + " :" + message) |
| 118 | |
| 119 | def messageFromUser(self, user, message, msgtype=None): |
| 120 | """ Callback called when a message is received from a user. |
| 121 | @type user: string |
| 122 | @param user: The user who sent the message to the bot. |
| 123 | @type message: string |
| 124 | @param message: The message received. |
| 125 | @type msgtype: integer |
| 126 | @param msgtype: The type of message that was sent (PRIVMSG, NOTICE, etc). Not implemented yet.""" |
| 127 | dbg("Received message from '" + user + "':\n" + message) |
| 128 | |
| 129 | def sendMessageToUser(self, user, message): |
| 130 | """ Sends a message to a user. |
| 131 | @type user: string |
| 132 | @param user: The user to send the message to. |
| 133 | @type message: string |
| 134 | @param message: The message to be sent to the channel. """ |
| 135 | self.send("PRIVMSG " + user + " :" + message) |
| 136 | |
| 137 | def userEntered(self, user, channel): |
| 138 | """ Callback called when a new user enters a channel you are in. """ |
| 139 | dbg(user + " entered channel " + channel) |
| 140 | def act(self, data): |
| 141 | """ Callback which is passed raw data from the IRC server received from the socket. |
| 142 | @type data: string |
| 143 | @param data: The data received from the IRC server. """ |
| 144 | pass |
| 145 | |
| 146 | def reply(self, data, message): |
| 147 | """ Sends a message to either a channel or an individual, based on the text passed. |
| 148 | Raw IRC data passed is parsed and checked whether it is sent by a channel or by an individual. A message is then sent accordingly. This function only makes much sense in some of the lower level |
| 149 | callbacks where one cannot be certain who sent the message without regular expressions and such. |
| 150 | @type data: string |
| 151 | @param data: The unmodified string passed to the function by the server. |
| 152 | @type message: string |
| 153 | @param message: The message to send to the channel/user """ |
| 154 | # call getChannel passing True as the username param. |
| 155 | self.sendMessageToChannel(self.getChannel(data), message, True) |
| 156 | |
| 157 | def getSenderName(self, data): |
| 158 | """ Get the name of the person who sent a message, given the raw IRC message. |
| 159 | @type data: string |
| 160 | @param data: The raw IRC data from which to extract the nickname of the sender. |
| 161 | @rtype: string or None |
| 162 | @returns: The sender or None upon failure.""" |
| 163 | try: |
| 164 | regSenderName = re.compile(":[\w\s]*") |
| 165 | return re.sub("^:", "", regSenderName.findall(data)[0]) |
| 166 | except IndexError: |
| 167 | return None |
| 168 | |
| 169 | def getChannel(self, data, username=True): |
| 170 | """ Gets the channel a message was sent from. |
| 171 | The function looks for the channel that a message came from once passed the raw IRC string it is in. If it does not succeed in finding the channel and the username parameter is set to True it will try and see if the message came from a user and not a channel, returning the nickname of the user if it then finds it. |
| 172 | If neither channel nor nickname are found then it returns None. |
| 173 | @type data: string |
| 174 | @param data: The raw IRC data from which to extract the channel. |
| 175 | @type username: boolean |
| 176 | @param username: True if the function should search for a username upon failure extracting the channel, False otherwise. |
| 177 | @rtype: string or None |
| 178 | @returns: The channel or user that sent a message or None upon failure. """ |
| 179 | # Confusing and spaghetti-ish. @todo make self.reply handle the checking of user/channel. |
| 180 | if not data.count("#") and username: |
| 181 | return self.getSenderName(data) |
| 182 | else: |
| 183 | try: |
| 184 | channel = data[data.index("#"):] |
| 185 | return channel[:channel.index(" ")] |
| 186 | except ValueError: |
| 187 | return None |
| 188 | |
| 189 | def recv(self, amount): |
| 190 | """ Receives text from the IRC server. |
| 191 | @type amount: integer |
| 192 | @param amount: The buffer size of the text being read. |
| 193 | @returns: The data read from the IRC server. |
| 194 | @rtype: string """ |
| 195 | return self.sock.recv(amount) |
| 196 | |
| 197 | def send(self, text): |
| 198 | """ Send a raw IRC command to the server. |
| 199 | @type text: string |
| 200 | @param text: The command to send to send to the IRC server. """ |
| 201 | self.sock.send(text + "\n\r") |
| 202 | dbg("bot -> " + text) |
| 203 | |
| 204 | def startConnection(self): |
| 205 | """ Connects to the IRC server and identifies the bot on it.""" |
| 206 | try: |
| 207 | self.sock.connect((self.server, self.port)) |
| 208 | except: |
| 209 | sys.stderr.write("Error connecting to " + self.server + " on port " + `self.port`) |
| 210 | usercommand = "USER " + self.nick + " cerulean.pyresoft.com cerulean.pyresoft.com :" + self.name |
| 211 | nickcommand = "NICK " + self.nick + " cerulean.pyresoft.com" |
| 212 | self.send(usercommand) |
| 213 | self.send(nickcommand) |
| 214 | |
| 215 | def baseAct(self, data): |
| 216 | """ Parses the received data and responds accordingly. It parses the data and calls the necessary callbacks. |
| 217 | @type data: string |
| 218 | @param data: The data from the server to be parsed. """ |
| 219 | # Call the callbacks. |
| 220 | channel = self.getChannel(data, False) |
| 221 | sender = self.getSenderName(data) |
| 222 | # messageFromChannel |
| 223 | if channel: |
| 224 | # Messages from a channel are sent by someone.. who is that someone: |
| 225 | if not sender: |
| 226 | sender = None |
| 227 | self.messageFromChannel(channel, sender, data[data.find(" :") + 2:]) |
| 228 | # userEntered |
| 229 | elif sender and data[data.find(" ") + 1:].startswith("JOIN :"): |
| 230 | self.userEntered(sender, data[data.find("JOIN :") + 6:]) |
| 231 | elif sender: |
| 232 | # Make sure this is a message (NOTICE or PRIVMSG). |
| 233 | ok = False |
| 234 | if data[data.find(" ") + 1:].startswith("NOTICE %s :" % self.nick): |
| 235 | ok = True |
| 236 | elif data[data.find(" ") + 1:].startswith("PRIVMSG %s :" % self.nick): |
| 237 | ok = True |
| 238 | # messageFromUser |
| 239 | if ok: |
| 240 | self.messageFromUser(sender, data[data.find(" :") + 2:]) |
| 241 | # Pass the data to self.act |
| 242 | self.act(data) |
| 243 | |
| 244 | def main(): |
| 245 | """ Called when this module is ran directly. """ |
| 246 | class HelloBot(IRCBot): |
| 247 | def messageFromChannel(self, channel, user, message): |
| 248 | if message.startswith("hello"): |
| 249 | self.sendMessageToChannel(channel, "Hello to you too " + user + "!") |
| 250 | |
| 251 | bot = HelloBot("HelloBot", "irc.freenode.net", ["#hellobot"]) |
| 252 | bot.execute() ; bot.close() |
| 253 | |
| 254 | if __name__ == "__main__": |
| 255 | main() |
| wiioa/pyquake3.py |
| 1 | """ |
| 2 | Python Quake 3 Library |
| 3 | http://misc.slowchop.com/misc/wiki/pyquake3 |
| 4 | Copyright (C) 2006-2007 Gerald Kaszuba |
| 5 | |
| 6 | This program is free software; you can redistribute it and/or |
| 7 | modify it under the terms of the GNU General Public License |
| 8 | as published by the Free Software Foundation; either version 2 |
| 9 | of the License, or (at your option) any later version. |
| 10 | |
| 11 | This program is distributed in the hope that it will be useful, |
| 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | GNU General Public License for more details. |
| 15 | |
| 16 | You should have received a copy of the GNU General Public License |
| 17 | along with this program; if not, write to the Free Software |
| 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 19 | """ |
| 20 | |
| 21 | import socket |
| 22 | import re |
| 23 | |
| 24 | class Player: |
| 25 | def __init__(self, name, frags, ping, address=None, bot=-1): |
| 26 | self.name = name |
| 27 | self.frags = frags |
| 28 | self.ping = ping |
| 29 | self.address = address |
| 30 | self.bot = bot |
| 31 | def __str__(self): |
| 32 | return self.name |
| 33 | def __repr__(self): |
| 34 | return str(self) |
| 35 | |
| 36 | class PyQuake3: |
| 37 | packet_prefix = '\xff' * 4 |
| 38 | player_reo = re.compile(r'^(\d+) (\d+) "(.*)"') |
| 39 | def __init__(self, server, rcon_password=''): |
| 40 | self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 41 | self.set_server(server) |
| 42 | self.set_rcon_password(rcon_password) |
| 43 | def set_server(self, server): |
| 44 | try: |
| 45 | self.address, self.port = server.split(':') |
| 46 | except: |
| 47 | raise Exception('Server address must be in the format of \ |
| 48 | "address:port"') |
| 49 | self.port = int(self.port) |
| 50 | self.s.connect((self.address, self.port)) |
| 51 | def get_address(self): |
| 52 | return '%s:%s' % (self.address, self.port) |
| 53 | def set_rcon_password(self, rcon_password): |
| 54 | self.rcon_password = rcon_password |
| 55 | def send_packet(self, data): |
| 56 | self.s.send('%s%s\n' % (self.packet_prefix, data)) |
| 57 | def recv(self, timeout=1): |
| 58 | self.s.settimeout(timeout) |
| 59 | try: |
| 60 | return self.s.recv(4096) |
| 61 | except socket.error, e: |
| 62 | raise Exception('Error receiving the packet: %s' % \ |
| 63 | e[1]) |
| 64 | def command(self, cmd, timeout=1, retries=3): |
| 65 | while retries: |
| 66 | self.send_packet(cmd) |
| 67 | try: |
| 68 | data = self.recv(timeout) |
| 69 | except: |
| 70 | data = None |
| 71 | if data: |
| 72 | return self.parse_packet(data) |
| 73 | retries -= 1 |
| 74 | raise Exception('Server response timed out') |
| 75 | def rcon(self, cmd): |
| 76 | r = self.command('rcon "%s" %s' % (self.rcon_password, cmd)) |
| 77 | if r[1] == 'No rconpassword set on the server.\n' or r[1] == \ |
| 78 | 'Bad rconpassword.\n': |
| 79 | raise Exception(r[1][:-1]) |
| 80 | return r |
| 81 | def parse_packet(self, data): |
| 82 | if data.find(self.packet_prefix) != 0: |
| 83 | raise Exception('Malformed packet') |
| 84 | first_line_length = data.find('\n') |
| 85 | if first_line_length == -1: |
| 86 | raise Exception('Malformed packet') |
| 87 | response_type = data[len(self.packet_prefix):first_line_length] |
| 88 | response_data = data[first_line_length+1:] |
| 89 | return response_type, response_data |
| 90 | def parse_status(self, data): |
| 91 | split = data[1:].split('\\') |
| 92 | values = dict(zip(split[::2], split[1::2])) |
| 93 | # if there are \n's in one of the values, it's the list of players |
| 94 | for var, val in values.items(): |
| 95 | pos = val.find('\n') |
| 96 | if pos == -1: |
| 97 | continue |
| 98 | split = val.split('\n', 1) |
| 99 | values[var] = split[0] |
| 100 | self.parse_players(split[1]) |
| 101 | return values |
| 102 | def parse_players(self, data): |
| 103 | self.players = [] |
| 104 | for player in data.split('\n'): |
| 105 | if not player: |
| 106 | continue |
| 107 | match = self.player_reo.match(player) |
| 108 | if not match: |
| 109 | print 'couldnt match', player |
| 110 | continue |
| 111 | frags, ping, name = match.groups() |
| 112 | self.players.append(Player(name, frags, ping)) |
| 113 | def update(self): |
| 114 | cmd, data = self.command('getstatus') |
| 115 | self.vars = self.parse_status(data) |
| 116 | def rcon_update(self): |
| 117 | cmd, data = self.rcon('status') |
| 118 | lines = data.split('\n') |
| 119 | players = lines[3:] |
| 120 | self.players = [] |
| 121 | for p in players: |
| 122 | while p.find(' ') != -1: |
| 123 | p = p.replace(' ', ' ') |
| 124 | while p.find(' ') == 0: |
| 125 | p = p[1:] |
| 126 | if p == '': |
| 127 | continue |
| 128 | p = p.split(' ') |
| 129 | self.players.append(Player(p[3][:-2], p[0], p[1], p[5], p[6])) |
| 130 | |
| 131 | if __name__ == '__main__': |
| 132 | q = PyQuake3('localhost:27960','hello') |
| 133 | q.update() |
| 134 | print 'The name of %s is %s, running map %s with %s player(s).' % \ |
| 135 | (q.get_address(), q.vars['sv_hostname'], \ |
| 136 | q.vars['mapname'], len(q.players)) |
| 137 | for player in q.players: |
| 138 | print '%s with %s frags and a %sms ping' % (player.name, \ |
| 139 | player.frags, player.ping) |
| 140 | q.rcon_update() |
| 141 | for player in q.players: |
| 142 | print '%s has an address of %s' % (player.name, player.address) |
| 143 | |
| wiioa/wiioa.py |
| 1 | # wiioa.py |
| 2 | # |
| 3 | # Copyright 2007 Antoine 'NaPs' Millet <antoine@inaps.org> |
| 4 | # |
| 5 | # This program is free software; you can redistribute it and/or modify |
| 6 | # it under the terms of the GNU General Public License as published by |
| 7 | # the Free Software Foundation; either version 2 of the License, or |
| 8 | # (at your option) any later version. |
| 9 | # |
| 10 | # This program is distributed in the hope that it will be useful, |
| 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | # GNU General Public License for more details. |
| 14 | # |
| 15 | # You should have received a copy of the GNU General Public License |
| 16 | # along with this program; if not, write to the Free Software |
| 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |
| 18 | # MA 02110-1301, USA. |
| 19 | |
| 20 | from pyquake3 import PyQuake3 |
| 21 | from bot import IRCBot |
| 22 | |
| 23 | class WiiOA(IRCBot): |
| 24 | |
| 25 | oa = PyQuake3('saiks.org:27960', '') |
| 26 | |
| 27 | def messageFromChannel(self, channel, user, message): |
| 28 | if message.startswith("oa ?"): |
| 29 | try: |
| 30 | self.oa.update() |
| 31 | players = self.oa.players |
| 32 | playerslist = [] |
| 33 | for player in players: |
| 34 | playerslist.append('%s (%s)' % (player.name, player.frags)) |
| 35 | self.sendMessageToChannel(channel, "%s : La map en cours est %s (t: %s/%s)" % (user, self.oa.vars['mapname'], self.oa.vars['g_gametype'], self.oa.vars['fraglimit'])) |
| 36 | if len(playerslist) == 0: |
| 37 | self.sendMessageToChannel(channel, '%s : et personne ne joue sur le serveur :(.' % user) |
| 38 | elif len(playerslist) == 1: |
| 39 | self.sendMessageToChannel(channel, '%s : et %s est tout seul sur le serveur :|.' % (user, playerslist[0])) |
| 40 | else: |
| 41 | self.sendMessageToChannel(channel, "%s : et %s sont en train de jouer sur le serveur :)." % (user, ', '.join(playerslist))) |
| 42 | except: |
| 43 | self.sendMessageToChannel(channel, 'Saiks is dead, webs sucks.') |
| 44 | elif message.startswith('!say '): |
| 45 | self.oa.rcon('say (from IRC) <%s> %s' % (user, message.lstrip('!say '))) |
| 46 | |
| 47 | if __name__ == '__main__': |
| 48 | bot = WiiOA("WiiOA", "irc.freenode.net", ["#escalope"]) |
| 49 | bot.execute() ; bot.close() |