From 2a945f2ff96b92770be6e88b07770fa214b32de8 Mon Sep 17 00:00:00 2001 From: Fulgen301 Date: Wed, 16 Aug 2017 17:44:07 +0200 Subject: Add basic IRC implementation --- pycrctrl.py | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 139 insertions(+), 16 deletions(-) diff --git a/pycrctrl.py b/pycrctrl.py index 06dcb92..0b2f731 100644 --- a/pycrctrl.py +++ b/pycrctrl.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # -*- coding:utf-8 -*- #Copyright (c) 2017, George Tokmaji @@ -14,9 +15,10 @@ #ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import sys import os +sys = os.sys +import socket import subprocess import queue from _thread import start_new_thread @@ -29,7 +31,23 @@ from io import BytesIO import logging import urllib.request -import supybot.ircmsgs as ircmsgs + +class _ircmsgs(object): + @staticmethod + def msg(cmd, msg, encoding : str = "utf-8"): + return b"%b %b" % tuple((i.encode(encoding) if hasattr(i, "encode") else i) for i in [cmd, msg]) + + @staticmethod + def topic(channel : str, text : str, encoding : str = "utf-8"): + return _ircmsgs.msg(b"TOPIC", "{} {}".format(channel, text, encoding)) + + @staticmethod + def privmsg(to : str, text : str, encoding : str = "utf-8"): + return _ircmsgs.msg(b"PRIVMSG", "{} {}".format(to, text, encoding)) +try: + import supybot.ircmsgs as ircmsgs +except ImportError: + ircmsgs = _ircmsgs import random @@ -116,10 +134,10 @@ class Updater(object): break if not x: - self.parent.log.error("Updater: Update regular expression doesn't match!") + self.parent.log.error("Updater.checkForUpdates: Regular expression doesn't match!") except Exception as e: - cerr << str(e) << endl + self.parent.log.exception(str(e)) finally: sleep(10) @@ -252,29 +270,35 @@ class PyCRCtrl(object): elif not os.path.exists(conf): c = """[General] -UseLogfile=true Prefix=@ [Clonk] Engine=clonk Encoding=utf-8 -Commandline=/fullscreen /lobby:300 /config:config.txt /record /faircrew +Commandline=/fullscreen /lobby:300 /record /faircrew Autohost=false [IRC] -Ingamechat=true - - [Channels] - Parent="#clonk-SGGP" - Ingame="#clonk-SGGP-ingame" +Ingamechat=false + +[Channels] +Parent= +Ingame= [Updater] Enabled=false - [Addresses] - snapshotList=http://openclonk.org/nightly-builds - snapshotDownload=http://openclonk.org/builds/nightly/snapshots/{} - autobuildList=https://autobuild.openclonk.org/api/v1/jobs - autobuildAddress=https://autobuild.openclonk.org/static/binaries/{} +BinaryName= +SplitString= + +[Addresses] +snapshotList=http://openclonk.org/nightly-builds +snapshotDownload=http://openclonk.org/builds/nightly/snapshots/{} +autobuildList=https://autobuild.openclonk.org/api/v1/jobs +autobuildAddress=https://autobuild.openclonk.org/static/binaries/{} + +[Logging] +File=pycrctrl.log +Level=INFO [RegExps] LobbyStart=((?:Los geht's!|Action go!)\\s*) @@ -591,3 +615,102 @@ Snapshot=openclonk-snapshot-(.*)-(.*)-{}-{}-.""" self.setTopic("Kein laufendes Spiel.") logging.shutdown() self.shutdowned = True + +class PyCRCtrlIRC(object): + parent = PyCRCtrl = None + irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + encoding = "utf-8" + + def setParent(parent): + self.parent = parent + self.encoding = parent.config["Clonk"]["Encoding"] + + def sendMsg(self, msg : bytes): + if type(msg) != bytes: + msg = str(msg).encode(self.encoding, "replace") + try: + self.irc.send(msg + b"\r") + except (socket.error, socket.timeout, OSError): + self.reconnect() + self.sendMsg(msg) + + def connect(self): + try: + self.irc.connect((self.parent.config["IRC"]["Server"], self.parent.config["IRC"]["Port"])) + except (socket.error, socket.timeout, OSError): + self.reconnect() + return + + nick = self.parent.config["IRC"]["Nick"] + self.sendMsg(ircmsgs.msg("USER", "{nick} {nick} {nick} :PyCRCtrl".format(nick=nick), self.encoding)) + self.sendMsg(ircmsgs.msg("NICK", "{}".format(nick), self.encoding)) # security + + try: + self.irc.sendMsg(ircmsgs.msg("PASSWORD", "{}".format(self.parent.config["IRC"]["Password"]), self.encoding)) + except KeyError: + pass + + self.parent.log.info("Successfully connected to {}".format(self.parent.config["IRC"]["Server"])) + + start_new_thread(self.checkForMessages, ()) + + def reconnect(self): + sleep(10) + start_new_thread(self.connect, ()) + try: + self.irc.close() + except Exception: + pass + del self.irc + self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + start_new_thread(self.connect, ()) + + def checkForMessages(self): + while True: + try: + text = self.irc.recv(512).decode(self.encoding, "replace") + except (socket.error, socket.timeout, OSError): + self.reconnect() + return + + x = re.match(r"^(?:[:](\S+) )?(\S+)(?: (?!:)(.+?))?(?: [:](.+))?$") + if not x: + continue + + nick, command, channel, msg = x.group(1), x.group(2), x.group(3), x.group(4) + + if command == "PING": + self.sendMsg(ircmsgs.msg("PONG", msg)) + + elif command == "PRIVMSG": + y = re.match(r"{}(\w+) (.*)".format(self.parent.config["General"]["Prefix"])) + if not y: + continue + + func, args = y.group(1), y.group(2) + if func in self.parent.commands: + self.parent.log.info("{} called by {}".format(func, nick)) + try: + self.sendMsg(ircmsgs.privmsg(self.parent.commands[func](args))) + except Exception as e: + self.parent.log.exception(e.args[0]) + +if __name__ == "__main__": + ircmsgs = _ircmsgs + + irc = PyCRCtrlIRC() + ctrl = PyCRCtrl(irc, sys.argv[1]) + ctrl.addCommand(ctrl.start, "start") + ctrl.addCommand(ctrl.stop, "stop") + ctrl.addCommand(ctrl.host, "host") + ctrl.addCommand(ctrl.displayQueue, "queue") + ctrl.addCommand(ctrl.help, "help") + ctrl.addCommand(ctrl.list, "list") + ctrl.addCommand(ctrl.ircCommands, "irc") + irc.setParent(ctrl) + irc.connect() + while True: + try: + sleep(600) + except KeyboardInterrupt: + sys.exit(0) -- cgit v1.2.3-54-g00ecf From e08aab2b2f5bac8682ae5a4a6134122595ed5343 Mon Sep 17 00:00:00 2001 From: Fulgen301 Date: Sat, 25 Nov 2017 15:52:29 +0100 Subject: Rewrite PyCRCtrl using asyncirc --- pycrctrl.py | 530 ++++++++++++++++++++++-------------------------------------- 1 file changed, 191 insertions(+), 339 deletions(-) mode change 100644 => 100755 pycrctrl.py diff --git a/pycrctrl.py b/pycrctrl.py old mode 100644 new mode 100755 index 0b2f731..88ec029 --- a/pycrctrl.py +++ b/pycrctrl.py @@ -21,41 +21,27 @@ sys = os.sys import socket import subprocess import queue -from _thread import start_new_thread +import threading from time import sleep from platform import architecture import re import configparser from io import BytesIO -import logging - import urllib.request - -class _ircmsgs(object): - @staticmethod - def msg(cmd, msg, encoding : str = "utf-8"): - return b"%b %b" % tuple((i.encode(encoding) if hasattr(i, "encode") else i) for i in [cmd, msg]) - - @staticmethod - def topic(channel : str, text : str, encoding : str = "utf-8"): - return _ircmsgs.msg(b"TOPIC", "{} {}".format(channel, text, encoding)) - - @staticmethod - def privmsg(to : str, text : str, encoding : str = "utf-8"): - return _ircmsgs.msg(b"PRIVMSG", "{} {}".format(to, text, encoding)) -try: - import supybot.ircmsgs as ircmsgs -except ImportError: - ircmsgs = _ircmsgs - import random +import traceback import tarfile from enum import IntEnum from gzip import GzipFile +import asyncio +from blinker import signal +from asyncirc import irc +import asyncirc.plugins.addressed + # # Helpers # @@ -81,13 +67,6 @@ class list(list): def isEmpty(self): return len(self) == 0 -class CmdResult(IntEnum): - UnknownCommand = -1 - Success = 0 - SyntaxFail = 1 - RightsFail = 2 - RuntimeError = 3 - class Updater(object): parent = None __current_revision = "" @@ -134,22 +113,22 @@ class Updater(object): break if not x: - self.parent.log.error("Updater.checkForUpdates: Regular expression doesn't match!") + print("Updater.checkForUpdates: Regular expression doesn't match!", file=sys.stderr) except Exception as e: - self.parent.log.exception(str(e)) + traceback.print_exc() finally: sleep(10) def loadNewSnapshot(self, reg): - self.parent.log.info("Downloading snapshot with id {}".format(self.current_revision)) + print(f"Updater: Downloading snapshot with id {self.current_revision}") with open(os.path.join(self.parent.path, "snapshot"), "wb") as fobj: fobj.write(urllib.request.urlopen(self.parent.config["Addresses"]["snapshotDownload"].format(reg.group(0).split("' title")[0])).read()) #extract the snapshot tar = tarfile.open(os.path.join(self.parent.path, "snapshot"), mode="r:bz2") tar.extractall(path=self.parent.path) - self.parent.log.info("New snapshot has been extracted.") + print("Updater: New snapshot has been extracted.") #get the openclonk-server autobuild site = json.loads(urllib.request.urlopen(self.parent.config["Addresses"]["autobuildList"]).read().decode("utf-8")) @@ -160,14 +139,14 @@ class Updater(object): for b in build["components"]: reg = re.match(self.parent.config["RegExps"]["Autobuild"], str(b["path"])) #skip the engine check as the only useful one is openclonk-server if reg and (reg.group(1), reg.group(2), reg.group(3)) == (self.current_revision[:-3], sys.platform, self.lookuptable[architecture()[0]]): - self.parent.log.info("Downloading autobuild with id {}".format(self.current_revision)) + print(f"Updater: Downloading autobuild with id {self.current_revision}") buffer = BytesIO() buffer.write(urllib.request.urlopen(self.parent.config["Addresses"]["autobuildDownload"].format(b["path"])).read()) buffer.seek(0) with open(os.path.join(self.parent.path, self.parent.config["Updater"]["BinaryName"]), "wb") as fobj: fobj.write(GzipFile(fileobj=buffer).read()) - self.parent.log.info("New openclonk-server build has been extracted.") + print("Updater: New openclonk-server build has been extracted.") os.chmod(os.path.join(self.parent.path, self.parent.config["Updater"]["BinaryName"]), os.stat(os.path.join(self.parent.path, self.parent.config["Updater"]["BinaryName"])).st_mode | 64) return True @@ -175,9 +154,10 @@ class Updater(object): class PyCRCtrl(object): """Server control""" + irc = None clonk = None - thread_started = False + server_thread = None stopped = False config = configparser.ConfigParser() @@ -195,7 +175,6 @@ class PyCRCtrl(object): __ingamechat = "aktiviert" updater = None - log = None shutdowned = False @property @@ -218,13 +197,12 @@ class PyCRCtrl(object): self.__ingamechat = text self.state = self.state - def __init__(self, irc, path, config="pycrctrl.ini"): - self.irc = irc + def __init__(self, path, config="pycrctrl.ini"): self.path = path self.loadConfigFile(config) - self.setupLog() - self.ingamechat = "aktiviert" if self.config["Clonk"].getboolean("Autohost") else "deaktiviert" + self.ingamechat = "aktiviert" if self.config["IRC"].getboolean("Ingamechat") else "deaktiviert" self.loadScenarioList() + self.startIRC() self.queue = queue.Queue(5) if self.config["Updater"].getboolean("Enabled"): @@ -240,20 +218,13 @@ class PyCRCtrl(object): with open(os.path.join(self.path, "scenarios_league.lst"), "r") as fobj: self.league_scenlist = fobj.readlines() - self.log.debug("Scenario lists loaded.") + print("DEBUG: Scenario lists loaded.") - def setupLog(self) -> None: - self.log = logging.getLogger(type(self).__name__) - self.log.setLevel(getattr(logging, self.config["Logging"]["Level"], logging.INFO)) - - ch = logging.FileHandler( - os.path.join(self.path, self.config["Logging"]["File"]) - ) - ch.setLevel(getattr(logging, self.config["Logging"]["Level"], logging.INFO)) - ch.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")) - - self.log.addHandler(ch) - self.log.info("PyCRCtrl started.") + def startIRC(self) -> None: + asyncirc.plugins.addressed.register_command_character(self.config["General"]["Prefix"]) + self.irc = irc.connect(self.config["IRC"]["Server"], self.config["IRC"]["Port"], use_ssl=self.config["IRC"].getboolean("SSL")) + self.irc.register(*([self.config["IRC"]["Nick"]] * 3), password=self.config["IRC"]["Password"]) + self.irc.join([*(list(self.config["Channels"].values())), "#openclonk-atlantis"]) def loadConfigFile(self, config) -> None: if self.path == None: @@ -280,6 +251,11 @@ Autohost=false [IRC] Ingamechat=false +Nick=PyCRCtrl +Password= +Server=irc.euirc.net +Port=6667 +SSL=false [Channels] Parent= @@ -296,10 +272,6 @@ snapshotDownload=http://openclonk.org/builds/nightly/snapshots/{} autobuildList=https://autobuild.openclonk.org/api/v1/jobs autobuildAddress=https://autobuild.openclonk.org/static/binaries/{} -[Logging] -File=pycrctrl.log -Level=INFO - [RegExps] LobbyStart=((?:Los geht's!|Action go!)\\s*) Start=Start! @@ -336,15 +308,13 @@ Snapshot=openclonk-snapshot-(.*)-(.*)-{}-{}-.""" self.state = "Lobby" self.readServerOutput() if self.config["Clonk"].getboolean("Autohost") == False: - self.thread_started = False + self.server_thread = None self.setTopic("Kein laufendes Spiel.") break finally: if self.clonk: self.clonk.stdin.close() - - return (CmdResult.Success, "") def readServerOutput(self): while True: @@ -361,48 +331,28 @@ Snapshot=openclonk-snapshot-(.*)-(.*)-{}-{}-.""" return elif output: - output = output.splitlines()[0] - #output = output.decode("utf-8").splitlines()[0] - output = output[(output.find("] ") if output.find("] ") != -1 else -2)+len("] "):] + output = output.strip() + output = output[(output.find("] ") if output.find("] ") != -1 else -2) + len("] "):] # TODO: Make this code readable if output[0] == ">": output = output[1:] - part = self.isMessage(output) - if part and part.group(0) != self.irc.nick: - self.log.info(output) - cmd = part.group(3).split(" ", 1) - found = False - x = None - if len(cmd) > 0: - for key in self.commands.keys(): - if key == cmd[0].splitlines()[0]: - found = True - try: - x = self.commands[key](cmd[1].split(" "), user=part.group(1)) - except IndexError: - x = self.commands[key](user=part.group(1)) - break - - if not found: - self.writeToServer('Unbekannter Befehl: "' + part.group(2) + '"!') - if x: - if type(x) == tuple and x[1] != "": - self.writeToServer(x[1]) - del x + print(output) + + self.checkForCommands(output, reply=self.writeToServer) + if re.match(self.config["RegExps"]["LobbyStart"], output): self.state = "Lädt" elif re.match(self.config["RegExps"]["Start"], output): self.state = "Läuft" - if self.irc and self.ingamechat == "aktiviert": - if output.find("<" + self.irc.nick + ">") == -1: - if self.isMessage(output) and output.find("[IRC]") == -1 and output.find(self.config["General"]["Prefix"]) == -1: - self.irc.reply("[Clonk]{}".format(output), to=self.config["Channels"]["Ingame"]) + if self.ingamechat == "aktiviert": + if self.isMessage(output) and f"<{self.irc.nickname}>" not in output: + self.irc.say(self.config["Channels"]["Ingame"], f"[Clonk]{output}") - elif any((re.match(self.config["RegExps"]["PlayerJoin"], output), re.match(self.config["RegExps"]["PlayerLeave"], output))): - self.irc.reply(output, to=self.config["Channels"]["Ingame"]) + elif (re.match(self.config["RegExps"]["PlayerJoin"], output) or re.match(self.config["RegExps"]["PlayerLeave"], output)): + self.irc.say(self.config["Channels"]["Ingame"], output) except KeyboardInterrupt: @@ -410,137 +360,36 @@ Snapshot=openclonk-snapshot-(.*)-(.*)-{}-{}-.""" self.clonk.stdin.close() except Exception as e: - self.log.exception(e.args[0]) + traceback.print_exc() continue - - return (CmdResult.Success, "") - - def doPrivmsg(self, msg): - if not (self.irc and self.ingamechat == "aktiviert"): - return - for channel in msg.args[0].split(","): - if channel == self.config["Channels"]["Ingame"] and msg.nick != self.irc.nick: - self.writeToServer("[IRC]<{}> {}".format(msg.nick, msg.args[1])) - - # - # Commands - # - - def host(self, scenario=None, user=None) -> str: - if not scenario: - return (CmdResult.SyntaxFail, "Bitte gib einen Szenarionamen an!") - if hasattr(scenario, "decode"): - try: - scenario = scenario.decode(self.config["Clonk"]["Encoding"], "replace") - except: - self.log.warning("Unable to decode {}".format(scenario)) - return (CmdResult.RuntimeError, "Dekodierfehler. Bitte kontaktiere den Hoster dieses Servers.") - - scenario = scenario.splitlines()[0] - if scenario == "random": - scenario = random.choice(self.scenlist).splitlines()[0] - - elif scenario not in self.scenlist: - return (CmdResult.SyntaxFail,'Szenario "{}" wurde nicht gefunden!'.format(scenario)) - - if self.thread_started == False: - self.scenario = scenario - self.thread_started = True - start_new_thread(self.startClonk,()) - return (CmdResult.Success, 'Szenario "{}" wird jetzt gehostet.'.format(scenario)) - - if not self.queue.full(): - self.queue.put(scenario) - return (CmdResult.Success, 'Szenario "{}" wurde der Warteschlange hinzugefügt.'.format(scenario)) - else: - return (CmdResult.RuntimeError, "Die Warteschlange ist voll!") - - def start(self, time=None, user=""): - """Startet das Spiel.""" - self.stopped = False - if time: - try: - time = int(time[0]) - except: - time = 5 - self.writeToServer("/start {}".format(time)) - else: - self.writeToServer("/start") - - return (CmdResult.Success, "") - - - def stop(self, prm=None, user=""): - """Stoppt den Countdown.""" - def stopping(self): - while self.clonk and self.stopped: - self.writeToServer("/start 60000") - if self.stopped == False: - return - sleep(100) - - if self.stopped == False: - self.stopped = True - start_new_thread(stopping, (self,)) - - return (CmdResult.Success, "") - - def help(self, prm=None, user=""): - """Gibt die Hilfe aus.""" - self.writeToServer("Verfügbare Befehle:") - for text, function in self.commands.items(): - self.writeToServer("{} -- {}".format(text, function.__doc__)) - - return (CmdResult.Success, "") - - - def displayQueue(self, prm=None, user=""): - """Gibt die Warteschlange aus.""" - self.writeToServer("Warteschlange:") - - for i,scen in enumerate(self.queue.queue): - self.writeToServer("{}. {}".format(i+1, scen)) - - return (CmdResult.Success, "") - - - def list(self, prm=None, user=""): - """Zeigt die Szenarioliste an.""" - self.writeToServer("Szenarienlist:\n-------------") - for scen in self.scenlist: - self.writeToServer(scen + ("(Liga)" if scen in self.league_scenlist else "")) - - return (CmdResult.Success, "") - - def ircCommands(self, prm=None, user=""): - """Enthält Befehle zur Steuerung der IRC-Funktionen.""" - if not prm: - return (CmdResult.SyntaxFail, "") - - if prm[0] == "ingamechat": - if prm[1] == "off": - self.ingamechat = "deaktiviert" - elif prm[1] == "on": - self.ingamechat = "aktiviert" - - return (CmdResult.Success, "") - - # - # Methods - # + def checkForCommands(self, msg, reply) -> None: + part = self.isCommand(msg) + if part and part[1] != self.irc.nick: + cmd = part[2].split(" ", 1) + found = False + if len(cmd) > 0: + key = cmd[0].strip() + + if signal(f"cmd-{key}").receivers: + found = True + + msg = [reply] + msg.extend(cmd[1].strip().split(" ") if len(cmd) > 1 else []) + + signal(f"cmd-{key}").send(msg) + + if not found: + reply('Unbekannter Befehl: "' + part[2] + '"!') def isMessage(self, msg): - return re.match(self.config["RegExps"]["Message"].format(prefix=self.config["General"]["Prefix"]), msg) - - def getMessageNick(self, msg): - m = self.isMessage(msg) - if m: - return m.group(1) + if self.isCommand(msg): + return + + return re.match(self.config["RegExps"]["Message"], msg) - def addCommand(self, function, text): - self.commands[text.split(" ")[0]] = function - return self + def isCommand(self, msg): + return re.match(self.config["RegExps"]["Command"].format(prefix=self.config["General"]["Prefix"]), msg) def addScenario(self, link): name = "" @@ -559,43 +408,27 @@ Snapshot=openclonk-snapshot-(.*)-(.*)-{}-{}-.""" self.scenlist.append(name) return self - def writeToServer(self, text=None) -> tuple: - if text == None and self.clonk == None: - return (CmdResult.RuntimeError, "") - elif type(text) != str: - try: - text = text.decode(self.config["Clonk"]["Encoding"]) - except: - raise IOError("Cannot write anything else to the server except the following data types: bytes, str (got {})".format(type(text).__name__)) + def writeToServer(self, text : str) -> None: + if not self.clonk: + return - if self.clonk and self.clonk.stdin: - self.clonk.stdin.write(text + "\n") + elif self.clonk.stdin: + self.clonk.stdin.write(f"{text}\n") self.clonk.stdin.flush() - return (CmdResult.Success, "") def setTopic(self, text=None) -> None: if not self.irc: return - if type(text) != str: - try: - text = text.decode(self.config["Clonk"]["Encoding"]) - except: - raise TypeError("bytes or str expected, got {}".format(type(text).__name__)) - if self.topic != text: self.topic = text channel = self.config["Channels"]["Ingame"] - if not channel.startswith("#"): - raise ValueError("Invalid channel: {} (ident: {})".format(channel, channel == "#clonk-SGGP-ingame")) - self.irc.sendMsg(ircmsgs.topic(self.config["Channels"]["Ingame"], text)) + self.irc.writeln(f"TOPIC {self.config['Channels']['Ingame']} :{text}") def shutdown(self): if self.shutdowned: return - self.log.info("Shutting down...") - del self.updater if self.clonk and self.clonk.stdin and not self.clonk.stdin.closed: @@ -603,7 +436,7 @@ Snapshot=openclonk-snapshot-(.*)-(.*)-{}-{}-.""" with open(self.config_path, "w") as fobj: self.config.write(fobj) - self.log.debug("Config file saved.") + print("Config file saved.") with open(os.path.join(self.path, "scenarios.lst"), "w") as fobj: fobj.writelines(self.scenlist) @@ -611,106 +444,125 @@ Snapshot=openclonk-snapshot-(.*)-(.*)-{}-{}-.""" with open(os.path.join(self.path, "scenarios_league.lst"), "w") as fobj: fobj.writelines(self.league_scenlist) - self.log.debug("Scenario lists saved.") + print("Scenario lists saved.") self.setTopic("Kein laufendes Spiel.") - logging.shutdown() self.shutdowned = True - -class PyCRCtrlIRC(object): - parent = PyCRCtrl = None - irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - encoding = "utf-8" - - def setParent(parent): - self.parent = parent - self.encoding = parent.config["Clonk"]["Encoding"] - - def sendMsg(self, msg : bytes): - if type(msg) != bytes: - msg = str(msg).encode(self.encoding, "replace") - try: - self.irc.send(msg + b"\r") - except (socket.error, socket.timeout, OSError): - self.reconnect() - self.sendMsg(msg) - - def connect(self): - try: - self.irc.connect((self.parent.config["IRC"]["Server"], self.parent.config["IRC"]["Port"])) - except (socket.error, socket.timeout, OSError): - self.reconnect() - return - - nick = self.parent.config["IRC"]["Nick"] - self.sendMsg(ircmsgs.msg("USER", "{nick} {nick} {nick} :PyCRCtrl".format(nick=nick), self.encoding)) - self.sendMsg(ircmsgs.msg("NICK", "{}".format(nick), self.encoding)) # security - - try: - self.irc.sendMsg(ircmsgs.msg("PASSWORD", "{}".format(self.parent.config["IRC"]["Password"]), self.encoding)) - except KeyError: - pass - - self.parent.log.info("Successfully connected to {}".format(self.parent.config["IRC"]["Server"])) - - start_new_thread(self.checkForMessages, ()) - def reconnect(self): - sleep(10) - start_new_thread(self.connect, ()) - try: - self.irc.close() - except Exception: - pass - del self.irc - self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - start_new_thread(self.connect, ()) - - def checkForMessages(self): - while True: - try: - text = self.irc.recv(512).decode(self.encoding, "replace") - except (socket.error, socket.timeout, OSError): - self.reconnect() - return - - x = re.match(r"^(?:[:](\S+) )?(\S+)(?: (?!:)(.+?))?(?: [:](.+))?$") - if not x: - continue - - nick, command, channel, msg = x.group(1), x.group(2), x.group(3), x.group(4) - - if command == "PING": - self.sendMsg(ircmsgs.msg("PONG", msg)) - - elif command == "PRIVMSG": - y = re.match(r"{}(\w+) (.*)".format(self.parent.config["General"]["Prefix"])) - if not y: - continue - - func, args = y.group(1), y.group(2) - if func in self.parent.commands: - self.parent.log.info("{} called by {}".format(func, nick)) - try: - self.sendMsg(ircmsgs.privmsg(self.parent.commands[func](args))) - except Exception as e: - self.parent.log.exception(e.args[0]) + def on(self, e): + def process(f): + signal(e).connect(f) + return f + return process -if __name__ == "__main__": - ircmsgs = _ircmsgs - - irc = PyCRCtrlIRC() - ctrl = PyCRCtrl(irc, sys.argv[1]) - ctrl.addCommand(ctrl.start, "start") - ctrl.addCommand(ctrl.stop, "stop") - ctrl.addCommand(ctrl.host, "host") - ctrl.addCommand(ctrl.displayQueue, "queue") - ctrl.addCommand(ctrl.help, "help") - ctrl.addCommand(ctrl.list, "list") - ctrl.addCommand(ctrl.ircCommands, "irc") - irc.setParent(ctrl) - irc.connect() - while True: - try: - sleep(600) - except KeyboardInterrupt: - sys.exit(0) +# Usage: ./pycrctrl.py path config +server = PyCRCtrl(sys.argv[1], sys.argv[2]) + +@server.irc.on("message") +def on_message(message, user, target, text): + channel = server.config["Channels"]["Ingame"] + if target == channel and server.ingamechat == "aktiviert" and user.nick != server.config["IRC"]["Nick"]: + server.writeToServer(f"[IRC]<{user.nick}> {text}") + +@server.irc.on("addressed") +def on_addressed(message, user, target, text): + if target == server.config["Channels"]["Ingame"] and server.ingamechat == "aktiviert": + return + + def reply(text): + server.irc.say(target, text) + + try: + server.checkForCommands(f"<{user}> {server.config['General']['Prefix']}{text}", reply=reply) + except Exception as e: + traceback.print_exc() + +@server.on("cmd-start") +def start(args): + server.stopped = False + try: + time = int(args[1]) + except Exception: + time = 5 + args[0](f"/start {time}") + +@server.on("cmd-stop") +def stop(args): + def stopping(): + while server.clonk and server.stopped: + server.writeToServer("/start 60000") + sleep(100) + + if server.stopped == False: + server.stopped = True + threading.Thread(target=stopping).start() + +@server.on("cmd-queue") +def queue(args): + reply = args[0] + reply("Warteschlange:") + for i, scen in enumerate(server.queue.queue, start=1): + reply(f"{i}. {scen}") + +@server.on("cmd-list") +def lst(args): + reply = args[0] + if reply != server.writeToServer: + reply("Die Szenarienliste kann nur ingame angesehen werden!") + return + + reply("Verfügbare Szenarien:") + for scen in server.scenlist: + reply(scen) + +@server.on("cmd-irc") +def cmd_irc(args): + reply = args[0] + if not args: + reply("Keine Parameter angegeben!") + + if args[1] == "ingamechat": + if len(args) <= 1 or args[2] == "off": + server.ingamechat = "deaktivert" + elif args[2] == "on": + server.ingamechat = "aktiviert" + +@server.on("cmd-host") +def host(args): + reply = args[0] + if len(args) <= 1: + reply("Bitte gib einen Szenarionamen an!") + scenario = args[1].strip() + + if scenario == "random": + scenario = random.choice(server.scenlist).strip() + + elif scenario not in server.scenlist: + reply(f"Szenario {scenario} nicht gefunden!") + + if not server.server_thread: + server.scenario = scenario + server.server_thread = threading.Thread(target=server.startClonk) + server.server_thread.start() + reply(f"Szenario {scenario} wird jetzt gehostet.") + + elif not server.queue.full(): + server.queue.put(scenario) + reply(f"Szenario {scenario} wurde der Warteschlange hinzugefügt.") + + else: + reply("Warteschlange ist voll!") + +@server.on("cmd-quit") +def quit(args): + reply = args[0] + if server.clonk and server.clonk.stdin: + server.clonk.stdin.close() + server.server_thread.terminate() + server.shutdown() + sleep(5) + raise SystemExit + +try: + asyncio.get_event_loop().run_forever() +except KeyboardInterrupt: + sys.exit(0) -- cgit v1.2.3-54-g00ecf