From 86111fb8ce2594468bea1935a855d23751068f4b Mon Sep 17 00:00:00 2001 From: Elijah Lazkani Date: Sat, 24 Feb 2018 23:40:44 -0500 Subject: [PATCH 1/5] Implementing a buffering system for flood protection --- boots/admin.py | 31 ++++---- boots/admin_commands.py | 1 + boots/boots.py | 41 +++++----- boots/eightball.py | 1 + boots/robot.py | 165 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 201 insertions(+), 38 deletions(-) diff --git a/boots/admin.py b/boots/admin.py index 8dc2fb2..5f4972a 100644 --- a/boots/admin.py +++ b/boots/admin.py @@ -4,6 +4,7 @@ import logging import pickle import hashlib import aiofiles + import robot @@ -284,36 +285,36 @@ class Admin: kwargs['message'] = \ "login - Login as " \ "an admin with your account" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) if match_help_cmd.group(1) == 'logout': kwargs['message'] = \ "logout - Log out from your account" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) if match_help_cmd.group(1) == 'passwd': kwargs['message'] = \ "passwd - Change your" \ " account\'s password" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) if match_help_cmd.group(1) == 'add': kwargs['message'] = \ "add - adds" \ " an admin account to the list of admins" \ " with provided level" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) if match_help_cmd.group(1) == 'rm': kwargs['message'] = \ "rm - removes an admin from the list" \ " of admins" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) if match_help_cmd.group(1) == 'list': kwargs['message'] = "list - lists all the admins" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) elif match_help: kwargs['message'] = "help [command]" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) kwargs['message'] = \ "commands: login logout passwd add rm list" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) async def log_in(self, nick: str, @@ -369,7 +370,7 @@ class Admin: nick, match.group(1)) self.logger.debug("We have logged in {} successfully, " "notifying".format(nick)) - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) self.logger.debug("We are calling save_config()") await self.save_config() @@ -402,7 +403,7 @@ class Admin: " successfully".format(nick, admin) self.logger.debug("We have successfully logged {}" " out, notifying".format(nick)) - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) self.logger.debug("We are calling save_config") await self.save_config() @@ -440,7 +441,7 @@ class Admin: self.logger.debug( "We have successfully changed {}'s password," " notifying".format(nick)) - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) self.logger.debug("We are calling save_config()") await self.save_config() kwargs['target'] = self.client.nick @@ -488,7 +489,7 @@ class Admin: self.logger.warn( "We detected that {} has already been added," " notifying {}".format(match.group(1), nick)) - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) async def admin_rm(self, nick: str, @@ -542,7 +543,7 @@ class Admin: "We detected that {0} does not have enough" " access to delete {1}, notifying {0}".format( nick, match.group(1))) - self.client.send("PRIVMSG", **kwags) + self.client.msg(**kwags) def admin_list(self, nick: str, target: str, **kwargs) -> None: """ @@ -566,11 +567,11 @@ class Admin: key, self.admins[key]['level']) kwargs['target'] = nick kwargs['message'] = "List of Administrators:" - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) kwargs['message'] = admins self.logger.debug("We are returning admin list page to" " {}".format(kwargs)) - self.client.send("PRIVMSG", **kwargs) + self.client.msg(**kwargs) def is_admin(self, user: str, host: str): """ diff --git a/boots/admin_commands.py b/boots/admin_commands.py index 8422f91..90ff5ab 100644 --- a/boots/admin_commands.py +++ b/boots/admin_commands.py @@ -3,6 +3,7 @@ import re import functools import logging import asyncio + import admin diff --git a/boots/boots.py b/boots/boots.py index 3597fa8..eb7153a 100644 --- a/boots/boots.py +++ b/boots/boots.py @@ -21,21 +21,21 @@ async def plugins(bot: robot.Bot): bot.send("PRIVMSG", target=target, message=message) await bot.wait("NOTICE") -# Code below will not work, it is awaiting a PR upstream -# @bot.on("USERMODE") -# def umode(**kwargs): -# logger.debug("USERMODE {}".format(kwargs)) + # Code below is an example + @bot.on("USERMODE") + def umode(**kwargs): + logger.debug("USERMODE {}".format(kwargs)) -# @bot.on("CHANNELMODE") -# def cmode(**kwargs): -# logger.debug("CHANNELMODE {}".format(kwargs)) + @bot.on("CHANNELMODE") + def cmode(**kwargs): + logger.debug("CHANNELMODE {}".format(kwargs)) # 8Ball magic_ball = eightball.EightBall(bot) @magic_ball.on_keyword async def ball(target, message, **kwargs): - bot.send("PRIVMSG", target=target, message=message) + await bot.msg(target=target, message=message) administrator = admin.Admin(bot) await administrator.init() @@ -43,29 +43,28 @@ async def plugins(bot: robot.Bot): admin_cmd = admin_commands.AdminCmd(administrator) @admin_cmd.on_command("join") - def join(target, message, **kwargs): - bot.send("JOIN", channel=message) + async def join(target, message, **kwargs): + await bot.join(channel=message) @admin_cmd.on_command("part") - def part(target, message, **kwargs): - bot.send("PART", channel=message) + async def part(target, message, **kwargs): + await bot.part(channel=message) @admin_cmd.on_command("msg") - def msg(target, message, **kwargs): + async def msg(target, message, **kwargs): kwargs['target'] = message.split(' ')[0] kwargs['message'] = " ".join(message.split(' ')[1:]) - bot.send("PRIVMSG", **kwargs) + await bot.msg(**kwargs) @admin_cmd.on_command("action") - def action(target, message, **kwargs): + async def action(target, message, **kwargs): kwargs['target'] = message.split(' ')[0] - kwargs['message'] = \ - "\x01ACTION {}\x01".format(" ".join(message.split(' ')[1:])) - bot.send("PRIVMSG", **kwargs) + kwargs['message'] = " ".join(message.split(' ')[1:]) + await bot.action(**kwargs) @admin_cmd.on_command("quit") async def quit(target, message, **kwargs): - bot.send("QUIT", message=message) + await bot.quit(message=message) await bot.disconnect() # Exit the event loop cleanly bot.loop.stop() @@ -78,11 +77,11 @@ def main(): ssl = False nick = "LuckyBoots" - channel = "#boots" + channel = ["#boots"] bot = robot.Bot(host=host, port=port, ssl=ssl, nick=nick, - channels=[channel]) + channels=channel) bot.loop.create_task(bot.connect()) diff --git a/boots/eightball.py b/boots/eightball.py index c73f362..7ba6f09 100644 --- a/boots/eightball.py +++ b/boots/eightball.py @@ -4,6 +4,7 @@ import random import re import functools import asyncio + import robot diff --git a/boots/robot.py b/boots/robot.py index 11eaf6a..3eaeb0a 100644 --- a/boots/robot.py +++ b/boots/robot.py @@ -1,7 +1,9 @@ import logging import asyncio import types +import typing import functools +import datetime import bottom @@ -17,8 +19,15 @@ class Bot(bottom.Client): nick: str, user: str = None, realname: str = None, - channels: list = None) -> None: - super().__init__(host=host, port=port, ssl=ssl) + channels: list = None, + msg_flood_size_threshold = 3, + msg_flood_time_threshold = 5, + action_flood_size_threshold=3, + action_flood_time_threshold=5, + global_flood_size_threshold=3, + global_flood_time_threshold=5, + loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None: + super().__init__(host=host, port=port, ssl=ssl, loop=loop) self.logger = logging.getLogger(self.__class__.__name__) self.logger.debug("Initializing...") self.nick = nick @@ -26,6 +35,15 @@ class Bot(bottom.Client): self.realname = realname or self.nick self.channels = channels or [] self.pre_pub = [] + self.msg_flood_size_threshold = msg_flood_size_threshold + self.msg_flood_time_threshold = msg_flood_time_threshold + self.action_flood_size_threshold = action_flood_size_threshold + self.action_flood_time_threshold = action_flood_time_threshold + self.global_flood_size_threshold = global_flood_size_threshold + self.global_flood_time_threshold = global_flood_time_threshold + self.msg_timestamp_queue = [] + self.action_timestamp_queue = [] + self.global_timestamp_queue = [] self.on("ping", self.keepalive) self.on("CLIENT_CONNECT", self.on_connect) self.on("PRIVMSG", self.privmsg) @@ -103,6 +121,149 @@ class Bot(bottom.Client): self.pre_pub.extend([wrapped]) return func + async def global_buffer(self, command: str, **kwargs) -> None: + """ + This method will buffer the communication sent to the server + to keep the client from getting disconnected + + It uses global_* variables as configuration + + NOTE: this method is to be called *only* by other buffer methods + + :param command str: the command to send to the server + :param kwargs: the information required for the command + :return None + """ + timestamp = datetime.datetime.now() + if self.global_timestamp_queue.__len__() < self.global_flood_size_threshold: + self.global_timestamp_queue.extend([timestamp]) + else: + del self.global_timestamp_queue[0] + self.global_timestamp_queue.extend([timestamp]) + + time_diff = self.global_timestamp_queue[-1] - self.global_timestamp_queue[0] + + if len(self.global_timestamp_queue) == self.global_flood_size_threshold and \ + time_diff.total_seconds() < self.global_flood_time_threshold: + self.logger.info("Waiting {}s".format(self.global_flood_time_threshold - time_diff.total_seconds())) + await asyncio.sleep(self.global_flood_time_threshold - time_diff.total_seconds()) + + self.send(command, **kwargs) + + async def msg_buffer(self, command: str, **kwargs) -> None: + """ + This method will buffer the messages sent to the server + to keep the client from getting disconnected + + messages are of type: PRIVMSG which includes ACTION + + It uses msg_* variables as configuration + + NOTE: this method is to be called *only* by bot methods + + :param command str: the message command to send to the server + :param kwargs: the information required for the message command + :return None + """ + timestamp = datetime.datetime.now() + if self.msg_timestamp_queue.__len__() < self.msg_flood_size_threshold: + self.msg_timestamp_queue.extend([timestamp]) + else: + del self.msg_timestamp_queue[0] + self.msg_timestamp_queue.extend([timestamp]) + + time_diff = self.msg_timestamp_queue[-1] - self.msg_timestamp_queue[0] + + if len(self.msg_timestamp_queue) == self.msg_flood_size_threshold and \ + time_diff.total_seconds() < self.msg_flood_time_threshold: + self.logger.info("Waiting {}s".format(self.msg_flood_time_threshold - time_diff.total_seconds())) + await asyncio.sleep(self.msg_flood_time_threshold - time_diff.total_seconds()) + + await self.global_buffer(command, **kwargs) + + async def action_buffer(self, command: str, **kwargs): + """ + This method will buffer the actions sent to the server + to keep the client from getting disconnected + + actions are of type: JOIN, PART and QUIT + + It uses action_* variables as configuration + + NOTE: this method is to be called *only* by bot methods + + :param command str: the action command to send to the server + :param kwargs: the information required for the action command + :return None + """ + timestamp = datetime.datetime.now() + if self.action_timestamp_queue.__len__() < self.action_flood_size_threshold: + self.action_timestamp_queue.extend([timestamp]) + else: + del self.action_timestamp_queue[0] + self.action_timestamp_queue.extend([timestamp]) + + time_diff = self.action_timestamp_queue[-1] - self.action_timestamp_queue[0] + + if len(self.action_timestamp_queue) == self.action_flood_size_threshold and \ + time_diff.total_seconds() < self.action_flood_time_threshold: + self.logger.info("Waiting {}s".format(self.action_flood_time_threshold - time_diff.total_seconds())) + await asyncio.sleep(self.action_flood_time_threshold - time_diff.total_seconds()) + + await self.global_buffer(command, **kwargs) + + async def msg(self, **kwargs) -> None: + """ + This method will send a private message to the messages buffer + + :param kwargs: the information required for the command + :return None + """ + await self.msg_buffer("PRIVMSG", **kwargs) + + async def action(self, **kwargs) -> None: + """ + This method will send an action message to the messages buffer + + :param kwargs: the information required for the command + :return None + """ + if kwargs.get('message', None): + kwargs['message'] = \ + "\x01ACTION {}\x01".format(kwargs['message']) + + await self.msg_buffer("PRIVMSG", **kwargs) + else: + # TODO: better error handling for the future + self.logger.error("ACTION does not have a message\n{}".format(kwargs)) + + async def join(self, **kwargs) -> None: + """ + This method will send a join command to the action buffer + + :param kwargs: the information required for the command + :return None + """ + await self.action_buffer("JOIN", **kwargs) + + async def part(self, **kwargs) -> None: + """ + This method will send a part command to the action buffer + + :param kwargs: the information required for the command + :return None + """ + await self.action_buffer("PART", **kwargs) + + async def quit(self, **kwargs) -> None: + """ + This method will send a quit command to the action buffer + + :param kwargs: the information required for the command + :return None + """ + await self.action_buffer("QUIT", **kwargs) + def privmsg(self, **kwargs) -> None: self.logger.debug("PRIVMSG {}".format(kwargs)) -- 2.45.2 From 626dc04a18609239b4fafba30d36e20cf0826a7d Mon Sep 17 00:00:00 2001 From: Elijah Lazkani Date: Mon, 26 Feb 2018 18:09:35 -0500 Subject: [PATCH 2/5] Refactoring code into a new private async _wait() function --- boots/robot.py | 81 +++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/boots/robot.py b/boots/robot.py index 3eaeb0a..1a26783 100644 --- a/boots/robot.py +++ b/boots/robot.py @@ -121,6 +121,35 @@ class Bot(bottom.Client): self.pre_pub.extend([wrapped]) return func + @staticmethod + async def _wait(timestamp_queue: list, flood_size_threshold: int, + flood_time_threshold: int, logger: logging.Logger) -> list: + """ + This method will figure out if the calling function needs to wait. + If so, it will wait the correct amount of time before returning. + + :param timestamp_queue list: list of timestamp of the size of flood_size_threshold + :param flood_size_threshold int: total size of the flood threshold + :param flood_time_threshold int: total time of the flood threshold + :param logger logging.Logger: logger to use + :return timetamp_queue list: the new timestamp_queue after modifications + """ + timestamp = datetime.datetime.now() + if timestamp_queue.__len__() < flood_size_threshold: + timestamp_queue.extend([timestamp]) + else: + del timestamp_queue[0] + timestamp_queue.extend([timestamp]) + + time_diff = timestamp_queue[-1] - timestamp_queue[0] + + if len(timestamp_queue) == flood_size_threshold and \ + time_diff.total_seconds() < flood_time_threshold: + logger.debug("Waiting {}s".format(flood_time_threshold - time_diff.total_seconds())) + await asyncio.sleep(flood_time_threshold - time_diff.total_seconds()) + + return timestamp_queue + async def global_buffer(self, command: str, **kwargs) -> None: """ This method will buffer the communication sent to the server @@ -134,20 +163,10 @@ class Bot(bottom.Client): :param kwargs: the information required for the command :return None """ - timestamp = datetime.datetime.now() - if self.global_timestamp_queue.__len__() < self.global_flood_size_threshold: - self.global_timestamp_queue.extend([timestamp]) - else: - del self.global_timestamp_queue[0] - self.global_timestamp_queue.extend([timestamp]) - - time_diff = self.global_timestamp_queue[-1] - self.global_timestamp_queue[0] - - if len(self.global_timestamp_queue) == self.global_flood_size_threshold and \ - time_diff.total_seconds() < self.global_flood_time_threshold: - self.logger.info("Waiting {}s".format(self.global_flood_time_threshold - time_diff.total_seconds())) - await asyncio.sleep(self.global_flood_time_threshold - time_diff.total_seconds()) - + self.global_timestamp_queue = await self._wait(self.global_timestamp_queue, + self.global_flood_size_threshold, + self.global_flood_time_threshold, + self.logger) self.send(command, **kwargs) async def msg_buffer(self, command: str, **kwargs) -> None: @@ -165,19 +184,10 @@ class Bot(bottom.Client): :param kwargs: the information required for the message command :return None """ - timestamp = datetime.datetime.now() - if self.msg_timestamp_queue.__len__() < self.msg_flood_size_threshold: - self.msg_timestamp_queue.extend([timestamp]) - else: - del self.msg_timestamp_queue[0] - self.msg_timestamp_queue.extend([timestamp]) - - time_diff = self.msg_timestamp_queue[-1] - self.msg_timestamp_queue[0] - - if len(self.msg_timestamp_queue) == self.msg_flood_size_threshold and \ - time_diff.total_seconds() < self.msg_flood_time_threshold: - self.logger.info("Waiting {}s".format(self.msg_flood_time_threshold - time_diff.total_seconds())) - await asyncio.sleep(self.msg_flood_time_threshold - time_diff.total_seconds()) + self.msg_timestamp_queue = await self._wait(self.msg_timestamp_queue, + self.msg_flood_size_threshold, + self.msg_flood_time_threshold, + self.logger) await self.global_buffer(command, **kwargs) @@ -196,19 +206,10 @@ class Bot(bottom.Client): :param kwargs: the information required for the action command :return None """ - timestamp = datetime.datetime.now() - if self.action_timestamp_queue.__len__() < self.action_flood_size_threshold: - self.action_timestamp_queue.extend([timestamp]) - else: - del self.action_timestamp_queue[0] - self.action_timestamp_queue.extend([timestamp]) - - time_diff = self.action_timestamp_queue[-1] - self.action_timestamp_queue[0] - - if len(self.action_timestamp_queue) == self.action_flood_size_threshold and \ - time_diff.total_seconds() < self.action_flood_time_threshold: - self.logger.info("Waiting {}s".format(self.action_flood_time_threshold - time_diff.total_seconds())) - await asyncio.sleep(self.action_flood_time_threshold - time_diff.total_seconds()) + self.action_timestamp_queue = await self._wait(self.action_timestamp_queue, + self.action_flood_size_threshold, + self.action_flood_time_threshold, + self.logger) await self.global_buffer(command, **kwargs) -- 2.45.2 From bf76c3327cab5dd1454e1430836e9041be8e611e Mon Sep 17 00:00:00 2001 From: Elijah Lazkani Date: Sat, 3 Mar 2018 12:44:27 -0500 Subject: [PATCH 3/5] * Refactoring code * No longer using kwargs to send data, it is all parametrized now * API calls to msg, action, join, part, quit have been parametrized * New API call to notice --- boots/admin.py | 250 ++++++++++++++++++++++------------------ boots/admin_commands.py | 13 ++- boots/boots.py | 22 ++-- boots/robot.py | 66 +++++++---- 4 files changed, 201 insertions(+), 150 deletions(-) diff --git a/boots/admin.py b/boots/admin.py index 5f4972a..fb67422 100644 --- a/boots/admin.py +++ b/boots/admin.py @@ -206,52 +206,53 @@ class Admin: :param kwargs: for API compatibility :return: None """ - self.logger.debug("We have received nick={} target={} " - "message={} kwargs={}".format( - nick, target, message, kwargs)) + self.logger.debug( + "We have received nick={}, target={}, message={}, kwargs={}".format( + nick, target, message, kwargs)) # Set the admin flag admin = self.is_admin(nick, kwargs['host']) self.logger.debug( "We are checking to see if {} is an admin".format(nick)) if admin: - kwargs['is_admin'] = True - kwargs['level'] = self.admins[admin]['level'] + is_admin = True + level = self.admins[admin]['level'] else: - kwargs['is_admin'] = False - if kwargs.get('level', None) is not None: - del kwargs['level'] + is_admin = False + level = 0 + # TODO: fix self.logger.debug("We are parsing the message sent by {}".format(nick)) if self.HELP.match(message): - self.admin_help(nick, target, message, **kwargs) + await self.admin_help(nick, target, message, is_admin, **kwargs) elif self.LOGIN.match(message): - await self.log_in(nick, target, message, **kwargs) + await self.log_in(nick, target, message, is_admin, **kwargs) elif self.LOGOUT.match(message): - await self.log_out(nick, target, **kwargs) + await self.log_out(nick, target, is_admin, **kwargs) elif self.PASSWD.match(message): - await self.passwd(nick, target, message, **kwargs) + await self.passwd(nick, target, message, is_admin, **kwargs) elif self.ADD.match(message): - await self.admin_add(nick, target, message, **kwargs) + await self.admin_add(nick, target, message, is_admin, level, **kwargs) elif self.RM.match(message): - await self.admin_rm(nick, target, message, **kwargs) + await self.admin_rm(nick, target, message, is_admin, level, **kwargs) elif self.LIST.match(message): - self.admin_list(nick, target, **kwargs) + await self.admin_list(nick, target, is_admin, **kwargs) - self.logger.debug("We are modifying kwargs") - kwargs['nick'] = nick - kwargs['target'] = target - kwargs['message'] = message self.logger.debug( - "We are triggering a new event called ADMIN with {}".format( - kwargs)) - self.client.trigger('ADMIN', **kwargs) + "We are triggering a new event called ADMIN with " + "nick={}, target={}, message={}, is_admin={}, level={}, " + "**kwargs={}".format(nick, target, message, is_admin, level, + kwargs)) + self.client.trigger('ADMIN', nick=nick, target=target, + message=message, is_admin=is_admin, + level=level, **kwargs) - def admin_help(self, - nick: str, - target: str, - message: str, - **kwargs) -> None: + async def admin_help(self, + nick: str, + target: str, + message: str, + is_admin: bool, + **kwargs) -> None: """ This method will reply back to the user a help manual of the available commands @@ -269,57 +270,59 @@ class Admin: :param nick str: the nickname of the caller :param target str: the target where the message was sent to :param message str: the message sent to the target + :param is_admin bool: the user is an admin :param kwargs: for API compatibility :return: None """ if target == self.client.nick: self.logger.debug( "We have received a help request from {}".format(nick)) - if kwargs.get("is_admin", None) is True: + if is_admin: match_help = self.HELP.match(message) match_help_cmd = self.HELP_CMD.match(message) - kwargs['target'] = nick + _target = nick if match_help_cmd: if len(match_help_cmd.groups()) == 1: if match_help_cmd.group(1) == 'login': - kwargs['message'] = \ + _message = \ "login - Login as " \ "an admin with your account" - self.client.msg(**kwargs) + await self.client.msg(target=_target, message=_message) if match_help_cmd.group(1) == 'logout': - kwargs['message'] = \ + _message = \ "logout - Log out from your account" - self.client.msg(**kwargs) + await self.client.msg(target=_target, message=_message) if match_help_cmd.group(1) == 'passwd': - kwargs['message'] = \ + _message = \ "passwd - Change your" \ " account\'s password" - self.client.msg(**kwargs) + await self.client.msg(target=_target, message=_message) if match_help_cmd.group(1) == 'add': - kwargs['message'] = \ + _message = \ "add - adds" \ " an admin account to the list of admins" \ " with provided level" - self.client.msg(**kwargs) + await self.client.msg(target=_target, message=_message) if match_help_cmd.group(1) == 'rm': - kwargs['message'] = \ + _message = \ "rm - removes an admin from the list" \ " of admins" - self.client.msg(**kwargs) + await self.client.msg(target=_target, message=_message) if match_help_cmd.group(1) == 'list': - kwargs['message'] = "list - lists all the admins" - self.client.msg(**kwargs) + _message = "list - lists all the admins" + await self.client.msg(target=_target, message=_message) elif match_help: - kwargs['message'] = "help [command]" - self.client.msg(**kwargs) - kwargs['message'] = \ + _message = "help [command]" + await self.client.msg(target=_target, message=_message) + _message = \ "commands: login logout passwd add rm list" - self.client.msg(**kwargs) + await self.client.msg(target=_target, message=_message) async def log_in(self, nick: str, target: str, message: str, + is_admin: bool, **kwargs) -> None: """ This method is called when a user attempts to login to the bot. @@ -334,6 +337,7 @@ class Admin: :param nick str: the nickname of the caller :param target str: location the message was sent to :param message str: message coming from `nick` + :param is_admin bool: the user is an admin :param kwargs: for API compatibility :return: None """ @@ -344,17 +348,16 @@ class Admin: if user: login = user['LOGIN'] if login.match(self.hash_pass(message)): - if kwargs.get('is_admin', None) is True and \ - match.group(1) != self.is_admin(nick, - kwargs['host']): + if is_admin and \ + match.group(1) != self.is_admin(nick, kwargs['host']): self.logger.warn( "We detected that {} is already logged in as" " different user, logging him out".format(nick)) await self.log_out(nick, target, **kwargs) if match.group(1) == self.is_admin(nick, kwargs['host']): - kwargs['target'] = nick - kwargs['message'] = "{} you are already logged in" \ - "...".format(nick) + _target = nick + _message = "{} you are already logged in" \ + "...".format(_target) self.logger.warn("We detected that {} is already " "logged in, notifying".format(nick)) else: @@ -363,18 +366,23 @@ class Admin: str(nick) self.admins[match.group(1)]['logged_in_hostname'] = \ str(kwargs['host']) - kwargs['is_admin'] = True - kwargs['target'] = nick - kwargs['message'] = "{}, you are logged in to {}" \ - " successfully".format( - nick, match.group(1)) + is_admin = True + _target = nick + _message = "{}, you are logged in to {}" \ + " successfully".format( + _target, match.group(1)) self.logger.debug("We have logged in {} successfully, " - "notifying".format(nick)) - self.client.msg(**kwargs) + "notifying".format(_target)) + + await self.client.msg(target=_target, message=_message) self.logger.debug("We are calling save_config()") await self.save_config() - async def log_out(self, nick: str, target: str, **kwargs) -> None: + async def log_out(self, + nick: str, + target: str, + is_admin: bool, + **kwargs) -> None: """ This method is called when a user attempts to logout to the bot. It will mark the user as logged out if they are the user logged @@ -386,24 +394,26 @@ class Admin: :param nick str: the nickname of the caller :param target str: location where the message was sent to + :param is_admin bool: the user is an admin :param kwargs: for API compatibility :return: None """ if target == self.client.nick: self.logger.debug("We are being called by {}".format(nick)) - if kwargs.get('is_admin', None) is True: + if is_admin: admin = self.is_admin(nick, kwargs['host']) if admin: self.admins[admin]['logged_in'] = False self.admins[admin]['logged_in_nick'] = None self.admins[admin]['logged_in_hostname'] = None - kwargs['is_admin'] = False - kwargs['target'] = nick - kwargs['message'] = "{}, you are logged out of" \ - " successfully".format(nick, admin) + is_admin = False + _target = nick + _message = "{}, you are logged out of " \ + "successfully".format(_target, admin) + self.logger.debug("We have successfully logged {}" - " out, notifying".format(nick)) - self.client.msg(**kwargs) + " out, notifying".format(_target)) + await self.client.msg(target=_target, message=_message) self.logger.debug("We are calling save_config") await self.save_config() @@ -411,6 +421,7 @@ class Admin: nick: str, target: str, message: str, + is_admin: bool, **kwargs) -> None: """ This method will change the password to the administrator currently @@ -421,12 +432,13 @@ class Admin: :param nick str: the nickname of the caller :param target str: the target where the message was sent to :param message str: the message sent to target + :param is_admin bool: the user is an admin :param kwargs: for API compatibility :return: None """ if target == self.client.nick: self.logger.debug("We are being called by {}".format(nick)) - if kwargs.get('is_admin', None) is True: + if is_admin: match = self.PASSWD.match(message) if len(match.groups()) == 1: admin = self.is_admin(nick, kwargs['host']) @@ -434,24 +446,26 @@ class Admin: self.admins[admin]['LOGIN'] = re.compile( r'^login\s+({})\s+({})\s*$'.format( admin, re.escape(self.hash(match.group(1))))) - kwargs['target'] = nick - kwargs['message'] = \ + _target = nick + _message = \ '{}, password for {} has been successfully' \ - ' changed...'.format(nick, admin) + ' changed...'.format(_target, admin) self.logger.debug( "We have successfully changed {}'s password," - " notifying".format(nick)) - self.client.msg(**kwargs) + " notifying".format(_target)) + await self.client.msg(target=_target, message=_message) self.logger.debug("We are calling save_config()") await self.save_config() - kwargs['target'] = self.client.nick - self.logger.debug("We are logging {} out".format(nick)) - await self.log_out(nick, **kwargs) + _target = self.client.nick + self.logger.debug("We are logging {} out".format(_target)) + await self.log_out(nick=nick, target=_target, is_admin=is_admin, **kwargs) async def admin_add(self, nick: str, target: str, message: str, + is_admin: bool, + level: int, **kwargs) -> None: """ This method will add an administrator to the list of administrators @@ -462,39 +476,42 @@ class Admin: :param nick str: the nickname of the caller :param target str: location where the message was sent to :param message str: the message being sent to the target + :param is_admin bool: the user is an admin + :param level int: the access level of the user being added :param kwargs: for API compatibility :return: None """ if target == self.client.nick: self.logger.debug("We are being called by {}".format(nick)) - if kwargs.get('is_admin', None) is True: + if is_admin: match = self.ADD.match(message) if len(match.groups()) == 3: if self.admins.get(match.group(1), None) is None: - kwargs['target'] = nick - level = self.level_up(kwargs['level'], + _target = nick + level = self.level_up(level, int(match.group(3))) - kwargs['message'] = "{} has been added with " \ - "level {}...".format( - match.group(1), level) + _message = "{} has been added with " \ + "level {}...".format(match.group(1), level) self.logger.debug( "We have added {} with level {}, notifying" - " {}".format(match.group(1), level, nick)) + " {}".format(match.group(1), level, _target)) await self.add_admin( match.group(1), match.group(2), level) else: - kwargs['target'] = nick - kwargs['message'] = "{} has already been added" \ - "...".format(match.group(1)) + _target = nick + _message = "{} has already been added" \ + "...".format(match.group(1)) self.logger.warn( "We detected that {} has already been added," - " notifying {}".format(match.group(1), nick)) - self.client.msg(**kwargs) + " notifying {}".format(match.group(1), _target)) + await self.client.msg(target=_target, message=_message) async def admin_rm(self, nick: str, target: str, message: str, + is_admin: bool, + level: int, **kwags) -> None: """ This method will remove an administrator from the list of @@ -507,56 +524,59 @@ class Admin: :param nick str: the nickname of the caller :param target str: location where the message was sent to :param message str: the message being sent to the target + :param is_admin bool: the user is an admin + :param level int: the access level of the user being removed :param kwags: for API compatibility :return: None """ if target == self.client.nick: self.logger.debug("We are being called by {}".format(nick)) - if kwags.get('is_admin', None) is True: + if is_admin: match = self.RM.match(message) if len(match.groups()) == 1: if self.admins.get(match.group(1), None) is None: - kwags['target'] = nick - kwags['message'] = "{} is not on the list" \ - "...".format(match.group(1)) + _target = nick + _message = "{} is not on the list" \ + "...".format(match.group(1)) self.logger.warn("admin_rm() ") else: - if kwags['level'] > \ - self.admins[match.group(1)]['level'] \ - or (kwags['level'] == 1000 - and self.admins[match.group(1)]['level'] - and kwags['level'] == - self.admins[match.group(1)]['level']): - kwags['target'] = nick - kwags['message'] = "{} has been removed" \ - "...".format(match.group(1)) + if level > self.admins[match.group(1)]['level'] \ + or (level == 1000 and self.admins[match.group(1)]['level'] + and level == self.admins[match.group(1)]['level']): + _target = nick + _message = "{} has been removed" \ + "...".format(match.group(1)) self.logger.debug("We removed {} successfully," " notifying {}".format( - match.group(1), nick)) + match.group(1), _target)) await self.rm_admin(match.group(1)) else: - kwags['target'] = nick - kwags['message'] = "{}, you do not have enough" \ - " access to delete {}".format( - nick, match.group(1)) + _target = nick + _message = "{}, you do not have enough" \ + " access to delete {}".format(_target, match.group(1)) self.logger.warn( "We detected that {0} does not have enough" " access to delete {1}, notifying {0}".format( - nick, match.group(1))) - self.client.msg(**kwags) + _target, match.group(1))) + await self.client.msg(target=_target, message=_message) - def admin_list(self, nick: str, target: str, **kwargs) -> None: + async def admin_list(self, + nick: str, + target: str, + is_admin: bool, + **kwargs) -> None: """ This method returns to the admin the admin list :param nick str: the nickname of the caller :param target str: location where the message was sent to + :param is_admin bool: the user is an admin :param kwargs: for API compatibility :return: None """ if target == self.client.nick: self.logger.debug("We are being called by {}".format(nick)) - if kwargs.get('is_admin', None) is True: + if is_admin: admins = "" for key, _ in self.admins.items(): if admins: @@ -565,13 +585,13 @@ class Admin: else: admins = "{}({})".format( key, self.admins[key]['level']) - kwargs['target'] = nick - kwargs['message'] = "List of Administrators:" - self.client.msg(**kwargs) - kwargs['message'] = admins + _target = nick + _message = "List of Administrators:" + await self.client.msg(target=_target, message=_message) + _message = admins self.logger.debug("We are returning admin list page to" - " {}".format(kwargs)) - self.client.msg(**kwargs) + " {}".format(_target)) + await self.client.msg(target=_target, message=_message) def is_admin(self, user: str, host: str): """ diff --git a/boots/admin_commands.py b/boots/admin_commands.py index 90ff5ab..056b0e9 100644 --- a/boots/admin_commands.py +++ b/boots/admin_commands.py @@ -22,18 +22,25 @@ class AdminCmd: self.services = {} admin.client.on("ADMIN")(self._handle) - def _handle(self, target: str, message: str, **kwargs) -> None: + def _handle(self, + nick: str, + target: str, + message: str, + is_admin: bool, + **kwargs) -> None: """ client callback on event trigger + :param nick str: the nickname of the user triggering the ADMIN event :param target str: the target the message was sent to :param message str: the message sent to the target + :param is_admin bool: the user is an admin :param kwargs: for API compatibility :return: None """ - if bool(kwargs.get('is_admin', None)): + if is_admin: self.logger.debug( - "We are being called by {}".format(kwargs['nick'])) + "We are being called by {}".format(nick)) for regex, (func, pattern) in self.services.items(): match = regex.match(message) if match: diff --git a/boots/boots.py b/boots/boots.py index eb7153a..a2fd858 100644 --- a/boots/boots.py +++ b/boots/boots.py @@ -48,19 +48,27 @@ async def plugins(bot: robot.Bot): @admin_cmd.on_command("part") async def part(target, message, **kwargs): - await bot.part(channel=message) + _target = message.split(' ')[0] + _message = " ".join(message.split(' ')[1:]) + await bot.part(channel=_target, message=_message) @admin_cmd.on_command("msg") async def msg(target, message, **kwargs): - kwargs['target'] = message.split(' ')[0] - kwargs['message'] = " ".join(message.split(' ')[1:]) - await bot.msg(**kwargs) + _target = message.split(' ')[0] + _message = " ".join(message.split(' ')[1:]) + await bot.msg(target=_target, message=_message) @admin_cmd.on_command("action") async def action(target, message, **kwargs): - kwargs['target'] = message.split(' ')[0] - kwargs['message'] = " ".join(message.split(' ')[1:]) - await bot.action(**kwargs) + _target = message.split(' ')[0] + _message = " ".join(message.split(' ')[1:]) + await bot.action(target=_target, message=_message) + + @admin_cmd.on_command("notice") + async def msg(target, message, **kwargs): + _target = message.split(' ')[0] + _message = " ".join(message.split(' ')[1:]) + await bot.notice(target=_target, message=_message) @admin_cmd.on_command("quit") async def quit(target, message, **kwargs): diff --git a/boots/robot.py b/boots/robot.py index 1a26783..2a925c5 100644 --- a/boots/robot.py +++ b/boots/robot.py @@ -46,8 +46,8 @@ class Bot(bottom.Client): self.global_timestamp_queue = [] self.on("ping", self.keepalive) self.on("CLIENT_CONNECT", self.on_connect) - self.on("PRIVMSG", self.privmsg) - self.on("NOTICE", self.notice) + self.on("PRIVMSG", self.on_privmsg) + self.on("NOTICE", self.on_notice) async def keepalive(self, message: str = None, **kwargs) -> None: """ @@ -95,6 +95,9 @@ class Bot(bottom.Client): self.logger.debug("Running {}".format(func)) await self.loop.create_task(func(**kwargs)) + # Wait 5 seconds to make sure everything synched up + await asyncio.sleep(5) + self.logger.info( "We are auto-joining channels {}".format(self.channels)) for channel in self.channels: @@ -213,62 +216,75 @@ class Bot(bottom.Client): await self.global_buffer(command, **kwargs) - async def msg(self, **kwargs) -> None: + async def msg(self, target: str, message: str, **kwargs) -> None: """ This method will send a private message to the messages buffer - :param kwargs: the information required for the command + :param target str: the target for the message to be sent to + :param message str: the message to send to target + :param kwargs: any other parameters that needs to be sent :return None """ - await self.msg_buffer("PRIVMSG", **kwargs) + await self.msg_buffer("PRIVMSG", target=target, message=message, **kwargs) - async def action(self, **kwargs) -> None: + async def action(self, target: str, message: str, **kwargs) -> None: """ This method will send an action message to the messages buffer - :param kwargs: the information required for the command + :param target str: the target for the message to be sent to + :param message str: the message to send to target + :param kwargs: any other parameters that needs to be sent :return None """ - if kwargs.get('message', None): - kwargs['message'] = \ - "\x01ACTION {}\x01".format(kwargs['message']) + _message = "\x01ACTION {}\x01".format(message) + await self.msg_buffer("PRIVMSG", target=target, message=_message, **kwargs) - await self.msg_buffer("PRIVMSG", **kwargs) - else: - # TODO: better error handling for the future - self.logger.error("ACTION does not have a message\n{}".format(kwargs)) + async def notice(self, target: str, message: str, **kwargs) -> None: + """ + This method will send a notice message to the messages buffer - async def join(self, **kwargs) -> None: + :param target str: the target for the notice to be sent to + :param message str: the message to send to target + :param kwargs: any other parameters that needs to be sent + :return None + """ + await self.msg_buffer("NOTICE", target=target, message=message, **kwargs) + + async def join(self, channel: str, **kwargs) -> None: """ This method will send a join command to the action buffer - :param kwargs: the information required for the command + :param channel str: the channel to join + :param kwargs: any other parameters that needs to be sent :return None """ - await self.action_buffer("JOIN", **kwargs) + await self.action_buffer("JOIN", channel=channel, **kwargs) - async def part(self, **kwargs) -> None: + async def part(self, channel: str, message: str = None, **kwargs) -> None: """ This method will send a part command to the action buffer - :param kwargs: the information required for the command + :param channel str: the channel to part + :param message str: the message to send on part + :param kwargs: any other parameters that needs to be sent :return None """ - await self.action_buffer("PART", **kwargs) + await self.action_buffer("PART", channel=channel, message=message, **kwargs) - async def quit(self, **kwargs) -> None: + async def quit(self, message: str, **kwargs) -> None: """ This method will send a quit command to the action buffer - :param kwargs: the information required for the command + :param message str: the message to send on quit + :param kwargs: any other parameters that needs to be sent :return None """ - await self.action_buffer("QUIT", **kwargs) + await self.action_buffer("QUIT", message=message, **kwargs) - def privmsg(self, **kwargs) -> None: + def on_privmsg(self, **kwargs) -> None: self.logger.debug("PRIVMSG {}".format(kwargs)) - def notice(self, **kwargs) -> None: + def on_notice(self, **kwargs) -> None: self.logger.debug("NOTICE {}".format(kwargs)) async def on_disconnect(self, **kwargs) -> None: -- 2.45.2 From a3cd1fceb018c9729d5756f6b5d1b6d48089a603 Mon Sep 17 00:00:00 2001 From: Elijah Lazkani Date: Sat, 17 Mar 2018 01:10:06 -0400 Subject: [PATCH 4/5] Adding usermode and channelmode capabilities with extra !mode, !op and !deop commands. --- boots/admin_commands.py | 2 +- boots/boots.py | 79 +++++++++++++++++++++++++++++++++++++---- boots/robot.py | 26 ++++++++++++++ 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/boots/admin_commands.py b/boots/admin_commands.py index 056b0e9..a3c869b 100644 --- a/boots/admin_commands.py +++ b/boots/admin_commands.py @@ -50,7 +50,7 @@ class AdminCmd: split_msg = message.split(" ") message = " ".join(split_msg[1:]) self.client.loop.create_task( - func(target, message, **kwargs)) + func(nick, target, message, **kwargs)) def on_command(self, command: str, diff --git a/boots/boots.py b/boots/boots.py index a2fd858..2292352 100644 --- a/boots/boots.py +++ b/boots/boots.py @@ -1,3 +1,4 @@ +import re import logging import asyncio import robot @@ -43,35 +44,101 @@ async def plugins(bot: robot.Bot): admin_cmd = admin_commands.AdminCmd(administrator) @admin_cmd.on_command("join") - async def join(target, message, **kwargs): + async def join(nick, target, message, **kwargs): await bot.join(channel=message) @admin_cmd.on_command("part") - async def part(target, message, **kwargs): + async def part(nick, target, message, **kwargs): _target = message.split(' ')[0] _message = " ".join(message.split(' ')[1:]) await bot.part(channel=_target, message=_message) @admin_cmd.on_command("msg") - async def msg(target, message, **kwargs): + async def msg(nick, target, message, **kwargs): _target = message.split(' ')[0] _message = " ".join(message.split(' ')[1:]) await bot.msg(target=_target, message=_message) @admin_cmd.on_command("action") - async def action(target, message, **kwargs): + async def action(nick, target, message, **kwargs): _target = message.split(' ')[0] _message = " ".join(message.split(' ')[1:]) await bot.action(target=_target, message=_message) @admin_cmd.on_command("notice") - async def msg(target, message, **kwargs): + async def msg(nick, arget, message, **kwargs): _target = message.split(' ')[0] _message = " ".join(message.split(' ')[1:]) await bot.notice(target=_target, message=_message) + @admin_cmd.on_command("mode") + async def chanmode(nick, target, message, **kwargs): + chanmode_pattern = r'^(?P(\#|\#\#|\&)[a-zA-Z0-9]+)?' \ + r'((^|\s+)(?P[\+\-][a-zA-Z]+))' \ + r'(\s+(?P.+)?)?' + compiled = re.compile(chanmode_pattern) + m = compiled.match(message) + + if m.group("channel"): + _channel = m.group("channel") + else: + _channel = target + + if m.group("modes"): + _modes = m.group("modes") + else: + await bot.msg(target=target, message="{}, you did not provide a mode".format(nick)) + return + + if m.group("params"): + _params = m.group("params") + else: + _params = None + + await bot.channelmode(channel=_channel, modes=_modes, params=_params) + + @admin_cmd.on_command("op") + async def op(nick, target, message, **kwargs): + _split_msg = message.split(' ') + nick_list = None + _channel = target + + if _split_msg[0]: + if _split_msg[0].startswith('#') or _split_msg[0].startswith('&'): + _channel = _split_msg[0] + if _split_msg.__len__() > 1: + nick_list = _split_msg[1:] + else: + nick_list = _split_msg + + if nick_list: + _modes = "+" + "o" * nick_list.__len__() + await bot.channelmode(channel=_channel, modes=_modes, params=" ".join(nick_list)) + else: + await bot.channelmode(channel=_channel, modes="+o", params=nick) + + @admin_cmd.on_command("deop") + async def op(nick, target, message, **kwargs): + _split_msg = message.split(' ') + nick_list = None + _channel = target + + if _split_msg[0]: + if _split_msg[0].startswith('#') or _split_msg[0].startswith('&'): + _channel = _split_msg[0] + if _split_msg.__len__() > 1: + nick_list = _split_msg[1:] + else: + nick_list = _split_msg + + if nick_list: + _modes = "-" + "o" * nick_list.__len__() + await bot.channelmode(channel=_channel, modes=_modes, params=" ".join(nick_list)) + else: + await bot.channelmode(channel=_channel, modes="-o", params=nick) + @admin_cmd.on_command("quit") - async def quit(target, message, **kwargs): + async def quit(nick, target, message, **kwargs): await bot.quit(message=message) await bot.disconnect() # Exit the event loop cleanly diff --git a/boots/robot.py b/boots/robot.py index 2a925c5..c2b7b66 100644 --- a/boots/robot.py +++ b/boots/robot.py @@ -260,6 +260,32 @@ class Bot(bottom.Client): """ await self.action_buffer("JOIN", channel=channel, **kwargs) + async def usermode(self, nick: str, modes: str, **kwargs): + """ + This method will send a usermode command to the server + + :param nick str: the nick to apply usermode action to + :param modes str: the mode to apply to the nick + :param kwargs: any other parameters that needs to be sent + :return None + """ + self.send("USERMODE", nick=nick, modes=modes) + + async def channelmode(self, channel: str, modes: str, params: str = None ,**kwargs): + """ + This method will send a channelmode command to the server + + :param channel str: the nick to apply usermode action to + :param modes str: the mode to apply to the channel + :param params str: the parameters to apply to the modes + :param kwargs: any other parameters that needs to be sent + :return None + """ + if params: + self.send("CHANNELMODE", channel=channel, modes=modes, params=params) + else: + self.send("CHANNELMODE", channel=channel, modes=modes) + async def part(self, channel: str, message: str = None, **kwargs) -> None: """ This method will send a part command to the action buffer -- 2.45.2 From 30ca9bc559922f454aa23c53eb69153f8fd80bf2 Mon Sep 17 00:00:00 2001 From: Elijah Lazkani Date: Sat, 17 Mar 2018 10:50:04 -0400 Subject: [PATCH 5/5] Fixing ugly exit with adding a clean interrupt exit. --- boots/boots.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/boots/boots.py b/boots/boots.py index 2292352..f428b63 100644 --- a/boots/boots.py +++ b/boots/boots.py @@ -161,7 +161,12 @@ def main(): bot.loop.create_task(bot.connect()) asyncio.ensure_future(plugins(bot)) - bot.loop.run_forever() + + try: + bot.loop.run_forever() + except KeyboardInterrupt: + bot.loop.stop() + bot.loop.close() -- 2.45.2