From b552c35baa4207445f4eede8920a7a9ac5848764 Mon Sep 17 00:00:00 2001 From: iou1name Date: Tue, 8 Oct 2019 12:39:13 -0400 Subject: [PATCH] refactored config --- .gitignore | 1 + README.md | 46 ----------------- bot.py | 50 ++++++++----------- config.py | 116 ------------------------------------------- config.template.py | 33 ++++++++++++ db.py | 8 +-- loader.py | 17 +++---- modules/bq.py | 2 +- modules/countdown.py | 2 +- modules/crypto.py | 3 +- modules/currency.py | 2 +- modules/movie.py | 5 +- modules/remind.py | 3 +- modules/sed.py | 2 +- modules/seen.py | 5 +- modules/tell.py | 5 +- modules/wolfram.py | 8 +-- run.py | 23 ++------- tools/time.py | 6 ++- trigger.py | 23 +++++---- 20 files changed, 107 insertions(+), 253 deletions(-) delete mode 100755 config.py create mode 100755 config.template.py diff --git a/.gitignore b/.gitignore index 5f68f7d..21c039a 100755 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ logs/ *.txt *.db-journal tourettes.py +config.py diff --git a/README.md b/README.md index 2d6a752..73cb25e 100755 --- a/README.md +++ b/README.md @@ -1,56 +1,10 @@ # Fulvia -## NIGGER DICKS 2: Electric Boogaloo It's like Sopel, except rewritten from scratch using Twisted as a base and over half the features ripped out. ## Requirements Python 3.6+ Python packages: `twisted python-dateutil requests bs4 wolframalpha pyenchant emoji Pillow xml2dict ipython numpy numpngw` -## Config -`nickname` - The nickname the bot will use. -`realname` - The realname the bot will use. -`username` - The user ident the bot will use. -`prefix` - The command prefix the bot will listen for. -`homedir` - The bot's home directory. Required for the bot to get certain pathing right. In the future that will be obtained automatically. -`server` - The server address to connect to. -`port` - The server port to connect to. SSL will probably not work. -`use_ssl` - Place holder. -`channels` - Which channels to join upon connection. -`db_filename` - Filename to use for the bot's database. -`owner` - The bot's owner. Use for permission purposes on restricted commands. Outranks admins. Should be the full hostmask of the user. -`admins` - Comma-delineated list of admins the bot will recognize for restricted commands. Should be the full hostmask for each one. -`default_time_format` - The format used for all timestamp operations. See the official python docs for the `time` library for more information. -`disabled_modules` - Comma-delineated list of modules *not* to load on startup. Modules should be specified without the `.py` extension. - -### Example default.cfg -``` -[core] -nickname = DiceBot9002 -realname = DiceBot9002 -username = DiceBot9002 -prefix = . -homedir = /home/iou1name/fulvia -server = irc.steelbea.me -port = 6667 -use_ssl = false -channels = #SomaIsGay,#test -db_filename = DiceBot9002.db -owner = iou1name!~iou1name@operational.operator -admins = -default_time_format = [%Y-%m-%d %H:%M:%S] -disabled_modules = countdown - -[wolfram] -app_id = API_KEY -units = nonmetric - -[movie] -tmdb_api_key = API_KEY - -[currency] -api_key = API_KEY -``` - ## TODO Fix the movie table Consider re-adding the following modules: `etymology, ip` diff --git a/bot.py b/bot.py index e154973..fff5255 100755 --- a/bot.py +++ b/bot.py @@ -5,7 +5,6 @@ The core bot class for Fulvia. import os import sys import time -import functools import threading import traceback from datetime import datetime @@ -15,35 +14,30 @@ from twisted.words.protocols import irc import db import tools +import config import loader from trigger import Trigger class Fulvia(irc.IRCClient): - def __init__(self, config): - self.config = config - """The bot's config, loaded from file.""" - - self.nickname = config.core.nickname - self.nick = config.core.nickname + def __init__(self): + self.nickname = config.nickname + self.nick = config.nickname """The bot's current nickname.""" - self.realname = config.core.realname + self.realname = config.realname """The bot's 'real name', used in whois.""" - self.username = config.core.username + self.username = config.username """The bot's username ident used for logging into the server.""" self.host = "" """The bot's host, virtual or otherwise. To be filled in later.""" - self.prefix = config.core.prefix - """The command prefix the bot watches for.""" - - self.static = os.path.join(config.homedir, "static") + self.static = "static" os.makedirs(self.static, exist_ok=True) """The path to the bot's static file directory.""" - self.log_path = os.path.join(config.homedir, "logs") + self.log_path = "logs" os.makedirs(self.static, exist_ok=True) """The path to the bot's log files.""" @@ -68,7 +62,7 @@ class Fulvia(irc.IRCClient): modules need it. """ - self.db = db.FulviaDB(self.config) + self.db = db.FulviaDB() """ A class with some basic interactions for the bot's sqlite3 databse. """ @@ -100,7 +94,7 @@ class Fulvia(irc.IRCClient): self._user_joined = [] """These get called when a user joins a channel.""" - self._disabled_modules = self.config.core.disabled_modules.split(",") + self._disabled_modules = config.disabled_modules """These modules will NOT be loaded when load_modules() is called.""" self.load_modules() @@ -117,7 +111,7 @@ class Fulvia(irc.IRCClient): self.url_callbacks = {} # ensure they're empty - modules = loader.find_modules(self.config.homedir) + modules = loader.find_modules() loaded = 0 failed = 0 for name, path in modules.items(): @@ -215,7 +209,7 @@ class Fulvia(irc.IRCClient): """ # TODO: use time module instead of datetime t = datetime.fromtimestamp(time.time()) - timestamp = t.strftime(self.config.core.default_time_format) + timestamp = t.strftime(config.default_time_format) self._log_dump[channel].append(timestamp + " " + text) if time.time() - self._last_log_dump > 1: @@ -248,9 +242,9 @@ class Fulvia(irc.IRCClient): self.log(channel, line) funcs = [] - if message.startswith(self.prefix) and message != self.prefix: + if message.startswith(config.prefix) and message != config.prefix: command = message.partition(" ")[0] - command = command.replace(self.prefix, "", 1) + command = command.replace(config.prefix, "", 1) cmd = self.commands.get(command) if not cmd: return @@ -259,7 +253,7 @@ class Fulvia(irc.IRCClient): funcs += self._hooks for func in funcs: - trigger = Trigger(user, channel, message, "PRIVMSG", self.config) + trigger = Trigger(user, channel, message, "PRIVMSG") bot = FulviaWrapper(self, trigger) if func.rate: @@ -352,8 +346,8 @@ class Fulvia(irc.IRCClient): def signedOn(self): """Called when the bot successfully connects to the server.""" - if self.config.core.oper_password: - self.sendLine("OPER " + self.config.core.nickname + ' ' + self.config.core.oper_password) + if config.oper_password: + self.sendLine("OPER " + config.nickname + ' ' + config.oper_password) print(f"Signed on as {self.nickname}") self.whois(self.nickname) @@ -361,7 +355,7 @@ class Fulvia(irc.IRCClient): line += f"{self.nickname}!{self.username}@{self.host}" self.log(self.hostname, line) - for channel in self.config.core.channels.split(","): + for channel in config.channels: self.join(channel) @@ -400,7 +394,7 @@ class Fulvia(irc.IRCClient): self.channels[channel].users[nick] = user for func in self._user_joined: - trigger = Trigger(user, channel, f"{user} has joined", "PRIVMSG", self.config) + trigger = Trigger(user, channel, f"{user} has joined", "PRIVMSG") bot = FulviaWrapper(self, trigger) t = threading.Thread(target=self.call,args=(func, bot, trigger)) t.start() @@ -667,8 +661,4 @@ class FulviaWrapper(): class FulviaFactory(protocol.ReconnectingClientFactory): - # black magic going on here - protocol = property(lambda s: functools.partial(Fulvia, s.config)) - - def __init__(self, config): - self.config = config + protocol = Fulvia diff --git a/config.py b/config.py deleted file mode 100755 index de9b27f..0000000 --- a/config.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -""" -For parsing and generating config files. -""" -from configparser import ConfigParser - -class Config(): - def __init__(self, filename): - """ - The bot's configuration. - - The given filename will be associated with the configuration, and is - the file which will be written if write() is called. If load is not - given or True, the configuration object will load the attributes from - the file at filename. - - A few default values will be set here if they are not defined in the - config file, or a config file is not loaded. They are documented below. - """ - self.filename = filename - """The config object's associated file, as noted above.""" - - self.parser = ConfigParser(allow_no_value=True, interpolation=None) - self.parser.read(self.filename) - - @property - def homedir(self): - """An alias to config.core.homedir""" - # Technically it's the other way around, so we can bootstrap filename - # attributes in the core section, but whatever. - configured = None - if self.parser.has_option('core', 'homedir'): - configured = self.parser.get('core', 'homedir') - if configured: - return configured - else: - return os.path.dirname(self.filename) - - def save(self): - """Save all changes to the config file.""" - with open(self.filename, 'w') as cfgfile: - self.parser.write(cfgfile) - - def add_section(self, name): - """ - Add a section to the config file. - - Returns ``False`` if already exists. - """ - try: - return self.parser.add_section(name) - except ConfigParser.DuplicateSectionError: - return False - - def __getattr__(self, name): - """Allows sections to be called like class attributes.""" - if name in self.parser.sections(): - items = self.parser.items(name) - section = ConfigSection(name, items, self) # Return a section - setattr(self, name, section) - return section - else: - raise AttributeError("%r object has no attribute %r" - % (type(self).__name__, name)) - - -class ConfigSection(object): - """ - Represents a section of the config file. - - Contains all keys in thesection as attributes. - """ - def __init__(self, name, items, parent): - object.__setattr__(self, '_name', name) - object.__setattr__(self, '_parent', parent) - for item in items: - value = item[1].strip() - if not value.lower() == 'none': - if value.lower() == 'false': - value = False - object.__setattr__(self, item[0], value) - - def __getattr__(self, name): - return None - - def __setattr__(self, name, value): - object.__setattr__(self, name, value) - if type(value) is list: - value = ','.join(value) - self._parent.parser.set(self._name, name, value) - - def get_list(self, name): - value = getattr(self, name) - if not value: - return [] - if isinstance(value, str): - value = value.split(',') - # Keep the split value, so we don't have to keep doing this - setattr(self, name, value) - return value - - -def readConfig(filename): - """ - Parses the provided filename and returns the config object. - """ - config = ConfigParser(allow_no_value=True, interpolation=None) - config.read(filename) - return config - - -def generateConfig(filename): - """ - Generates a blank config file with minimal defaults. - """ - pass diff --git a/config.template.py b/config.template.py new file mode 100755 index 0000000..98b90ca --- /dev/null +++ b/config.template.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +""" +The bot's config file. +""" + +""" Core """ +nickname = 'DiceBot9002' +realname = 'DiceBot9002' +username = 'DiceBot9002' +prefix = '.' +server = 'irc.steelbea.me' +port = 6667 +use_ssl = False +channels = ['#test', '#SomaIsGay'] +db_filename = 'DiceBot9002.db' +owner = 'iou1name!~iou1name@operational.operator' +admins = [] +default_time_format = '[%Y-%m-%d %H:%M:%S]' +disabled_modules = ['countdown'] +oper_password = 'password' + +""" Wolfram """ +wolfram_app_id = 'password' +wolfram_units = 'nonmetric' + +""" Movie """ +tmdb_api_key = 'password' + +""" Currency """ +exchangerate_api_key = 'password' + +""" Crypto """ +coinlib_api_key = 'password' diff --git a/db.py b/db.py index 0253edd..5266583 100755 --- a/db.py +++ b/db.py @@ -6,13 +6,15 @@ import os import sqlite3 import threading -class FulviaDB(object): +import config + +class FulviaDB: """ Defines a basic interface and some convenience functionsfor the bot's database. """ - def __init__(self, config): - path = config.core.db_filename + def __init__(self): + path = config.db_filename self.filename = path self.db_lock = threading.Lock() diff --git a/loader.py b/loader.py index 385911d..c46f9e7 100755 --- a/loader.py +++ b/loader.py @@ -14,7 +14,7 @@ def load_module(bot, path): module = importlib.import_module(path) if hasattr(module, 'setup'): module.setup(bot) - relevant_parts = process_module(module, bot.config) + relevant_parts = process_module(module) for part in relevant_parts: bot.register_callable(part) @@ -34,14 +34,13 @@ def unload_module(bot, name): del sys.modules[name] -def find_modules(homedir): +def find_modules(): """ - Searches through homedir/modules for python files and returns a dictionary + Searches through 'modules/' for python files and returns a dictionary with the module name as the key and the path as the value. """ - modules_dir = os.path.join(homedir, "modules") modules = {} - for file in os.listdir(modules_dir): + for file in os.listdir('modules'): if not file.endswith(".py"): continue name = file.replace(".py", "") @@ -49,7 +48,7 @@ def find_modules(homedir): return modules -def process_module(module, config): +def process_module(module): """ Takes a module object and extracts relevant data objects out of it. Returns all callables(read: functions) and shutdowns(?). @@ -58,7 +57,7 @@ def process_module(module, config): for key, obj in dict.items(vars(module)): if callable(obj): if is_triggerable(obj): - process_callable(obj, config) + process_callable(obj) callables.append(obj) return callables @@ -72,12 +71,10 @@ def is_triggerable(obj): return any(hasattr(obj, attr) for attr in triggerable_attributes) -def process_callable(func, config): +def process_callable(func): """ Sets various helper atributes about a given function. """ - prefix = config.core.prefix - func.thread = getattr(func, "thread", True) func.hook = getattr(func, "hook", False) func.rate = getattr(func, "rate", 0) diff --git a/modules/bq.py b/modules/bq.py index 08cc075..75dac23 100755 --- a/modules/bq.py +++ b/modules/bq.py @@ -15,6 +15,6 @@ def BQstatus(bot, trigger): status = "\x0304DEAD" deathdate = "[2017-02-16 00:19:00]" msg = "Banished Quest status: " + status + "\nTime since death: " - msg += relativeTime(bot.config, datetime.now(), deathdate) + " ago " + msg += relativeTime(datetime.now(), deathdate) + " ago " msg += deathdate bot.msg(msg) diff --git a/modules/countdown.py b/modules/countdown.py index 5eb200a..f1f8745 100755 --- a/modules/countdown.py +++ b/modules/countdown.py @@ -27,6 +27,6 @@ def generic_countdown(bot, trigger): except: return bot.msg("Please use correct format: .countdown 2012 12 21") - msg = relativeTime(bot.config, datetime.now(), date) + msg = relativeTime(datetime.now(), date) msg += " until " + trigger.group(2) bot.msg(msg) diff --git a/modules/crypto.py b/modules/crypto.py index 43a2d00..0f12347 100644 --- a/modules/crypto.py +++ b/modules/crypto.py @@ -6,6 +6,7 @@ import re import requests +import config from module import commands, example, require_admin URI = "https://coinlib.io/api/v1" @@ -19,7 +20,7 @@ def crypto(bot, trigger): Queries coinlib.io for information about various crytocurrencies. """ params = { - "key": bot.config.crypto.api_key, + "key": config.coinlib_api_key, "pref": "USD", } symbol = trigger.group(3) diff --git a/modules/currency.py b/modules/currency.py index cba9c30..92e8585 100755 --- a/modules/currency.py +++ b/modules/currency.py @@ -33,7 +33,7 @@ def exchange(bot, trigger): cur_to = cur_to.upper() cur_from = cur_from.upper() - api_key = bot.config.currency.api_key + api_key = config.exchangerate_api_key url = CUR_URI.format(**{"API_KEY": api_key, "CUR_FROM": cur_from}) res = requests.get(url, verify=True) res.raise_for_status() diff --git a/modules/movie.py b/modules/movie.py index 034ce93..143cb6d 100755 --- a/modules/movie.py +++ b/modules/movie.py @@ -11,6 +11,7 @@ from sqlite3 import IntegrityError, OperationalError import bs4 import requests +import config from module import commands, example, require_admin def setup(bot): @@ -50,7 +51,7 @@ def movieInfo(bot, trigger): return bot.reply("What movie?") word = word.replace(" ", "+") - api_key = bot.config.movie.tmdb_api_key + api_key = config.tmdb_api_key uri = "https://api.themoviedb.org/3/search/movie?" + \ f"api_key={api_key}&query={word}" data = requests.get(uri, timeout=30, verify=True).json() @@ -151,7 +152,7 @@ def pickMovie(bot, trigger): bot.reply(movie[0]) if trigger.group(2) == "-m": - trigger.set_group(f".movie {movie}", bot.config) + trigger.set_group(f".movie {movie}") movieInfo(bot, trigger) diff --git a/modules/remind.py b/modules/remind.py index f8b47e3..cb90394 100755 --- a/modules/remind.py +++ b/modules/remind.py @@ -10,6 +10,7 @@ import datetime import threading import collections +import config from module import commands, example class MonitorThread(threading.Thread): @@ -213,7 +214,7 @@ def create_reminder(bot, trigger, duration, message): if duration >= 60: remind_at = datetime.datetime.fromtimestamp(t) - t_format = bot.config.core.default_time_format + t_format = config.default_time_format timef = datetime.datetime.strftime(remind_at, t_format) bot.reply('Okay, will remind at %s' % timef) diff --git a/modules/sed.py b/modules/sed.py index 6a7f8dc..7e7ed6a 100755 --- a/modules/sed.py +++ b/modules/sed.py @@ -73,7 +73,7 @@ def findandreplace(bot, trigger): if not group: return g = (trigger.group(0),) + group.groups() - trigger.set_group(g, bot.config) + trigger.set_group(g) # Correcting other person vs self. rnick = (trigger.group(1) or trigger.nick) diff --git a/modules/seen.py b/modules/seen.py index 1467c45..2b61612 100755 --- a/modules/seen.py +++ b/modules/seen.py @@ -11,6 +11,7 @@ from sqlite3 import OperationalError from requests.structures import CaseInsensitiveDict +import config from tools.time import relativeTime from module import commands, example, hook, require_chanmsg, rate @@ -87,9 +88,9 @@ def seen(bot, trigger): return bot.msg(f"I haven't seen \x0308{args.nick}") timestamp = datetime.fromtimestamp(timestamp) - t_format = bot.config.core.default_time_format + t_format = config.default_time_format timestamp = datetime.strftime(timestamp, t_format) - reltime = relativeTime(bot.config, datetime.now(), timestamp) + reltime = relativeTime(datetime.now(), timestamp) if args.first: msg = "First" diff --git a/modules/tell.py b/modules/tell.py index c55fddc..b2f26c4 100755 --- a/modules/tell.py +++ b/modules/tell.py @@ -9,6 +9,7 @@ import threading from datetime import datetime from sqlite3 import OperationalError +import config from tools.time import relativeTime from module import commands, example, hook @@ -107,8 +108,8 @@ def tell_hook(bot, trigger): teller, unixtime, message = tell telldate = datetime.fromtimestamp(unixtime) - reltime = relativeTime(bot.config, datetime.now(), telldate) - t_format = bot.config.core.default_time_format + reltime = relativeTime(datetime.now(), telldate) + t_format = config.default_time_format telldate = datetime.strftime(telldate, t_format) msg = f"{tellee}: \x0310{message}\x03 (\x0308{teller}\x03) {telldate}" \ diff --git a/modules/wolfram.py b/modules/wolfram.py index adca29c..7ff1a8e 100755 --- a/modules/wolfram.py +++ b/modules/wolfram.py @@ -4,9 +4,9 @@ Querying Wolfram Alpha. """ import wolframalpha +import config from module import commands, example - @commands('wa', 'wolfram') @example('.wa 2+2', '[W|A] 2+2 = 4') @example(".wa python language release date", @@ -17,11 +17,11 @@ def wa_command(bot, trigger): """ if not trigger.group(2): return bot.reply("You must provide a query.") - if not bot.config.wolfram.app_id: + if not config.wolfram_app_id: bot.reply("Wolfram|Alpha API app ID not configured.") query = trigger.group(2).strip() - app_id = bot.config.wolfram.app_id - units = bot.config.wolfram.units + app_id = config.wolfram_app_id + units = config.wolfram_units res = wa_query(query, app_id, units) diff --git a/run.py b/run.py index da845ee..5da134f 100755 --- a/run.py +++ b/run.py @@ -6,26 +6,11 @@ import os from twisted.internet import reactor +import config from bot import FulviaFactory -from config import Config if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser( - description="Fulvia IRC Bot") - parser.add_argument( - "-c", - "--config", - help="Use a specific config file.") - args = parser.parse_args() - - if not args.config: - args.config = "default.cfg" - - config = Config(args.config) - - server = config.core.server - port = int(config.core.port) - reactor.connectTCP(server, port, FulviaFactory(config)) + server = config.server + port = config.port + reactor.connectTCP(server, port, FulviaFactory()) reactor.run() diff --git a/tools/time.py b/tools/time.py index ba6f8bb..8d4ca68 100755 --- a/tools/time.py +++ b/tools/time.py @@ -6,15 +6,17 @@ from datetime import datetime from dateutil.relativedelta import relativedelta +import config -def relativeTime(config, time_1, time_2): + +def relativeTime(time_1, time_2): """ Returns the relative time difference between 'time_1' and 'time_2'. If either 'time_1' or 'time_2' is a string, it will be converted to a datetime object according to the 'default_time_format' variable in the config. """ - t_format = config.core.default_time_format + t_format = config.default_time_format if type(time_1) == str: time_1 = datetime.strptime(time_1, t_format) if type(time_2) == str: diff --git a/trigger.py b/trigger.py index 05e94f3..744b099 100755 --- a/trigger.py +++ b/trigger.py @@ -4,6 +4,8 @@ The trigger abstraction layer. """ import datetime +import config + def split_user(user): """ Splits a user hostmask into !@ @@ -21,14 +23,14 @@ class Group(list): Custom list class that permits calling it like a function so as to emulate a re.group instance. """ - def __init__(self, message, config): + def __init__(self, message): """ Initializes the group class. If 'message' is a string, we split it into groups according to the usual trigger.group structure. Otherwise we assume it's already been split appropriately. """ if type(message) == str: - message = self.split_group(message, config) + message = self.split_group(message) list.__init__(self, message) @@ -45,7 +47,7 @@ class Group(list): return item - def split_group(self, message, config): + def split_group(self, message): """ Splits the message by spaces. group(0) is always the entire message. @@ -54,11 +56,10 @@ class Group(list): group(2) is always the entire message after the first word. group(3+) is always every individual word after the first word. """ - prefix = config.core.prefix group = [] group.append(message) words = message.split() - group.append(words[0].replace(prefix, "", 1)) + group.append(words[0].replace(config.prefix, "", 1)) group.append(" ".join(words[1:])) group += words[1:] @@ -66,7 +67,7 @@ class Group(list): class Trigger(): - def __init__(self, user, channel, message, event, config): + def __init__(self, user, channel, message, event): self.channel = channel """ The channel from which the message was sent. @@ -112,7 +113,7 @@ class Trigger(): message. """ - self.group = Group(message, config) + self.group = Group(message) """The ``group`` function of the ``match`` attribute. See Python :mod:`re` documentation for details.""" @@ -125,14 +126,14 @@ class Trigger(): ``('#example', '-m')`` """ - admins = config.core.admins.split(",") + [config.core.owner] + admins = config.admins + [config.owner] self.admin = any([self.compare_hostmask(admin) for admin in admins]) """ True if the nick which triggered the command is one of the bot's admins. """ - self.owner = self.compare_hostmask(config.core.owner) + self.owner = self.compare_hostmask(config.owner) """True if the nick which triggered the command is the bot's owner.""" @@ -147,9 +148,9 @@ class Trigger(): return compare_against == "@".join(self.nick, self.host) - def set_group(self, line, config): + def set_group(self, line): """ Allows a you to easily change the current group to a new Group instance. """ - self.group = Group(line, config) + self.group = Group(line)