mirror of
https://github.com/oddluck/limnoria-plugins.git
synced 2025-05-04 09:21:14 -05:00
1729 lines
67 KiB
Python
1729 lines
67 KiB
Python
# -*- coding: utf-8 -*-
|
||
###
|
||
# This file is part of Soap.
|
||
#
|
||
# Soap 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, version 2.
|
||
#
|
||
# Soap is distributed in the hope that it will be useful, but WITHOUT ANY
|
||
# WARRANTY; without even the implied warranty of MERCfHANTABILITY or FITNESS FOR
|
||
# A PARTICULAR PURPOSE.
|
||
#
|
||
# See the GNU General Public License for more details. You should have receivedFd
|
||
# a copy of the GNU General Public License along with Soap. If not, see
|
||
# <http://www.gnu.org/licenses/>.
|
||
###
|
||
|
||
from supybot.commands import *
|
||
import supybot.conf as conf
|
||
import supybot.callbacks as callbacks
|
||
|
||
from datetime import datetime
|
||
import os.path
|
||
import queue
|
||
import random
|
||
import socket
|
||
from subprocess import Popen, PIPE, CalledProcessError
|
||
import sys
|
||
import threading
|
||
import time
|
||
|
||
from . import soaputils as utils
|
||
from .soapclient import SoapClient
|
||
from .enums import *
|
||
|
||
from libottdadmin2.trackingclient import poll, POLLIN, POLLOUT, POLLERR, POLLHUP, POLLPRI, POLL_MOD
|
||
from libottdadmin2.constants import *
|
||
from libottdadmin2.enums import *
|
||
from libottdadmin2.packets import *
|
||
|
||
class Suds(callbacks.Plugin):
|
||
"""
|
||
This plug-in allows supybot to interface to OpenTTD via its built-in
|
||
adminport protocol
|
||
"""
|
||
|
||
def __init__(self, irc):
|
||
self.__parent = super(Suds, self)
|
||
self.__parent.__init__(irc)
|
||
|
||
self._pollObj = poll()
|
||
self.channels = self.registryValue('channels')
|
||
self.connections = {}
|
||
self.registeredConnections = {}
|
||
self.connectionIds = []
|
||
for channel in self.channels:
|
||
serverID = self.registryValue('serverID', channel)
|
||
conn = SoapClient(channel, serverID)
|
||
self._attachEvents(conn)
|
||
self._initSoapClient(conn, irc)
|
||
self.connections[channel.lower()] = conn
|
||
if self.registryValue('autoConnect', channel):
|
||
self._connectOTTD(irc, conn, channel)
|
||
self.stopPoll = threading.Event()
|
||
self.pollingThread = threading.Thread(
|
||
target = self._pollThread,
|
||
name = 'SoapPollingThread')
|
||
self.pollingThread.daemon = True
|
||
self.pollingThread.start()
|
||
self.kickdict = dict()
|
||
self.ipdict = dict()
|
||
|
||
def die(self):
|
||
for conn in self.connections.values():
|
||
try:
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
conn.connectionstate = ConnectionState.DISCONNECTING
|
||
utils.disconnect(conn, False)
|
||
except NameError:
|
||
pass
|
||
self.stopPoll.set()
|
||
self.pollingThread.join()
|
||
|
||
def doJoin(self, irc, msg):
|
||
channel = msg.args[0].lower()
|
||
conn = None
|
||
if msg.nick == irc.nick and self.channels.count(channel) >=1:
|
||
conn = self.connections.get(channel)
|
||
if not conn:
|
||
return
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
text = 'Connected to %s (Version %s)' % (
|
||
conn.serverinfo.name, conn.serverinfo.version)
|
||
utils.msgChannel(conn._irc, conn.channel, text)
|
||
|
||
|
||
|
||
# Connection management
|
||
|
||
def _attachEvents(self, conn):
|
||
conn.soapEvents.connected += self._connected
|
||
conn.soapEvents.disconnected += self._disconnected
|
||
|
||
conn.soapEvents.shutdown += self._rcvShutdown
|
||
conn.soapEvents.new_game += self._rcvNewGame
|
||
|
||
conn.soapEvents.new_map += self._rcvNewMap
|
||
|
||
conn.soapEvents.clientjoin += self._rcvClientJoin
|
||
conn.soapEvents.clientupdate += self._rcvClientUpdate
|
||
conn.soapEvents.clientquit += self._rcvClientQuit
|
||
|
||
conn.soapEvents.chat += self._rcvChat
|
||
conn.soapEvents.rcon += self._rcvRcon
|
||
conn.soapEvents.rconend += self._rcvRconEnd
|
||
conn.soapEvents.console += self._rcvConsole
|
||
conn.soapEvents.cmdlogging += self._rcvCmdLogging
|
||
|
||
conn.soapEvents.pong += self._rcvPong
|
||
|
||
def _connectOTTD(self, irc, conn, source = None, text = 'Connecting...'):
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
if source and not source == conn.channel:
|
||
utils.msgChannel(irc, source, text)
|
||
conn = utils.refreshConnection(
|
||
self.connections, self.registeredConnections, conn)
|
||
self._initSoapClient(conn, irc)
|
||
conn.connectionstate = ConnectionState.CONNECTING
|
||
if not conn.connect():
|
||
conn.connectionstate = ConnectionState.DISCONNECTED
|
||
text = 'Connection failed'
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
|
||
def _connected(self, connChan):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
|
||
conn.connectionstate = ConnectionState.AUTHENTICATING
|
||
pwInterval = self.registryValue('passwordInterval', conn.channel)
|
||
if pwInterval != 0:
|
||
connectionid = utils.getConnectionID(conn)
|
||
pwThread = threading.Thread(
|
||
target = self._passwordThread,
|
||
name = 'PasswordRotation.%s' % connectionid,
|
||
args = [conn])
|
||
pwThread.daemon = True
|
||
pwThread.start()
|
||
else:
|
||
command = 'set server_password *'
|
||
conn.rconState = RconStatus.ACTIVE
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
conn.clientPassword = None
|
||
|
||
def _disconnected(self, connChan, canRetry):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
if conn.is_connected:
|
||
return
|
||
irc = conn.irc
|
||
fileno = conn.filenumber
|
||
|
||
try:
|
||
del self.registeredConnections[fileno]
|
||
except KeyError:
|
||
pass
|
||
try:
|
||
self._pollObj.unregister(fileno)
|
||
except KeyError:
|
||
pass
|
||
except IOError:
|
||
pass
|
||
|
||
conn.logger.debug('>>--DEBUG--<< Disconnected')
|
||
if conn.serverinfo.name:
|
||
text = 'Disconnected from %s' % (conn.serverinfo.name)
|
||
utils.msgChannel(conn.irc, conn.channel, text)
|
||
conn.serverinfo.name = None
|
||
logMessage = '<DISCONNECTED>'
|
||
conn.logger.info(logMessage)
|
||
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
# We didn't disconnect on purpose, set this so we will reconnect
|
||
conn.connectionstate == ConnectionState.DISCONNECTED
|
||
text = 'Attempting to reconnect...'
|
||
self._connectOTTD(irc, conn, text = text)
|
||
else:
|
||
conn.connectionstate = ConnectionState.DISCONNECTED
|
||
|
||
def _initSoapClient(self, conn, irc):
|
||
conn.configure(
|
||
irc = irc,
|
||
ID = self.registryValue('serverID', conn.channel),
|
||
password = self.registryValue('password', conn.channel),
|
||
host = self.registryValue('host', conn.channel),
|
||
port = self.registryValue('port', conn.channel),
|
||
name = '%s-Soap' % irc.nick)
|
||
utils.initLogger(conn, self.registryValue('logdir'), self.registryValue('logHistory'))
|
||
self._pollObj.register(conn.fileno(),
|
||
POLLIN | POLLERR | POLLHUP | POLLPRI)
|
||
conn.filenumber = conn.fileno()
|
||
self.registeredConnections[conn.filenumber] = conn
|
||
|
||
|
||
|
||
# Thread functions
|
||
|
||
def _commandThread(self, conn, irc, ofsCommand, successText = None, delay = 0):
|
||
time.sleep(delay)
|
||
ofs = self.registryValue('ofslocation', conn.channel)
|
||
command = ofs.replace('{OFS}', ofsCommand)
|
||
if ofs.startswith('ssh'):
|
||
useshell = True
|
||
elif os.path.isfile(command.split()[0]):
|
||
useshell = False
|
||
else:
|
||
irc.reply('OFS location invalid. Please review plugins.Soap.ofslocation')
|
||
return
|
||
|
||
self.log.info('executing: %s' % command)
|
||
if not useshell:
|
||
command = command.split()
|
||
try:
|
||
commandObject = Popen(command, shell=useshell, stdout = PIPE)
|
||
except OSError as e:
|
||
irc.reply('Couldn\'t start %s, Please review plugins.Soap.ofslocation'
|
||
% ofsCommand.split()[0])
|
||
return
|
||
output = commandObject.stdout.read()
|
||
commandObject.stdout.close()
|
||
commandObject.wait()
|
||
for line in output.splitlines():
|
||
self.log.info('%s output: %s' % (ofsCommand.split()[0], line))
|
||
if commandObject.returncode:
|
||
ofsFile = ofsCommand.split()[0]
|
||
code = commandObject.returncode
|
||
if ofsFile == 'ofs-getsave.py':
|
||
irc.reply(utils.ofsGetsaveExitcodeToText(code))
|
||
elif ofsFile == 'ofs-start.py':
|
||
utils.msgChannel(irc, conn.channel, utils.ofsStartExitcodeToText(code))
|
||
elif ofsFile == 'ofs-svntobin.py':
|
||
irc.reply(utils.ofsSvnToBinExitcodeToText(code))
|
||
elif ofsFile == 'ofs-svnupdate.py':
|
||
irc.reply(utils.ofsSvnUpdateExitcodeToText(code))
|
||
elif ofsFile == 'ofs-transfersave.py':
|
||
irc.reply(utils.ofsTransferSaveExitcodeToText(code))
|
||
else:
|
||
irc.reply('%s reported an error, exitcode: %s. See bot-log for more information.'
|
||
% (ofsFile, code))
|
||
irc.reply('PS this message should not be seen, please thwack Taede to get a proper error message')
|
||
return
|
||
if successText:
|
||
if not ofsCommand.startswith('ofs-start.py'):
|
||
irc.reply(successText, prefixNick = False)
|
||
else:
|
||
utils.msgChannel(irc, conn.channel, successText)
|
||
|
||
if ofsCommand.startswith('ofs-svnupdate.py'):
|
||
conn.rconState = RconStatus.UPDATESAVED
|
||
rconcommand = 'save autosave/autosavesoap'
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = rconcommand)
|
||
elif ofsCommand.startswith('ofs-svntobin.py'):
|
||
ofsCommand = 'ofs-start.py'
|
||
successText = 'Server is starting'
|
||
connectionid = utils.getConnectionID(conn)
|
||
cmdThread = threading.Thread(
|
||
target = self._commandThread,
|
||
name = 'Command.%s.%s' % (connectionid, ofsCommand.split()[0]),
|
||
args = [conn, irc, ofsCommand, successText])
|
||
cmdThread.daemon = True
|
||
cmdThread.start()
|
||
|
||
def _passwordThread(self, conn):
|
||
pluginDir = os.path.dirname(__file__)
|
||
pwFileName = os.path.join(pluginDir, 'passwords.txt')
|
||
|
||
# delay password changing until connection is fully established,
|
||
# abort if it takes longer than 10 seconds
|
||
for second in range(10):
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
break
|
||
time.sleep(1)
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
return
|
||
|
||
while True:
|
||
interval = self.registryValue('passwordInterval', conn.channel)
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
break
|
||
if conn.rconState == RconStatus.IDLE:
|
||
if interval > 0:
|
||
newPassword = random.choice(list(open(pwFileName)))
|
||
newPassword = newPassword.strip()
|
||
newPassword = newPassword.lower()
|
||
command = 'set server_password %s' % newPassword
|
||
conn.rconState = RconStatus.ACTIVE
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
conn.clientPassword = newPassword
|
||
time.sleep(interval)
|
||
else:
|
||
command = 'set server_password *'
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
|
||
conn.clientPassword = None
|
||
break
|
||
else:
|
||
time.sleep(interval)
|
||
|
||
def _pollThread(self):
|
||
timeout = 1.0
|
||
|
||
while True:
|
||
if len(self.registeredConnections) >= 1:
|
||
events = self._pollObj.poll(timeout * POLL_MOD)
|
||
for fileno, event in events:
|
||
conn = self.registeredConnections.get(fileno)
|
||
if not conn:
|
||
continue
|
||
if (event & POLLIN) or (event & POLLPRI):
|
||
packet = conn.recv_packet()
|
||
if packet == None:
|
||
logMessage = '>>--DEBUG--<< Received empty packet, forcing disconnect'
|
||
conn.logger.debug(logMessage)
|
||
utils.disconnect(conn, True)
|
||
else:
|
||
logMessage = '>>--DEBUG--<< Received packet: %s' % str(packet)
|
||
conn.logger.debug(logMessage)
|
||
elif (event & POLLERR) or (event & POLLHUP):
|
||
logMessage = '>>--DEBUG--<< Received POLLERR or POLLHUP, forcing disconnect'
|
||
conn.logger.debug(logMessage)
|
||
utils.disconnect(conn, True)
|
||
else:
|
||
time.sleep(0.001)
|
||
# lets not use up 100% cpu if there are no active connections
|
||
else:
|
||
time.sleep(1)
|
||
if self.stopPoll.isSet():
|
||
break
|
||
|
||
|
||
|
||
# Miscelanious functions
|
||
|
||
def _ircCommandInit(self, irc, msg, serverID, needsPermission):
|
||
source = msg.args[0].lower()
|
||
if source == irc.nick.lower():
|
||
source = msg.nick
|
||
conn = utils.getConnection(
|
||
self.connections, self.channels, source, serverID)
|
||
if not conn:
|
||
return (None, None)
|
||
allowOps = self.registryValue('allowOps', conn.channel)
|
||
|
||
if not needsPermission:
|
||
return (source, conn)
|
||
elif utils.checkPermission(irc, msg, conn.channel, allowOps):
|
||
return (source, conn)
|
||
else:
|
||
return (None, None)
|
||
|
||
def _ircRconInit(self, irc, msg, firstWord, remainder, command, needsPermission):
|
||
if not remainder:
|
||
remainder = ''
|
||
source = msg.args[0].lower()
|
||
if source == irc.nick.lower():
|
||
source = msg.nick
|
||
conn = None
|
||
for c in self.connections.values():
|
||
if (firstWord.lower() == c.channel.lower()
|
||
or firstWord.lower() == c.ID.lower()):
|
||
conn = c
|
||
if command:
|
||
command += ' %s' % remainder
|
||
else:
|
||
command = remainder
|
||
|
||
if not conn:
|
||
if command:
|
||
command += ' %s %s' % (firstWord, remainder)
|
||
else:
|
||
command = '%s %s' % (firstWord, remainder)
|
||
conn = utils.getConnection(
|
||
self.connections, self.channels, source)
|
||
if not conn:
|
||
return (None, None, None)
|
||
|
||
allowOps = self.registryValue('allowOps', conn.channel)
|
||
if not needsPermission:
|
||
return (source, conn, command)
|
||
elif utils.checkPermission(irc, msg, conn.channel, allowOps):
|
||
return (source, conn, command)
|
||
else:
|
||
return (None, None, None)
|
||
|
||
|
||
|
||
# Packet Handlers
|
||
|
||
def _rcvShutdown(self, connChan):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
text = 'Server Shutting down'
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
logMessage = '<SHUTDOWN> Server is shutting down'
|
||
conn.logger.info(logMessage)
|
||
conn.connectionstate = ConnectionState.SHUTDOWN
|
||
|
||
def _rcvNewGame(self, connChan):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
text = 'Starting new game'
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
logMessage = '<END> End of game'
|
||
conn.logger.info(logMessage)
|
||
if not conn.logger == None and len(conn.logger.handlers):
|
||
for handler in conn.logger.handlers:
|
||
try:
|
||
handler.doRollover()
|
||
except OSError as e:
|
||
pass
|
||
logMessage = '<NEW> New log file started'
|
||
conn.logger.info(logMessage)
|
||
|
||
def _rcvNewMap(self, connChan, mapinfo, serverinfo):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
conn.connectionstate = ConnectionState.CONNECTED
|
||
text = 'Now playing on %s (Version %s)' % (
|
||
conn.serverinfo.name, conn.serverinfo.version)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
command = 'set min_active_clients %s' % self.registryValue(
|
||
'minPlayers', conn.channel)
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
logMessage = '-' * 80
|
||
conn.logger.info(logMessage)
|
||
logMessage = '<CONNECTED> Version: %s, Name: \'%s\' Mapname: \'%s\' Mapsize: %dx%d' % (
|
||
conn.serverinfo.version, conn.serverinfo.name, conn.mapinfo.name, conn.mapinfo.x, conn.mapinfo.y)
|
||
conn.logger.info(logMessage)
|
||
|
||
def _rcvClientJoin(self, connChan, client):
|
||
conn = self.connections.get(connChan)
|
||
if not conn or isinstance(client, int):
|
||
return
|
||
irc = conn.irc
|
||
|
||
if client.name in self.registryValue('nameBlacklist'):
|
||
text = '*** %s tried to join with a blacklisted name, auto-kicking' % client.name
|
||
logMessage = '<JOIN-AUTOKICK> Name: \'%s\' (Host: %s, ClientID: %s)' % (
|
||
client.name, client.hostname, client.id)
|
||
conn.logger.info(logMessage)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
|
||
command = 'kick %s' % client.id
|
||
conn.rcon = conn.channel
|
||
conn.send_packet(AdminRcon, command = command)
|
||
return
|
||
|
||
text = '*** %s has joined' % client.name
|
||
logMessage = '<JOIN> Name: \'%s\' (Host: %s, ClientID: %s)' % (
|
||
client.name, client.hostname, client.id)
|
||
conn.logger.info(logMessage)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
|
||
if self.registryValue('checkClientVPN', conn.channel):
|
||
utils.checkIP(irc, conn, client, self.registryValue('checkClientVPNWhitelist'), self.ipdict)
|
||
|
||
welcome = self.registryValue('welcomeMessage', conn.channel)
|
||
if welcome:
|
||
replacements = {
|
||
'{clientname}': client.name,
|
||
'{servername}': conn.serverinfo.name,
|
||
'{serverversion}': conn.serverinfo.version}
|
||
for line in welcome:
|
||
for word, newword in replacements.items():
|
||
line = line.replace(word, newword)
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT_CLIENT,
|
||
destType = DestType.CLIENT,
|
||
clientID = client.id,
|
||
message = line)
|
||
|
||
def _rcvClientUpdate(self, connChan, old, client, changed):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
if 'name' in changed:
|
||
text = '*** %s has changed his/her name to %s' % (
|
||
old.name, client.name)
|
||
utils.msgChannel(conn.irc, conn.channel, text)
|
||
logMessage = '<NAMECHANGE> Old name: \'%s\' New Name: \'%s\' (Host: %s)' % (
|
||
old.name, client.name, client.hostname)
|
||
conn.logger.info(logMessage)
|
||
|
||
def _rcvClientQuit(self, connChan, client, errorcode):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
if not isinstance(client, int):
|
||
if errorcode:
|
||
reason = '%s' % utils.getQuitReasonFromNumber(errorcode)
|
||
else:
|
||
reason = 'Leaving'
|
||
text = '*** %s has left the game (%s)' % (client.name, reason)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
logMessage = '<QUIT> Name: \'%s\' (Host: %s, ClientID: %s, Reason: \'%s\')' % (
|
||
client.name, client.hostname, client.id, reason)
|
||
conn.logger.info(logMessage)
|
||
|
||
# garbage collect the kickdict
|
||
if client.id in self.kickdict:
|
||
del self.kickdict[client.id]
|
||
|
||
def _rcvChat(self, connChan, client, action, destType, clientID, message, data):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
clientName = str(clientID)
|
||
if client != clientID:
|
||
clientName = client.name
|
||
|
||
if action == Action.CHAT:
|
||
rulesUrl = self.registryValue('rulesUrl', conn.channel)
|
||
text = '<%s> %s' % (clientName, message)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
if message.startswith('!admin'):
|
||
demand = message.partition(' ')[2]
|
||
demand = demand.strip()
|
||
if len(demand) > 0:
|
||
text = '*[ADM]* %s requested an admin (reason: %s)' % (clientName, demand)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = text)
|
||
else:
|
||
text = 'You\'re about to call for an admin to attend - abusing this can result in severe penalties. To use this command, use !admin + a reason'
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = text)
|
||
elif message.startswith('!nick ') or message.startswith('!name '):
|
||
newName = message.partition(' ')[2]
|
||
newName = newName.strip()
|
||
if len(newName) > 0:
|
||
command = 'client_name %s %s' % (clientID, newName)
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
elif message.startswith('!rules') and not rulesUrl.lower() == 'none':
|
||
self.log.info('should display rules now')
|
||
text = 'Server rules can be found here: %s' % rulesUrl
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = text)
|
||
elif message.startswith('!resetme') or message.startswith('!reset'):
|
||
if client.play_as == 255:
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT_CLIENT,
|
||
destType = DestType.CLIENT,
|
||
clientID = client.id,
|
||
message = 'You can\'t dissolve the spectators, that\'d be silly!')
|
||
else:
|
||
companyparticipants = []
|
||
companyID = client.play_as
|
||
companyclients = 0
|
||
for c in list(conn.clients.values()):
|
||
if client.play_as == companyID:
|
||
companyclients+1
|
||
if (companyclients > 1):
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT_CLIENT,
|
||
destType = DestType.CLIENT,
|
||
clientID = client.id,
|
||
message = 'Your company has other players in it - get them to leave before resetting!')
|
||
else:
|
||
company = conn.companies.get(client.play_as)
|
||
self.log.info('Resetting company %s (%s)' % (company.id+1, company.name))
|
||
# notify the public
|
||
text = '*** %s has reset his/her company (%s)' % (clientName, company.name)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = text)
|
||
# move the client out of the company
|
||
command = 'move %s 255' % client.id
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
# reset the company
|
||
command = 'reset_company %s' % (company.id+1)
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
elif action == Action.COMPANY_JOIN or action == Action.COMPANY_NEW:
|
||
if not isinstance(client, int):
|
||
company = conn.companies.get(client.play_as)
|
||
if action == Action.COMPANY_JOIN:
|
||
text = ('*** %s has joined company #%d' %
|
||
(client.name, company.id+1))
|
||
joining = 'JOIN'
|
||
else:
|
||
text = ('*** %s has started a new company #%d' %
|
||
(client.name, company.id+1))
|
||
joining = 'NEW'
|
||
|
||
logMessage = '<COMPANY %s> Name: \'%s\' Company Name: \'%s\' Company ID: %s' % (
|
||
joining, clientName, company.name, company.id+1)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
conn.logger.info(logMessage)
|
||
|
||
playAsPlayer = self.registryValue('playAsPlayer', conn.channel)
|
||
kickcount = self.registryValue('playerKickCount', conn.channel)
|
||
if clientName.lower().startswith('player') and not playAsPlayer:
|
||
utils.moveToSpectators(irc, conn, client, kickcount, self.kickdict)
|
||
elif action == Action.COMPANY_SPECTATOR:
|
||
text = '*** %s has joined spectators' % clientName
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
logMessage = '<SPECTATOR JOIN> Name: \'%s\'' % clientName
|
||
conn.logger.info(logMessage)
|
||
|
||
def _rcvRcon(self, connChan, result, colour):
|
||
conn = self.connections.get(connChan)
|
||
if not conn or conn.rconState == RconStatus.IDLE:
|
||
return
|
||
irc = conn.irc
|
||
|
||
if conn.rconNick:
|
||
resultobj = conn.rconResults.get(conn.rconNick)
|
||
if not resultobj:
|
||
return
|
||
resultobj.results.put(result)
|
||
elif conn.rconState == RconStatus.SHUTDOWNSAVED:
|
||
if result.startswith('Map successfully saved'):
|
||
utils.msgChannel(irc, conn.channel, 'Successfully saved game as autosavesoap.sav')
|
||
conn.connectionstate = ConnectionState.SHUTDOWN
|
||
command = 'quit'
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
return
|
||
elif conn.rconState == RconStatus.UPDATESAVED:
|
||
if result.startswith('Map successfully saved'):
|
||
message = 'Game saved. Shutting down server to finish update. We\'ll be back shortly'
|
||
utils.msgChannel(irc, conn.channel, message)
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = message)
|
||
conn.connectionstate = ConnectionState.SHUTDOWN
|
||
command = 'quit'
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
|
||
ofsCommand = 'ofs-svntobin.py'
|
||
successText = None
|
||
connectionid = utils.getConnectionID(conn)
|
||
cmdThread = threading.Thread(
|
||
target = self._commandThread,
|
||
name = 'Command.%s.%s' % (connectionid, ofsCommand.split()[0]),
|
||
args = [conn, irc, ofsCommand, successText, 15])
|
||
cmdThread.daemon = True
|
||
cmdThread.start()
|
||
return
|
||
elif conn.rconState == RconStatus.RESTARTSAVED:
|
||
if result.startswith('Map successfully saved'):
|
||
message = 'Game saved. Restarting server...'
|
||
utils.msgChannel(irc, conn.channel, message)
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = message)
|
||
conn.connectionstate = ConnectionState.SHUTDOWN
|
||
command = 'quit'
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
|
||
ofsCommand = 'ofs-start.py'
|
||
successText = 'Server is starting'
|
||
connectionid = utils.getConnectionID(conn)
|
||
cmdThread = threading.Thread(
|
||
target = self._commandThread,
|
||
name = 'Command.%s.%s' % (connectionid, ofsCommand.split()[0]),
|
||
args = [conn, irc, ofsCommand, successText, 15])
|
||
cmdThread.daemon = True
|
||
cmdThread.start()
|
||
return
|
||
|
||
def _rcvRconEnd(self, connChan, command):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
|
||
nick = conn.rconNick
|
||
rconresult = conn.rconResults.get(nick)
|
||
if conn.rconCommands.empty():
|
||
conn.rconNick = None
|
||
conn.rconState = RconStatus.IDLE
|
||
if rconresult:
|
||
irc = rconresult.irc
|
||
for i in range(5):
|
||
if not rconresult.results.empty():
|
||
text = rconresult.results.get()
|
||
if text[3:].startswith('***') and (
|
||
command.startswith('move') or
|
||
command.startswith('kick')):
|
||
pass
|
||
else:
|
||
irc.reply(text, prefixNick = False)
|
||
else:
|
||
break
|
||
if rconresult.results.empty():
|
||
del conn.rconResults[nick]
|
||
elif rconresult.results.qsize() <= 2:
|
||
for i in range(2):
|
||
if not rconresult.results.empty():
|
||
text = rconresult.results.get()
|
||
irc.reply(text, prefixNick = False)
|
||
else:
|
||
break
|
||
else:
|
||
actionChar = conf.get(conf.supybot.reply.whenAddressedBy.chars)[:1]
|
||
text = 'You have %d more messages. Type %sless to view them' % (
|
||
rconresult.results.qsize(), actionChar)
|
||
irc.reply(text)
|
||
if rconresult.succestext:
|
||
irc.reply(rconresult.succestext)
|
||
else:
|
||
command = conn.rconCommands.get()
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
|
||
def _rcvConsole(self, connChan, origin, message):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
if (message.startswith('Game Load Failed') or
|
||
message.startswith('ERROR: Game Load Failed') or
|
||
message.startswith('Content server connection') or
|
||
message.startswith('[udp] advertising to the master server is failing') or
|
||
('reported an error' in message and not 'IRC' in message)):
|
||
ircMessage = message.replace("\n", ", ")
|
||
ircMessage = ircMessage.replace("?", ", ")
|
||
utils.msgChannel(irc, conn.channel, ircMessage)
|
||
else:
|
||
ircMessage = message[3:]
|
||
if ircMessage.startswith('***') and 'paused' in ircMessage:
|
||
utils.msgChannel(irc, conn.channel, ircMessage)
|
||
|
||
def _rcvCmdLogging(self, connChan, frame, param1, param2, tile, text, company, commandID, clientID):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
|
||
if clientID == 1:
|
||
name = 'Server'
|
||
else:
|
||
client = conn.clients.get(clientID)
|
||
if client:
|
||
name = client.name
|
||
else:
|
||
name = clientID
|
||
commandName = conn.commands.get(commandID)
|
||
if not commandName:
|
||
commandName = commandID
|
||
|
||
if commandName == 'CmdPause':
|
||
pass
|
||
|
||
logMessage = '<COMMAND> Frame: %s Name: \'%s\' Command: %s Tile: %s Param1: %s Param2: %s Text: \'%s\'' % (
|
||
frame, name, commandName, tile, param1, param2, text)
|
||
conn.logger.info(logMessage)
|
||
|
||
|
||
def _rcvPong(self, connChan, start, end, delta):
|
||
conn = self.connections.get(connChan)
|
||
if not conn:
|
||
return
|
||
irc = conn.irc
|
||
|
||
text = 'Dong! reply took %s' % str(delta)
|
||
utils.msgChannel(irc, conn.channel, text)
|
||
|
||
|
||
|
||
# IRC commands
|
||
|
||
def apconnect(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
connect to AdminPort of the [specified] OpenTTD server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
irc.reply('Already connected!!', prefixNick = False)
|
||
else:
|
||
# just in case an existing connection failed to de-register upon disconnect
|
||
try:
|
||
self._pollObj.unregister(conn.filenumber)
|
||
except KeyError:
|
||
pass
|
||
except IOError:
|
||
pass
|
||
self._connectOTTD(irc, conn, source)
|
||
apconnect = wrap(apconnect, [optional('text')])
|
||
|
||
def apdisconnect(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
disconnect from the [specified] server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
conn.connectionstate = ConnectionState.DISCONNECTING
|
||
utils.disconnect(conn, False)
|
||
else:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
apdisconnect = wrap(apdisconnect, [optional('text')])
|
||
|
||
def date(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
display the ingame date of the [specified] server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
# This is done in a slightly weird way because dates earlier than 1900 are possible in OpenTTD
|
||
# and strftime (in Python 2 at least) doesn't allow for dates before then
|
||
message = '{0.day:02d}/{0.month:02d}/{0.year:4d}'.format(conn.date)
|
||
irc.reply(message, prefixNick = False)
|
||
date = wrap(date, [optional('text')])
|
||
|
||
def rcon(self, irc, msg, args, parameters):
|
||
""" [Server ID or channel] <rcon command>
|
||
|
||
sends a rcon command to the [specified] openttd server
|
||
"""
|
||
|
||
command = ''
|
||
(firstWord, dummy, remainder) = parameters.partition(' ')
|
||
(source, conn, command) = self._ircRconInit(irc, msg, firstWord, remainder, command, True)
|
||
if not conn:
|
||
return
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if conn.rconState != RconStatus.IDLE:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
return
|
||
|
||
if len(command) >= NETWORK_RCONCOMMAND_LENGTH:
|
||
message = "RCON Command too long (%d/%d)" % (
|
||
len(command), NETWORK_RCONCOMMAND_LENGTH)
|
||
irc.reply(message, prefixNick = False)
|
||
return
|
||
|
||
logMessage = '<RCON> Nick: %s, command: %s' % (msg.nick, command)
|
||
conn.logger.info(logMessage)
|
||
|
||
conn.rconNick = msg.nick
|
||
conn.rconState = RconStatus.ACTIVE
|
||
resultdict = utils.RconResults({
|
||
'irc':irc,
|
||
'succestext':None,
|
||
'command':command,
|
||
'results':queue.Queue()
|
||
})
|
||
conn.rconResults[conn.rconNick] = resultdict
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
rcon = wrap(rcon, ['text'])
|
||
|
||
def less(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
outputs remaining rcon output which didn't get shown in the previous rounds
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
rconresult = conn.rconResults.get(msg.nick)
|
||
if rconresult:
|
||
for i in range(5):
|
||
if not rconresult.results.empty():
|
||
text = rconresult.results.get()
|
||
irc.reply(text, prefixNick = False)
|
||
else:
|
||
break
|
||
if rconresult.results.empty():
|
||
del conn.rconResults[msg.nick]
|
||
elif rconresult.results.qsize() <= 2:
|
||
for i in range(2):
|
||
if not rconresult.results.empty():
|
||
text = rconresult.results.get()
|
||
irc.reply(text, prefixNick = False)
|
||
else:
|
||
break
|
||
else:
|
||
actionChar = conf.get(conf.supybot.reply.whenAddressedBy.chars)[:1]
|
||
text = 'You have %d more messages. Type %sless to view them' % (
|
||
rconresult.results.qsize(), actionChar)
|
||
irc.reply(text)
|
||
else:
|
||
text = 'There are no more messages to display kemosabi'
|
||
irc.reply(text)
|
||
less = wrap(less, [optional('text')])
|
||
|
||
def shutdown(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
pauses the [specified] game server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
if conn.rconState == RconStatus.IDLE:
|
||
conn.rconCommands.put('save autosave/autosavesoap')
|
||
conn.rconState = RconStatus.SHUTDOWNSAVED
|
||
command = conn.rconCommands.get()
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
shutdown = wrap(shutdown, [optional('text')])
|
||
|
||
def restart(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
pauses the [specified] game server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
if conn.rconState == RconStatus.IDLE:
|
||
conn.rconState = RconStatus.RESTARTSAVED
|
||
command = 'save autosave/autosavesoap'
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
restart = wrap(restart, [optional('text')])
|
||
|
||
def contentupdate(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Gets the server to update its list of online contents
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if conn.rconState == RconStatus.IDLE:
|
||
conn.rconCommands.put('content update')
|
||
conn.rconNick = msg.nick
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
resultdict = utils.RconResults({
|
||
'irc':irc,
|
||
'succestext':None,
|
||
'command':command,
|
||
'results':queue.Queue()
|
||
})
|
||
conn.rconResults[conn.rconNick] = resultdict
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
irc.reply('Performing content update')
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
contentupdate = wrap(contentupdate, [optional('text')])
|
||
|
||
def content(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Gets the server to update the downloaded content to the latest versions
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if conn.rconState == RconStatus.IDLE:
|
||
conn.rconCommands.put('content select all')
|
||
conn.rconCommands.put('content upgrade')
|
||
conn.rconCommands.put('content download')
|
||
conn.rconNick = msg.nick
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
resultdict = utils.RconResults({
|
||
'irc':irc,
|
||
'succestext':None,
|
||
'command':command,
|
||
'results':queue.Queue()
|
||
})
|
||
conn.rconResults[conn.rconNick] = resultdict
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
content = wrap(content, [optional('text')])
|
||
|
||
def rescan(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
rescans the server's downloaded content
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if conn.rconState == RconStatus.IDLE:
|
||
irc.reply('Scanning content directories')
|
||
conn.rconCommands.put('rescannewgrf')
|
||
conn.rconCommands.put('rescanai')
|
||
conn.rconCommands.put('rescangame')
|
||
conn.rconNick = msg.nick
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
resultdict = utils.RconResults({
|
||
'irc':irc,
|
||
'succestext':'Rescan completed',
|
||
'command':command,
|
||
'results':queue.Queue()
|
||
})
|
||
conn.rconResults[conn.rconNick] = resultdict
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
rescan = wrap(rescan, [optional('text')])
|
||
|
||
def save(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
saves the game as game.sav in the save directory
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if conn.rconState == RconStatus.IDLE:
|
||
conn.rconCommands.put('save game')
|
||
conn.rconNick = msg.nick
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
resultdict = utils.RconResults({
|
||
'irc':irc,
|
||
'succestext':None,
|
||
'command':command,
|
||
'results':queue.Queue()
|
||
})
|
||
conn.rconResults[conn.rconNick] = resultdict
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
save = wrap(save, [optional('text')])
|
||
|
||
def pause(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
pauses the [specified] game server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if conn.rconState == RconStatus.IDLE:
|
||
conn.rconCommands.put('pause')
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
pause = wrap(pause, [optional('text')])
|
||
|
||
def auto(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
unpauses the [specified] game server, or if min_active_clients >= 1,
|
||
changes the server to autopause mode
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if conn.rconState == RconStatus.IDLE:
|
||
minPlayers = self.registryValue('minPlayers', conn.channel)
|
||
if minPlayers > 0:
|
||
conn.rconCommands.put('set min_active_clients %s' %
|
||
minPlayers)
|
||
conn.rconCommands.put('unpause')
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
auto = wrap(auto, [optional('text')])
|
||
|
||
def unpause(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
unpauses the [specified] game server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
if conn.rconState == RconStatus.IDLE:
|
||
conn.rconCommands.put('set min_active_clients 0')
|
||
conn.rconCommands.put('unpause')
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
unpause = wrap(unpause, [optional('text')])
|
||
|
||
def ding(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Dings the [specified] server, normally called ping
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
conn.ping()
|
||
ding = wrap(ding, [optional('text')])
|
||
|
||
def ip(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Replies with the [specified] server's public address for players to connect to
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
text = self.registryValue('publicAddress', conn.channel)
|
||
irc.reply(text)
|
||
ip = wrap(ip, [optional('text')])
|
||
|
||
def info(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Shows some basic information about the server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
version = conn.serverinfo.version
|
||
name = conn.serverinfo.name
|
||
size = '%dx%d' % (conn.mapinfo.x, conn.mapinfo.y)
|
||
ip = self.registryValue('publicAddress', conn.channel)
|
||
if ip and not ip == 'None' and not ip == 'Not Available':
|
||
ip = ', address: %s' % ip
|
||
else:
|
||
ip = ''
|
||
|
||
# This is done in a slightly weird way because dates earlier than 1900 are possible in OpenTTD
|
||
# and strftime (in Python 2 at least) doesn't allow for dates before then
|
||
date = '{0.day:02d}/{0.month:02d}/{0.year:4d}'.format(conn.date)
|
||
clients = len(conn.clients)
|
||
if conn.serverinfo.dedicated:
|
||
clients -= 1 # deduct server-client for dedicated servers
|
||
if clients >= 1:
|
||
clients = ', clients connected: %d' % clients
|
||
else:
|
||
clients = ''
|
||
irc.reply('%s, Version: %s, date: %s%s, map size: %s%s' %
|
||
(name, version, date, clients, size, ip))
|
||
info = wrap(info, [optional('text')])
|
||
|
||
def vehicles(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Gives a count of all vehicles in the game, sorted by type
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
if list(conn.companies.values()):
|
||
text = 'Total vehicles per type: Rail: %d, Road: %d, Water: %d, Air: %d' %\
|
||
utils.vehicleCount(conn.companies)
|
||
irc.reply(text)
|
||
else:
|
||
irc.reply('There are currently no companies in existence. '\
|
||
'Without companies, there cannot be vehicles')
|
||
vehicles = wrap(vehicles, [optional('text')])
|
||
|
||
def revision(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Shows the OpenTTD revision that is currently running on the server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
version = conn.serverinfo.version
|
||
irc.reply('Game version is %s. Use Download <os-version> to get a direct download link.' % version)
|
||
revision = wrap(revision, [optional('text')])
|
||
|
||
def password(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
tells you the current password needed for joining the [specified] game server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
if not conn.clientPassword:
|
||
irc.reply('Free entry, no passwords needed')
|
||
else:
|
||
irc.reply(conn.clientPassword)
|
||
password = wrap(password, [optional('text')])
|
||
|
||
def rules(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
tells you the current password needed for joining the [specified] game server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
rulesUrl = self.registryValue('rulesUrl', conn.channel)
|
||
if not rulesUrl.lower() == 'none':
|
||
text = 'Server rules can be found here: %s' % rulesUrl
|
||
irc.reply(text, prefixNick = False)
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = text)
|
||
rules = wrap(rules, [optional('text')])
|
||
|
||
def companies(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Show a list of players currently playing
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
if list(conn.companies.values()):
|
||
for company in list(conn.companies.values()):
|
||
if not company.id == 255:
|
||
companyColour = utils.getColourNameFromNumber(company.colour)
|
||
companyFounded = 'Founded in %d' % company.startyear
|
||
companyVehicles = 'Vehicles owned: %d Trains, %s Roadvehicles, %s Ships and %s Aeroplanes.' % (
|
||
company.vehicles.train,
|
||
company.vehicles.lorry + company.vehicles.bus,
|
||
company.vehicles.ship,
|
||
company.vehicles.plane)
|
||
companyEconomy = 'Value: %d, Loan: %d, Income: %d, Cargo: %d' % (
|
||
company.economy.history[0]['companyValue'],
|
||
company.economy.currentLoan,
|
||
company.economy.income,
|
||
company.economy.deliveredCargo)
|
||
text = 'Company \'%d\' (%s): %s, %s, %s %s' % (
|
||
company.id+1, companyColour, company.name,
|
||
companyFounded, companyVehicles, companyEconomy)
|
||
if company.ai:
|
||
text = 'AI ' + text
|
||
irc.reply(text)
|
||
else:
|
||
irc.reply('There are currently no companies in existence. '\
|
||
'I smell an opportunity...')
|
||
companies = wrap(companies, [optional('text')])
|
||
|
||
def players(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Show a list of players currently playing
|
||
"""
|
||
|
||
isOp = True
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
isOp = False
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
if isOp:
|
||
spectators = []
|
||
players = []
|
||
for client in list(conn.clients.values()):
|
||
if client.play_as == 255:
|
||
if conn.serverinfo.dedicated and client.id == 1:
|
||
pass
|
||
else:
|
||
spectators.append('Client %d (%s)' % (client.id, client.name))
|
||
else:
|
||
company = conn.companies.get(client.play_as)
|
||
companyColour = utils.getColourNameFromNumber(company.colour)
|
||
players.append('Client %d (%s) is %s, in company %s (%s)' %
|
||
(client.id, companyColour, client.name, company.id+1,
|
||
company.name))
|
||
spectators.sort()
|
||
players.sort()
|
||
for player in players:
|
||
irc.reply(player)
|
||
if spectators:
|
||
spectators = ', '.join(spectators)
|
||
irc.reply('Spectators: %s' % spectators)
|
||
if not players and not spectators:
|
||
irc.reply('The server is empty, noone is connected. '\
|
||
'Feel free to remedy this situation')
|
||
else:
|
||
irc.reply(utils.playercount(conn))
|
||
players = wrap(players, [optional('text')])
|
||
|
||
def playercount(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Tells you the number of players and spectators on the server at this moment
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
irc.reply(utils.playercount(conn))
|
||
playercount = wrap(playercount, [optional('text')])
|
||
|
||
def toggledebug(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Toggles debug logging for the connection. The debug info will be logged
|
||
to the standard gamelog
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
|
||
if conn.debugLog:
|
||
conn.debugLog = False
|
||
irc.reply('Debug logging for %s is now: off' % conn.ID)
|
||
conn.logger.info('>> Debug logging turned off <<')
|
||
else:
|
||
conn.debugLog = True
|
||
irc.reply('Debug logging for %s is now: ON' % conn.ID)
|
||
conn.logger.info('>> Debug logging turned ON <<')
|
||
toggledebug = wrap(toggledebug, [optional('text')])
|
||
|
||
def setdef(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Reads commands from a file specified in plugins.Soap.defaultSettings to
|
||
set game-settings to default values
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
rconFile = self.registryValue('defaultSettings', conn.channel)
|
||
if not os.path.isfile(rconFile):
|
||
irc.reply('Cannot read from %s, please set it to a valid bot-readable file. Absolute path is a must'
|
||
% rconFile)
|
||
return
|
||
if conn.rconState == RconStatus.IDLE:
|
||
commandlist = []
|
||
with open(rconFile) as rf:
|
||
for line in rf.readlines():
|
||
commandlist.append(line.rstrip())
|
||
if len(commandlist):
|
||
commandlist.sort()
|
||
for item in commandlist:
|
||
conn.rconCommands.put(item)
|
||
conn.rconState = RconStatus.ACTIVE
|
||
command = conn.rconCommands.get()
|
||
conn.logger.debug('>>--DEBUG--<< Sending rcon: %s' % command)
|
||
conn.send_packet(AdminRcon, command = command)
|
||
irc.reply('Setting default settings: %s' % format('%L', commandlist))
|
||
else:
|
||
irc.reply('No commands found in %s.' % rconFile)
|
||
else:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
setdef = wrap(setdef, [optional('text')])
|
||
|
||
def download(self, irc, msg, args, osType, serverID):
|
||
""" [OS type/program] [Server ID or channel]
|
||
|
||
Returns the url to download the client for the current game. Also links
|
||
to some starter programs
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, False)
|
||
if not conn:
|
||
return
|
||
|
||
url = None
|
||
if not osType:
|
||
actionChar = conf.get(conf.supybot.reply.whenAddressedBy.chars)
|
||
irc.reply('%sdownload lin|lin64|osx|ottdau|source|win32|win64|win9x'
|
||
% actionChar)
|
||
if osType == 'ottdau':
|
||
url = 'http://www.openttdcoop.org/winupdater'
|
||
else:
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
irc.reply('Not connected!!', prefixNick = False)
|
||
return
|
||
customUrl = self.registryValue('downloadUrl', conn.channel)
|
||
if not customUrl or customUrl.startswith('None'):
|
||
url = utils.generateDownloadUrl(
|
||
irc, conn.serverinfo.version, osType)
|
||
else:
|
||
url = customUrl
|
||
if url:
|
||
irc.reply(url)
|
||
else:
|
||
irc.reply('Couldn\'t decipher download url')
|
||
download = wrap(download, [optional(('literal',
|
||
['lin', 'lin64', 'osx', 'ottdau', 'win32', 'win64', 'win9x', 'source'])),
|
||
optional('text')])
|
||
|
||
def help(self, irc, msg, args):
|
||
""" Takes no arguments
|
||
|
||
Returns the url with a list of commands
|
||
"""
|
||
|
||
irc.reply('http://wiki.openttdcoop.org/Soap')
|
||
help = wrap(help)
|
||
|
||
|
||
|
||
# Relay IRC back ingame
|
||
|
||
def doPrivmsg(self, irc, msg):
|
||
|
||
(source, text) = msg.args
|
||
conn = None
|
||
source = source.lower()
|
||
if irc.isChannel(source) and source in self.channels:
|
||
conn = self.connections.get(source)
|
||
if not conn:
|
||
return
|
||
if conn.connectionstate != ConnectionState.CONNECTED:
|
||
return
|
||
|
||
actionChar = conf.get(conf.supybot.reply.whenAddressedBy.chars, source)
|
||
if actionChar and actionChar in text[:1]:
|
||
return
|
||
if not 'ACTION' in text:
|
||
message = 'IRC <%s> %s' % (msg.nick, text)
|
||
else:
|
||
text = text.split(' ',1)[1]
|
||
text = text[:-1]
|
||
message = 'IRC ** %s %s' % (msg.nick, text)
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = message)
|
||
|
||
|
||
|
||
# ofs related commands
|
||
|
||
def getsave(self, irc, msg, args, saveUrl, serverID):
|
||
""" [Server ID or channel] <Http Url of savegame>
|
||
|
||
Downloads a savegame file over HTTP and saves it in the saves dir of the [specified] server
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if saveUrl[-4:] == '.sav':
|
||
irc.reply('Starting download...', prefixNick = False)
|
||
ofsCommand = 'ofs-getsave.py %s' % saveUrl
|
||
successText = 'Savegame successfully downloaded'
|
||
connectionid = utils.getConnectionID(conn)
|
||
cmdThread = threading.Thread(
|
||
target = self._commandThread,
|
||
name = 'Command.%s.%s' % (connectionid, ofsCommand.split()[0]),
|
||
args = [conn, irc, ofsCommand, successText])
|
||
cmdThread.daemon = True
|
||
cmdThread.start()
|
||
else:
|
||
irc.reply('Sorry, only .sav files are supported')
|
||
getsave = wrap(getsave, ['httpUrl', optional('text')])
|
||
|
||
def start(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Starts the specified server. Only available if plugins.Soap.local is True
|
||
"""
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
irc.reply('I am connected to %s, so it\'s safe to assume that its already running'
|
||
% conn.serverinfo.name, prefixNick = False)
|
||
return
|
||
|
||
ofsCommand = 'ofs-start.py'
|
||
successText = 'Server is starting...'
|
||
connectionid = utils.getConnectionID(conn)
|
||
cmdThread = threading.Thread(
|
||
target = self._commandThread,
|
||
name = 'Command.%s.%s' % (connectionid, ofsCommand.split()[0]),
|
||
args = [conn, irc, ofsCommand, successText])
|
||
cmdThread.daemon = True
|
||
cmdThread.start()
|
||
start = wrap(start, [optional('text')])
|
||
|
||
def transfer(self, irc, msg, args, gameNo, savegame, serverID):
|
||
""" <Game Number> <savegame> [Server ID or channel]
|
||
|
||
Transfers 'savegame'. Destination directory and name are configured in
|
||
ofs-transfersave.py. Game number is used to uniquely name the
|
||
destination file. Warning: this is the only command where the optional
|
||
Server ID is the last argument
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if savegame.endswith('.sav'):
|
||
saveUrl = self.registryValue('saveUrl', conn.channel)
|
||
self.log.info(saveUrl)
|
||
saveUrl = saveUrl.replace('{ID}', str(gameNo))
|
||
self.log.info(saveUrl)
|
||
irc.reply('Attempting to transfer %s' % savegame)
|
||
ofsCommand = 'ofs-transfersave.py %d %s' % (gameNo, savegame)
|
||
successText = 'Transfer done. File now at %s' % saveUrl
|
||
connectionid = utils.getConnectionID(conn)
|
||
cmdThread = threading.Thread(
|
||
target = self._commandThread,
|
||
name = 'Command.%s.%s' % (connectionid, ofsCommand.split()[0]),
|
||
args = [conn, irc, ofsCommand, successText])
|
||
cmdThread.daemon = True
|
||
cmdThread.start()
|
||
transfer = wrap(transfer, ['int', 'something', optional('text')])
|
||
|
||
def update(self, irc, msg, args, serverID):
|
||
""" [Server ID or channel]
|
||
|
||
Updates OpenTTD to the newest revision in its branch
|
||
"""
|
||
|
||
source, conn = self._ircCommandInit(irc, msg, serverID, True)
|
||
if not conn:
|
||
return
|
||
|
||
if conn.rconState != RconStatus.IDLE:
|
||
message = 'Sorry, still processing previous rcon command'
|
||
irc.reply(message, prefixNick = False)
|
||
return
|
||
|
||
irc.reply('Starting update...', prefixNick = False)
|
||
if conn.connectionstate == ConnectionState.CONNECTED:
|
||
message = 'Server is being updated, and will shut down in a bit...'
|
||
conn.send_packet(AdminChat,
|
||
action = Action.CHAT,
|
||
destType = DestType.BROADCAST,
|
||
clientID = ClientID.SERVER,
|
||
message = message)
|
||
ofsCommand = 'ofs-svnupdate.py'
|
||
successText = 'Game successfully updated'
|
||
connectionid = utils.getConnectionID(conn)
|
||
cmdThread = threading.Thread(
|
||
target = self._commandThread,
|
||
name = 'Command.%s.%s' % (connectionid, ofsCommand.split()[0]),
|
||
args = [conn, irc, ofsCommand, successText])
|
||
cmdThread.daemon = True
|
||
cmdThread.start()
|
||
update = wrap(update, [optional('text')])
|
||
|
||
Class = Suds
|
||
|
||
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|