commit c109a0116b5b468f02a52827ba0655a423ffe6ea Author: dece Date: Thu Oct 8 18:46:45 2020 +0200 edmond: init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d344ba6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..b723d01 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..9a51a28 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,20 @@ +{ + "_meta": { + "hash": { + "sha256": "7e7ef69da7248742e869378f8421880cf8f0017f96d94d086813baa518a65489" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": {} +} diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..b9b6e36 --- /dev/null +++ b/config.json.example @@ -0,0 +1,6 @@ +{ + "host": "irc.freenode.net", + "port": 6667, + "nick": "edm0nd", + "channels": ["#idi0crates"] +} diff --git a/edmond/__init__.py b/edmond/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/edmond/__main__.py b/edmond/__main__.py new file mode 100644 index 0000000..342995a --- /dev/null +++ b/edmond/__main__.py @@ -0,0 +1,20 @@ +import argparse + +import edmond.bot +import edmond.config +import edmond.log + + +def main(): + argparser = argparse.ArgumentParser() + argparser.add_argument("-c", "--config", default="config.json") + args = argparser.parse_args() + + logger = edmond.log.get_logger(name="edmond") + config = edmond.config.load_config(args.config, logger=logger) + bot = edmond.bot.Bot(config, logger) + bot.run() + + +if __name__ == "__main__": + main() diff --git a/edmond/bot.py b/edmond/bot.py new file mode 100644 index 0000000..602ee7d --- /dev/null +++ b/edmond/bot.py @@ -0,0 +1,47 @@ +import irc.client +from irc.client import NickMask + +from edmond.log import Logger + + +class Bot(irc.client.SimpleIRCClient, Logger): + + def __init__(self, config, logger): + super().__init__() + self.config = config + self.logger = logger + + @property + def nick(self): + return self.config["nick"] + + def on_welcome(self, connection, event): + self.log_i(f"Connected to server {event.source}.") + for channel in self.config["channels"]: + connection.join(channel) + + def on_join(self, connection, event): + self.log_i(f"Joined {event.target}.") + + def on_part(self, connection, event): + self.log_i(f"Left {event.target} (args: {event.arguments[0]}).") + + def on_pubmsg(self, connection, event): + channel = event.target + nick = NickMask(event.source).nick + message = event.arguments[0] + self.log_d(f"Message in {channel} from {nick}: {message}") + + def on_privmsg(self, connection, event): + nick = NickMask(event.source).nick + target = event.target + message = event.arguments[0] + self.log_d(f"Private message from {nick} to {target}: {message}") + + def run(self): + self.log_i("Starting Edmond.") + self.connect(self.config["host"], self.config["port"], self.nick) + try: + self.start() + except KeyboardInterrupt: + self.log_i("Stopping Edmond.") diff --git a/edmond/config.py b/edmond/config.py new file mode 100644 index 0000000..a5aa93d --- /dev/null +++ b/edmond/config.py @@ -0,0 +1,9 @@ +import json + + +def load_config(config_path, logger=None): + try: + with open(config_path, "rt") as config_file: + return json.load(config_file) + except OSError as exc: + logger.critical(f"Could not load config file: {exc}") diff --git a/edmond/log.py b/edmond/log.py new file mode 100644 index 0000000..f4fcc4c --- /dev/null +++ b/edmond/log.py @@ -0,0 +1,170 @@ +"""A cross-platform, package independant, colored stream/file logger.""" + +import logging +import platform + +import ctypes +import ctypes.util + + +class _AnsiColorStreamHandler(logging.StreamHandler): + + DEFAULT = '\x1b[0m' + RED = '\x1b[31m' + GREEN = '\x1b[32m' + YELLOW = '\x1b[33m' + CYAN = '\x1b[36m' + + CRITICAL = RED + ERROR = RED + WARNING = YELLOW + INFO = GREEN + DEBUG = CYAN + + def __init__(self, stream=None): + super().__init__(stream) + + def format(self, record): + text = super().format(record) + color = self._get_color_code(record.levelno) + return color + text + self.DEFAULT + + @classmethod + def _get_color_code(cls, level): + if level >= logging.CRITICAL: + return cls.CRITICAL + elif level >= logging.ERROR: + return cls.ERROR + elif level >= logging.WARNING: + return cls.WARNING + elif level >= logging.INFO: + return cls.INFO + elif level >= logging.DEBUG: + return cls.DEBUG + else: + return cls.DEFAULT + + +# Disable protected member access warning for MSVC functions. +# pylint: disable=W0212 +class _WinColorStreamHandler(logging.StreamHandler): + + STD_INPUT_HANDLE = -10 + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + + FOREGROUND_BLACK = 0x0000 + FOREGROUND_BLUE = 0x0001 + FOREGROUND_GREEN = 0x0002 + FOREGROUND_CYAN = 0x0003 + FOREGROUND_RED = 0x0004 + FOREGROUND_MAGENTA = 0x0005 + FOREGROUND_YELLOW = 0x0006 + FOREGROUND_GREY = 0x0007 + FOREGROUND_INTENSITY = 0x0008 + FOREGROUND_WHITE = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + + BACKGROUND_BLACK = 0x0000 + BACKGROUND_BLUE = 0x0010 + BACKGROUND_GREEN = 0x0020 + BACKGROUND_CYAN = 0x0030 + BACKGROUND_RED = 0x0040 + BACKGROUND_MAGENTA = 0x0050 + BACKGROUND_YELLOW = 0x0060 + BACKGROUND_GREY = 0x0070 + BACKGROUND_INTENSITY = 0x0080 + + DEFAULT = FOREGROUND_WHITE + CRITICAL = FOREGROUND_RED | FOREGROUND_INTENSITY + ERROR = FOREGROUND_RED | FOREGROUND_INTENSITY + WARNING = FOREGROUND_YELLOW | FOREGROUND_INTENSITY + INFO = FOREGROUND_GREEN + DEBUG = FOREGROUND_CYAN + + def __init__(self, stream=None): + super().__init__(stream) + self.output_handle = self._get_output_handle(stream) + + @classmethod + def _get_output_handle(cls, stream): + if stream is None: + return ctypes.windll.kernel32.GetStdHandle(cls.STD_OUTPUT_HANDLE) + else: + msvcrt_loc = ctypes.util.find_msvcrt() + msvcrt_lib = ctypes.cdll.LoadLibrary(msvcrt_loc) + return msvcrt_lib._get_osfhandle(stream.fileno()) + + def emit(self, record): + color_code = self._get_color_code(record.levelno) + self._set_color_code(color_code) + super().emit(record) + self._set_color_code(self.FOREGROUND_WHITE) + + @classmethod + def _get_color_code(cls, level): + if level >= logging.CRITICAL: + return cls.CRITICAL + elif level >= logging.ERROR: + return cls.ERROR + elif level >= logging.WARNING: + return cls.WARNING + elif level >= logging.INFO: + return cls.INFO + elif level >= logging.DEBUG: + return cls.DEBUG + else: + return cls.DEFAULT + + def _set_color_code(self, code): + ctypes.windll.kernel32.SetConsoleTextAttribute(self.output_handle, code) + + +if platform.system() == "Windows": + ColorStreamHandler = _WinColorStreamHandler +else: + ColorStreamHandler = _AnsiColorStreamHandler + + +_LOG_LEVEL = logging.DEBUG +_FORMAT = "%(asctime)s %(levelname)-8s %(message)s" +_DATE_FORMAT = "%H:%M:%S" + + +def get_logger( name="pyshgck", level=_LOG_LEVEL + , log_format=_FORMAT, date_format=_DATE_FORMAT + , into_stderr=True, into_log_file=None ): + logger = logging.getLogger(name) + logger.setLevel(level) + formatter = logging.Formatter(fmt=log_format, datefmt=date_format) + + if into_stderr: + stream_handler = ColorStreamHandler() + stream_handler.setLevel(level) + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + if into_log_file is not None: + file_handler = logging.FileHandler(into_log_file, mode="w") + file_handler.setLevel(level) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + return logger + + +class Logger: + + def log_d(self, message): + self.logger.debug(message) + + def log_i(self, message): + self.logger.info(message) + + def log_w(self, message): + self.logger.warning(message) + + def log_e(self, message): + self.logger.error(message) + + def log_c(self, message): + self.logger.critical(message)