From ecb9d7cb3f4435457560e03201bbed57a469d548 Mon Sep 17 00:00:00 2001 From: jesopo Date: Mon, 24 Sep 2018 15:13:27 +0100 Subject: Move most code in root directory to src/ --- Config.py | 17 -- Database.py | 370 ---------------------------- EventManager.py | 226 ----------------- Exports.py | 44 ---- IRCBot.py | 200 --------------- IRCBuffer.py | 48 ---- IRCChannel.py | 132 ---------- IRCLineHandler.py | 593 --------------------------------------------- IRCServer.py | 402 ------------------------------ IRCUser.py | 62 ----- Logging.py | 48 ---- ModuleManager.py | 147 ----------- Timer.py | 39 --- Utils.py | 287 ---------------------- modules/8ball.py | 2 +- modules/accept_invite.py | 2 +- modules/auto_mode.py | 2 +- modules/bitcoin.py | 2 +- modules/books.py | 2 +- modules/channel_op.py | 2 +- modules/check_urls.py | 2 +- modules/coins.py | 2 +- modules/commands.py | 2 +- modules/ctcp.py | 2 +- modules/define.py | 2 +- modules/dice.py | 2 +- modules/ducks.py | 4 +- modules/eval.py | 2 +- modules/geoip.py | 2 +- modules/google.py | 2 +- modules/haveibeenpwned.py | 2 +- modules/imdb.py | 2 +- modules/in.py | 2 +- modules/isgd.py | 2 +- modules/karma.py | 2 +- modules/lastfm.py | 2 +- modules/modules.py | 2 +- modules/nickserv.py | 2 +- modules/nr.py | 2 +- modules/perform.py | 2 +- modules/pong.py | 2 +- modules/print_activity.py | 2 +- modules/sed.py | 2 +- modules/seen.py | 2 +- modules/shakespeare.py | 2 +- modules/signals.py | 2 +- modules/soundcloud.py | 2 +- modules/spotify.py | 2 +- modules/stats.py | 2 +- modules/tfl.py | 2 +- modules/thesaurus.py | 2 +- modules/title.py | 2 +- modules/to.py | 2 +- modules/trakt.py | 2 +- modules/translate.py | 2 +- modules/tweets.py | 2 +- modules/upc.py | 2 +- modules/urbandictionary.py | 2 +- modules/weather.py | 2 +- modules/wikipedia.py | 2 +- modules/wolframalpha.py | 2 +- modules/words.py | 2 +- modules/youtube.py | 2 +- src/Config.py | 16 ++ src/Database.py | 369 ++++++++++++++++++++++++++++ src/EventManager.py | 226 +++++++++++++++++ src/Exports.py | 44 ++++ src/IRCBot.py | 197 +++++++++++++++ src/IRCBuffer.py | 48 ++++ src/IRCChannel.py | 132 ++++++++++ src/IRCLineHandler.py | 593 +++++++++++++++++++++++++++++++++++++++++++++ src/IRCServer.py | 402 ++++++++++++++++++++++++++++++ src/IRCUser.py | 62 +++++ src/Logging.py | 48 ++++ src/ModuleManager.py | 147 +++++++++++ src/Timer.py | 39 +++ src/Utils.py | 287 ++++++++++++++++++++++ start.py | 26 +- 78 files changed, 2677 insertions(+), 2674 deletions(-) delete mode 100644 Config.py delete mode 100644 Database.py delete mode 100644 EventManager.py delete mode 100644 Exports.py delete mode 100644 IRCBot.py delete mode 100644 IRCBuffer.py delete mode 100644 IRCChannel.py delete mode 100644 IRCLineHandler.py delete mode 100644 IRCServer.py delete mode 100644 IRCUser.py delete mode 100644 Logging.py delete mode 100644 ModuleManager.py delete mode 100644 Timer.py delete mode 100644 Utils.py create mode 100644 src/Config.py create mode 100644 src/Database.py create mode 100644 src/EventManager.py create mode 100644 src/Exports.py create mode 100644 src/IRCBot.py create mode 100644 src/IRCBuffer.py create mode 100644 src/IRCChannel.py create mode 100644 src/IRCLineHandler.py create mode 100644 src/IRCServer.py create mode 100644 src/IRCUser.py create mode 100644 src/Logging.py create mode 100644 src/ModuleManager.py create mode 100644 src/Timer.py create mode 100644 src/Utils.py diff --git a/Config.py b/Config.py deleted file mode 100644 index 894c9eb0..00000000 --- a/Config.py +++ /dev/null @@ -1,17 +0,0 @@ -import configparser, os - -class Config(object): - def __init__(self, bot, location="bot.conf"): - self.bot = bot - self.location = location - self.full_location = os.path.join(bot.bot_directory, - self.location) - self.bot.config = {} - self.load_config() - - def load_config(self): - if os.path.isfile(self.full_location): - with open(self.full_location) as config_file: - parser = configparser.ConfigParser() - parser.read_string(config_file.read()) - return dict(parser["bot"].items()) diff --git a/Database.py b/Database.py deleted file mode 100644 index 1e186025..00000000 --- a/Database.py +++ /dev/null @@ -1,370 +0,0 @@ -import json, os, sqlite3, threading, time - -class Table(object): - def __init__(self, database): - self.database = database - -class Servers(Table): - def add(self, alias, hostname, port, password, ipv4, tls, nickname, - username=None, realname=None): - username = username or nickname - realname = realname or nickname - self.database.execute( - """INSERT INTO servers (alias, hostname, port, password, ipv4, - tls, nickname, username, realname) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ?)""", - [hostname, port, password, ipv4, tls, nickname, username, realname]) - def get_all(self): - return self.database.execute_fetchall( - "SELECT server_id, alias FROM servers") - def get(self, id): - return self.database.execute_fetchone( - """SELECT server_id, alias, hostname, port, password, ipv4, - tls, nickname, username, realname FROM servers WHERE - server_id=?""", - [id]) - -class Channels(Table): - def add(self, server_id, name): - self.database.execute("""INSERT OR IGNORE INTO channels - (server_id, name) VALUES (?, ?)""", - [server_id, name.lower()]) - def delete(self, channel_id): - self.database.execute("DELETE FROM channels WHERE channel_id=?", - [channel_id]) - def get_id(self, server_id, name): - value = self.database.execute_fetchone("""SELECT channel_id FROM - channels WHERE server_id=? AND name=?""", - [server_id, name.lower()]) - return value if value == None else value[0] - -class Users(Table): - def add(self, server_id, nickname): - self.database.execute("""INSERT OR IGNORE INTO users - (server_id, nickname) VALUES (?, ?)""", - [server_id, nickname.lower()]) - def delete(self, user_id): - self.database.execute("DELETE FROM users WHERE user_id=?", - [user_id]) - def get_id(self, server_id, nickname): - value = self.database.execute_fetchone("""SELECT user_id FROM - users WHERE server_id=? and nickname=?""", - [server_id, nickname.lower()]) - return value if value == None else value[0] - -class BotSettings(Table): - def set(self, setting, value): - self.database.execute( - "INSERT OR REPLACE INTO bot_settings VALUES (?, ?)", - [setting.lower(), json.dumps(value)]) - def get(self, setting, default=None): - value = self.database.execute_fetchone( - "SELECT value FROM bot_settings WHERE setting=?", - [setting.lower()]) - if value: - return json.loads(value[0]) - return default - def find(self, pattern, default=[]): - values = self.database.execute_fetchall( - "SELECT setting, value FROM bot_settings WHERE setting LIKE ?", - [pattern.lower()]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default - def find_prefix(self, prefix, default=[]): - return self.find_bot_settings("%s%" % prefix, default) - def delete(self, setting): - self.database.execute( - "DELETE FROM bot_settings WHERE setting=?", - [setting.lower()]) - -class ServerSettings(Table): - def set(self, server_id, setting, value): - self.database.execute( - "INSERT OR REPLACE INTO server_settings VALUES (?, ?, ?)", - [server_id, setting.lower(), json.dumps(value)]) - def get(self, server_id, setting, default=None): - value = self.database.execute_fetchone( - """SELECT value FROM server_settings WHERE - server_id=? AND setting=?""", - [server_id,setting.lower()]) - if value: - return json.loads(value[0]) - return default - def find(self, server_id, pattern, default=[]): - values = self.database.execute_fetchall( - """SELECT setting, value FROM server_settings WHERE - server_id=? AND setting LIKE ?""", - [server_id, pattern.lower()]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default - def find_prefix(self, server_id, prefix, default=[]): - return self.find_server_settings(server_id, "%s%" % prefix, default) - def delete(self, server_id, setting): - self.database.execute( - "DELETE FROM server_settings WHERE server_id=? AND setting=?", - [server_id, setting.lower()]) - -class ChannelSettings(Table): - def set(self, channel_id, setting, value): - self.database.execute( - "INSERT OR REPLACE INTO channel_settings VALUES (?, ?, ?)", - [channel_id, setting.lower(), json.dumps(value)]) - def get(self, channel_id, setting, default=None): - value = self.database.execute_fetchone( - """SELECT value FROM channel_settings WHERE - channel_id=? AND setting=?""", [channel_id, setting.lower()]) - if value: - return json.loads(value[0]) - return default - def find(self, channel_id, pattern, default=[]): - values = self.database.execute_fetchall( - """SELECT setting, value FROM channel_settings WHERE - channel_id=? setting LIKE '?'""", [channel_id, pattern.lower()]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default - def find_prefix(self, channel_id, prefix, default=[]): - return self.find_channel_settings(channel_id, "%s%" % prefix, - default) - def delete(self, channel_id, setting): - self.database.execute( - """DELETE FROM channel_settings WHERE channel_id=? - AND setting=?""", [channel_id, setting.lower()]) - -class UserSettings(Table): - def set(self, user_id, setting, value): - self.database.execute( - "INSERT OR REPLACE INTO user_settings VALUES (?, ?, ?)", - [user_id, setting.lower(), json.dumps(value)]) - def get(self, user_id, setting, default=None): - value = self.database.execute_fetchone( - """SELECT value FROM user_settings WHERE - user_id=? and setting=?""", [user_id, setting.lower()]) - if value: - return json.loads(value[0]) - return default - def find_all_by_setting(self, server_id, setting, default=[]): - values = self.database.execute_fetchall( - """SELECT users.nickname, user_settings.value FROM - user_settings INNER JOIN users ON - user_settings.user_id=users.user_id WHERE - users.server_id=? AND user_settings.setting=?""", - [server_id, setting]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default - def find(self, user_id, pattern, default=[]): - values = self.database.execute( - """SELECT setting, value FROM user_settings WHERE - user_id=? AND setting LIKE '?'""", [user_id, pattern.lower()]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default - def find_prefix(self, user_id, prefix, default=[]): - return self.find_user_settings(user_id, "%s%" % prefix, default) - def delete(self, user_id, setting): - self.database.execute( - """DELETE FROM user_settings WHERE - user_id=? AND setting=?""", [user_id, setting.lower()]) - -class UserChannelSettings(Table): - def set(self, user_id, channel_id, setting, value): - self.database.execute( - """INSERT OR REPLACE INTO user_channel_settings VALUES - (?, ?, ?, ?)""", - [user_id, channel_id, setting.lower(), json.dumps(value)]) - def get(self, user_id, channel_id, setting, default=None): - value = self.database.execute_fetchone( - """SELECT value FROM user_channel_settings WHERE - user_id=? AND channel_id=? AND setting=?""", - [user_id, channel_id, setting.lower()]) - if value: - return json.loads(value[0]) - return default - def find(self, user_id, channel_id, pattern, default=[]): - values = self.database.execute_fetchall( - """SELECT setting, value FROM user_channel_settings WHERE - user_id=? AND channel_id=? AND setting LIKE '?'""", - [user_id, channel_id, pattern.lower()]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default - def find_prefix(self, user_id, channel_id, prefix, default=[]): - return self.find_user_settings(user_id, channel_id, "%s%" % prefix, - default) - def find_by_setting(self, user_id, setting, default=[]): - values = self.database.execute_fetchall( - """SELECT channels.name, user_channel_settings.value FROM - user_channel_settings INNER JOIN channels ON - user_channel_settings.channel_id=channels.channel_id - WHERE user_channel_settings.setting=? - AND user_channel_settings.user_id=?""", [setting, user_id]) - if values: - for i, value in enumerate(values): - values[i] = value[0], json.loads(value[1]) - return values - return default - def find_all_by_setting(self, server_id, setting, default=[]): - values = self.database.execute_fetchall( - """SELECT channels.name, users.nickname, - user_channel_settings.value FROM - user_channel_settings INNER JOIN channels ON - user_channel_settings.channel_id=channels.channel_id - INNER JOIN users on user_channel_settings.user_id=users.user_id - WHERE user_channel_settings.setting=? AND - users.server_id=?""", [setting, server_id]) - if values: - for i, value in enumerate(values): - values[i] = value[0], value[1], json.loads(value[2]) - return values - return default - def delete(self, user_id, channel_id, setting): - self.database.execute( - """DELETE FROM user_channel_settings WHERE - user_id=? AND channel_id=? AND setting=?""", - [user_id, channel_id, setting.lower()]) - -class Database(object): - def __init__(self, bot, location="bot.db"): - self.bot = bot - self.location = location - self.full_location = os.path.join(bot.bot_directory, - self.location) - self.database = sqlite3.connect(self.full_location, - check_same_thread=False, isolation_level=None) - self.database.execute("PRAGMA foreign_keys = ON") - self._cursor = None - - self.make_servers_table() - self.make_channels_table() - self.make_users_table() - self.make_bot_settings_table() - self.make_server_settings_table() - self.make_channel_settings_table() - self.make_user_settings_table() - self.make_user_channel_settings_table() - - self.servers = Servers(self) - self.channels = Channels(self) - self.users = Users(self) - self.bot_settings = BotSettings(self) - self.server_settings = ServerSettings(self) - self.channel_settings = ChannelSettings(self) - self.user_settings = UserSettings(self) - self.user_channel_settings = UserChannelSettings(self) - - def cursor(self): - if self._cursor == None: - self._cursor = self.database.cursor() - return self._cursor - - def _execute_fetch(self, query, fetch_func, params=[]): - printable_query = " ".join(query.split()) - self.bot.log.debug("executing query: \"%s\" (params: %s)", - [printable_query, params]) - start = time.monotonic() - - cursor = self.cursor() - cursor.execute(query, params) - value = fetch_func(cursor) - - end = time.monotonic() - total_milliseconds = (end - start) * 1000 - self.bot.log.debug("executed in %fms", [total_milliseconds]) - - return value - def execute_fetchall(self, query, params=[]): - return self._execute_fetch(query, - lambda cursor: cursor.fetchall(), params) - def execute_fetchone(self, query, params=[]): - return self._execute_fetch(query, - lambda cursor: cursor.fetchone(), params) - def execute(self, query, params=[]): - return self._execute_fetch(query, lambda cursor: None, params) - - def has_table(self, table_name): - result = self.execute_fetchone("""SELECT COUNT(*) FROM - sqlite_master WHERE type='table' AND name=?""", - [table_name]) - return result[0] == 1 - - def make_servers_table(self): - if not self.has_table("servers"): - self.execute("""CREATE TABLE servers - (server_id INTEGER PRIMARY KEY, alias TEXT, hostname TEXT, - port INTEGER,password TEXT,ipv4 BOOLEAN, tls BOOLEAN, - nickname TEXT, username TEXT, realname TEXT)""") - def make_channels_table(self): - if not self.has_table("channels"): - self.execute("""CREATE TABLE channels - (channel_id INTEGER PRIMARY KEY, server_id INTEGER, - name TEXT, FOREIGN KEY (server_id) REFERENCES - servers (server_id) ON DELETE CASCADE, - UNIQUE (server_id, name))""") - self.execute("""CREATE INDEX channels_index - on channels (server_id, name)""") - def make_users_table(self): - if not self.has_table("users"): - self.execute("""CREATE TABLE users - (user_id INTEGER PRIMARY KEY, server_id INTEGER, - nickname TEXT, FOREIGN KEY (server_id) REFERENCES - servers (server_id) ON DELETE CASCADE, - UNIQUE (server_id, nickname))""") - self.execute("""CREATE INDEX users_index - on users (server_id, nickname)""") - def make_bot_settings_table(self): - if not self.has_table("bot_settings"): - self.execute("""CREATE TABLE bot_settings - (setting TEXT PRIMARY KEY, value TEXT)""") - self.execute("""CREATE INDEX bot_settings_index - ON bot_settings (setting)""") - def make_server_settings_table(self): - if not self.has_table("server_settings"): - self.execute("""CREATE TABLE server_settings - (server_id INTEGER, setting TEXT, value TEXT, - FOREIGN KEY(server_id) REFERENCES - servers(server_id) ON DELETE CASCADE, - PRIMARY KEY (server_id, setting))""") - self.execute("""CREATE INDEX server_settings_index - ON server_settings (server_id, setting)""") - def make_channel_settings_table(self): - if not self.has_table("channel_settings"): - self.execute("""CREATE TABLE channel_settings - (channel_id INTEGER, setting TEXT, value TEXT, - FOREIGN KEY (channel_id) REFERENCES channels(channel_id) - ON DELETE CASCADE, PRIMARY KEY (channel_id, setting))""") - self.execute("""CREATE INDEX channel_settings_index - ON channel_settings (channel_id, setting)""") - def make_user_settings_table(self): - if not self.has_table("user_settings"): - self.execute("""CREATE TABLE user_settings - (user_id INTEGER, setting TEXT, value TEXT, - FOREIGN KEY (user_id) REFERENCES users(user_id) - ON DELETE CASCADE, PRIMARY KEY (user_id, setting))""") - self.execute("""CREATE INDEX user_settings_index ON - user_settings (user_id, setting)""") - def make_user_channel_settings_table(self): - if not self.has_table("user_channel_settings"): - self.execute("""CREATE TABLE user_channel_settings - (user_id INTEGER, channel_id INTEGER, setting TEXT, - value TEXT, FOREIGN KEY (user_id) REFERENCES - users(user_id) ON DELETE CASCADE, FOREIGN KEY - (channel_id) REFERENCES channels(channel_id) ON - DELETE CASCADE, PRIMARY KEY (user_id, channel_id, - setting))""") - self.execute("""CREATE INDEX user_channel_settings_index - ON user_channel_settings (user_id, channel_id, setting)""") diff --git a/EventManager.py b/EventManager.py deleted file mode 100644 index 604ae337..00000000 --- a/EventManager.py +++ /dev/null @@ -1,226 +0,0 @@ -import itertools, time, traceback - -PRIORITY_URGENT = 0 -PRIORITY_HIGH = 1 -PRIORITY_MEDIUM = 2 -PRIORITY_LOW = 3 -PRIORITY_MONITOR = 4 - -DEFAULT_PRIORITY = PRIORITY_MEDIUM -DEFAULT_DELIMITER = "." - -class Event(object): - def __init__(self, bot, name, **kwargs): - self.bot = bot - self.name = name - self.kwargs = kwargs - self.eaten = False - def __getitem__(self, key): - return self.kwargs[key] - def get(self, key, default=None): - return self.kwargs.get(key, default) - def __contains__(self, key): - return key in self.kwargs - def eat(self): - self.eaten = True - -class EventCallback(object): - def __init__(self, function, bot, priority, kwargs): - self.function = function - self.bot = bot - self.priority = priority - self.kwargs = kwargs - def call(self, event): - return self.function(event) - -class MultipleEventHook(object): - def __init__(self): - self._event_hooks = set([]) - def _add(self, event_hook): - self._event_hooks.add(event_hook) - - def hook(self, function, **kwargs): - for event_hook in self._event_hooks: - event_hook.hook(function, **kwargs) - - def call_limited(self, maximum, **kwargs): - returns = [] - for event_hook in self._event_hooks: - returns.append(event_hook.call_limited(maximum, **kwargs)) - return returns - def call(self, **kwargs): - returns = [] - for event_hook in self._event_hooks: - returns.append(event_hook.call(**kwargs)) - return returns - -class EventHookContext(object): - def __init__(self, parent, context): - self._parent = parent - self.context = context - def hook(self, function, priority=DEFAULT_PRIORITY, replay=False, - **kwargs): - self._parent._context_hook(self.context, function, priority, replay, - kwargs) - def on(self, subevent, *extra_subevents, delimiter=DEFAULT_DELIMITER): - return self._parent._context_on(self.context, subevent, - extra_subevents, delimiter) - def call_for_result(self, default=None, **kwargs): - return self._parent.call_for_result(default, **kwargs) - def assure_call(self, **kwargs): - self._parent.assure_call(**kwargs) - def call(self, **kwargs): - return self._parent.call(**kwargs) - def call_limited(self, maximum, **kwargs): - return self._parent.call_limited(maximum, **kwargs) - def get_hooks(self): - return self._parent.get_hooks() - def get_children(self): - return self._parent.get_children() - -class EventHook(object): - def __init__(self, bot, name=None, parent=None): - self.bot = bot - self.name = name - self.parent = parent - self._children = {} - self._hooks = [] - self._stored_events = [] - self._context_hooks = {} - - def _make_event(self, kwargs): - return Event(self.bot, self.name, **kwargs) - - def _get_path(self): - path = [] - parent = self - while not parent == None and not parent.name == None: - path.append(parent.name) - parent = parent.parent - return DEFAULT_DELIMITER.join(path[::-1]) - - def new_context(self, context): - return EventHookContext(self, context) - - def hook(self, function, priority=DEFAULT_PRIORITY, replay=False, - **kwargs): - self._hook(function, None, priority, replay, kwargs) - def _context_hook(self, context, function, priority, replay, kwargs): - self._hook(function, context, priority, replay, kwargs) - def _hook(self, function, context, priority, replay, kwargs): - callback = EventCallback(function, self.bot, priority, kwargs) - - if context == None: - self._hooks.append(callback) - else: - if not context in self._context_hooks: - self._context_hooks[context] = [] - self._context_hooks[context].append(callback) - - if replay and not self._stored_events == None: - for kwargs in self._stored_events: - self._call(kwargs) - self._stored_events = None - - def on(self, subevent, *extra_subevents, delimiter=DEFAULT_DELIMITER): - return self._on(subevent, extra_subevents, None, delimiter) - def _context_on(self, context, subevent, extra_subevents, - delimiter=DEFAULT_DELIMITER): - return self._on(subevent, extra_subevents, context, delimiter) - def _on(self, subevent, extra_subevents, context, delimiter): - if delimiter in subevent: - event_chain = subevent.split(delimiter) - event_obj = self - for event_name in event_chain: - event_obj = event_obj.get_child(event_name) - if not context == None: - return event_obj.new_context(context) - return event_obj - - if extra_subevents: - multiple_event_hook = MultipleEventHook() - for extra_subevent in (subevent,)+extra_subevents: - child = self.get_child(extra_subevent) - if not context == None: - child = child.new_context(context) - multiple_event_hook._add(child) - return multiple_event_hook - - child = self.get_child(subevent) - if not context == None: - child = child.new_context(context) - return child - - def call_for_result(self, default=None, **kwargs): - results = self.call_limited(1, **kwargs) - return default if not len(results) else results[0] - def assure_call(self, **kwargs): - if not self._stored_events == None: - self._stored_events.append(kwargs) - else: - self._call(kwargs) - def call(self, **kwargs): - return self._call(kwargs) - def call_limited(self, maximum, **kwargs): - return self._call(kwargs, maximum=maximum) - def _call(self, kwargs, maximum=None): - event_path = self._get_path() - self.bot.log.debug("calling event: \"%s\" (params: %s)", - [event_path, kwargs]) - start = time.monotonic() - - event = self._make_event(kwargs) - returns = [] - for hook in self.get_hooks()[:maximum]: - if event.eaten: - break - try: - returns.append(hook.call(event)) - except Exception as e: - traceback.print_exc() - self.bot.log.error("failed to call event \"%s\"", [ - event_path], exc_info=True) - - total_milliseconds = (time.monotonic() - start) * 1000 - self.bot.log.debug("event \"%s\" called in %fms", [ - event_path, total_milliseconds]) - - self.check_purge() - - return returns - - def get_child(self, child_name): - child_name_lower = child_name.lower() - if not child_name_lower in self._children: - self._children[child_name_lower] = EventHook(self.bot, - child_name_lower, self) - return self._children[child_name_lower] - def remove_child(self, child_name): - child_name_lower = child_name.lower() - if child_name_lower in self._children: - del self._children[child_name_lower] - - def check_purge(self): - if self.is_empty() and not self.parent == None: - self.parent.remove_child(self.name) - self.parent.check_purge() - - def remove_context(self, context): - del self._context_hooks[context] - def has_context(self, context): - return context in self._context_hooks - def purge_context(self, context): - if self.has_context(context): - self.remove_context(context) - - for child_name in self.get_children()[:]: - child = self.get_child(child_name) - child.purge_context(context) - - def get_hooks(self): - return sorted(self._hooks + sum(self._context_hooks.values(), []), - key=lambda e: e.priority) - def get_children(self): - return list(self._children.keys()) - def is_empty(self): - return len(self.get_hooks() + self.get_children()) == 0 diff --git a/Exports.py b/Exports.py deleted file mode 100644 index 8baca50d..00000000 --- a/Exports.py +++ /dev/null @@ -1,44 +0,0 @@ - - -class ExportsContext(object): - def __init__(self, parent, context): - self._parent = parent - self.context = context - - def add(self, setting, value): - self._parent._context_add(self.context, setting, value) - def get_all(self, setting): - return self._parent.get_all(setting) - -class Exports(object): - def __init__(self): - self._exports = {} - self._context_exports = {} - - def new_context(self, context): - return ExportsContext(self, context) - - def add(self, setting, value): - self._add(None, setting, value) - def _context_add(self, context, setting, value): - self._add(context, setting, value) - def _add(self, context, setting, value): - if context == None: - if not setting in self_exports: - self._exports[setting] = [] - self._exports[setting].append(value) - else: - if not context in self._context_exports: - self._context_exports[context] = {} - if not setting in self._context_exports[context]: - self._context_exports[context][setting] = [] - self._context_exports[context][setting].append(value) - - def get_all(self, setting): - return self._exports.get(setting, []) + sum([ - exports.get(setting, []) for exports in - self._context_exports.values()], []) - - def purge_context(self, context): - if context in self._context_exports: - del self._context_exports[context] diff --git a/IRCBot.py b/IRCBot.py deleted file mode 100644 index 536e01ee..00000000 --- a/IRCBot.py +++ /dev/null @@ -1,200 +0,0 @@ -import os, select, sys, threading, time, traceback, uuid - -import EventManager, Exports, IRCLineHandler, IRCServer, Logging -import ModuleManager, Timer - -class Bot(object): - def __init__(self): - self.start_time = time.time() - self.lock = threading.Lock() - self.args = None - self.database = None - self.config = None - self.bot_directory = os.path.dirname(os.path.realpath(__file__)) - self.servers = {} - self.running = True - self.poll = select.epoll() - self._events = EventManager.EventHook(self) - self._exports = Exports.Exports() - self.modules = ModuleManager.ModuleManager(self, self._events, - self._exports) - self.log = Logging.Log(self) - self.line_handler = IRCLineHandler.LineHandler(self, self._events) - self.timers = [] - self._events.on("timer.reconnect").hook(self.reconnect) - self._events.on("boot.done").hook(self.setup_timers) - - def add_server(self, server_id, connect=True): - (_, alias, hostname, port, password, ipv4, tls, nickname, - username, realname) = self.database.servers.get(server_id) - - new_server = IRCServer.Server(self, self._events, server_id, alias, - hostname, port, password, ipv4, tls, nickname, username, - realname) - if not new_server.get_setting("connect", True): - return - self._events.on("new.server").call(server=new_server) - if connect and new_server.get_setting("connect", True): - self.connect(new_server) - return new_server - def connect(self, server): - try: - server.connect() - except: - sys.stderr.write("Failed to connect to %s\n" % str(server)) - traceback.print_exc() - return False - self.servers[server.fileno()] = server - self.poll.register(server.fileno(), select.EPOLLOUT) - return True - def setup_timers(self, event): - for setting, value in self.find_settings("timer-%"): - id = setting.split("timer-", 1)[1] - self.add_timer(value["event-name"], value["delay"], value[ - "next-due"], id, **value["kwargs"]) - def timer_setting(self, timer): - self.set_setting("timer-%s" % timer.id, { - "event-name": timer.event_name, "delay": timer.delay, - "next-due": timer.next_due, "kwargs": timer.kwargs}) - def timer_setting_remove(self, timer): - self.timers.remove(timer) - self.del_setting("timer-%s" % timer.id) - def add_timer(self, event_name, delay, next_due=None, id=None, persist=True, - **kwargs): - id = id or uuid.uuid4().hex - timer = Timer.Timer(id, self, self._events, event_name, delay, - next_due, **kwargs) - if id: - timer.id = id - elif persist: - self.timer_setting(timer) - self.timers.append(timer) - def next_timer(self): - next = None - for timer in self.timers: - time_left = timer.time_left() - if next == None or time_left < next: - next = time_left - - if next == None: - return None - if next < 0: - return 0 - return next - def call_timers(self): - for timer in self.timers[:]: - if timer.due(): - timer.call() - if timer.done(): - self.timer_setting_remove(timer) - def next_send(self): - next = None - for server in self.servers.values(): - timeout = server.send_throttle_timeout() - if server.waiting_send() and (next == None or timeout < next): - next = timeout - return next - - def next_ping(self): - timeouts = [] - for server in self.servers.values(): - timeout = server.until_next_ping() - if not timeout == None: - timeouts.append(timeout) - if not timeouts: - return None - return min(timeouts) - def next_read_timeout(self): - timeouts = [] - for server in self.servers.values(): - timeouts.append(server.until_read_timeout()) - if not timeouts: - return None - return min(timeouts) - - def get_poll_timeout(self): - timeouts = [] - timeouts.append(self.next_timer()) - timeouts.append(self.next_send()) - timeouts.append(self.next_ping()) - timeouts.append(self.next_read_timeout()) - return min([timeout for timeout in timeouts if not timeout == None]) - - def register_read(self, server): - self.poll.modify(server.fileno(), select.EPOLLIN) - def register_write(self, server): - self.poll.modify(server.fileno(), select.EPOLLOUT) - def register_both(self, server): - self.poll.modify(server.fileno(), - select.EPOLLIN|select.EPOLLOUT) - - def disconnect(self, server): - try: - self.poll.unregister(server.fileno()) - except FileNotFoundError: - pass - del self.servers[server.fileno()] - - def reconnect(self, event): - server_details = self.database.servers.get(event["server_id"]) - server = self.add_server(*(server_details + (False,))) - if self.connect(server): - self.servers[server.fileno()] = server - else: - event["timer"].redo() - - def set_setting(self, setting, value): - self.database.bot_settings.set(setting, value) - def get_setting(self, setting, default=None): - return self.database.bot_settings.get(setting, default) - def find_settings(self, pattern, default=[]): - return self.database.bot_settings.find(pattern, default) - def find_settings_prefix(self, prefix, default=[]): - return self.database.bot_settings.find_prefix( - prefix, default) - def del_setting(self, setting): - self.database.bot_settings.delete(setting) - - def run(self): - while self.running: - self.lock.acquire() - events = self.poll.poll(self.get_poll_timeout()) - self.call_timers() - for fd, event in events: - if fd in self.servers: - server = self.servers[fd] - if event & select.EPOLLIN: - lines = server.read() - for line in lines: - if self.args.verbose: - self.log.info("<%s | %s", [str(server), line]) - else: - self.log.debug("%s (raw) | %s", [str(server), - line]) - server.parse_line(line) - elif event & select.EPOLLOUT: - server._send() - self.register_read(server) - elif event & select.EPULLHUP: - print("hangup") - server.disconnect() - - for server in list(self.servers.values()): - if server.read_timed_out(): - print("pingout from %s" % str(server)) - server.disconnect() - elif server.ping_due() and not server.ping_sent: - server.send_ping() - server.ping_sent = True - if not server.connected: - self.disconnect(server) - - reconnect_delay = self.config.get("reconnect-delay", 10) - self.add_timer("reconnect", reconnect_delay, None, None, False, - server_id=server.id) - - print("disconnected from %s, reconnecting in %d seconds" % ( - str(server), reconnect_delay)) - elif server.waiting_send() and server.throttle_done(): - self.register_both(server) - self.lock.release() diff --git a/IRCBuffer.py b/IRCBuffer.py deleted file mode 100644 index e6bd24f9..00000000 --- a/IRCBuffer.py +++ /dev/null @@ -1,48 +0,0 @@ -import re -import Utils - -class BufferLine(object): - def __init__(self, sender, message, action, from_self): - self.sender = sender - self.message = message - self.action = action - self.from_self = from_self - -class Buffer(object): - def __init__(self, bot, server): - self.bot = bot - self.server = server - self.lines = [] - self.max_lines = 64 - self._skip_next = False - def add_line(self, sender, message, action, from_self=False): - if not self._skip_next: - line = BufferLine(sender, message, action, from_self) - self.lines.insert(0, line) - if len(self.lines) > self.max_lines: - self.lines.pop() - self._skip_next = False - def get(self, index=0, **kwargs): - from_self = kwargs.get("from_self", True) - for line in self.lines: - if line.from_self and not from_self: - continue - return line - def find(self, pattern, **kwargs): - from_self = kwargs.get("from_self", True) - for_user = kwargs.get("for_user", "") - for_user = Utils.irc_lower(self.server, for_user - ) if for_user else None - not_pattern = kwargs.get("not_pattern", None) - for line in self.lines: - if line.from_self and not from_self: - continue - elif re.search(pattern, line.message): - if not_pattern and re.search(not_pattern, line.message): - continue - if for_user and not Utils.irc_lower(self.server, line.sender - ) == for_user: - continue - return line - def skip_next(self): - self._skip_next = True diff --git a/IRCChannel.py b/IRCChannel.py deleted file mode 100644 index ced8648f..00000000 --- a/IRCChannel.py +++ /dev/null @@ -1,132 +0,0 @@ -import uuid -import IRCBuffer, Utils - -class Channel(object): - def __init__(self, name, id, server, bot): - self.name = Utils.irc_lower(server, name) - self.id = id - self.server = server - self.bot = bot - self.topic = "" - self.topic_setter_nickname = None - self.topic_setter_username = None - self.topic_setter_hostname = None - self.topic_time = 0 - self.users = set([]) - self.modes = {} - self.created_timestamp = None - self.buffer = IRCBuffer.Buffer(bot, server) - - def __repr__(self): - return "IRCChannel.Channel(%s|%s)" % (self.server.name, self.name) - - def set_topic(self, topic): - self.topic = topic - def set_topic_setter(self, nickname, username=None, hostname=None): - self.topic_setter_nickname = nickname - self.topic_setter_username = username - self.topic_setter_hostname = hostname - def set_topic_time(self, unix_timestamp): - self.topic_time = unix_timestamp - - def add_user(self, user): - self.users.add(user) - def remove_user(self, user): - self.users.remove(user) - for mode in list(self.modes.keys()): - if mode in self.server.mode_prefixes.values( - ) and user in self.modes[mode]: - self.modes[mode].discard(user) - if not len(self.modes[mode]): - del self.modes[mode] - def has_user(self, user): - return user in self.users - - def add_mode(self, mode, arg=None): - if not mode in self.modes: - self.modes[mode] = set([]) - if arg: - if mode in self.server.mode_prefixes.values(): - user = self.server.get_user(arg) - if user: - self.modes[mode].add(user) - else: - self.modes[mode].add(arg.lower()) - def remove_mode(self, mode, arg=None): - if not arg: - del self.modes[mode] - else: - if mode in self.server.mode_prefixes.values(): - user = self.server.get_user(arg) - if user: - self.modes[mode].discard(user) - else: - self.modes[mode].discard(arg.lower()) - if not len(self.modes[mode]): - del self.modes[mode] - def change_mode(self, remove, mode, arg=None): - if remove: - self.remove_mode(mode, arg) - else: - self.add_mode(mode, arg) - - def set_setting(self, setting, value): - self.bot.database.channel_settings.set(self.id, setting, value) - def get_setting(self, setting, default=None): - return self.bot.database.channel_settings.get(self.id, setting, - default) - def find_settings(self, pattern, default=[]): - return self.bot.database.channel_settings.find(self.id, pattern, - default) - def find_settings_prefix(self, prefix, default=[]): - return self.bot.database.channel_settings.find_prefix(self.id, - prefix, default) - def del_setting(self, setting): - self.bot.database.channel_settings.delete(self.id, setting) - - def set_user_setting(self, user_id, setting, value): - self.bot.database.user_channel_settings.set(user_id, self.id, - setting, value) - def get_user_setting(self, user_id, setting, default=None): - return self.bot.database.user_channel_settings.get(user_id, - self.id, setting, default) - def find_user_settings(self, user_i, pattern, default=[]): - return self.bot.database.user_channel_settings.find(user_id, - self.id, pattern, default) - def find_user_settings_prefix(self, user_id, prefix, default=[]): - return self.bot.database.user_channel_settings.find_prefix( - user_id, self.id, prefix, default) - def del_user_setting(self, user_id, setting): - self.bot.database.user_channel_settings.delete(user_id, self.id, - setting) - def find_all_by_setting(self, setting, default=[]): - return self.bot.database.user_channel_settings.find_all_by_setting( - self.id, setting, default) - - def send_message(self, text, prefix=None): - self.server.send_message(self.name, text, prefix=prefix) - def send_mode(self, mode=None, target=None): - self.server.send_mode(self.name, mode, target) - def send_kick(self, target, reason=None): - self.server.send_kick(self.name, target, reason) - def send_ban(self, hostmask): - self.server.send_mode(self.name, "+b", hostmask) - def send_unban(self, hostmask): - self.server.send_mode(self.name, "-b", hostmask) - def send_topic(self, topic): - self.server.send_topic(self.name, topic) - - def mode_or_above(self, user, mode): - mode_orders = list(self.server.mode_prefixes.values()) - mode_index = mode_orders.index(mode) - for mode in mode_orders[:mode_index+1]: - if user in self.modes.get(mode, []): - return True - return False - - def get_user_status(self, user): - modes = "" - for mode in self.server.mode_prefixes.values(): - if user in self.modes.get(mode, []): - modes += mode - return modes diff --git a/IRCLineHandler.py b/IRCLineHandler.py deleted file mode 100644 index 062ff2d5..00000000 --- a/IRCLineHandler.py +++ /dev/null @@ -1,593 +0,0 @@ -import re, threading -import Utils - -RE_PREFIXES = re.compile(r"\bPREFIX=\((\w+)\)(\W+)(?:\b|$)") -RE_CHANMODES = re.compile( - r"\bCHANMODES=(\w*),(\w*),(\w*),(\w*)(?:\b|$)") -RE_CHANTYPES = re.compile(r"\bCHANTYPES=(\W+)(?:\b|$)") -RE_CASEMAPPING = re.compile(r"\bCASEMAPPING=(\S+)") -RE_MODES = re.compile(r"[-+]\w+") - -CAPABILITIES = {"multi-prefix", "chghost", "invite-notify", "account-tag", - "account-notify", "extended-join", "away-notify", "userhost-in-names", - "draft/message-tags-0.2", "server-time", "cap-notify", - "batch", "draft/labeled-response"} - -class LineHandler(object): - def __init__(self, bot, events): - self.bot = bot - self.events = events - events.on("raw.PING").hook(self.ping) - - events.on("raw.001").hook(self.handle_001, default_event=True) - events.on("raw.005").hook(self.handle_005) - events.on("raw.311").hook(self.handle_311, default_event=True) - events.on("raw.332").hook(self.handle_332) - events.on("raw.333").hook(self.handle_333) - events.on("raw.353").hook(self.handle_353, default_event=True) - events.on("raw.366").hook(self.handle_366, default_event=True) - events.on("raw.421").hook(self.handle_421, default_event=True) - events.on("raw.352").hook(self.handle_352, default_event=True) - events.on("raw.354").hook(self.handle_354, default_event=True) - events.on("raw.324").hook(self.handle_324, default_event=True) - events.on("raw.329").hook(self.handle_329, default_event=True) - events.on("raw.433").hook(self.handle_433, default_event=True) - events.on("raw.477").hook(self.handle_477, default_event=True) - - events.on("raw.JOIN").hook(self.join) - events.on("raw.PART").hook(self.part) - events.on("raw.QUIT").hook(self.quit) - events.on("raw.NICK").hook(self.nick) - events.on("raw.MODE").hook(self.mode) - events.on("raw.KICK").hook(self.kick) - events.on("raw.INVITE").hook(self.invite) - events.on("raw.TOPIC").hook(self.topic) - events.on("raw.PRIVMSG").hook(self.privmsg) - events.on("raw.NOTICE").hook(self.notice) - - events.on("raw.CAP").hook(self.cap) - events.on("raw.AUTHENTICATE").hook(self.authenticate) - events.on("raw.CHGHOST").hook(self.chghost) - events.on("raw.ACCOUNT").hook(self.account) - events.on("raw.TAGMSG").hook(self.tagmsg) - events.on("raw.AWAY").hook(self.away) - events.on("raw.BATCH").hook(self.batch) - - def handle(self, server, line): - original_line = line - tags = {} - prefix = None - command = None - - if line[0] == "@": - tags_prefix, line = line[1:].split(" ", 1) - for tag in tags_prefix.split(";"): - if tag: - tag_split = tag.split("=", 1) - tags[tag_split[0]] = "".join(tag_split[1:]) - if "batch" in tags and tags["batch"] in server.batches: - server.batches[tag["batch"]].append(line) - return - - arbitrary = None - if " :" in line: - line, arbitrary = line.split(" :", 1) - if line.endswith(" "): - line = line[:-1] - if line[0] == ":": - prefix, command = line[1:].split(" ", 1) - prefix = Utils.seperate_hostmask(prefix) - if " " in command: - command, line = command.split(" ", 1) - else: - line = "" - else: - command = line - if " " in line: - command, line = line.split(" ", 1) - args = line.split(" ") - - hooks = self.events.on("raw").on(command).get_hooks() - default_event = False - for hook in hooks: - if hook.kwargs.get("default_event", False): - default_event = True - break - last = arbitrary or args[-1] - - #server, prefix, command, args, arbitrary - self.events.on("raw").on(command).call(server=server, last=last, - prefix=prefix, args=args, arbitrary=arbitrary, tags=tags) - if default_event or not hooks: - if command.isdigit(): - self.events.on("received.numeric").on(command).call( - line=original_line, server=server, tags=tags, last=last, - line_split=original_line.split(" "), number=command) - else: - self.events.on("received").on(command).call( - line=original_line, line_split=original_line.split(" "), - command=command, server=server, tags=tags, last=last) - - # ping from the server - def ping(self, event): - event["server"].send_pong(event["last"]) - - # first numeric line the server sends - def handle_001(self, event): - event["server"].name = event["prefix"].nickname - event["server"].set_own_nickname(event["args"][0]) - event["server"].send_whois(event["server"].nickname) - - # server telling us what it supports - def handle_005(self, event): - isupport_line = " ".join(event["args"][1:]) - - if "NAMESX" in isupport_line: - event["server"].send("PROTOCTL NAMESX") - - match = re.search(RE_PREFIXES, isupport_line) - if match: - event["server"].mode_prefixes.clear() - modes = match.group(1) - prefixes = match.group(2) - for i, prefix in enumerate(prefixes): - if i < len(modes): - event["server"].mode_prefixes[prefix] = modes[i] - match = re.search(RE_CHANMODES, isupport_line) - if match: - event["server"].channel_modes = list(match.group(4)) - match = re.search(RE_CHANTYPES, isupport_line) - if match: - event["server"].channel_types = list(match.group(1)) - - match = re.search(RE_CASEMAPPING, isupport_line) - if match: - event["server"].case_mapping = match.group(1) - - self.events.on("received.numeric.005").call( - isupport=isupport_line, server=event["server"]) - - # whois respose (nickname, username, realname, hostname) - def handle_311(self, event): - nickname = event["args"][1] - if event["server"].is_own_nickname(nickname): - target = event["server"] - else: - target = event["server"].get_user(nickname) - target.username = event["args"][2] - target.hostname = event["args"][3] - target.realname = event["arbitrary"] - - # on-join channel topic line - def handle_332(self, event): - channel = event["server"].get_channel(event["args"][1]) - - channel.set_topic(event["arbitrary"]) - self.events.on("received.numeric.332").call(channel=channel, - server=event["server"], topic=event["arbitrary"]) - - # channel topic changed - def topic(self, event): - user = event["server"].get_user(event["prefix"].nickname) - channel = event["server"].get_channel(event["args"][0]) - channel.set_topic(event["arbitrary"]) - self.events.on("received.topic").call(channel=channel, - server=event["server"], topic=event["arbitrary"], user=user) - - # on-join channel topic set by/at - def handle_333(self, event): - channel = event["server"].get_channel(event["args"][1]) - - topic_setter_hostmask = event["args"][2] - topic_setter = Utils.seperate_hostmask(topic_setter_hostmask) - topic_time = int(event["args"][3]) if event["args"][3].isdigit( - ) else None - - channel.set_topic_setter(topic_setter.nickname, topic_setter.username, - topic_setter.hostname) - channel.set_topic_time(topic_time) - self.events.on("received.numeric.333").call(channel=channel, - setter=topic_setter.nickname, set_at=topic_time, - server=event["server"]) - - # /names response, also on-join user list - def handle_353(self, event): - channel = event["server"].get_channel(event["args"][2]) - nicknames = event["arbitrary"].split() - for nickname in nicknames: - modes = set([]) - - while nickname[0] in event["server"].mode_prefixes: - modes.add(event["server"].mode_prefixes[nickname[0]]) - nickname = nickname[1:] - - if "userhost-in-names" in event["server"].capabilities: - hostmask = Utils.seperate_hostmask(nickname) - nickname = hostmask.nickname - user = event["server"].get_user(hostmask.nickname) - user.username = hostmask.username - user.hostname = hostmask.hostname - else: - user = event["server"].get_user(nickname) - user.join_channel(channel) - channel.add_user(user) - - for mode in modes: - channel.add_mode(mode, nickname) - - # on-join user list has finished - def handle_366(self, event): - event["server"].send_whox(event["args"][1], "n", "ahnrtu", "111") - - # on user joining channel - def join(self, event): - account = None - realname = None - if len(event["args"]) == 2: - channel = event["server"].get_channel(event["args"][0]) - if not event["args"] == "*": - account = event["args"][1] - realname = event["arbitrary"] - else: - channel = event["server"].get_channel(event["last"]) - - if not event["server"].is_own_nickname(event["prefix"].nickname): - user = event["server"].get_user(event["prefix"].nickname) - if not user.username and not user.hostname: - user.username = event["prefix"].username - user.hostname = event["prefix"].hostname - - if account: - user.identified_account = account - user.identified_account_id = event["server"].get_user( - account).get_id() - if realname: - user.realname = realname - - channel.add_user(user) - user.join_channel(channel) - self.events.on("received.join").call(channel=channel, - user=user, server=event["server"], account=account, - realname=realname) - else: - if channel.name in event["server"].attempted_join: - del event["server"].attempted_join[channel.name] - self.events.on("self.join").call(channel=channel, - server=event["server"], account=account, realname=realname) - channel.send_mode() - - # on user parting channel - def part(self, event): - channel = event["server"].get_channel(event["args"][0]) - reason = event["arbitrary"] or "" - - if not event["server"].is_own_nickname(event["prefix"].nickname): - user = event["server"].get_user(event["prefix"].nickname) - self.events.on("received.part").call(channel=channel, - reason=reason, user=user, server=event["server"]) - channel.remove_user(user) - user.part_channel(channel) - if not len(user.channels): - event["server"].remove_user(user) - else: - self.events.on("self.part").call(channel=channel, - reason=reason, server=event["server"]) - event["server"].remove_channel(channel) - - # unknown command sent by us, oops! - def handle_421(self, event): - print("warning: unknown command '%s'." % event["args"][1]) - - # a user has disconnected! - def quit(self, event): - reason = event["arbitrary"] or "" - - if not event["server"].is_own_nickname(event["prefix"].nickname): - user = event["server"].get_user(event["prefix"].nickname) - event["server"].remove_user(user) - self.events.on("received.quit").call(reason=reason, - user=user, server=event["server"]) - else: - event["server"].disconnect() - - # the server is telling us about its capabilities! - def cap(self, event): - capabilities_list = (event["arbitrary"] or "").split(" ") - capabilities = {} - for capability in capabilities_list: - argument = None - if "=" in capability: - capability, argument = capability.split("=", 1) - capabilities[capability] = argument - - subcommand = event["args"][1].lower() - is_multiline = len(event["args"]) > 2 and event["args"][2] == "*" - - if subcommand == "ls": - event["server"].server_capabilities.update(capabilities) - if not is_multiline: - matched_capabilities = set(event["server" - ].server_capabilities.keys()) & CAPABILITIES - if matched_capabilities: - event["server"].queue_capabilities(matched_capabilities) - - self.events.on("received.cap.ls").call( - capabilities=event["server"].server_capabilities, - server=event["server"]) - - if event["server"].has_capability_queue(): - event["server"].send_capability_queue() - else: - event["server"].send_capability_end() - elif subcommand == "new": - event["server"].capabilities.update(set(capabilities.keys())) - self.events.on("received.cap.new").call(server=event["server"], - capabilities=capabilities) - elif subcommand == "del": - event["server"].capabilities.difference_update(set( - capabilities.keys())) - self.events.on("received.cap.del").call(server=event["server"], - capabilities=capabilities) - elif subcommand == "ack": - event["server"].capabilities.update(capabilities) - if not is_multiline: - self.events.on("received.cap.ack").call( - capabilities=event["server"].capabilities, - server=event["server"]) - - if not event["server"].waiting_for_capabilities(): - event["server"].send_capability_end() - elif subcommand == "nack": - event["server"].send_capability_end() - - # the server is asking for authentication - def authenticate(self, event): - self.events.on("received.authenticate").call( - message=event["args"][0], server=event["server"]) - - # someone has changed their nickname - def nick(self, event): - new_nickname = event["arbitrary"] - if not event["server"].is_own_nickname(event["prefix"].nickname): - user = event["server"].get_user(event["prefix"].nickname) - old_nickname = user.nickname - user.set_nickname(new_nickname) - event["server"].change_user_nickname(old_nickname, new_nickname) - - self.events.on("received.nick").call(new_nickname=new_nickname, - old_nickname=old_nickname, user=user, server=event["server"]) - else: - old_nickname = event["server"].nickname - event["server"].set_own_nickname(new_nickname) - - self.events.on("self.nick").call(server=event["server"], - new_nickname=new_nickname, old_nickname=old_nickname) - - # something's mode has changed - def mode(self, event): - user = event["server"].get_user(event["prefix"].nickname) - target = event["args"][0] - is_channel = target[0] in event["server"].channel_types - if is_channel: - channel = event["server"].get_channel(target) - remove = False - args = event["args"][2:] - _args = args[:] - modes = RE_MODES.findall(event["args"][1]) - for chunk in modes: - remove = chunk[0] == "-" - for mode in chunk[1:]: - if mode in event["server"].channel_modes: - channel.change_mode(remove, mode) - elif mode in event["server"].mode_prefixes.values( - ) and len(args): - channel.change_mode(remove, mode, args.pop(0)) - else: - args.pop(0) - self.events.on("received.mode.channel").call(modes=modes, - mode_args=_args, channel=channel, server=event["server"], - user=user) - elif event["server"].is_own_nickname(target): - modes = RE_MODES.findall(event["last"]) - for chunk in modes: - remove = chunk[0] == "-" - for mode in chunk[1:]: - event["server"].change_own_mode(remove, mode) - self.events.on("self.mode").call(modes=modes, - server=event["server"]) - - # someone (maybe me!) has been invited somewhere - def invite(self, event): - target_channel = event["last"] - user = event["server"].get_user(event["prefix"].nickname) - target_user = event["server"].get_user(event["args"][0]) - self.events.on("received.invite").call(user=user, - target_channel=target_channel, server=event["server"], - target_user=target_user) - - # we've received a message - def privmsg(self, event): - user = event["server"].get_user(event["prefix"].nickname) - message = event["arbitrary"] or "" - message_split = message.split(" ") - target = event["args"][0] - action = message.startswith("\x01ACTION ") - if action: - message = message.replace("\x01ACTION ", "", 1) - if message.endswith("\x01"): - message = message[:-1] - - if "account" in event["tags"]: - user.identified_account = event["tags"]["account"] - user.identified_account_id = event["server"].get_user( - event["tags"]["account"]).get_id() - - kwargs = {"message": message, "message_split": message_split, - "server": event["server"], "tags": event["tags"], - "action": action} - - if target[0] in event["server"].channel_types: - channel = event["server"].get_channel(event["args"][0]) - self.events.on("received.message.channel").call( - user=user, channel=channel, **kwargs) - channel.buffer.add_line(user.nickname, message, action) - elif event["server"].is_own_nickname(target): - self.events.on("received.message.private").call( - user=user, **kwargs) - user.buffer.add_line(user.nickname, message, action) - - # we've received a notice - def notice(self, event): - message = event["arbitrary"] or "" - message_split = message.split(" ") - target = event["args"][0] - - if not event["prefix"] or event["prefix"].hostmask == event["server" - ].name or target == "*" or (not event["prefix"].hostname and - not event["server"].name): - event["server"].name = event["prefix"].hostmask - - self.events.on("received.server-notice").call( - message=message, message_split=message_split, - server=event["server"]) - else: - user = event["server"].get_user(event["prefix"].nickname) - - if target[0] in event["server"].channel_types: - channel = event["server"].get_channel(target) - self.events.on("received.notice.channel").call( - message=message, message_split=message_split, user=user, - server=event["server"], channel=channel, - tags=event["tags"]) - elif event["server"].is_own_nickname(target): - self.events.on("received.notice.private").call( - message=message, message_split=message_split, user=user, - server=event["server"], tags=event["tags"]) - - # IRCv3 TAGMSG, used to send tags without any other information - def tagmsg(self, event): - user = event["server"].get_user(event["prefix"].nickname) - target = event["args"][0] - - if target[0] in event["server"].channel_types: - channel = event["server"].get_channel(target) - self.events.on("received.tagmsg.channel").call(channel=channel, - user=user, tags=event["tags"], server=event["server"]) - elif event["server"].is_own_nickname(target): - self.events.on("received.tagmsg.private").call( - user=user, tags=event["tags"], server=event["server"]) - - # IRCv3 AWAY, used to notify us that a client we can see has changed /away - def away(self, event): - user = event["server"].get_user(event["prefix"].nickname) - message = event["arbitrary"] - if message: - user.away = True - self.events.on("received.away.on").call(user=user, - server=event["server"], message=message) - else: - user.away = False - self.events.on("received.away.off").call(user=user, - server=event["server"]) - - def batch(self, event): - identifier = event["args"][0] - modifier, identifier = identifier[0], identifier[1:] - if modifier == "+": - event["server"].batches[identifier] = [] - else: - lines = event["server"].batches[identifier] - del event["server"].batches[identifier] - for line in lines: - self.handle(event["server"], line) - - # IRCv3 CHGHOST, a user's username and/or hostname has changed - def chghost(self, event): - username = event["args"][0] - hostname = event["args"][1] - - if not event["server"].is_own_nickname(event["prefix"].nickname): - target = event["server"].get_user("nickanme") - else: - target = event["server"] - target.username = username - target.hostname = hostname - - def account(self, event): - user = event["server"].get_user(event["prefix"].nickname) - - if not event["args"][0] == "*": - user.identified_account = event["args"][0] - user.identified_account_id = event["server"].get_user( - event["args"][0]).get_id() - self.events.on("received.account.login").call(user=user, - server=event["server"], account=event["args"][0]) - else: - user.identified_account = None - user.identified_account_id = None - self.events.on("received.account.logout").call(user=user, - server=event["server"]) - - # response to a WHO command for user information - def handle_352(self, event): - user = event["server"].get_user(event["args"][5]) - user.username = event["args"][2] - user.hostname = event["args"][3] - # response to a WHOX command for user information, including account name - def handle_354(self, event): - if event["args"][1] == "111": - username = event["args"][2] - hostname = event["args"][3] - nickname = event["args"][4] - account = event["args"][5] - realname = event["last"] - - user = event["server"].get_user(nickname) - user.username = username - user.hostname = hostname - user.realname = realname - if not account == "0": - user.identified_account = account - - # response to an empty mode command - def handle_324(self, event): - channel = event["server"].get_channel(event["args"][1]) - modes = event["args"][2] - if modes[0] == "+" and modes[1:]: - for mode in modes[1:]: - if mode in event["server"].channel_modes: - channel.add_mode(mode) - - # channel creation unix timestamp - def handle_329(self, event): - channel = event["server"].get_channel(event["args"][1]) - channel.creation_timestamp = int(event["args"][2]) - - # nickname already in use - def handle_433(self, event): - pass - - # we need a registered nickname for this channel - def handle_477(self, event): - channel_name = Utils.irc_lower(event["server"], event["args"][1]) - if channel_name in event["server"].attempted_join: - self.bot.add_timer("rejoin", 5, - channel_name=event["args"][1], - key=event["server"].attempted_join[channel_name], - server_id=event["server"].id) - - # someone's been kicked from a channel - def kick(self, event): - user = event["server"].get_user(event["prefix"].nickname) - target = event["args"][1] - channel = event["server"].get_channel(event["args"][0]) - reason = event["arbitrary"] or "" - - if not event["server"].is_own_nickname(target): - target_user = event["server"].get_user(target) - self.events.on("received.kick").call(channel=channel, - reason=reason, target_user=target_user, user=user, - server=event["server"]) - else: - self.events.on("self.kick").call(channel=channel, - reason=reason, user=user, server=event["server"]) diff --git a/IRCServer.py b/IRCServer.py deleted file mode 100644 index 8bc2567f..00000000 --- a/IRCServer.py +++ /dev/null @@ -1,402 +0,0 @@ -import collections, socket, ssl, sys, time -import IRCChannel, IRCUser, Utils - -THROTTLE_LINES = 4 -THROTTLE_SECONDS = 1 -READ_TIMEOUT_SECONDS = 120 -PING_INTERVAL_SECONDS = 30 - -class Server(object): - def __init__(self, bot, events, id, alias, hostname, port, password, - ipv4, tls, nickname, username, realname): - self.connected = False - self.bot = bot - self.events = events - self.id = id - self.alias = alias - self.target_hostname = hostname - self.port = port - self.tls = tls - self.password = password - self.ipv4 = ipv4 - self.original_nickname = nickname - self.original_username = username or nickname - self.original_realname = realname or nickname - self.name = None - - self._capability_queue = set([]) - self._capabilities_waiting = set([]) - self.capabilities = set([]) - self.server_capabilities = {} - self.batches = {} - - self.write_buffer = b"" - self.buffered_lines = [] - self.read_buffer = b"" - self.recent_sends = [] - - self.users = {} - self.new_users = set([]) - self.channels = {} - - self.own_modes = {} - self.mode_prefixes = collections.OrderedDict( - {"@": "o", "+": "v"}) - self.channel_modes = [] - self.channel_types = ["#"] - self.case_mapping = "rfc1459" - - self.last_read = time.monotonic() - self.last_send = None - - self.attempted_join = {} - self.ping_sent = False - - if ipv4: - self.socket = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - else: - self.socket = socket.socket(socket.AF_INET6, - socket.SOCK_STREAM) - - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - self.socket.settimeout(5.0) - - if self.tls: - self.tls_wrap() - self.cached_fileno = self.socket.fileno() - self.events.on("timer.rejoin").hook(self.try_rejoin) - - def __repr__(self): - return "IRCServer.Server(%s)" % self.__str__() - def __str__(self): - if self.alias: - return self.alias - return "%s:%s%s" % (self.target_hostname, "+" if self.tls else "", - self.port) - def fileno(self): - fileno = self.socket.fileno() - return self.cached_fileno if fileno == -1 else fileno - - def tls_wrap(self): - context = ssl.SSLContext(ssl.PROTOCOL_TLS) - context.options |= ssl.OP_NO_SSLv2 - context.options |= ssl.OP_NO_SSLv3 - context.options |= ssl.OP_NO_TLSv1 - - context.load_default_certs() - if self.get_setting("ssl-verify", True): - context.verify_mode = ssl.CERT_REQUIRED - - client_certificate = self.bot.config.get("ssl-certificate", None) - client_key = self.bot.config.get("ssl-key", None) - if client_certificate and client_key: - context.load_cert_chain(client_certificate, keyfile=client_key) - - self.socket = context.wrap_socket(self.socket) - - def connect(self): - self.socket.connect((self.target_hostname, self.port)) - self.send_capibility_ls() - - if self.password: - self.send_pass(self.password) - - self.send_user(self.original_username, self.original_realname) - self.send_nick(self.original_nickname) - self.connected = True - def disconnect(self): - self.connected = False - try: - self.socket.shutdown(socket.SHUT_RDWR) - except: - pass - try: - self.socket.close() - except: - pass - - def set_setting(self, setting, value): - self.bot.database.server_settings.set(self.id, setting, - value) - def get_setting(self, setting, default=None): - return self.bot.database.server_settings.get(self.id, - setting, default) - def find_settings(self, pattern, default=[]): - return self.bot.database.server_settings.find(self.id, - pattern, default) - def find_settings_prefix(self, prefix, default=[]): - return self.bot.database.server_settings.find_prefix( - self.id, prefix, default) - def del_setting(self, setting): - self.bot.database.server_settings.delete(self.id, setting) - def get_all_user_settings(self, setting, default=[]): - return self.bot.database.user_settings.find_all_by_setting( - self.id, setting, default) - def find_all_user_channel_settings(self, setting, default=[]): - return self.bot.database.user_channel_settings.find_all_by_setting( - self.id, setting, default) - - def set_own_nickname(self, nickname): - self.nickname = nickname - self.nickname_lower = Utils.irc_lower(self, nickname) - def is_own_nickname(self, nickname): - return Utils.irc_equals(self, nickname, self.nickname) - - def add_own_mode(self, mode, arg=None): - self.own_modes[mode] = arg - def remove_own_mode(self, mode): - del self.own_modes[mode] - def change_own_mode(self, remove, mode, arg=None): - if remove: - self.remove_own_mode(mode) - else: - self.add_own_mode(mode, arg) - - def has_user(self, nickname): - return Utils.irc_lower(self, nickname) in self.users - def get_user(self, nickname): - if not self.has_user(nickname): - user_id = self.get_user_id(nickname) - new_user = IRCUser.User(nickname, user_id, self, self.bot) - self.events.on("new.user").call(user=new_user, server=self) - self.users[new_user.nickname_lower] = new_user - self.new_users.add(new_user) - return self.users[Utils.irc_lower(self, nickname)] - def get_user_id(self, nickname): - self.bot.database.users.add(self.id, nickname) - return self.bot.database.users.get_id(self.id, nickname) - def remove_user(self, user): - del self.users[user.nickname_lower] - for channel in user.channels: - channel.remove_user(user) - - def change_user_nickname(self, old_nickname, new_nickname): - user = self.users.pop(Utils.irc_lower(self, old_nickname)) - user._id = self.get_user_id(new_nickname) - self.users[Utils.irc_lower(self, new_nickname)] = user - def has_channel(self, channel_name): - return channel_name[0] in self.channel_types and Utils.irc_lower( - self, channel_name) in self.channels - def get_channel(self, channel_name): - if not self.has_channel(channel_name): - channel_id = self.get_channel_id(channel_name) - new_channel = IRCChannel.Channel(channel_name, channel_id, - self, self.bot) - self.events.on("new.channel").call(channel=new_channel, - server=self) - self.channels[new_channel.name] = new_channel - return self.channels[Utils.irc_lower(self, channel_name)] - def get_channel_id(self, channel_name): - self.bot.database.channels.add(self.id, channel_name) - return self.bot.database.channels.get_id(self.id, channel_name) - def remove_channel(self, channel): - for user in channel.users: - user.part_channel(channel) - del self.channels[channel.name] - def parse_line(self, line): - if not line: - return - self.bot.line_handler.handle(self, line) - self.check_users() - def check_users(self): - for user in self.new_users: - if not len(user.channels): - self.remove_user(user) - self.new_users.clear() - def read(self): - data = b"" - try: - data = self.read_buffer + self.socket.recv(4096) - except (ConnectionResetError, socket.timeout): - self.disconnect() - return [] - self.read_buffer = b"" - data_lines = [line.strip(b"\r") for line in data.split(b"\n")] - if data_lines[-1]: - self.read_buffer = data_lines[-1] - data_lines.pop(-1) - decoded_lines = [] - for line in data_lines: - try: - line = line.decode(self.get_setting( - "encoding", "utf8")) - except: - try: - line = line.decode(self.get_setting( - "fallback-encoding", "latin-1")) - except: - continue - decoded_lines.append(line) - if not decoded_lines: - self.disconnect() - self.last_read = time.monotonic() - self.ping_sent = False - return decoded_lines - - def until_next_ping(self): - if self.ping_sent: - return None - return max(0, (self.last_read+PING_INTERVAL_SECONDS - )-time.monotonic()) - def ping_due(self): - return self.until_next_ping() == 0 - - def until_read_timeout(self): - return max(0, (self.last_read+READ_TIMEOUT_SECONDS - )-time.monotonic()) - def read_timed_out(self): - return self.until_read_timeout == 0 - - def send(self, data): - encoded = data.split("\n")[0].strip("\r").encode("utf8") - if len(encoded) > 450: - encoded = encoded[:450] - self.buffered_lines.append(encoded + b"\r\n") - if self.bot.args.verbose: - self.bot.log.info(">%s | %s", [str(self), encoded.decode("utf8")]) - def _send(self): - if not len(self.write_buffer): - self.write_buffer = self.buffered_lines.pop(0) - self.write_buffer = self.write_buffer[self.socket.send( - self.write_buffer):] - - now = time.monotonic() - self.recent_sends.append(now) - self.last_send = now - def waiting_send(self): - return bool(len(self.write_buffer)) or bool(len(self.buffered_lines)) - def throttle_done(self): - return self.send_throttle_timeout() == 0 - def send_throttle_timeout(self): - if len(self.write_buffer): - return 0 - - now = time.monotonic() - popped = 0 - for i, recent_send in enumerate(self.recent_sends[:]): - time_since = now-recent_send - if time_since >= THROTTLE_SECONDS: - self.recent_sends.pop(i-popped) - popped += 1 - - if len(self.recent_sends) < THROTTLE_LINES: - return 0 - - time_left = self.recent_sends[0]+THROTTLE_SECONDS - time_left = time_left-now - return time_left - - def send_user(self, username, realname): - self.send("USER %s 0 * :%s" % (username, realname)) - def send_nick(self, nickname): - self.send("NICK %s" % nickname) - - def send_capibility_ls(self): - self.send("CAP LS 302") - def queue_capability(self, capability): - self._capability_queue.add(capability) - def queue_capabilities(self, capabilities): - self._capability_queue.update(capabilities) - def send_capability_queue(self): - if self.has_capability_queue(): - capabilities = " ".join(self._capability_queue) - self._capability_queue.clear() - self.send_capability_request(capabilities) - def has_capability_queue(self): - return bool(len(self._capability_queue)) - def send_capability_request(self, capability): - self.send("CAP REQ :%s" % capability) - def send_capability_end(self): - self.send("CAP END") - def send_authenticate(self, text): - self.send("AUTHENTICATE %s" % text) - def send_starttls(self): - self.send("STARTTLS") - - def waiting_for_capabilities(self): - return bool(len(self._capabilities_waiting)) - def wait_for_capability(self, capability): - self._capabilities_waiting.add(capability) - def capability_done(self, capability): - self._capabilities_waiting.remove(capability) - if not self._capabilities_waiting: - self.send_capability_end() - - def send_pass(self, password): - self.send("PASS %s" % password) - - def send_ping(self, nonce="hello"): - self.send("PING :%s" % nonce) - def send_pong(self, nonce="hello"): - self.send("PONG :%s" % nonce) - - def try_rejoin(self, event): - if event["server_id"] == self.id and event["channel_name" - ] in self.attempted_join: - self.send_join(event["channel_name"], event["key"]) - def send_join(self, channel_name, key=None): - self.send("JOIN %s%s" % (channel_name, - "" if key == None else " %s" % key)) - def send_part(self, channel_name, reason=None): - self.send("PART %s%s" % (channel_name, - "" if reason == None else " %s" % reason)) - def send_quit(self, reason="Leaving"): - self.send("QUIT :%s" % reason) - - def send_message(self, target, message, prefix=None): - full_message = message if not prefix else prefix+message - self.send("PRIVMSG %s :%s" % (target, full_message)) - - action = full_message.startswith("\01ACTION " - ) and full_message.endswith("\01") - - if action: - message = full_message.split("\01ACTION ", 1)[1][:-1] - - full_message_split = full_message.split() - if self.has_channel(target): - channel = self.get_channel(target) - channel.buffer.add_line(None, message, action, True) - self.events.on("self.message.channel").call( - message=full_message, message_split=full_message_split, - channel=channel, action=action, server=self) - else: - user = self.get_user(target) - user.buffer.add_line(None, message, action, True) - self.events.on("self.message.private").call( - message=full_message, message_split=full_message_split, - user=user, action=action, server=self) - - def send_notice(self, target, message): - self.send("NOTICE %s :%s" % (target, message)) - - def send_mode(self, target, mode=None, args=None): - self.send("MODE %s%s%s" % (target, "" if mode == None else " %s" % mode, - "" if args == None else " %s" % args)) - - def send_topic(self, channel_name, topic): - self.send("TOPIC %s :%s" % (channel_name, topic)) - def send_kick(self, channel_name, target, reason=None): - self.send("KICK %s %s%s" % (channel_name, target, - "" if reason == None else " :%s" % reason)) - def send_names(self, channel_name): - self.send("NAMES %s" % channel_name) - def send_list(self, search_for=None): - self.send( - "LIST%s" % "" if search_for == None else " %s" % search_for) - def send_invite(self, target, channel_name): - self.send("INVITE %s %s" % (target, channel_name)) - - def send_whois(self, target): - self.send("WHOIS %s" % target) - def send_whowas(self, target, amount=None, server=None): - self.send("WHOWAS %s%s%s" % (target, - "" if amount == None else " %s" % amount, - "" if server == None else " :%s" % server)) - def send_who(self, filter=None): - self.send("WHO%s" % ("" if filter == None else " %s" % filter)) - def send_whox(self, mask, filter, fields, label=None): - self.send("WHO %s %s%%%s%s" % (mask, filter, fields, - ","+label if label else "")) diff --git a/IRCUser.py b/IRCUser.py deleted file mode 100644 index 02ecefa5..00000000 --- a/IRCUser.py +++ /dev/null @@ -1,62 +0,0 @@ -import uuid -import IRCBuffer, Utils - -class User(object): - def __init__(self, nickname, id, server, bot): - self.server = server - self.set_nickname(nickname) - self._id = id - self.username = None - self.hostname = None - self.realname = None - self.bot = bot - self.channels = set([]) - - self.identified_account = None - self.identified_account_override = None - - self.identified_account_id = None - self.identified_account_id_override = None - self.away = False - self.buffer = IRCBuffer.Buffer(bot, server) - - def __repr__(self): - return "IRCUser.User(%s|%s)" % (self.server.name, self.name) - - def get_id(self): - return (self.identified_account_id_override or - self.identified_account_id or self._id) - def get_identified_account(self): - return (self.identified_account_override or self.identified_account) - - def set_nickname(self, nickname): - self.nickname = nickname - self.nickname_lower = Utils.irc_lower(self.server, nickname) - self.name = self.nickname_lower - def join_channel(self, channel): - self.channels.add(channel) - def part_channel(self, channel): - self.channels.remove(channel) - def set_setting(self, setting, value): - self.bot.database.user_settings.set(self.get_id(), setting, value) - def get_setting(self, setting, default=None): - return self.bot.database.user_settings.get(self.get_id(), setting, - default) - def find_settings(self, pattern, default=[]): - return self.bot.database.user_settings.find(self.get_id(), pattern, - default) - def find_settings_prefix(self, prefix, default=[]): - return self.bot.database.user_settings.find_prefix(self.get_id(), - prefix, default) - def del_setting(self, setting): - self.bot.database.user_settings.delete(self.get_id(), setting) - def get_channel_settings_per_setting(self, setting, default=[]): - return self.bot.database.user_channel_settings.find_by_setting( - self.get_id(), setting, default) - - def send_message(self, message, prefix=None): - self.server.send_message(self.nickname, message, prefix=prefix) - def send_notice(self, message): - self.server.send_notice(self.nickname, message) - def send_ctcp_response(self, command, args): - self.send_notice("\x01%s %s\x01" % (command, args)) diff --git a/Logging.py b/Logging.py deleted file mode 100644 index 22d10711..00000000 --- a/Logging.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging, logging.handlers, sys, time - -class BitBotFormatter(logging.Formatter): - def formatTime(self, record, datefmt=None): - ct = self.converter(record.created) - if datefmt: - if "%f" in datefmt: - msec = "%03d" % record.msecs - datefmt = datefmt.replace("%f", msec) - s = time.strftime(datefmt, ct) - else: - t = time.strftime("%Y-%m-%d %H:%M:%S", ct) - s = "%s.%03d" % (t, record.msecs) - return s - -class Log(object): - def __init__(self, bot): - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.DEBUG) - - formatter = BitBotFormatter( - "%(asctime)s [%(levelname)s] %(message)s", - "%Y-%m-%dT%H:%M:%S.%fZ") - formatter.converter = time.gmtime - - stdout_handler = logging.StreamHandler(sys.stdout) - stdout_handler.setLevel(logging.INFO) - stdout_handler.setFormatter(formatter) - self.logger.addHandler(stdout_handler) - - file_handler = logging.handlers.TimedRotatingFileHandler( - "bot.log", when="midnight", backupCount=5) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(formatter) - self.logger.addHandler(file_handler) - - def debug(self, message, params, **kwargs): - self._log(message, params, logging.DEBUG, kwargs) - def info(self, message, params, **kwargs): - self._log(message, params, logging.INFO, kwargs) - def warn(self, message, params, **kwargs): - self._log(message, params, logging.WARN, kwargs) - def error(self, message, params, **kwargs): - self._log(message, params, logging.ERROR, kwargs) - def critical(self, message, params, **kwargs): - self._log(message, params, logging.CRITICAL, kwargs) - def _log(self, message, params, level, kwargs): - self.logger.log(level, message, *params, **kwargs) diff --git a/ModuleManager.py b/ModuleManager.py deleted file mode 100644 index a9d9f7d4..00000000 --- a/ModuleManager.py +++ /dev/null @@ -1,147 +0,0 @@ -import glob, imp, inspect, os, sys, uuid - -BITBOT_HOOKS_MAGIC = "__bitbot_hooks" - -class ModuleException(Exception): - pass -class ModuleWarning(Exception): - pass - -class ModuleNotFoundException(ModuleException): - pass -class ModuleNameCollisionException(ModuleException): - pass -class ModuleLoadException(ModuleException): - pass -class ModuleUnloadException(ModuleException): - pass - -class ModuleNotLoadedWarning(ModuleWarning): - pass - -class BaseModule(object): - def __init__(self, bot, events, exports): - pass - -class ModuleManager(object): - def __init__(self, bot, events, exports, directory="modules"): - self.bot = bot - self.events = events - self.exports = exports - self.directory = directory - self.modules = {} - self.waiting_requirement = {} - def list_modules(self): - return sorted(glob.glob(os.path.join(self.directory, "*.py"))) - - def _module_name(self, path): - return os.path.basename(path).rsplit(".py", 1)[0].lower() - def _module_path(self, name): - return os.path.join(self.directory, "%s.py" % name) - - def _load_module(self, name): - path = self._module_path(name) - - with open(path) as module_file: - while True: - line = module_file.readline().strip() - line_split = line.split(" ") - if line and line.startswith("#--"): - # this is a hashflag - if line == "#--ignore": - # nope, ignore this module. - raise ModuleNotLoadedWarning("module ignored") - elif line_split[0] == "#--require-config" and len( - line_split) > 1: - if not line_split[1].lower() in self.bot.config or not self.bot.config[ - line_split[1].lower()]: - # nope, required config option not present. - raise ModuleNotLoadedWarning( - "required config not present") - elif line_split[0] == "#--require-module" and len( - line_split) > 1: - if not "bitbot_%s" % line_split[1].lower() in sys.modules: - if not line_split[1].lower() in self.waiting_requirement: - self.waiting_requirement[line_split[1].lower()] = set([]) - self.waiting_requirement[line_split[1].lower()].add(path) - raise ModuleNotLoadedWarning( - "waiting for requirement") - else: - break - module = imp.load_source(name, path) - - if not hasattr(module, "Module"): - raise ModuleLoadException("module '%s' doesn't have a " - "'Module' class.") - if not inspect.isclass(module.Module): - raise ModuleLoadException("module '%s' has a 'Module' attribute " - "but it is not a class.") - - context = str(uuid.uuid4()) - context_events = self.events.new_context(context) - context_exports = self.exports.new_context(context) - module_object = module.Module(self.bot, context_events, - context_exports) - - if not hasattr(module_object, "_name"): - module_object._name = name.title() - for attribute_name in dir(module_object): - attribute = getattr(module_object, attribute_name) - if inspect.ismethod(attribute) and hasattr(attribute, - BITBOT_HOOKS_MAGIC): - hooks = getattr(attribute, BITBOT_HOOKS_MAGIC) - for hook in hooks: - context_events.on(hook["event"]).hook(attribute, - **hook["kwargs"]) - - module_object._context = context - module_object._import_name = name - - assert not module_object._name in self.modules, ( - "module name '%s' attempted to be used twice.") - return module_object - - def load_module(self, name): - try: - module = self._load_module(name) - except ModuleWarning as warning: - self.bot.log.error("Module '%s' not loaded", [name]) - raise - except Exception as e: - self.bot.log.error("Failed to load module \"%s\": %s", - [name, e.msg]) - raise - - self.modules[module._import_name] = module - if name in self.waiting_requirement: - for requirement_name in self.waiting_requirement: - self.load_module(requirement_name) - self.bot.log.info("Module '%s' loaded", [name]) - - def load_modules(self, whitelist=[], blacklist=[]): - for path in self.list_modules(): - name = self._module_name(path) - if name in whitelist or (not whitelist and not name in blacklist): - try: - self.load_module(name) - except ModuleWarning: - pass - - def unload_module(self, name): - if not name in self.modules: - raise ModuleNotFoundException() - module = self.modules[name] - del self.modules[name] - - context = module._context - self.events.purge_context(context) - self.exports.purge_context(context) - - del sys.modules[name] - references = sys.getrefcount(module) - del module - references -= 1 # 'del module' removes one reference - references -= 1 # one of the refs is from getrefcount - - self.bot.log.info("Module '%s' unloaded (%d reference%s)", - [name, references, "" if references == 1 else "s"]) diff --git a/Timer.py b/Timer.py deleted file mode 100644 index 7ac83630..00000000 --- a/Timer.py +++ /dev/null @@ -1,39 +0,0 @@ -import time, uuid - -class Timer(object): - def __init__(self, id, bot, events, event_name, delay, - next_due=None, **kwargs): - self.id = id - self.bot = bot - self.events = events - self.event_name = event_name - self.delay = delay - if next_due: - self.next_due = next_due - else: - self.set_next_due() - self.kwargs = kwargs - self._done = False - self.call_count = 0 - - def set_next_due(self): - self.next_due = time.time()+self.delay - - def due(self): - return self.time_left() <= 0 - - def time_left(self): - return self.next_due-time.time() - - def call(self): - self._done = True - self.call_count +=1 - self.events.on("timer").on(self.event_name).call( - timer=self, **self.kwargs) - - def redo(self): - self._done = False - self.set_next_due() - - def done(self): - return self._done diff --git a/Utils.py b/Utils.py deleted file mode 100644 index 40445606..00000000 --- a/Utils.py +++ /dev/null @@ -1,287 +0,0 @@ -import json, re, traceback, urllib.request, urllib.parse, urllib.error, ssl -import string -import bs4 -import ModuleManager - -USER_AGENT = ("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 " - "(KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36") -REGEX_HTTP = re.compile("https?://", re.I) -ASCII_UPPER = string.ascii_uppercase -ASCII_LOWER = string.ascii_lowercase -STRICT_RFC1459_UPPER = ASCII_UPPER+r'\[]' -STRICT_RFC1459_LOWER = ASCII_LOWER+r'|{}' -RFC1459_UPPER = STRICT_RFC1459_UPPER+"^" -RFC1459_LOWER = STRICT_RFC1459_LOWER+"~" - -def remove_colon(s): - if s.startswith(":"): - s = s[1:] - return s - -def arbitrary(s, n): - return remove_colon(" ".join(s[n:])) - -# case mapping lowercase/uppcase logic -def _multi_replace(s, chars1, chars2): - for char1, char2 in zip(chars1, chars2): - s = s.replace(char1, char2) - return s -def irc_lower(server, s): - if server.case_mapping == "ascii": - return _multi_replace(s, ASCII_UPPER, ASCII_LOWER) - elif server.case_mapping == "rfc1459": - return _multi_replace(s, RFC1459_UPPER, RFC1459_LOWER) - elif server.case_mapping == "strict-rfc1459": - return _multi_replace(s, STRICT_RFC1459_UPPER, STRICT_RFC1459_LOWER) - else: - raise ValueError("unknown casemapping '%s'" % server.case_mapping) - -# compare a string while respecting case mapping -def irc_equals(server, s1, s2): - return irc_lower(server, s1) == irc_lower(server, s2) - -class IRCHostmask(object): - def __init__(self, nickname, username, hostname, hostmask): - self.nickname = nickname - self.username = username - self.hostname = hostname - self.hostmask = hostmask - def __repr__(self): - return "Utils.IRCHostmask(%s)" % self.__str__() - def __str__(self): - return self.hostmask - -def seperate_hostmask(hostmask): - hostmask = remove_colon(hostmask) - first_delim = hostmask.find("!") - second_delim = hostmask.find("@") - nickname, username, hostname = hostmask, None, None - if first_delim > -1 and second_delim > first_delim: - nickname, username = hostmask.split("!", 1) - username, hostname = username.split("@", 1) - elif second_delim > -1: - nickname, hostname = hostmask.split("@", 1) - return IRCHostmask(nickname, username, hostname, hostmask) - -def get_url(url, **kwargs): - if not urllib.parse.urlparse(url).scheme: - url = "http://%s" % url - url_parsed = urllib.parse.urlparse(url) - - method = kwargs.get("method", "GET") - get_params = kwargs.get("get_params", "") - post_params = kwargs.get("post_params", None) - headers = kwargs.get("headers", {}) - if get_params: - get_params = "?%s" % urllib.parse.urlencode(get_params) - if post_params: - post_params = urllib.parse.urlencode(post_params).encode("utf8") - url = "%s%s" % (url, get_params) - try: - url.encode("latin-1") - except UnicodeEncodeError: - if kwargs.get("code"): - return 0, False - return False - - request = urllib.request.Request(url, post_params) - request.add_header("Accept-Language", "en-US") - request.add_header("User-Agent", USER_AGENT) - for header, value in headers.items(): - request.add_header(header, value) - request.method = method - - try: - response = urllib.request.urlopen(request, timeout=5) - except urllib.error.HTTPError as e: - traceback.print_exc() - if kwargs.get("code"): - return e.code, False - return False - except urllib.error.URLError as e: - traceback.print_exc() - if kwargs.get("code"): - return -1, False - return False - except ssl.CertificateError as e: - traceback.print_exc() - if kwargs.get("code"): - return -1, False, - return False - - response_content = response.read() - encoding = response.info().get_content_charset() - if kwargs.get("soup"): - return bs4.BeautifulSoup(response_content, kwargs.get("parser", "lxml")) - if not encoding: - soup = bs4.BeautifulSoup(response_content, kwargs.get("parser", "lxml")) - metas = soup.find_all("meta") - for meta in metas: - if "charset=" in meta.get("content", ""): - encoding = meta.get("content").split("charset=", 1)[1 - ].split(";", 1)[0] - elif meta.get("charset", ""): - encoding = meta.get("charset") - else: - continue - break - if not encoding: - for item in soup.contents: - if isinstance(item, bs4.Doctype): - if item == "html": - encoding = "utf8" - else: - encoding = "latin-1" - break - response_content = response_content.decode(encoding or "utf8") - data = response_content - if kwargs.get("json") and data: - try: - data = json.loads(response_content) - except json.decoder.JSONDecodeError: - traceback.print_exc() - return False - if kwargs.get("code"): - return response.code, data - else: - return data - -COLOR_WHITE, COLOR_BLACK, COLOR_BLUE, COLOR_GREEN = 0, 1, 2, 3 -COLOR_RED, COLOR_BROWN, COLOR_PURPLE, COLOR_ORANGE = 4, 5, 6, 7 -COLOR_YELLOW, COLOR_LIGHTGREEN, COLOR_CYAN, COLOR_LIGHTCYAN = (8, 9, - 10, 11) -COLOR_LIGHTBLUE, COLOR_PINK, COLOR_GREY, COLOR_LIGHTGREY = (12, 13, - 14, 15) -FONT_BOLD, FONT_ITALIC, FONT_UNDERLINE, FONT_INVERT = ("\x02", "\x1D", - "\x1F", "\x16") -FONT_COLOR, FONT_RESET = "\x03", "\x0F" - -def color(s, foreground, background=None): - foreground = str(foreground).zfill(2) - if background: - background = str(background).zfill(2) - return "%s%s%s%s%s" % (FONT_COLOR, foreground, - "" if not background else ",%s" % background, s, FONT_COLOR) - -def bold(s): - return "%s%s%s" % (FONT_BOLD, s, FONT_BOLD) - -def underline(s): - return "%s%s%s" % (FONT_UNDERLINE, s, FONT_UNDERLINE) - -TIME_SECOND = 1 -TIME_MINUTE = TIME_SECOND*60 -TIME_HOUR = TIME_MINUTE*60 -TIME_DAY = TIME_HOUR*24 -TIME_WEEK = TIME_DAY*7 - -def time_unit(seconds): - since = None - unit = None - if seconds >= TIME_WEEK: - since = seconds/TIME_WEEK - unit = "week" - elif seconds >= TIME_DAY: - since = seconds/TIME_DAY - unit = "day" - elif seconds >= TIME_HOUR: - since = seconds/TIME_HOUR - unit = "hour" - elif seconds >= TIME_MINUTE: - since = seconds/TIME_MINUTE - unit = "minute" - else: - since = seconds - unit = "second" - since = int(since) - if since > 1: - unit = "%ss" % unit # pluralise the unit - return [since, unit] - -REGEX_PRETTYTIME = re.compile("\d+[wdhms]", re.I) - -SECONDS_MINUTES = 60 -SECONDS_HOURS = SECONDS_MINUTES*60 -SECONDS_DAYS = SECONDS_HOURS*24 -SECONDS_WEEKS = SECONDS_DAYS*7 - -def from_pretty_time(pretty_time): - seconds = 0 - for match in re.findall(REGEX_PRETTYTIME, pretty_time): - number, unit = int(match[:-1]), match[-1].lower() - if unit == "m": - number = number*SECONDS_MINUTES - elif unit == "h": - number = number*SECONDS_HOURS - elif unit == "d": - number = number*SECONDS_DAYS - elif unit == "w": - number = number*SECONDS_WEEKS - seconds += number - if seconds > 0: - return seconds - -UNIT_SECOND = 5 -UNIT_MINUTE = 4 -UNIT_HOUR = 3 -UNIT_DAY = 2 -UNIT_WEEK = 1 -def to_pretty_time(total_seconds, minimum_unit=UNIT_SECOND, max_units=6): - minutes, seconds = divmod(total_seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - weeks, days = divmod(days, 7) - out = "" - - units = 0 - if weeks and minimum_unit >= UNIT_WEEK and units < max_units: - out += "%dw" % weeks - units += 1 - if days and minimum_unit >= UNIT_DAY and units < max_units: - out += "%dd" % days - units += 1 - if hours and minimum_unit >= UNIT_HOUR and units < max_units: - out += "%dh" % hours - units += 1 - if minutes and minimum_unit >= UNIT_MINUTE and units < max_units: - out += "%dm" % minutes - units += 1 - if seconds and minimum_unit >= UNIT_SECOND and units < max_units: - out += "%ds" % seconds - units += 1 - return out - -IS_TRUE = ["true", "yes", "on", "y"] -IS_FALSE = ["false", "no", "off", "n"] -def bool_or_none(s): - s = s.lower() - if s in IS_TRUE: - return True - elif s in IS_FALSE: - return False -def int_or_none(s): - stripped_s = s.lstrip("0") - if stripped_s.isdigit(): - return int(stripped_s) - -def get_closest_setting(event, setting, default=None): - server = event["server"] - if "channel" in event: - closest = event["channel"] - elif "target" in event and "is_channel" in event and event["is_channel"]: - closest = event["target"] - else: - closest = event["user"] - return closest.get_setting(setting, server.get_setting(setting, default)) - -def prevent_highlight(nickname): - return nickname[0]+"\u200d"+nickname[1:] - -def hook(event, **kwargs): - def _hook_func(func): - if not hasattr(func, ModuleManager.BITBOT_HOOKS_MAGIC): - setattr(func, ModuleManager.BITBOT_HOOKS_MAGIC, []) - getattr(func, ModuleManager.BITBOT_HOOKS_MAGIC).append( - {"event": event, "kwargs": kwargs}) - return func - return _hook_func diff --git a/modules/8ball.py b/modules/8ball.py index 710ab448..37d38743 100644 --- a/modules/8ball.py +++ b/modules/8ball.py @@ -1,5 +1,5 @@ import random -import Utils +from src import Utils CHOICES = [ "Definitely", diff --git a/modules/accept_invite.py b/modules/accept_invite.py index e79cbfe9..40888f8a 100644 --- a/modules/accept_invite.py +++ b/modules/accept_invite.py @@ -1,4 +1,4 @@ -import Utils +from src import Utils class Module(object): def __init__(self, bot, events, exports): diff --git a/modules/auto_mode.py b/modules/auto_mode.py index cfb7af94..af6dc606 100644 --- a/modules/auto_mode.py +++ b/modules/auto_mode.py @@ -1,4 +1,4 @@ -import Utils +from src import Utils class Module(object): _name = "AutoMode" diff --git a/modules/bitcoin.py b/modules/bitcoin.py index 5be8bb96..c85e6bf1 100644 --- a/modules/bitcoin.py +++ b/modules/bitcoin.py @@ -1,4 +1,4 @@ -import Utils +from src import Utils class Module(object): _name = "BTC" diff --git a/modules/books.py b/modules/books.py index 409c8613..e0569a76 100644 --- a/modules/books.py +++ b/modules/books.py @@ -1,5 +1,5 @@ import json, re -import Utils +from src import Utils URL_GOOGLEBOOKS = "https://www.googleapis.com/books/v1/volumes" URL_BOOKINFO = "https://books.google.co.uk/books?id=%s" diff --git a/modules/channel_op.py b/modules/channel_op.py index 1cdc4e15..ad2233d9 100644 --- a/modules/channel_op.py +++ b/modules/channel_op.py @@ -1,4 +1,4 @@ -import Utils +from src import Utils class Module(object): _name = "Channel Op" diff --git a/modules/check_urls.py b/modules/check_urls.py index 221f548d..fa05695e 100644 --- a/modules/check_urls.py +++ b/modules/check_urls.py @@ -1,7 +1,7 @@ #--require-config virustotal-api-key import re -import Utils +from src import Utils URL_VIRUSTOTAL = "https://www.virustotal.com/vtapi/v2/url/report" RE_URL = re.compile(r"https?://\S+", re.I) diff --git a/modules/coins.py b/modules/coins.py index fef48258..e66aff6c 100644 --- a/modules/coins.py +++ b/modules/coins.py @@ -1,5 +1,5 @@ import datetime, decimal, math, random, re, time -import Utils +from src import Utils SIDES = {"heads": 0, "tails": 1} DEFAULT_REDEEM_DELAY = 600 # 600 seconds, 10 minutes diff --git a/modules/commands.py b/modules/commands.py index 9df70ffc..02787791 100644 --- a/modules/commands.py +++ b/modules/commands.py @@ -1,5 +1,5 @@ import re -import EventManager, Utils +from src import EventManager, Utils STR_MORE = "%s (more...)" % Utils.FONT_RESET STR_CONTINUED = "(...continued) " diff --git a/modules/ctcp.py b/modules/ctcp.py index 70bb4d25..a9144974 100644 --- a/modules/ctcp.py +++ b/modules/ctcp.py @@ -1,5 +1,5 @@ import datetime -import Utils +from src import Utils class Module(object): def __init__(self, bot, events, exports): diff --git a/modules/define.py b/modules/define.py index bbc37f70..1b88e83b 100644 --- a/modules/define.py +++ b/modules/define.py @@ -1,7 +1,7 @@ #--require-config wordnik-api-key -import Utils import time +from src import Utils URL_WORDNIK = "https://api.wordnik.com/v4/word.json/%s/definitions" URL_WORDNIK_RANDOM = "https://api.wordnik.com/v4/words.json/randomWord" diff --git a/modules/dice.py b/modules/dice.py index 1b1fc0ab..30e4f319 100644 --- a/modules/dice.py +++ b/modules/dice.py @@ -1,5 +1,5 @@ import random -import Utils +from src import Utils class Module(object): def __init__(self, bot, events, exports): diff --git a/modules/ducks.py b/modules/ducks.py index c2ae696c..a9c4f6b5 100644 --- a/modules/ducks.py +++ b/modules/ducks.py @@ -1,9 +1,7 @@ import random from operator import itemgetter from time import time -import EventManager - -import Utils +from src import EventManager, Utils DUCK_TAIL = "・゜゜・。。・゜゜" DUCK_HEAD = ["\_o< ", "\_O< ", "\_0< ", "\_\u00f6< ", "\_\u00f8< ", diff --git a/modules/eval.py b/modules/eval.py index f63c0297..ab68a368 100644 --- a/modules/eval.py +++ b/modules/eval.py @@ -1,5 +1,5 @@ import socket -import Utils +from src import Utils EVAL_URL = "https://eval.appspot.com/eval" diff --git a/modules/geoip.py b/modules/geoip.py index 5eb6f017..5bc464b1 100644 --- a/modules/geoip.py +++ b/modules/geoip.py @@ -1,4 +1,4 @@ -import Utils +from src import Utils URL_GEOIP = "http://ip-api.com/json/%s" diff --git a/modules/google.py b/modules/google.py index 986a7073..42acf954 100644 --- a/modules/google.py +++ b/modules/google.py @@ -1,7 +1,7 @@ #--require-config google-api-key #--require-config google-search-id -import Utils +from src import Utils URL_GOOGLESEARCH = "https://www.googleapis.com/customsearch/v1" diff --git a/modules/haveibeenpwned.py b/modules/haveibeenpwned.py index ce2b788d..79662913 100644 --- a/modules/haveibeenpwned.py +++ b/modules/haveibeenpwned.py @@ -1,4 +1,4 @@ -import Utils +from src import Utils URL_HAVEIBEENPWNEDAPI = "https://haveibeenpwned.com/api/v2/breachedaccount/%s" URL_HAVEIBEENPWNED = "https://haveibeenpwned.com/" diff --git a/modules/imdb.py b/modules/imdb.py index 1e668523..741b2955 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -1,7 +1,7 @@ #--require-config omdbapi-api-key import json -import Utils +from src import Utils URL_OMDB = "http://www.omdbapi.com/" URL_IMDBTITLE = "http://imdb.com/title/%s" diff --git a/modules/in.py b/modules/in.py index 3a1ddb87..2351fb11 100644 --- a/modules/in.py +++ b/modules/in.py @@ -1,5 +1,5 @@ import time -import Utils +from src import Utils SECONDS_MAX = Utils.SECONDS_WEEKS*8 SECONDS_MAX_DESCRIPTION = "8 weeks" diff --git a/modules/isgd.py b/modules/isgd.py index 86af7447..3ceacaff 100644 --- a/modules/isgd.py +++ b/modules/isgd.py @@ -1,5 +1,5 @@ import re -import Utils +from src import Utils ISGD_API_URL = "https://is.gd/create.php" REGEX_URL = re.compile("https?://", re.I) diff --git a/modules/karma.py b/modules/karma.py index 6f6dc8a3..897f0ea2 100644 --- a/modules/karma.py +++ b/modules/karma.py @@ -1,5 +1,5 @@ import re, time -import EventManager, Utils +from src import EventManager, Utils REGEX_KARMA = re.compile("^(.*[^-+])[-+]*(\+{2,}|\-{2,})$") KARMA_DELAY_SECONDS = 3 diff --git a/modules/lastfm.py b/modules/lastfm.py index 68b6249b..d46bc50b 100644 --- a/modules/lastfm.py +++ b/modules/lastfm.py @@ -1,7 +1,7 @@ #--require-config lastfm-api-key -import Utils from datetime import datetime, timezone +from src import Utils URL_SCROBBLER = "http://ws.audioscrobbler.com/2.0/" diff --git a/modules/modules.py b/modules/modules.py index 0d4844e9..91bc02a7 100644 --- a/modules/modules.py +++ b/modules/modules.py @@ -1,4 +1,4 @@ -import ModuleManager +from src import ModuleManager class Module(object): def __init__(self, bot, events, exports): diff --git a/modules/nickserv.py b/modules/nickserv.py index 031c81d9..5ba2a992 100644 --- a/modules/nickserv.py +++ b/modules/nickserv.py @@ -1,5 +1,5 @@ import base64 -import EventManager +from src import EventManager class Module(object): def __init__(self, bot, events, exports): diff --git a/modules/nr.py b/modules/nr.py index 0958967d..e1b605cd 100644 --- a/modules/nr.py +++ b/modules/nr.py @@ -2,7 +2,7 @@ import collections, re, time from datetime import datetime, date from collections import Counter -import Utils +from src import Utils from suds.client import Client from suds import WebFault diff --git a/modules/perform.py b/modules/perform.py index 5245cadf..f5572fdc 100644 --- a/modules/perform.py +++ b/modules/perform.py @@ -1,4 +1,4 @@ -import EventManager +from src import EventManager class Module(object): def __init__(self, bot, events, exports): diff --git a/modules/pong.py b/modules/pong.py index fc12d0c2..c45e3aef 100644 --- a/modules/pong.py +++ b/modules/pong.py @@ -1,4 +1,4 @@ -import ModuleManager, Utils +from src import ModuleManager, Utils class Module(ModuleManager.BaseModule): @Utils.hook("received.command.ping", help="Ping pong!") diff --git a/modules/print_activity.py b/modules/print_activity.py index 4d2b90a2..69c8d1b3 100644 --- a/modules/print_activity.py +++ b/modules/print_activity.py @@ -1,5 +1,5 @@ import datetime -import EventManager +from src import EventManager class Module(object): def __init__(self, bot, events, exports): diff --git a/modules/sed.py b/modules/sed.py index e4a61c70..c9ef778d 100644 --- a/modules/sed.py +++ b/modules/sed.py @@ -1,5 +1,5 @@ import re, traceback -import Utils +from src import Utils REGEX_SPLIT = re.compile("(? self.max_lines: + self.lines.pop() + self._skip_next = False + def get(self, index=0, **kwargs): + from_self = kwargs.get("from_self", True) + for line in self.lines: + if line.from_self and not from_self: + continue + return line + def find(self, pattern, **kwargs): + from_self = kwargs.get("from_self", True) + for_user = kwargs.get("for_user", "") + for_user = Utils.irc_lower(self.server, for_user + ) if for_user else None + not_pattern = kwargs.get("not_pattern", None) + for line in self.lines: + if line.from_self and not from_self: + continue + elif re.search(pattern, line.message): + if not_pattern and re.search(not_pattern, line.message): + continue + if for_user and not Utils.irc_lower(self.server, line.sender + ) == for_user: + continue + return line + def skip_next(self): + self._skip_next = True diff --git a/src/IRCChannel.py b/src/IRCChannel.py new file mode 100644 index 00000000..31c3ee19 --- /dev/null +++ b/src/IRCChannel.py @@ -0,0 +1,132 @@ +import uuid +from . import IRCBuffer, Utils + +class Channel(object): + def __init__(self, name, id, server, bot): + self.name = Utils.irc_lower(server, name) + self.id = id + self.server = server + self.bot = bot + self.topic = "" + self.topic_setter_nickname = None + self.topic_setter_username = None + self.topic_setter_hostname = None + self.topic_time = 0 + self.users = set([]) + self.modes = {} + self.created_timestamp = None + self.buffer = IRCBuffer.Buffer(bot, server) + + def __repr__(self): + return "IRCChannel.Channel(%s|%s)" % (self.server.name, self.name) + + def set_topic(self, topic): + self.topic = topic + def set_topic_setter(self, nickname, username=None, hostname=None): + self.topic_setter_nickname = nickname + self.topic_setter_username = username + self.topic_setter_hostname = hostname + def set_topic_time(self, unix_timestamp): + self.topic_time = unix_timestamp + + def add_user(self, user): + self.users.add(user) + def remove_user(self, user): + self.users.remove(user) + for mode in list(self.modes.keys()): + if mode in self.server.mode_prefixes.values( + ) and user in self.modes[mode]: + self.modes[mode].discard(user) + if not len(self.modes[mode]): + del self.modes[mode] + def has_user(self, user): + return user in self.users + + def add_mode(self, mode, arg=None): + if not mode in self.modes: + self.modes[mode] = set([]) + if arg: + if mode in self.server.mode_prefixes.values(): + user = self.server.get_user(arg) + if user: + self.modes[mode].add(user) + else: + self.modes[mode].add(arg.lower()) + def remove_mode(self, mode, arg=None): + if not arg: + del self.modes[mode] + else: + if mode in self.server.mode_prefixes.values(): + user = self.server.get_user(arg) + if user: + self.modes[mode].discard(user) + else: + self.modes[mode].discard(arg.lower()) + if not len(self.modes[mode]): + del self.modes[mode] + def change_mode(self, remove, mode, arg=None): + if remove: + self.remove_mode(mode, arg) + else: + self.add_mode(mode, arg) + + def set_setting(self, setting, value): + self.bot.database.channel_settings.set(self.id, setting, value) + def get_setting(self, setting, default=None): + return self.bot.database.channel_settings.get(self.id, setting, + default) + def find_settings(self, pattern, default=[]): + return self.bot.database.channel_settings.find(self.id, pattern, + default) + def find_settings_prefix(self, prefix, default=[]): + return self.bot.database.channel_settings.find_prefix(self.id, + prefix, default) + def del_setting(self, setting): + self.bot.database.channel_settings.delete(self.id, setting) + + def set_user_setting(self, user_id, setting, value): + self.bot.database.user_channel_settings.set(user_id, self.id, + setting, value) + def get_user_setting(self, user_id, setting, default=None): + return self.bot.database.user_channel_settings.get(user_id, + self.id, setting, default) + def find_user_settings(self, user_i, pattern, default=[]): + return self.bot.database.user_channel_settings.find(user_id, + self.id, pattern, default) + def find_user_settings_prefix(self, user_id, prefix, default=[]): + return self.bot.database.user_channel_settings.find_prefix( + user_id, self.id, prefix, default) + def del_user_setting(self, user_id, setting): + self.bot.database.user_channel_settings.delete(user_id, self.id, + setting) + def find_all_by_setting(self, setting, default=[]): + return self.bot.database.user_channel_settings.find_all_by_setting( + self.id, setting, default) + + def send_message(self, text, prefix=None): + self.server.send_message(self.name, text, prefix=prefix) + def send_mode(self, mode=None, target=None): + self.server.send_mode(self.name, mode, target) + def send_kick(self, target, reason=None): + self.server.send_kick(self.name, target, reason) + def send_ban(self, hostmask): + self.server.send_mode(self.name, "+b", hostmask) + def send_unban(self, hostmask): + self.server.send_mode(self.name, "-b", hostmask) + def send_topic(self, topic): + self.server.send_topic(self.name, topic) + + def mode_or_above(self, user, mode): + mode_orders = list(self.server.mode_prefixes.values()) + mode_index = mode_orders.index(mode) + for mode in mode_orders[:mode_index+1]: + if user in self.modes.get(mode, []): + return True + return False + + def get_user_status(self, user): + modes = "" + for mode in self.server.mode_prefixes.values(): + if user in self.modes.get(mode, []): + modes += mode + return modes diff --git a/src/IRCLineHandler.py b/src/IRCLineHandler.py new file mode 100644 index 00000000..7d2336dc --- /dev/null +++ b/src/IRCLineHandler.py @@ -0,0 +1,593 @@ +import re, threading +from . import Utils + +RE_PREFIXES = re.compile(r"\bPREFIX=\((\w+)\)(\W+)(?:\b|$)") +RE_CHANMODES = re.compile( + r"\bCHANMODES=(\w*),(\w*),(\w*),(\w*)(?:\b|$)") +RE_CHANTYPES = re.compile(r"\bCHANTYPES=(\W+)(?:\b|$)") +RE_CASEMAPPING = re.compile(r"\bCASEMAPPING=(\S+)") +RE_MODES = re.compile(r"[-+]\w+") + +CAPABILITIES = {"multi-prefix", "chghost", "invite-notify", "account-tag", + "account-notify", "extended-join", "away-notify", "userhost-in-names", + "draft/message-tags-0.2", "server-time", "cap-notify", + "batch", "draft/labeled-response"} + +class LineHandler(object): + def __init__(self, bot, events): + self.bot = bot + self.events = events + events.on("raw.PING").hook(self.ping) + + events.on("raw.001").hook(self.handle_001, default_event=True) + events.on("raw.005").hook(self.handle_005) + events.on("raw.311").hook(self.handle_311, default_event=True) + events.on("raw.332").hook(self.handle_332) + events.on("raw.333").hook(self.handle_333) + events.on("raw.353").hook(self.handle_353, default_event=True) + events.on("raw.366").hook(self.handle_366, default_event=True) + events.on("raw.421").hook(self.handle_421, default_event=True) + events.on("raw.352").hook(self.handle_352, default_event=True) + events.on("raw.354").hook(self.handle_354, default_event=True) + events.on("raw.324").hook(self.handle_324, default_event=True) + events.on("raw.329").hook(self.handle_329, default_event=True) + events.on("raw.433").hook(self.handle_433, default_event=True) + events.on("raw.477").hook(self.handle_477, default_event=True) + + events.on("raw.JOIN").hook(self.join) + events.on("raw.PART").hook(self.part) + events.on("raw.QUIT").hook(self.quit) + events.on("raw.NICK").hook(self.nick) + events.on("raw.MODE").hook(self.mode) + events.on("raw.KICK").hook(self.kick) + events.on("raw.INVITE").hook(self.invite) + events.on("raw.TOPIC").hook(self.topic) + events.on("raw.PRIVMSG").hook(self.privmsg) + events.on("raw.NOTICE").hook(self.notice) + + events.on("raw.CAP").hook(self.cap) + events.on("raw.AUTHENTICATE").hook(self.authenticate) + events.on("raw.CHGHOST").hook(self.chghost) + events.on("raw.ACCOUNT").hook(self.account) + events.on("raw.TAGMSG").hook(self.tagmsg) + events.on("raw.AWAY").hook(self.away) + events.on("raw.BATCH").hook(self.batch) + + def handle(self, server, line): + original_line = line + tags = {} + prefix = None + command = None + + if line[0] == "@": + tags_prefix, line = line[1:].split(" ", 1) + for tag in tags_prefix.split(";"): + if tag: + tag_split = tag.split("=", 1) + tags[tag_split[0]] = "".join(tag_split[1:]) + if "batch" in tags and tags["batch"] in server.batches: + server.batches[tag["batch"]].append(line) + return + + arbitrary = None + if " :" in line: + line, arbitrary = line.split(" :", 1) + if line.endswith(" "): + line = line[:-1] + if line[0] == ":": + prefix, command = line[1:].split(" ", 1) + prefix = Utils.seperate_hostmask(prefix) + if " " in command: + command, line = command.split(" ", 1) + else: + line = "" + else: + command = line + if " " in line: + command, line = line.split(" ", 1) + args = line.split(" ") + + hooks = self.events.on("raw").on(command).get_hooks() + default_event = False + for hook in hooks: + if hook.kwargs.get("default_event", False): + default_event = True + break + last = arbitrary or args[-1] + + #server, prefix, command, args, arbitrary + self.events.on("raw").on(command).call(server=server, last=last, + prefix=prefix, args=args, arbitrary=arbitrary, tags=tags) + if default_event or not hooks: + if command.isdigit(): + self.events.on("received.numeric").on(command).call( + line=original_line, server=server, tags=tags, last=last, + line_split=original_line.split(" "), number=command) + else: + self.events.on("received").on(command).call( + line=original_line, line_split=original_line.split(" "), + command=command, server=server, tags=tags, last=last) + + # ping from the server + def ping(self, event): + event["server"].send_pong(event["last"]) + + # first numeric line the server sends + def handle_001(self, event): + event["server"].name = event["prefix"].nickname + event["server"].set_own_nickname(event["args"][0]) + event["server"].send_whois(event["server"].nickname) + + # server telling us what it supports + def handle_005(self, event): + isupport_line = " ".join(event["args"][1:]) + + if "NAMESX" in isupport_line: + event["server"].send("PROTOCTL NAMESX") + + match = re.search(RE_PREFIXES, isupport_line) + if match: + event["server"].mode_prefixes.clear() + modes = match.group(1) + prefixes = match.group(2) + for i, prefix in enumerate(prefixes): + if i < len(modes): + event["server"].mode_prefixes[prefix] = modes[i] + match = re.search(RE_CHANMODES, isupport_line) + if match: + event["server"].channel_modes = list(match.group(4)) + match = re.search(RE_CHANTYPES, isupport_line) + if match: + event["server"].channel_types = list(match.group(1)) + + match = re.search(RE_CASEMAPPING, isupport_line) + if match: + event["server"].case_mapping = match.group(1) + + self.events.on("received.numeric.005").call( + isupport=isupport_line, server=event["server"]) + + # whois respose (nickname, username, realname, hostname) + def handle_311(self, event): + nickname = event["args"][1] + if event["server"].is_own_nickname(nickname): + target = event["server"] + else: + target = event["server"].get_user(nickname) + target.username = event["args"][2] + target.hostname = event["args"][3] + target.realname = event["arbitrary"] + + # on-join channel topic line + def handle_332(self, event): + channel = event["server"].get_channel(event["args"][1]) + + channel.set_topic(event["arbitrary"]) + self.events.on("received.numeric.332").call(channel=channel, + server=event["server"], topic=event["arbitrary"]) + + # channel topic changed + def topic(self, event): + user = event["server"].get_user(event["prefix"].nickname) + channel = event["server"].get_channel(event["args"][0]) + channel.set_topic(event["arbitrary"]) + self.events.on("received.topic").call(channel=channel, + server=event["server"], topic=event["arbitrary"], user=user) + + # on-join channel topic set by/at + def handle_333(self, event): + channel = event["server"].get_channel(event["args"][1]) + + topic_setter_hostmask = event["args"][2] + topic_setter = Utils.seperate_hostmask(topic_setter_hostmask) + topic_time = int(event["args"][3]) if event["args"][3].isdigit( + ) else None + + channel.set_topic_setter(topic_setter.nickname, topic_setter.username, + topic_setter.hostname) + channel.set_topic_time(topic_time) + self.events.on("received.numeric.333").call(channel=channel, + setter=topic_setter.nickname, set_at=topic_time, + server=event["server"]) + + # /names response, also on-join user list + def handle_353(self, event): + channel = event["server"].get_channel(event["args"][2]) + nicknames = event["arbitrary"].split() + for nickname in nicknames: + modes = set([]) + + while nickname[0] in event["server"].mode_prefixes: + modes.add(event["server"].mode_prefixes[nickname[0]]) + nickname = nickname[1:] + + if "userhost-in-names" in event["server"].capabilities: + hostmask = Utils.seperate_hostmask(nickname) + nickname = hostmask.nickname + user = event["server"].get_user(hostmask.nickname) + user.username = hostmask.username + user.hostname = hostmask.hostname + else: + user = event["server"].get_user(nickname) + user.join_channel(channel) + channel.add_user(user) + + for mode in modes: + channel.add_mode(mode, nickname) + + # on-join user list has finished + def handle_366(self, event): + event["server"].send_whox(event["args"][1], "n", "ahnrtu", "111") + + # on user joining channel + def join(self, event): + account = None + realname = None + if len(event["args"]) == 2: + channel = event["server"].get_channel(event["args"][0]) + if not event["args"] == "*": + account = event["args"][1] + realname = event["arbitrary"] + else: + channel = event["server"].get_channel(event["last"]) + + if not event["server"].is_own_nickname(event["prefix"].nickname): + user = event["server"].get_user(event["prefix"].nickname) + if not user.username and not user.hostname: + user.username = event["prefix"].username + user.hostname = event["prefix"].hostname + + if account: + user.identified_account = account + user.identified_account_id = event["server"].get_user( + account).get_id() + if realname: + user.realname = realname + + channel.add_user(user) + user.join_channel(channel) + self.events.on("received.join").call(channel=channel, + user=user, server=event["server"], account=account, + realname=realname) + else: + if channel.name in event["server"].attempted_join: + del event["server"].attempted_join[channel.name] + self.events.on("self.join").call(channel=channel, + server=event["server"], account=account, realname=realname) + channel.send_mode() + + # on user parting channel + def part(self, event): + channel = event["server"].get_channel(event["args"][0]) + reason = event["arbitrary"] or "" + + if not event["server"].is_own_nickname(event["prefix"].nickname): + user = event["server"].get_user(event["prefix"].nickname) + self.events.on("received.part").call(channel=channel, + reason=reason, user=user, server=event["server"]) + channel.remove_user(user) + user.part_channel(channel) + if not len(user.channels): + event["server"].remove_user(user) + else: + self.events.on("self.part").call(channel=channel, + reason=reason, server=event["server"]) + event["server"].remove_channel(channel) + + # unknown command sent by us, oops! + def handle_421(self, event): + print("warning: unknown command '%s'." % event["args"][1]) + + # a user has disconnected! + def quit(self, event): + reason = event["arbitrary"] or "" + + if not event["server"].is_own_nickname(event["prefix"].nickname): + user = event["server"].get_user(event["prefix"].nickname) + event["server"].remove_user(user) + self.events.on("received.quit").call(reason=reason, + user=user, server=event["server"]) + else: + event["server"].disconnect() + + # the server is telling us about its capabilities! + def cap(self, event): + capabilities_list = (event["arbitrary"] or "").split(" ") + capabilities = {} + for capability in capabilities_list: + argument = None + if "=" in capability: + capability, argument = capability.split("=", 1) + capabilities[capability] = argument + + subcommand = event["args"][1].lower() + is_multiline = len(event["args"]) > 2 and event["args"][2] == "*" + + if subcommand == "ls": + event["server"].server_capabilities.update(capabilities) + if not is_multiline: + matched_capabilities = set(event["server" + ].server_capabilities.keys()) & CAPABILITIES + if matched_capabilities: + event["server"].queue_capabilities(matched_capabilities) + + self.events.on("received.cap.ls").call( + capabilities=event["server"].server_capabilities, + server=event["server"]) + + if event["server"].has_capability_queue(): + event["server"].send_capability_queue() + else: + event["server"].send_capability_end() + elif subcommand == "new": + event["server"].capabilities.update(set(capabilities.keys())) + self.events.on("received.cap.new").call(server=event["server"], + capabilities=capabilities) + elif subcommand == "del": + event["server"].capabilities.difference_update(set( + capabilities.keys())) + self.events.on("received.cap.del").call(server=event["server"], + capabilities=capabilities) + elif subcommand == "ack": + event["server"].capabilities.update(capabilities) + if not is_multiline: + self.events.on("received.cap.ack").call( + capabilities=event["server"].capabilities, + server=event["server"]) + + if not event["server"].waiting_for_capabilities(): + event["server"].send_capability_end() + elif subcommand == "nack": + event["server"].send_capability_end() + + # the server is asking for authentication + def authenticate(self, event): + self.events.on("received.authenticate").call( + message=event["args"][0], server=event["server"]) + + # someone has changed their nickname + def nick(self, event): + new_nickname = event["arbitrary"] + if not event["server"].is_own_nickname(event["prefix"].nickname): + user = event["server"].get_user(event["prefix"].nickname) + old_nickname = user.nickname + user.set_nickname(new_nickname) + event["server"].change_user_nickname(old_nickname, new_nickname) + + self.events.on("received.nick").call(new_nickname=new_nickname, + old_nickname=old_nickname, user=user, server=event["server"]) + else: + old_nickname = event["server"].nickname + event["server"].set_own_nickname(new_nickname) + + self.events.on("self.nick").call(server=event["server"], + new_nickname=new_nickname, old_nickname=old_nickname) + + # something's mode has changed + def mode(self, event): + user = event["server"].get_user(event["prefix"].nickname) + target = event["args"][0] + is_channel = target[0] in event["server"].channel_types + if is_channel: + channel = event["server"].get_channel(target) + remove = False + args = event["args"][2:] + _args = args[:] + modes = RE_MODES.findall(event["args"][1]) + for chunk in modes: + remove = chunk[0] == "-" + for mode in chunk[1:]: + if mode in event["server"].channel_modes: + channel.change_mode(remove, mode) + elif mode in event["server"].mode_prefixes.values( + ) and len(args): + channel.change_mode(remove, mode, args.pop(0)) + else: + args.pop(0) + self.events.on("received.mode.channel").call(modes=modes, + mode_args=_args, channel=channel, server=event["server"], + user=user) + elif event["server"].is_own_nickname(target): + modes = RE_MODES.findall(event["last"]) + for chunk in modes: + remove = chunk[0] == "-" + for mode in chunk[1:]: + event["server"].change_own_mode(remove, mode) + self.events.on("self.mode").call(modes=modes, + server=event["server"]) + + # someone (maybe me!) has been invited somewhere + def invite(self, event): + target_channel = event["last"] + user = event["server"].get_user(event["prefix"].nickname) + target_user = event["server"].get_user(event["args"][0]) + self.events.on("received.invite").call(user=user, + target_channel=target_channel, server=event["server"], + target_user=target_user) + + # we've received a message + def privmsg(self, event): + user = event["server"].get_user(event["prefix"].nickname) + message = event["arbitrary"] or "" + message_split = message.split(" ") + target = event["args"][0] + action = message.startswith("\x01ACTION ") + if action: + message = message.replace("\x01ACTION ", "", 1) + if message.endswith("\x01"): + message = message[:-1] + + if "account" in event["tags"]: + user.identified_account = event["tags"]["account"] + user.identified_account_id = event["server"].get_user( + event["tags"]["account"]).get_id() + + kwargs = {"message": message, "message_split": message_split, + "server": event["server"], "tags": event["tags"], + "action": action} + + if target[0] in event["server"].channel_types: + channel = event["server"].get_channel(event["args"][0]) + self.events.on("received.message.channel").call( + user=user, channel=channel, **kwargs) + channel.buffer.add_line(user.nickname, message, action) + elif event["server"].is_own_nickname(target): + self.events.on("received.message.private").call( + user=user, **kwargs) + user.buffer.add_line(user.nickname, message, action) + + # we've received a notice + def notice(self, event): + message = event["arbitrary"] or "" + message_split = message.split(" ") + target = event["args"][0] + + if not event["prefix"] or event["prefix"].hostmask == event["server" + ].name or target == "*" or (not event["prefix"].hostname and + not event["server"].name): + event["server"].name = event["prefix"].hostmask + + self.events.on("received.server-notice").call( + message=message, message_split=message_split, + server=event["server"]) + else: + user = event["server"].get_user(event["prefix"].nickname) + + if target[0] in event["server"].channel_types: + channel = event["server"].get_channel(target) + self.events.on("received.notice.channel").call( + message=message, message_split=message_split, user=user, + server=event["server"], channel=channel, + tags=event["tags"]) + elif event["server"].is_own_nickname(target): + self.events.on("received.notice.private").call( + message=message, message_split=message_split, user=user, + server=event["server"], tags=event["tags"]) + + # IRCv3 TAGMSG, used to send tags without any other information + def tagmsg(self, event): + user = event["server"].get_user(event["prefix"].nickname) + target = event["args"][0] + + if target[0] in event["server"].channel_types: + channel = event["server"].get_channel(target) + self.events.on("received.tagmsg.channel").call(channel=channel, + user=user, tags=event["tags"], server=event["server"]) + elif event["server"].is_own_nickname(target): + self.events.on("received.tagmsg.private").call( + user=user, tags=event["tags"], server=event["server"]) + + # IRCv3 AWAY, used to notify us that a client we can see has changed /away + def away(self, event): + user = event["server"].get_user(event["prefix"].nickname) + message = event["arbitrary"] + if message: + user.away = True + self.events.on("received.away.on").call(user=user, + server=event["server"], message=message) + else: + user.away = False + self.events.on("received.away.off").call(user=user, + server=event["server"]) + + def batch(self, event): + identifier = event["args"][0] + modifier, identifier = identifier[0], identifier[1:] + if modifier == "+": + event["server"].batches[identifier] = [] + else: + lines = event["server"].batches[identifier] + del event["server"].batches[identifier] + for line in lines: + self.handle(event["server"], line) + + # IRCv3 CHGHOST, a user's username and/or hostname has changed + def chghost(self, event): + username = event["args"][0] + hostname = event["args"][1] + + if not event["server"].is_own_nickname(event["prefix"].nickname): + target = event["server"].get_user("nickanme") + else: + target = event["server"] + target.username = username + target.hostname = hostname + + def account(self, event): + user = event["server"].get_user(event["prefix"].nickname) + + if not event["args"][0] == "*": + user.identified_account = event["args"][0] + user.identified_account_id = event["server"].get_user( + event["args"][0]).get_id() + self.events.on("received.account.login").call(user=user, + server=event["server"], account=event["args"][0]) + else: + user.identified_account = None + user.identified_account_id = None + self.events.on("received.account.logout").call(user=user, + server=event["server"]) + + # response to a WHO command for user information + def handle_352(self, event): + user = event["server"].get_user(event["args"][5]) + user.username = event["args"][2] + user.hostname = event["args"][3] + # response to a WHOX command for user information, including account name + def handle_354(self, event): + if event["args"][1] == "111": + username = event["args"][2] + hostname = event["args"][3] + nickname = event["args"][4] + account = event["args"][5] + realname = event["last"] + + user = event["server"].get_user(nickname) + user.username = username + user.hostname = hostname + user.realname = realname + if not account == "0": + user.identified_account = account + + # response to an empty mode command + def handle_324(self, event): + channel = event["server"].get_channel(event["args"][1]) + modes = event["args"][2] + if modes[0] == "+" and modes[1:]: + for mode in modes[1:]: + if mode in event["server"].channel_modes: + channel.add_mode(mode) + + # channel creation unix timestamp + def handle_329(self, event): + channel = event["server"].get_channel(event["args"][1]) + channel.creation_timestamp = int(event["args"][2]) + + # nickname already in use + def handle_433(self, event): + pass + + # we need a registered nickname for this channel + def handle_477(self, event): + channel_name = Utils.irc_lower(event["server"], event["args"][1]) + if channel_name in event["server"].attempted_join: + self.bot.add_timer("rejoin", 5, + channel_name=event["args"][1], + key=event["server"].attempted_join[channel_name], + server_id=event["server"].id) + + # someone's been kicked from a channel + def kick(self, event): + user = event["server"].get_user(event["prefix"].nickname) + target = event["args"][1] + channel = event["server"].get_channel(event["args"][0]) + reason = event["arbitrary"] or "" + + if not event["server"].is_own_nickname(target): + target_user = event["server"].get_user(target) + self.events.on("received.kick").call(channel=channel, + reason=reason, target_user=target_user, user=user, + server=event["server"]) + else: + self.events.on("self.kick").call(channel=channel, + reason=reason, user=user, server=event["server"]) diff --git a/src/IRCServer.py b/src/IRCServer.py new file mode 100644 index 00000000..d88cc38a --- /dev/null +++ b/src/IRCServer.py @@ -0,0 +1,402 @@ +import collections, socket, ssl, sys, time +from . import IRCChannel, IRCUser, Utils + +THROTTLE_LINES = 4 +THROTTLE_SECONDS = 1 +READ_TIMEOUT_SECONDS = 120 +PING_INTERVAL_SECONDS = 30 + +class Server(object): + def __init__(self, bot, events, id, alias, hostname, port, password, + ipv4, tls, nickname, username, realname): + self.connected = False + self.bot = bot + self.events = events + self.id = id + self.alias = alias + self.target_hostname = hostname + self.port = port + self.tls = tls + self.password = password + self.ipv4 = ipv4 + self.original_nickname = nickname + self.original_username = username or nickname + self.original_realname = realname or nickname + self.name = None + + self._capability_queue = set([]) + self._capabilities_waiting = set([]) + self.capabilities = set([]) + self.server_capabilities = {} + self.batches = {} + + self.write_buffer = b"" + self.buffered_lines = [] + self.read_buffer = b"" + self.recent_sends = [] + + self.users = {} + self.new_users = set([]) + self.channels = {} + + self.own_modes = {} + self.mode_prefixes = collections.OrderedDict( + {"@": "o", "+": "v"}) + self.channel_modes = [] + self.channel_types = ["#"] + self.case_mapping = "rfc1459" + + self.last_read = time.monotonic() + self.last_send = None + + self.attempted_join = {} + self.ping_sent = False + + if ipv4: + self.socket = socket.socket(socket.AF_INET, + socket.SOCK_STREAM) + else: + self.socket = socket.socket(socket.AF_INET6, + socket.SOCK_STREAM) + + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + self.socket.settimeout(5.0) + + if self.tls: + self.tls_wrap() + self.cached_fileno = self.socket.fileno() + self.events.on("timer.rejoin").hook(self.try_rejoin) + + def __repr__(self): + return "IRCServer.Server(%s)" % self.__str__() + def __str__(self): + if self.alias: + return self.alias + return "%s:%s%s" % (self.target_hostname, "+" if self.tls else "", + self.port) + def fileno(self): + fileno = self.socket.fileno() + return self.cached_fileno if fileno == -1 else fileno + + def tls_wrap(self): + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context.options |= ssl.OP_NO_SSLv2 + context.options |= ssl.OP_NO_SSLv3 + context.options |= ssl.OP_NO_TLSv1 + + context.load_default_certs() + if self.get_setting("ssl-verify", True): + context.verify_mode = ssl.CERT_REQUIRED + + client_certificate = self.bot.config.get("ssl-certificate", None) + client_key = self.bot.config.get("ssl-key", None) + if client_certificate and client_key: + context.load_cert_chain(client_certificate, keyfile=client_key) + + self.socket = context.wrap_socket(self.socket) + + def connect(self): + self.socket.connect((self.target_hostname, self.port)) + self.send_capibility_ls() + + if self.password: + self.send_pass(self.password) + + self.send_user(self.original_username, self.original_realname) + self.send_nick(self.original_nickname) + self.connected = True + def disconnect(self): + self.connected = False + try: + self.socket.shutdown(socket.SHUT_RDWR) + except: + pass + try: + self.socket.close() + except: + pass + + def set_setting(self, setting, value): + self.bot.database.server_settings.set(self.id, setting, + value) + def get_setting(self, setting, default=None): + return self.bot.database.server_settings.get(self.id, + setting, default) + def find_settings(self, pattern, default=[]): + return self.bot.database.server_settings.find(self.id, + pattern, default) + def find_settings_prefix(self, prefix, default=[]): + return self.bot.database.server_settings.find_prefix( + self.id, prefix, default) + def del_setting(self, setting): + self.bot.database.server_settings.delete(self.id, setting) + def get_all_user_settings(self, setting, default=[]): + return self.bot.database.user_settings.find_all_by_setting( + self.id, setting, default) + def find_all_user_channel_settings(self, setting, default=[]): + return self.bot.database.user_channel_settings.find_all_by_setting( + self.id, setting, default) + + def set_own_nickname(self, nickname): + self.nickname = nickname + self.nickname_lower = Utils.irc_lower(self, nickname) + def is_own_nickname(self, nickname): + return Utils.irc_equals(self, nickname, self.nickname) + + def add_own_mode(self, mode, arg=None): + self.own_modes[mode] = arg + def remove_own_mode(self, mode): + del self.own_modes[mode] + def change_own_mode(self, remove, mode, arg=None): + if remove: + self.remove_own_mode(mode) + else: + self.add_own_mode(mode, arg) + + def has_user(self, nickname): + return Utils.irc_lower(self, nickname) in self.users + def get_user(self, nickname): + if not self.has_user(nickname): + user_id = self.get_user_id(nickname) + new_user = IRCUser.User(nickname, user_id, self, self.bot) + self.events.on("new.user").call(user=new_user, server=self) + self.users[new_user.nickname_lower] = new_user + self.new_users.add(new_user) + return self.users[Utils.irc_lower(self, nickname)] + def get_user_id(self, nickname): + self.bot.database.users.add(self.id, nickname) + return self.bot.database.users.get_id(self.id, nickname) + def remove_user(self, user): + del self.users[user.nickname_lower] + for channel in user.channels: + channel.remove_user(user) + + def change_user_nickname(self, old_nickname, new_nickname): + user = self.users.pop(Utils.irc_lower(self, old_nickname)) + user._id = self.get_user_id(new_nickname) + self.users[Utils.irc_lower(self, new_nickname)] = user + def has_channel(self, channel_name): + return channel_name[0] in self.channel_types and Utils.irc_lower( + self, channel_name) in self.channels + def get_channel(self, channel_name): + if not self.has_channel(channel_name): + channel_id = self.get_channel_id(channel_name) + new_channel = IRCChannel.Channel(channel_name, channel_id, + self, self.bot) + self.events.on("new.channel").call(channel=new_channel, + server=self) + self.channels[new_channel.name] = new_channel + return self.channels[Utils.irc_lower(self, channel_name)] + def get_channel_id(self, channel_name): + self.bot.database.channels.add(self.id, channel_name) + return self.bot.database.channels.get_id(self.id, channel_name) + def remove_channel(self, channel): + for user in channel.users: + user.part_channel(channel) + del self.channels[channel.name] + def parse_line(self, line): + if not line: + return + self.bot.line_handler.handle(self, line) + self.check_users() + def check_users(self): + for user in self.new_users: + if not len(user.channels): + self.remove_user(user) + self.new_users.clear() + def read(self): + data = b"" + try: + data = self.read_buffer + self.socket.recv(4096) + except (ConnectionResetError, socket.timeout): + self.disconnect() + return [] + self.read_buffer = b"" + data_lines = [line.strip(b"\r") for line in data.split(b"\n")] + if data_lines[-1]: + self.read_buffer = data_lines[-1] + data_lines.pop(-1) + decoded_lines = [] + for line in data_lines: + try: + line = line.decode(self.get_setting( + "encoding", "utf8")) + except: + try: + line = line.decode(self.get_setting( + "fallback-encoding", "latin-1")) + except: + continue + decoded_lines.append(line) + if not decoded_lines: + self.disconnect() + self.last_read = time.monotonic() + self.ping_sent = False + return decoded_lines + + def until_next_ping(self): + if self.ping_sent: + return None + return max(0, (self.last_read+PING_INTERVAL_SECONDS + )-time.monotonic()) + def ping_due(self): + return self.until_next_ping() == 0 + + def until_read_timeout(self): + return max(0, (self.last_read+READ_TIMEOUT_SECONDS + )-time.monotonic()) + def read_timed_out(self): + return self.until_read_timeout == 0 + + def send(self, data): + encoded = data.split("\n")[0].strip("\r").encode("utf8") + if len(encoded) > 450: + encoded = encoded[:450] + self.buffered_lines.append(encoded + b"\r\n") + if self.bot.args.verbose: + self.bot.log.info(">%s | %s", [str(self), encoded.decode("utf8")]) + def _send(self): + if not len(self.write_buffer): + self.write_buffer = self.buffered_lines.pop(0) + self.write_buffer = self.write_buffer[self.socket.send( + self.write_buffer):] + + now = time.monotonic() + self.recent_sends.append(now) + self.last_send = now + def waiting_send(self): + return bool(len(self.write_buffer)) or bool(len(self.buffered_lines)) + def throttle_done(self): + return self.send_throttle_timeout() == 0 + def send_throttle_timeout(self): + if len(self.write_buffer): + return 0 + + now = time.monotonic() + popped = 0 + for i, recent_send in enumerate(self.recent_sends[:]): + time_since = now-recent_send + if time_since >= THROTTLE_SECONDS: + self.recent_sends.pop(i-popped) + popped += 1 + + if len(self.recent_sends) < THROTTLE_LINES: + return 0 + + time_left = self.recent_sends[0]+THROTTLE_SECONDS + time_left = time_left-now + return time_left + + def send_user(self, username, realname): + self.send("USER %s 0 * :%s" % (username, realname)) + def send_nick(self, nickname): + self.send("NICK %s" % nickname) + + def send_capibility_ls(self): + self.send("CAP LS 302") + def queue_capability(self, capability): + self._capability_queue.add(capability) + def queue_capabilities(self, capabilities): + self._capability_queue.update(capabilities) + def send_capability_queue(self): + if self.has_capability_queue(): + capabilities = " ".join(self._capability_queue) + self._capability_queue.clear() + self.send_capability_request(capabilities) + def has_capability_queue(self): + return bool(len(self._capability_queue)) + def send_capability_request(self, capability): + self.send("CAP REQ :%s" % capability) + def send_capability_end(self): + self.send("CAP END") + def send_authenticate(self, text): + self.send("AUTHENTICATE %s" % text) + def send_starttls(self): + self.send("STARTTLS") + + def waiting_for_capabilities(self): + return bool(len(self._capabilities_waiting)) + def wait_for_capability(self, capability): + self._capabilities_waiting.add(capability) + def capability_done(self, capability): + self._capabilities_waiting.remove(capability) + if not self._capabilities_waiting: + self.send_capability_end() + + def send_pass(self, password): + self.send("PASS %s" % password) + + def send_ping(self, nonce="hello"): + self.send("PING :%s" % nonce) + def send_pong(self, nonce="hello"): + self.send("PONG :%s" % nonce) + + def try_rejoin(self, event): + if event["server_id"] == self.id and event["channel_name" + ] in self.attempted_join: + self.send_join(event["channel_name"], event["key"]) + def send_join(self, channel_name, key=None): + self.send("JOIN %s%s" % (channel_name, + "" if key == None else " %s" % key)) + def send_part(self, channel_name, reason=None): + self.send("PART %s%s" % (channel_name, + "" if reason == None else " %s" % reason)) + def send_quit(self, reason="Leaving"): + self.send("QUIT :%s" % reason) + + def send_message(self, target, message, prefix=None): + full_message = message if not prefix else prefix+message + self.send("PRIVMSG %s :%s" % (target, full_message)) + + action = full_message.startswith("\01ACTION " + ) and full_message.endswith("\01") + + if action: + message = full_message.split("\01ACTION ", 1)[1][:-1] + + full_message_split = full_message.split() + if self.has_channel(target): + channel = self.get_channel(target) + channel.buffer.add_line(None, message, action, True) + self.events.on("self.message.channel").call( + message=full_message, message_split=full_message_split, + channel=channel, action=action, server=self) + else: + user = self.get_user(target) + user.buffer.add_line(None, message, action, True) + self.events.on("self.message.private").call( + message=full_message, message_split=full_message_split, + user=user, action=action, server=self) + + def send_notice(self, target, message): + self.send("NOTICE %s :%s" % (target, message)) + + def send_mode(self, target, mode=None, args=None): + self.send("MODE %s%s%s" % (target, "" if mode == None else " %s" % mode, + "" if args == None else " %s" % args)) + + def send_topic(self, channel_name, topic): + self.send("TOPIC %s :%s" % (channel_name, topic)) + def send_kick(self, channel_name, target, reason=None): + self.send("KICK %s %s%s" % (channel_name, target, + "" if reason == None else " :%s" % reason)) + def send_names(self, channel_name): + self.send("NAMES %s" % channel_name) + def send_list(self, search_for=None): + self.send( + "LIST%s" % "" if search_for == None else " %s" % search_for) + def send_invite(self, target, channel_name): + self.send("INVITE %s %s" % (target, channel_name)) + + def send_whois(self, target): + self.send("WHOIS %s" % target) + def send_whowas(self, target, amount=None, server=None): + self.send("WHOWAS %s%s%s" % (target, + "" if amount == None else " %s" % amount, + "" if server == None else " :%s" % server)) + def send_who(self, filter=None): + self.send("WHO%s" % ("" if filter == None else " %s" % filter)) + def send_whox(self, mask, filter, fields, label=None): + self.send("WHO %s %s%%%s%s" % (mask, filter, fields, + ","+label if label else "")) diff --git a/src/IRCUser.py b/src/IRCUser.py new file mode 100644 index 00000000..1b2fdbdc --- /dev/null +++ b/src/IRCUser.py @@ -0,0 +1,62 @@ +import uuid +from . import IRCBuffer, Utils + +class User(object): + def __init__(self, nickname, id, server, bot): + self.server = server + self.set_nickname(nickname) + self._id = id + self.username = None + self.hostname = None + self.realname = None + self.bot = bot + self.channels = set([]) + + self.identified_account = None + self.identified_account_override = None + + self.identified_account_id = None + self.identified_account_id_override = None + self.away = False + self.buffer = IRCBuffer.Buffer(bot, server) + + def __repr__(self): + return "IRCUser.User(%s|%s)" % (self.server.name, self.name) + + def get_id(self): + return (self.identified_account_id_override or + self.identified_account_id or self._id) + def get_identified_account(self): + return (self.identified_account_override or self.identified_account) + + def set_nickname(self, nickname): + self.nickname = nickname + self.nickname_lower = Utils.irc_lower(self.server, nickname) + self.name = self.nickname_lower + def join_channel(self, channel): + self.channels.add(channel) + def part_channel(self, channel): + self.channels.remove(channel) + def set_setting(self, setting, value): + self.bot.database.user_settings.set(self.get_id(), setting, value) + def get_setting(self, setting, default=None): + return self.bot.database.user_settings.get(self.get_id(), setting, + default) + def find_settings(self, pattern, default=[]): + return self.bot.database.user_settings.find(self.get_id(), pattern, + default) + def find_settings_prefix(self, prefix, default=[]): + return self.bot.database.user_settings.find_prefix(self.get_id(), + prefix, default) + def del_setting(self, setting): + self.bot.database.user_settings.delete(self.get_id(), setting) + def get_channel_settings_per_setting(self, setting, default=[]): + return self.bot.database.user_channel_settings.find_by_setting( + self.get_id(), setting, default) + + def send_message(self, message, prefix=None): + self.server.send_message(self.nickname, message, prefix=prefix) + def send_notice(self, message): + self.server.send_notice(self.nickname, message) + def send_ctcp_response(self, command, args): + self.send_notice("\x01%s %s\x01" % (command, args)) diff --git a/src/Logging.py b/src/Logging.py new file mode 100644 index 00000000..3f5815d6 --- /dev/null +++ b/src/Logging.py @@ -0,0 +1,48 @@ +import logging, logging.handlers, os, sys, time + +class BitBotFormatter(logging.Formatter): + def formatTime(self, record, datefmt=None): + ct = self.converter(record.created) + if datefmt: + if "%f" in datefmt: + msec = "%03d" % record.msecs + datefmt = datefmt.replace("%f", msec) + s = time.strftime(datefmt, ct) + else: + t = time.strftime("%Y-%m-%d %H:%M:%S", ct) + s = "%s.%03d" % (t, record.msecs) + return s + +class Log(object): + def __init__(self, bot, directory, filename): + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + + formatter = BitBotFormatter( + "%(asctime)s [%(levelname)s] %(message)s", + "%Y-%m-%dT%H:%M:%S.%fZ") + formatter.converter = time.gmtime + + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setLevel(logging.INFO) + stdout_handler.setFormatter(formatter) + self.logger.addHandler(stdout_handler) + + file_handler = logging.handlers.TimedRotatingFileHandler( + os.path.join(directory, filename), when="midnight", backupCount=5) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + self.logger.addHandler(file_handler) + + def debug(self, message, params, **kwargs): + self._log(message, params, logging.DEBUG, kwargs) + def info(self, message, params, **kwargs): + self._log(message, params, logging.INFO, kwargs) + def warn(self, message, params, **kwargs): + self._log(message, params, logging.WARN, kwargs) + def error(self, message, params, **kwargs): + self._log(message, params, logging.ERROR, kwargs) + def critical(self, message, params, **kwargs): + self._log(message, params, logging.CRITICAL, kwargs) + def _log(self, message, params, level, kwargs): + self.logger.log(level, message, *params, **kwargs) diff --git a/src/ModuleManager.py b/src/ModuleManager.py new file mode 100644 index 00000000..2a3ae713 --- /dev/null +++ b/src/ModuleManager.py @@ -0,0 +1,147 @@ +import glob, imp, inspect, os, sys, uuid + +BITBOT_HOOKS_MAGIC = "__bitbot_hooks" + +class ModuleException(Exception): + pass +class ModuleWarning(Exception): + pass + +class ModuleNotFoundException(ModuleException): + pass +class ModuleNameCollisionException(ModuleException): + pass +class ModuleLoadException(ModuleException): + pass +class ModuleUnloadException(ModuleException): + pass + +class ModuleNotLoadedWarning(ModuleWarning): + pass + +class BaseModule(object): + def __init__(self, bot, events, exports): + pass + +class ModuleManager(object): + def __init__(self, bot, events, exports, directory): + self.bot = bot + self.events = events + self.exports = exports + self.directory = directory + self.modules = {} + self.waiting_requirement = {} + def list_modules(self): + return sorted(glob.glob(os.path.join(self.directory, "*.py"))) + + def _module_name(self, path): + return os.path.basename(path).rsplit(".py", 1)[0].lower() + def _module_path(self, name): + return os.path.join(self.directory, "%s.py" % name) + + def _load_module(self, name): + path = self._module_path(name) + + with open(path) as module_file: + while True: + line = module_file.readline().strip() + line_split = line.split(" ") + if line and line.startswith("#--"): + # this is a hashflag + if line == "#--ignore": + # nope, ignore this module. + raise ModuleNotLoadedWarning("module ignored") + elif line_split[0] == "#--require-config" and len( + line_split) > 1: + if not line_split[1].lower() in self.bot.config or not self.bot.config[ + line_split[1].lower()]: + # nope, required config option not present. + raise ModuleNotLoadedWarning( + "required config not present") + elif line_split[0] == "#--require-module" and len( + line_split) > 1: + if not "bitbot_%s" % line_split[1].lower() in sys.modules: + if not line_split[1].lower() in self.waiting_requirement: + self.waiting_requirement[line_split[1].lower()] = set([]) + self.waiting_requirement[line_split[1].lower()].add(path) + raise ModuleNotLoadedWarning( + "waiting for requirement") + else: + break + module = imp.load_source(name, path) + + if not hasattr(module, "Module"): + raise ModuleLoadException("module '%s' doesn't have a " + "'Module' class.") + if not inspect.isclass(module.Module): + raise ModuleLoadException("module '%s' has a 'Module' attribute " + "but it is not a class.") + + context = str(uuid.uuid4()) + context_events = self.events.new_context(context) + context_exports = self.exports.new_context(context) + module_object = module.Module(self.bot, context_events, + context_exports) + + if not hasattr(module_object, "_name"): + module_object._name = name.title() + for attribute_name in dir(module_object): + attribute = getattr(module_object, attribute_name) + if inspect.ismethod(attribute) and hasattr(attribute, + BITBOT_HOOKS_MAGIC): + hooks = getattr(attribute, BITBOT_HOOKS_MAGIC) + for hook in hooks: + context_events.on(hook["event"]).hook(attribute, + **hook["kwargs"]) + + module_object._context = context + module_object._import_name = name + + assert not module_object._name in self.modules, ( + "module name '%s' attempted to be used twice.") + return module_object + + def load_module(self, name): + try: + module = self._load_module(name) + except ModuleWarning as warning: + self.bot.log.error("Module '%s' not loaded", [name]) + raise + except Exception as e: + self.bot.log.error("Failed to load module \"%s\": %s", + [name, str(e)]) + raise + + self.modules[module._import_name] = module + if name in self.waiting_requirement: + for requirement_name in self.waiting_requirement: + self.load_module(requirement_name) + self.bot.log.info("Module '%s' loaded", [name]) + + def load_modules(self, whitelist=[], blacklist=[]): + for path in self.list_modules(): + name = self._module_name(path) + if name in whitelist or (not whitelist and not name in blacklist): + try: + self.load_module(name) + except ModuleWarning: + pass + + def unload_module(self, name): + if not name in self.modules: + raise ModuleNotFoundException() + module = self.modules[name] + del self.modules[name] + + context = module._context + self.events.purge_context(context) + self.exports.purge_context(context) + + del sys.modules[name] + references = sys.getrefcount(module) + del module + references -= 1 # 'del module' removes one reference + references -= 1 # one of the refs is from getrefcount + + self.bot.log.info("Module '%s' unloaded (%d reference%s)", + [name, references, "" if references == 1 else "s"]) diff --git a/src/Timer.py b/src/Timer.py new file mode 100644 index 00000000..7ac83630 --- /dev/null +++ b/src/Timer.py @@ -0,0 +1,39 @@ +import time, uuid + +class Timer(object): + def __init__(self, id, bot, events, event_name, delay, + next_due=None, **kwargs): + self.id = id + self.bot = bot + self.events = events + self.event_name = event_name + self.delay = delay + if next_due: + self.next_due = next_due + else: + self.set_next_due() + self.kwargs = kwargs + self._done = False + self.call_count = 0 + + def set_next_due(self): + self.next_due = time.time()+self.delay + + def due(self): + return self.time_left() <= 0 + + def time_left(self): + return self.next_due-time.time() + + def call(self): + self._done = True + self.call_count +=1 + self.events.on("timer").on(self.event_name).call( + timer=self, **self.kwargs) + + def redo(self): + self._done = False + self.set_next_due() + + def done(self): + return self._done diff --git a/src/Utils.py b/src/Utils.py new file mode 100644 index 00000000..cd0e0cfa --- /dev/null +++ b/src/Utils.py @@ -0,0 +1,287 @@ +import json, re, traceback, urllib.request, urllib.parse, urllib.error, ssl +import string +import bs4 +from . import ModuleManager + +USER_AGENT = ("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36") +REGEX_HTTP = re.compile("https?://", re.I) +ASCII_UPPER = string.ascii_uppercase +ASCII_LOWER = string.ascii_lowercase +STRICT_RFC1459_UPPER = ASCII_UPPER+r'\[]' +STRICT_RFC1459_LOWER = ASCII_LOWER+r'|{}' +RFC1459_UPPER = STRICT_RFC1459_UPPER+"^" +RFC1459_LOWER = STRICT_RFC1459_LOWER+"~" + +def remove_colon(s): + if s.startswith(":"): + s = s[1:] + return s + +def arbitrary(s, n): + return remove_colon(" ".join(s[n:])) + +# case mapping lowercase/uppcase logic +def _multi_replace(s, chars1, chars2): + for char1, char2 in zip(chars1, chars2): + s = s.replace(char1, char2) + return s +def irc_lower(server, s): + if server.case_mapping == "ascii": + return _multi_replace(s, ASCII_UPPER, ASCII_LOWER) + elif server.case_mapping == "rfc1459": + return _multi_replace(s, RFC1459_UPPER, RFC1459_LOWER) + elif server.case_mapping == "strict-rfc1459": + return _multi_replace(s, STRICT_RFC1459_UPPER, STRICT_RFC1459_LOWER) + else: + raise ValueError("unknown casemapping '%s'" % server.case_mapping) + +# compare a string while respecting case mapping +def irc_equals(server, s1, s2): + return irc_lower(server, s1) == irc_lower(server, s2) + +class IRCHostmask(object): + def __init__(self, nickname, username, hostname, hostmask): + self.nickname = nickname + self.username = username + self.hostname = hostname + self.hostmask = hostmask + def __repr__(self): + return "Utils.IRCHostmask(%s)" % self.__str__() + def __str__(self): + return self.hostmask + +def seperate_hostmask(hostmask): + hostmask = remove_colon(hostmask) + first_delim = hostmask.find("!") + second_delim = hostmask.find("@") + nickname, username, hostname = hostmask, None, None + if first_delim > -1 and second_delim > first_delim: + nickname, username = hostmask.split("!", 1) + username, hostname = username.split("@", 1) + elif second_delim > -1: + nickname, hostname = hostmask.split("@", 1) + return IRCHostmask(nickname, username, hostname, hostmask) + +def get_url(url, **kwargs): + if not urllib.parse.urlparse(url).scheme: + url = "http://%s" % url + url_parsed = urllib.parse.urlparse(url) + + method = kwargs.get("method", "GET") + get_params = kwargs.get("get_params", "") + post_params = kwargs.get("post_params", None) + headers = kwargs.get("headers", {}) + if get_params: + get_params = "?%s" % urllib.parse.urlencode(get_params) + if post_params: + post_params = urllib.parse.urlencode(post_params).encode("utf8") + url = "%s%s" % (url, get_params) + try: + url.encode("latin-1") + except UnicodeEncodeError: + if kwargs.get("code"): + return 0, False + return False + + request = urllib.request.Request(url, post_params) + request.add_header("Accept-Language", "en-US") + request.add_header("User-Agent", USER_AGENT) + for header, value in headers.items(): + request.add_header(header, value) + request.method = method + + try: + response = urllib.request.urlopen(request, timeout=5) + except urllib.error.HTTPError as e: + traceback.print_exc() + if kwargs.get("code"): + return e.code, False + return False + except urllib.error.URLError as e: + traceback.print_exc() + if kwargs.get("code"): + return -1, False + return False + except ssl.CertificateError as e: + traceback.print_exc() + if kwargs.get("code"): + return -1, False, + return False + + response_content = response.read() + encoding = response.info().get_content_charset() + if kwargs.get("soup"): + return bs4.BeautifulSoup(response_content, kwargs.get("parser", "lxml")) + if not encoding: + soup = bs4.BeautifulSoup(response_content, kwargs.get("parser", "lxml")) + metas = soup.find_all("meta") + for meta in metas: + if "charset=" in meta.get("content", ""): + encoding = meta.get("content").split("charset=", 1)[1 + ].split(";", 1)[0] + elif meta.get("charset", ""): + encoding = meta.get("charset") + else: + continue + break + if not encoding: + for item in soup.contents: + if isinstance(item, bs4.Doctype): + if item == "html": + encoding = "utf8" + else: + encoding = "latin-1" + break + response_content = response_content.decode(encoding or "utf8") + data = response_content + if kwargs.get("json") and data: + try: + data = json.loads(response_content) + except json.decoder.JSONDecodeError: + traceback.print_exc() + return False + if kwargs.get("code"): + return response.code, data + else: + return data + +COLOR_WHITE, COLOR_BLACK, COLOR_BLUE, COLOR_GREEN = 0, 1, 2, 3 +COLOR_RED, COLOR_BROWN, COLOR_PURPLE, COLOR_ORANGE = 4, 5, 6, 7 +COLOR_YELLOW, COLOR_LIGHTGREEN, COLOR_CYAN, COLOR_LIGHTCYAN = (8, 9, + 10, 11) +COLOR_LIGHTBLUE, COLOR_PINK, COLOR_GREY, COLOR_LIGHTGREY = (12, 13, + 14, 15) +FONT_BOLD, FONT_ITALIC, FONT_UNDERLINE, FONT_INVERT = ("\x02", "\x1D", + "\x1F", "\x16") +FONT_COLOR, FONT_RESET = "\x03", "\x0F" + +def color(s, foreground, background=None): + foreground = str(foreground).zfill(2) + if background: + background = str(background).zfill(2) + return "%s%s%s%s%s" % (FONT_COLOR, foreground, + "" if not background else ",%s" % background, s, FONT_COLOR) + +def bold(s): + return "%s%s%s" % (FONT_BOLD, s, FONT_BOLD) + +def underline(s): + return "%s%s%s" % (FONT_UNDERLINE, s, FONT_UNDERLINE) + +TIME_SECOND = 1 +TIME_MINUTE = TIME_SECOND*60 +TIME_HOUR = TIME_MINUTE*60 +TIME_DAY = TIME_HOUR*24 +TIME_WEEK = TIME_DAY*7 + +def time_unit(seconds): + since = None + unit = None + if seconds >= TIME_WEEK: + since = seconds/TIME_WEEK + unit = "week" + elif seconds >= TIME_DAY: + since = seconds/TIME_DAY + unit = "day" + elif seconds >= TIME_HOUR: + since = seconds/TIME_HOUR + unit = "hour" + elif seconds >= TIME_MINUTE: + since = seconds/TIME_MINUTE + unit = "minute" + else: + since = seconds + unit = "second" + since = int(since) + if since > 1: + unit = "%ss" % unit # pluralise the unit + return [since, unit] + +REGEX_PRETTYTIME = re.compile("\d+[wdhms]", re.I) + +SECONDS_MINUTES = 60 +SECONDS_HOURS = SECONDS_MINUTES*60 +SECONDS_DAYS = SECONDS_HOURS*24 +SECONDS_WEEKS = SECONDS_DAYS*7 + +def from_pretty_time(pretty_time): + seconds = 0 + for match in re.findall(REGEX_PRETTYTIME, pretty_time): + number, unit = int(match[:-1]), match[-1].lower() + if unit == "m": + number = number*SECONDS_MINUTES + elif unit == "h": + number = number*SECONDS_HOURS + elif unit == "d": + number = number*SECONDS_DAYS + elif unit == "w": + number = number*SECONDS_WEEKS + seconds += number + if seconds > 0: + return seconds + +UNIT_SECOND = 5 +UNIT_MINUTE = 4 +UNIT_HOUR = 3 +UNIT_DAY = 2 +UNIT_WEEK = 1 +def to_pretty_time(total_seconds, minimum_unit=UNIT_SECOND, max_units=6): + minutes, seconds = divmod(total_seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + weeks, days = divmod(days, 7) + out = "" + + units = 0 + if weeks and minimum_unit >= UNIT_WEEK and units < max_units: + out += "%dw" % weeks + units += 1 + if days and minimum_unit >= UNIT_DAY and units < max_units: + out += "%dd" % days + units += 1 + if hours and minimum_unit >= UNIT_HOUR and units < max_units: + out += "%dh" % hours + units += 1 + if minutes and minimum_unit >= UNIT_MINUTE and units < max_units: + out += "%dm" % minutes + units += 1 + if seconds and minimum_unit >= UNIT_SECOND and units < max_units: + out += "%ds" % seconds + units += 1 + return out + +IS_TRUE = ["true", "yes", "on", "y"] +IS_FALSE = ["false", "no", "off", "n"] +def bool_or_none(s): + s = s.lower() + if s in IS_TRUE: + return True + elif s in IS_FALSE: + return False +def int_or_none(s): + stripped_s = s.lstrip("0") + if stripped_s.isdigit(): + return int(stripped_s) + +def get_closest_setting(event, setting, default=None): + server = event["server"] + if "channel" in event: + closest = event["channel"] + elif "target" in event and "is_channel" in event and event["is_channel"]: + closest = event["target"] + else: + closest = event["user"] + return closest.get_setting(setting, server.get_setting(setting, default)) + +def prevent_highlight(nickname): + return nickname[0]+"\u200d"+nickname[1:] + +def hook(event, **kwargs): + def _hook_func(func): + if not hasattr(func, ModuleManager.BITBOT_HOOKS_MAGIC): + setattr(func, ModuleManager.BITBOT_HOOKS_MAGIC, []) + getattr(func, ModuleManager.BITBOT_HOOKS_MAGIC).append( + {"event": event, "kwargs": kwargs}) + return func + return _hook_func diff --git a/start.py b/start.py index 04defccb..53b08023 100755 --- a/start.py +++ b/start.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 -import argparse, sys, time -import IRCBot, Config, Database +import argparse, os, sys, time +from src import Config, Database, EventManager, Exports, IRCBot +from src import IRCLineHandler, Logging, ModuleManager def bool_input(s): result = input("%s (Y/n): " % s) @@ -17,20 +18,29 @@ arg_parser.add_argument("--verbose", "-v", action="store_true") args = arg_parser.parse_args() +directory = os.path.dirname(os.path.realpath(__file__)) + bot = IRCBot.Bot() -database = Database.Database(bot, args.database) -config = Config.Config(bot, args.config) -bot.database = database -bot.config = config.load_config() +bot._events = events = EventManager.EventHook(bot) +bot._exports = exports = Exports.Exports() +bot.modules = modules = ModuleManager.ModuleManager(bot, events, exports, + os.path.join(directory, "modules")) +bot.line_handler = IRCLineHandler.LineHandler(bot, bot._events) +bot.log = Logging.Log(bot, directory, "bot.log") +bot.database = Database.Database(bot, directory, args.database) +bot.config = Config.Config(bot, directory, args.config).load_config() bot.args = args +bot._events.on("timer.reconnect").hook(bot.reconnect) +bot._events.on("boot.done").hook(bot.setup_timers) + whitelist = bot.get_setting("module-whitelist", []) blacklist = bot.get_setting("module-blacklist", []) bot.modules.load_modules(whitelist=whitelist, blacklist=blacklist) servers = [] -for server_id, alias in database.servers.get_all(): +for server_id, alias in bot.database.servers.get_all(): server = bot.add_server(server_id, connect=False) if not server == None: servers.append(server) @@ -54,7 +64,7 @@ else: nickname = input("nickname: ") username = input("username: ") realname = input("realname: ") - database.servers.add(alias, hostname, port, password, ipv4, + bot.database.servers.add(alias, hostname, port, password, ipv4, tls, nickname, username, realname) except KeyboardInterrupt: print() -- cgit v1.3.1-10-gc9f91