# # BigBrotherBot(B3) (www.bigbrotherbot.net) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # CHANGELOG # # 2011-03-30 : 0.1 # * first alpha test # 2011-09-28 : 0.2 # * First commit to repo # 2011-09-29 : 0.3 # * Added !maps, found !map functionality broken in Web Admin # 2011-09-30 : 0.4 # * Made webconnect a method and added comments to new methods # 2011-10-03 : 0.5 # * Seperate out Team and Global chat - squad chat is totally missing from log and web admin # * Make sure IP's get logged # * Remove rcon references and rcon.py # 2011-10-06 : 0.6 # * Kick client if on server when banned # * Keep running on map change # * Allow for username in xml file # 2011-10-8 : 0.7 # * Correct error in ban-kick # * Rewrite player names logic for extended characters # 2011-10-16 : 0.8 # * !map working # * Player with funny accented i character now show in !list # 2011-11-01 : 1.0 # * Some re-writes and corrections # * Implement !nextmap # 2011-11-02 : 1.1 # * Allow use of # instead of @ for client id nos (@ brings up console on some keyboard layouts and cannot go into chat) # 2011-12-19 : 1.2 # Don't process B3 messages as chat # Auth client if not already authed when chat used # 2011-12-28 : 1.3 # Allow Q3 Color Codes in names, since game doesn't filter them out # 2012-01-27 : 1.4 # Track team changes for eg teamspeak plugin # 2012-12-01 : 1.41 # Incorporate chat changes for server/game v 1.1.0.8 # 2014-04-25 : 1.42 # Allow for empty chat messages # 2014-07-16 : 1.43 added admin key in EVT_CLIENT_KICK data dict when available # # from b3 import functions from b3.parser import Parser from ftplib import FTP import b3 import b3.cron import ftplib import os import re import string import sys import time import urllib import urllib2 import cookielib import hashlib __author__ = 'Courgette, xlr8or, Freelander, 82ndab-Bravo17' __version__ = '1.43' class Ro2Parser(b3.parser.Parser): """ The Ref Orchestra 2 B3 parser class """ gameName = "redorchestra2" privateMsg = True PunkBuster = None # RO2 engine does not support color code, so we need this property # in order to get stripColors working _reColor = re.compile(r'(\^[0-9])') _reSteamId64 = re.compile(r'^[0-9]{17}$') ftpconfig = None _ftplib_debug_level = 0 # 0: no debug, 1: normal debug, 2: extended debug _ftpconnectionTimeout = 30 _playerlistInterval = 30 _server_banlist = {} _read_write_delay=1 _write_queue=[] _read_queue=[] _ini_file = False url='' login_page='' site='' user_agent='' username='' password='' password_hash='' cj=None headers = {} opener=None map_cycles = {} map_cycle_no = 0 active_map_cycle = 0 _gametypes = {"TE" : "ROGame.ROGameInfoTerritories", "CD" : "ROGame.ROGameInfoCountdown", "FF" : "ROGame.ROGameInfoFirefight" } _maps = {"TE" : ['TE-Apartments', 'TE-Barracks', 'TE-CommisarsHouse', 'TE-FallenFighters', 'TE-GrainElevator', 'TE-Gumrak', 'TE-PavlovsHouse', 'TE-RedOctoberFactory', 'TE-Spartanovka', 'TE-Station'], "CD" : ['CD-Apartments', 'CD-Barracks', 'CD-CommisarsHouse', 'CD-FallenFighters', 'CD-GrainElevator', 'CD-Gumrak', 'CD-PavlovsHouse', 'CD-RedOctoberFactory', 'CD-Spartanovka', 'CD-Station'], "FF" : ['FF-Apartments', 'FF-Barracks', 'FF-GrainElevator', 'FF-Station'] } _commands = {} _commands['message'] = '%(prefix)s %(message)s' _commands['say'] = ('%(prefix)s %(message)s') _commands['kick'] = ('adminkick+%(playerid)s') _commands['ban'] = ('adminkickban+%(playerid)s') _commands['tempban'] = ('adminkick+%(playerid)s') _settings = {'line_length': 90, 'min_wrap_length': 100} prefix = '%s: ' def startup(self): self.debug("startup()") # create the 'Admin' client self.clients.newClient('Admin', guid='Server', name='Admin', hide=True, pbid='Server', team=b3.TEAM_UNKNOWN) if self.config.has_option('server','ro2admin'): self.username = self.config.get('server', 'ro2admin') else: self.username="Admin" if self.config.has_option('server','inifile'): # open ini file ini_file = self.config.get('server','inifile') if ini_file[0:6] == 'ftp://': self.ftpconfig = functions.splitDSN(ini_file) self._ini_file = 'ftp' self.bot('ftp supported') elif ini_file[0:7] == 'sftp://': self.bot('sftp currently not supported') else: self.bot('Getting configs from %s', ini_file) f = self.config.getpath('server', 'inifile') if os.path.isfile(f): self.input = file(f, 'r') self._ini_file = f if not self._ini_file: self.debug('Incorrect ini file or no ini file specified, map commands other than nextmap not available') self.cron + b3.cron.CronTab(self.retrievePlayerList, second='*/%s' % self._playerlistInterval) self.user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' self.headers = {'User-Agent' : self.user_agent, "Accept": "ext/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language" : "en-us,en;q =0.5", "Content-type": "application/x-www-form-urlencoded", "Accept-Charset" : "ISO-8859-1,utf-8;q=0.7,*;q=0.7", "Referer" : ''} self.site=self._publicIp + ':' + str(self._rconPort) self.login_page="ServerAdmin" self.password=self._rconPassword self.password_hash = "$sha1$%s" % hashlib.sha1("%s%s" % (self.password, self.username)).hexdigest() self.url = "http://%s/%s" % (self.site, self.login_page) def handle_chat(self, data): """Handle the chat from players""" if string.capitalize(data['div_class']) == 'Chatnotice': return func = 'onChat_type%s' % (string.capitalize(data['div_class'])) if hasattr(self, func): self.debug('routing ----> %s' % func) func = getattr(self, func) event = func(data) if event: if event != 'Unable to Auth client': self.queueEvent(event) else: return else: self.warning('TODO handle: %s(%s)' % (func, data)) else: self.warning('TODO handle packet : %s' % packet) self.queueEvent(self.getEvent('EVT_UNKNOWN', packet)) def run(self): """Main worker thread for B3""" self.bot('Start listening ...') self.screen.write('Startup Complete : B3 is running! Let\'s get to work!\n\n') self.screen.write('(If you run into problems, check %s for detailed log info)\n' % self.config.getpath('b3', 'logfile')) self.updateDocumentation() #Connect to RO2 web server web_auth = self.webconnect() if web_auth: self.bot('Authenticated on Web Server') self.working=True while self.working: #While we are working, connect to the RO2 server self._paused=False if self._paused: if not self._pauseNotice: self.bot('PAUSED - Not parsing any lines, B3 will be out of sync.') self._pauseNotice = True else: self._pauseNotice = False counter = 0 while len(self._write_queue) == 0 and counter < 5: time.sleep(.2) counter +=1 if not len(self._write_queue): self.readwriteajax() else: self.debug('Go to ajax') message = self._write_queue.pop(0) self.debug(self._write_queue) self.readwriteajax(message) while len(self._read_queue): chat_data = self._read_queue.pop(0) self.handle_chat(chat_data) counter = 0 time.sleep(.5) self.bot('Stop listening.') if self.exiting.acquire(1) and self.exitcode: sys.exit(self.exitcode) def readwriteweb(self, data= None, referer=None, addurl=None): """Handles Reading and Writing to the web interface""" data_url = self.url + addurl if not referer: referer = data_url else: referer = self.url + referer self.headers['Referer'] = referer try: request_console = urllib2.Request(data_url, data, self.headers) console_read = self.opener.open(request_console) console_data = console_read.read() return console_data except Exception: self.debug('Failed to open URL') self.webconnect() return def webconnect(self): """Login and make initial connection to the web interface""" remember=-1 password='' login_url = self.url + '/' headers = {'Content-type' : 'application/x-www-form-urlencoded', 'User-Agent' : self.user_agent} self.cj = cookielib.LWPCookieJar() self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj)) urllib2.install_opener(self.opener) findpage_attempt = 0 self._paused = True response = "" while findpage_attempt < 11: try: request = urllib2.Request(login_url, None, headers) page = urllib2.urlopen(request) response = page.read() break except Exception: findpage_attempt += 1 if findpage_attempt > 10: self.debug('Failed to find web page - wait 10 seconds') time.sleep(10) findpage_attempt = 1 else: time.sleep(1) self.debug('Failed to find Web page %s - Wait 1 second' % findpage_attempt) # token_start = response.partition(' # #<82ndAB>1LT.Bravo17 : # # #
#')[2] #left most character 0 axis 1 allies color = data.partition(' | ')[0] data = data.partition('')[2] player['name'] = self.getUsername(data.partition(' | ')[0]) data = data.partition('')[2] player['ping'] = data.partition(' | ')[0] data = data.partition('')[2] player['ip'] = data.partition(' | ')[0] data = data.partition('')[2] player['guid'] = data.partition(' | ')[0] data = data.partition('')[2] player['steam_id'] = data.partition(' | ')[0] data = data.partition('')[2] player['admin'] = data.partition(' | ')[0] data = data.partition('')[2] spec = data.partition(' | ')[0] data = data.partition('<%ban.playername%>-->') == -1: self.debug('No bans in list') return ban_list while data.find('') != -1: data = data.partition('')[2] data = data.partition('')[2] banid = data.partition(' | ')[0] data = data.partition('There are no players') != -1: self.debug('No players on server') clients = {} else: clients = self.decodeplayers(playerlist_data) self.debug (clients) return clients def authorizeClients(self): """\ For all connected players, fill the client object with properties allowing to find the user in the database (usualy guid, or punkbuster id, ip) and call the Client.auth() method """ pass def findNewPlayers(self, c_client_list): """Gets a list of non-authed players on the server""" for c in c_client_list: cl = c_client_list[c] uid = cl['guid'] if len(uid) != 18: self.warning(u"weird UID : [%s]" % uid) # try to get the client by guid client = self.clients.getByGUID(uid) if not client: self.debug('adding client') self.debug(cl) client = self.clients.newClient(cl['playerid'], guid=uid, name=cl['name'], team=b3.TEAM_UNKNOWN, ip=cl['ip']) # update client data client.name = cl['name'] client.team = cl['team'] client.cid = cl['playerid'] client.ip = cl['ip'] else: if client.team != cl['team']: self.verbose2('Team change detected for %s' % client.name) client.team = cl['team'] self.verbose2('onServerPlayer: name: %s, team: %s' %( client.name, client.team )) def syncDeletions(self, connected_clients): """\ Check Clients list against all connected players returned by self.getServerPlayerList() and if required call the client.disconnect() method to remove a client from self.clients. """ client_cid_list = [] for cl in connected_clients.values(): client_cid_list.append(cl['playerid']) for client in self.clients.getList(): if client.cid not in client_cid_list: self.debug('Removing %s from list' % client.name) client.disconnect() def sync(self, connected_clients=None): """\ if connected_clients is None : get dict of connected players from self.getPlayerList() else use connected_clients as the list of connected players For all connected players, get the matching Client object from self.clients (with self.clients.getByCID(cid) or similar methods) and look for inconsistencies. If required call the client.disconnect() method to remove a client from self.clients. This is mainly useful for games where clients are identified by the slot number they occupy. On map change, a player A on slot 1 can leave making room for player B who connects on slot 1. """ if connected_clients is None: connected_clients = self.getPlayerList() self.debug("synchronizing clients") mlist = {} for client in connected_clients: mlist[client.cid] = client return mlist def say(self, msg): """\ broadcast a message to all players """ msg = self.stripMsgColors(msg) for line in self.getWrap(msg, self._settings['line_length'], self._settings['min_wrap_length']): line = self.stripMsgColors(line) self.write(self.getCommand('say', prefix=self.msgPrefix, message=line)) def message(self, client, text): """\ display a message to a given player """ # actually send private messages text = self.stripMsgColors(text) for line in self.getWrap(text, self._settings['line_length'], self._settings['min_wrap_length']): line = self.stripMsgColors(line) self.write(self.getCommand('message', uid=client.guid, prefix=self.msgPrefix, message=line)) def kick(self, client, reason='', admin=None, silent=False, *kwargs): """\ kick a given player """ self.debug('KICK : client: %s, reason: %s', client.cid, reason) if admin: fullreason = self.getMessage('kicked_by', self.getMessageVariables(client=client, reason=reason, admin=admin)) else: fullreason = self.getMessage('kicked', self.getMessageVariables(client=client, reason=reason)) fullreason = self.stripMsgColors(fullreason) reason = self.stripMsgColors(reason) if not silent and fullreason != '': self.say(fullreason) self.writeAdminCommand(self.getCommand('kick', playerid=client.cid)) self.queueEvent(self.getEvent('EVT_CLIENT_KICK', {'reason': reason, 'admin': admin}, client)) client.disconnect() def ban(self, client, reason='', admin=None, silent=False, *kwargs): """\ ban a given player """ self.debug('BAN : client: %s, reason: %s', client.cid, reason) if admin: fullreason = self.getMessage('banned_by', self.getMessageVariables(client=client, reason=reason, admin=admin)) else: fullreason = self.getMessage('banned', self.getMessageVariables(client=client, reason=reason)) fullreason = self.stripMsgColors(fullreason) reason = self.stripMsgColors(reason) if not silent and fullreason != '': self.say(fullreason) banid = client.guid bandata_url = '/policy/bans' data = 'action=add&uniqueid=' + banid referer = None self.debug('Ban data %s' % data) console_data = self.readwriteweb(data, referer, bandata_url) self.queueEvent(self.getEvent('EVT_CLIENT_BAN', {'reason': reason, 'admin': admin}, client)) #If client is on server kick them c = self.clients.getByGUID(banid) if c: self.writeAdminCommand(self.getCommand('kick', playerid=c.cid)) client.disconnect() def unban(self, client, reason='', admin=None, silent=False, *kwargs): """\ unban a given player """ ban_list = self.retrieveBanlist() self.debug('using guid to unban') banid = client.guid ban_no = None try: ban_no = ban_list[banid] except Exception: if admin: admin.message(' %s not in server banlist' %client.name) if ban_no: ban_no = str(ban_no[8:]) bandata_url = '/policy/bans' referer = None data = 'banid=plainid%3A' + ban_no + '&action=delete' banlist_data = self.readwriteweb(data, referer, bandata_url) if admin: admin.message('Removed %s from Server banlist' %client.name) if admin: admin.message('Removed %s from B3 banlist' %client.name) self.queueEvent(self.getEvent('EVT_CLIENT_UNBAN', reason, client)) def tempban(self, client, reason='', duration=2, admin=None, silent=False, *kwargs): """\ tempban a given player """ self.debug('TEMPBAN : client: %s, reason: %s', client.cid, reason) if admin: fullreason = self.getMessage('temp_banned_by', self.getMessageVariables(client=client, reason=reason, admin=admin, banduration=b3.functions.minutesStr(duration))) else: fullreason = self.getMessage('temp_banned', self.getMessageVariables(client=client, reason=reason, banduration=b3.functions.minutesStr(duration))) fullreason = self.stripMsgColors(fullreason) reason = self.stripMsgColors(reason) if not silent and fullreason != '': self.say(fullreason) self.writeAdminCommand(self.getCommand('kick', playerid=client.cid)) self.queueEvent(self.getEvent('EVT_CLIENT_BAN_TEMP', {'reason': reason, 'duration': duration, 'admin': admin} , client)) client.disconnect() def getMaps(self): """\ return the available maps/levels name """ map_rotation = [] self.map_cycles = {} self.map_cycle_no = 0 self.active_map_cycle = -1 if self._ini_file: if self._ini_file == 'ftp': self.getftpini() else: input = open(self._ini_file, 'r') for line in input: if line[0:15] == 'ActiveMapCycle=': self.active_map_cycle = int(line.partition('ActiveMapCycle=')[2]) if line[0:14] == 'GameMapCycles=': self.map_cycles[str(self.map_cycle_no)] = line self.map_cycle_no += 1 if self.active_map_cycle >= 0 and self.map_cycle_no > self.active_map_cycle: break input.close() map_line = self.map_cycles[str(self.active_map_cycle)] map_line = map_line.partition('Maps=("')[2] map_line = map_line.partition('"),RoundLimits=')[0] map_rotation.append(map_line.partition('","')[0]) while map_line.find('","') != -1: map_line= map_line.partition('","')[2] map_rotation.append(map_line.partition('","')[0]) return map_rotation def changeMap(self, map): """\ load a given map/level return a list of suggested map names in cases it fails to recognize the map that was provided """ #gametype=ROGame.ROGameInfoTerritories&map=TE-Barracks&mutatorGroupCount=0&urlextra=&action=change gametype = map[0:2] if self._gametypes.has_key(gametype) and self._maps[gametype].count(map) > 0: mapchange_url = '/current/change' data = 'gametype=' + self._gametypes[gametype] + '&map=' + map + '&mutatorGroupCount=0&urlextra=&action=change' referer = None console_data = self.readwriteweb(data, referer, mapchange_url) else: self.write(self.getCommand('say', prefix=self.msgPrefix, message='Incorrect Gametype-Map combination')) return def getMap(self): """\ load the next map/level """ #