From 62a39f71e7e4575e7975475f96b876605ece8c25 Mon Sep 17 00:00:00 2001 From: deathbybandaid Date: Wed, 9 Feb 2022 16:20:48 -0500 Subject: [PATCH] test --- SpiceBot/SpiceBotCore/__init__.py | 17 +- .../SpiceBotCore/interface/comms/__init__.py | 247 ++++++++++++++++++ .../{ => interface}/config/__init__.py | 0 .../{ => interface}/database/__init__.py | 0 .../{ => interface}/logger/__init__.py | 0 .../{ => interface}/versions/__init__.py | 0 6 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 SpiceBot/SpiceBotCore/interface/comms/__init__.py rename SpiceBot/SpiceBotCore/{ => interface}/config/__init__.py (100%) rename SpiceBot/SpiceBotCore/{ => interface}/database/__init__.py (100%) rename SpiceBot/SpiceBotCore/{ => interface}/logger/__init__.py (100%) rename SpiceBot/SpiceBotCore/{ => interface}/versions/__init__.py (100%) diff --git a/SpiceBot/SpiceBotCore/__init__.py b/SpiceBot/SpiceBotCore/__init__.py index 21aeb2e..83cc4fb 100644 --- a/SpiceBot/SpiceBotCore/__init__.py +++ b/SpiceBot/SpiceBotCore/__init__.py @@ -1,8 +1,9 @@ -from .config import Config -from .versions import Versions -from .logger import Logger -from .database import Database +from .interface.config import Config +from .interface.versions import Versions +from .interface.logger import Logger +from .interface.database import Database +from .interface.comms import Comms class SpiceBotCore_OBJ(): @@ -28,6 +29,10 @@ class SpiceBotCore_OBJ(): self.database = Database(self.config) self.logger.info("SpiceBot Database Interface Setup Complete.") + # Bypass Sopel's method for writing to IRC + self.comms = Comms(self.config) + self.logger.info("SpiceBot Comms Interface Setup Complete.") + def setup(self, bot): """This runs with the plugin setup routine""" @@ -36,3 +41,7 @@ class SpiceBotCore_OBJ(): # Re-initialize the bot config properly during plugin setup routine self.config.config = bot.config + + # OSD shortcut + def osd(self, messages, recipients=None, text_method='PRIVMSG', max_messages=-1): + return self.comms.osd(messages, recipients, text_method, max_messages) diff --git a/SpiceBot/SpiceBotCore/interface/comms/__init__.py b/SpiceBot/SpiceBotCore/interface/comms/__init__.py new file mode 100644 index 0000000..5f9c7c0 --- /dev/null +++ b/SpiceBot/SpiceBotCore/interface/comms/__init__.py @@ -0,0 +1,247 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function + +from sopel.tools import Identifier +from sopel.irc.utils import safe + +import threading +import sys +if sys.version_info.major >= 3: + from collections import abc + +if sys.version_info.major >= 3: + unicode = str + + +class Comms(): + + def __init__(self, config): + self.config = config + + self.backend = None + self.sending = threading.RLock() + self.stack = {} + + def append_bot(self, bot): + self.bot = bot + + @property + def botnick(self): + return self.config.core.nick + + @property + def bothostmask(self): + return self.bot.hostmask + + def write(self, args, text=None): + while not self.backend: + pass + args = [safe(arg) for arg in args] + self.backend.send_command(*args, text=text) + + def get_message_recipientgroups(self, recipients, text_method): + """ + Split recipients into groups based on server capabilities. + This defaults to 4 + Input can be + * unicode string + * a comma-seperated unicode string + * list + * dict_keys handy for list(bot.channels.keys()) + """ + + if sys.version_info.major >= 3: + if isinstance(recipients, abc.KeysView): + recipients = [x for x in recipients] + if isinstance(recipients, dict): + recipients = [x for x in recipients] + + if not isinstance(recipients, list): + recipients = recipients.split(",") + + if not len(recipients): + raise ValueError("Recipients list empty.") + + if text_method == 'NOTICE': + maxtargets = 4 + elif text_method in ['PRIVMSG', 'ACTION']: + maxtargets = 4 + maxtargets = int(maxtargets) + + recipientgroups = [] + while len(recipients): + recipients_part = ','.join(x for x in recipients[-maxtargets:]) + recipientgroups.append(recipients_part) + del recipients[-maxtargets:] + + return recipientgroups + + def get_available_message_bytes(self, recipientgroups, text_method): + """ + Get total available bytes for sending a message line + Total sendable bytes is 512 + * 15 are reserved for basic IRC NOTICE/PRIVMSG and a small buffer. + * The bots hostmask plays a role in this count + Note: if unavailable, we calculate the maximum length of a hostmask + * The recipients we send to also is a factor. Multiple recipients reduces + sendable message length + """ + + if text_method == 'ACTION': + text_method_bytes = (len('PRIVMSG') + + len("\x01ACTION \x01") + ) + else: + text_method_bytes = len(text_method) + + if self.bothostmask: + hostmaskbytes = len((self.bothostmask).encode('utf-8')) + else: + hostmaskbytes = (len((self.botnick).encode('utf-8')) # Bot's NICKLEN + + 1 # (! separator) + + len('~') # (for the optional ~ in user) + + 9 # max username length + + 1 # (@ separator) + + 63 # has a maximum length of 63 characters. + ) + + # find the maximum target group length, and use the max + groupbytes = [] + for recipients_part in recipientgroups: + groupbytes.append(len((recipients_part).encode('utf-8'))) + + max_recipients_bytes = max(groupbytes) + + allowedLength = (512 + - len(':') - hostmaskbytes + - len(' ') - text_method_bytes - len(' ') + - max_recipients_bytes + - len(' :') + - len('\r\n') + ) + + return allowedLength + + def get_sendable_message_list(self, messages, max_length=400): + """Get a sendable ``text`` message list. + :param str txt: unicode string of text to send + :param int max_length: maximum length of the message to be sendable + :return: a tuple of two values, the sendable text and its excess text + We're arbitrarily saying that the max is 400 bytes of text when + messages will be split. Otherwise, we'd have to account for the bot's + hostmask, which is hard. + The `max_length` is the max length of text in **bytes**, but we take + care of unicode 2-bytes characters, by working on the unicode string, + then making sure the bytes version is smaller than the max length. + """ + + if not isinstance(messages, list): + messages = [messages] + + messages_list = [''] + message_padding = 4 * " " + + for message in messages: + if len((messages_list[-1] + message_padding + message).encode('utf-8')) <= max_length: + if messages_list[-1] == '': + messages_list[-1] = message + else: + messages_list[-1] = messages_list[-1] + message_padding + message + else: + text_list = [] + while len(message.encode('utf-8')) > max_length and not message.isspace(): + last_space = message.rfind(' ', 0, max_length) + if last_space == -1: + # No last space, just split where it is possible + splitappend = message[:max_length] + if not splitappend.isspace(): + text_list.append(splitappend) + message = message[max_length:] + else: + # Split at the last best space found + splitappend = message[:last_space] + if not splitappend.isspace(): + text_list.append(splitappend) + message = message[last_space:] + if len(message.encode('utf-8')) and not message.isspace(): + text_list.append(message) + messages_list.extend(text_list) + + return messages_list + + def osd(self, messages, recipients=None, text_method='PRIVMSG', max_messages=-1): + """Send ``text`` as a PRIVMSG, CTCP ACTION, or NOTICE to ``recipients``. + In the context of a triggered callable, the ``recipient`` defaults to + the channel (or nickname, if a private message) from which the message + was received. + By default, unless specified in the configuration file, there is some + built-in flood protection. Messages displayed over 5 times in 2 minutes + will be displayed as '...'. + The ``recipient`` can be in list format or a comma seperated string, + with the ability to send to multiple recipients simultaneously. The + default recipients that the bot will send to is 4 if the IRC server + doesn't specify a limit for TARGMAX. + Text can be sent to this function in either string or list format. + List format will insert as small buffering space between entries in the + list. + There are 512 bytes available in a single IRC message. This includes + hostmask of the bot as well as around 15 bytes of reserved IRC message + type. This also includes the destinations/recipients of the message. + This will split given strings/lists into a displayable format as close + to the maximum 512 bytes as possible. + If ``max_messages`` is given, the split mesage will display in as many + lines specified by this argument. Specifying ``0`` or a negative number + will display without limitation. By default this is set to ``-1`` when + called directly. When called from the say/msg/reply/notice/action it + will default to ``1``. + """ + + if not hasattr(self, 'stack'): + self.stack = {} + + text_method = text_method.upper() + if text_method == 'SAY' or text_method not in ['NOTICE', 'ACTION']: + text_method = 'PRIVMSG' + + recipientgroups = self.get_message_recipientgroups(recipients, text_method) + available_bytes = self.get_available_message_bytes(recipientgroups, text_method) + messages_list = self.get_sendable_message_list(messages, available_bytes) + + if max_messages >= 1: + messages_list = messages_list[:max_messages] + + text_method_orig = text_method + + for recipientgroup in recipientgroups: + text_method = text_method_orig + + recipient_id = Identifier(recipientgroup) + + recipient_stack = self.stack.setdefault(recipient_id, { + 'messages': [], + 'flood_left': 999, + 'dots': 0, + }) + recipient_stack['dots'] = 0 + + with self.sending: + + for text in messages_list: + + if recipient_stack['dots'] <= 3: + if text_method == 'ACTION': + text = '\001ACTION {}\001'.format(text) + self.write(('PRIVMSG', recipientgroup), text) + text_method = 'PRIVMSG' + elif text_method == 'NOTICE': + self.write(('NOTICE', recipientgroup), text) + else: + self.write(('PRIVMSG', recipientgroup), text) + + def __getattr__(self, name): + """ + Quick and dirty shortcuts. Will only get called for undefined attributes. + """ + + if hasattr(self.bot, name): + return eval("self.bot.%s" % name) diff --git a/SpiceBot/SpiceBotCore/config/__init__.py b/SpiceBot/SpiceBotCore/interface/config/__init__.py similarity index 100% rename from SpiceBot/SpiceBotCore/config/__init__.py rename to SpiceBot/SpiceBotCore/interface/config/__init__.py diff --git a/SpiceBot/SpiceBotCore/database/__init__.py b/SpiceBot/SpiceBotCore/interface/database/__init__.py similarity index 100% rename from SpiceBot/SpiceBotCore/database/__init__.py rename to SpiceBot/SpiceBotCore/interface/database/__init__.py diff --git a/SpiceBot/SpiceBotCore/logger/__init__.py b/SpiceBot/SpiceBotCore/interface/logger/__init__.py similarity index 100% rename from SpiceBot/SpiceBotCore/logger/__init__.py rename to SpiceBot/SpiceBotCore/interface/logger/__init__.py diff --git a/SpiceBot/SpiceBotCore/versions/__init__.py b/SpiceBot/SpiceBotCore/interface/versions/__init__.py similarity index 100% rename from SpiceBot/SpiceBotCore/versions/__init__.py rename to SpiceBot/SpiceBotCore/interface/versions/__init__.py