diff --git a/requirements.txt b/requirements.txt index b8b4d4e..bb005cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ sopel>=6.0,<7 spicemanip>=0.1.6 gitpython -sopel-modules.osd fake-useragent diff --git a/setup.py b/setup.py index 6477787..2cad634 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ with open('dev-requirements.txt') as dev_requirements_file: setup( name='sopel_modules.SpiceBot', - version='0.1.0', + version='0.1.1', description='SpiceBot aims to run SpiceBot as an IRC service', long_description=readme + '\n\n' + history, author='Sam Zick', diff --git a/sopel_modules/SpiceBot_BotPrivs/BotPrivs.py b/sopel_modules/SpiceBot_BotPrivs/BotPrivs.py new file mode 100644 index 0000000..f6ae1b5 --- /dev/null +++ b/sopel_modules/SpiceBot_BotPrivs/BotPrivs.py @@ -0,0 +1,37 @@ +# coding=utf-8 + +from __future__ import unicode_literals, absolute_import, division, print_function + +import sopel.module + +import spicemanip + +from sopel_modules.SpiceBot_SBTools import sopel_triggerargs, bot_privs + + +def configure(config): + pass + + +def setup(bot): + pass + + +@sopel.module.nickname_commands('owners', 'owner') +def bot_command_owners(bot, trigger): + bot_command_process(bot, trigger) + + +@sopel.module.nickname_commands('admins') +def bot_command_admins(bot, trigger): + bot_command_process(bot, trigger) + + +def bot_command_process(bot, trigger): + triggerargs, triggercommand = sopel_triggerargs(bot, trigger, 'nickname_command') + + if triggercommand == 'owner': + triggercommand = 'owners' + + privlist = spicemanip.main(bot_privs(bot, triggercommand), 'andlist') + bot.osd(str(bot.nick) + " " + triggercommand + ": " + privlist) diff --git a/sopel_modules/SpiceBot_BotPrivs/__init__.py b/sopel_modules/SpiceBot_BotPrivs/__init__.py new file mode 100644 index 0000000..d8f5712 --- /dev/null +++ b/sopel_modules/SpiceBot_BotPrivs/__init__.py @@ -0,0 +1,8 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function + +from .BotPrivs import * + +__author__ = 'Sam Zick' +__email__ = 'sam@deathbybandaid.net' +__version__ = '0.1.1' diff --git a/sopel_modules/SpiceBot_Botevents/__init__.py b/sopel_modules/SpiceBot_Botevents/__init__.py index e4dc160..ca44086 100644 --- a/sopel_modules/SpiceBot_Botevents/__init__.py +++ b/sopel_modules/SpiceBot_Botevents/__init__.py @@ -12,4 +12,4 @@ from .Startup_Complete import * __author__ = 'Sam Zick' __email__ = 'sam@deathbybandaid.net' -__version__ = '0.1.0' +__version__ = '0.1.1' diff --git a/sopel_modules/SpiceBot_Channels/Channels.py b/sopel_modules/SpiceBot_Channels/Channels.py index 8985fea..05bc7f5 100644 --- a/sopel_modules/SpiceBot_Channels/Channels.py +++ b/sopel_modules/SpiceBot_Channels/Channels.py @@ -11,8 +11,6 @@ import time import spicemanip -import sopel_modules.osd - from sopel_modules.SpiceBot_SBTools import ( sopel_triggerargs, inlist, topic_compile, channel_privs, join_all_channels, chanadmin_all_channels, channel_list_current @@ -126,7 +124,8 @@ def watch_chanlist_complete(bot, trigger): @sopel.module.nickname_commands('channels', 'channel') def nickname_comand_chanstats(bot, trigger): - triggerargs = sopel_triggerargs(bot, trigger, 'nickname_command') + + triggerargs, triggercommand = sopel_triggerargs(bot, trigger, 'nickname_command') if not len(triggerargs): commandused = 'list' @@ -185,91 +184,22 @@ def nickname_comand_chanstats(bot, trigger): bot.osd("Topic for {}: {}".format(channel, topic)) return - # OP list - if commandused == 'op': - channel = spicemanip.main(triggerargs, 1) - if not inlist(bot, channel.lower(), bot.memory['SpiceBot_Channels']['channels'].keys()): - bot.osd("Channel name {} not valid.".format(channel)) - return - if not inlist(bot, channel.lower(), bot.channels.keys()): - bot.osd("I need to be in {} to see nick privileges.".format(channel)) - return - privlist = channel_privs(bot, channel, commandused.upper()) - dispmsg = [] - if not len(privlist): - dispmsg.append("There are no Channel " + commandused.upper() + "s for " + str(channel)) + if commandused.upper() in ['OP', 'HOP', 'VOICE', 'OWNER', 'ADMIN']: + if not len(triggerargs): + if trigger.is_privmsg: + bot.osd("Channel name required.") + return + else: + channel = trigger.sender else: - oplist = spicemanip.main(privlist, 'andlist') - dispmsg.append("Channel " + commandused.upper() + "s for " + str(channel) + " are: " + oplist) - bot.osd(dispmsg, trigger.nick, 'notice') - return + channel = spicemanip.main(triggerargs, 1) + if not inlist(bot, channel.lower(), bot.memory['SpiceBot_Channels']['channels'].keys()): + bot.osd("Channel name {} not valid.".format(channel)) + return + if not inlist(bot, channel.lower(), bot.channels.keys()): + bot.osd("I need to be in {} to see nick privileges.".format(channel)) + return - # HOP list - if commandused == 'hop': - channel = spicemanip.main(triggerargs, 1) - if not inlist(bot, channel.lower(), bot.memory['SpiceBot_Channels']['channels'].keys()): - bot.osd("Channel name {} not valid.".format(channel)) - return - if not inlist(bot, channel.lower(), bot.channels.keys()): - bot.osd("I need to be in {} to see nick privileges.".format(channel)) - return - privlist = channel_privs(bot, channel, commandused.upper()) - dispmsg = [] - if not len(privlist): - dispmsg.append("There are no Channel " + commandused.upper() + "s for " + str(channel)) - else: - oplist = spicemanip.main(privlist, 'andlist') - dispmsg.append("Channel " + commandused.upper() + "s for " + str(channel) + " are: " + oplist) - bot.osd(dispmsg, trigger.nick, 'notice') - return - - # Voice List - if commandused == 'voice': - channel = spicemanip.main(triggerargs, 1) - if not inlist(bot, channel.lower(), bot.memory['SpiceBot_Channels']['channels'].keys()): - bot.osd("Channel name {} not valid.".format(channel)) - return - if not inlist(bot, channel.lower(), bot.channels.keys()): - bot.osd("I need to be in {} to see nick privileges.".format(channel)) - return - privlist = channel_privs(bot, channel, commandused.upper()) - dispmsg = [] - if not len(privlist): - dispmsg.append("There are no Channel " + commandused.upper() + "s for " + str(channel)) - else: - oplist = spicemanip.main(privlist, 'andlist') - dispmsg.append("Channel " + commandused.upper() + "s for " + str(channel) + " are: " + oplist) - bot.osd(dispmsg, trigger.nick, 'notice') - return - - # owner List - if commandused == 'owner': - channel = spicemanip.main(triggerargs, 1) - if not inlist(bot, channel.lower(), bot.memory['SpiceBot_Channels']['channels'].keys()): - bot.osd("Channel name {} not valid.".format(channel)) - return - if not inlist(bot, channel.lower(), bot.channels.keys()): - bot.osd("I need to be in {} to see nick privileges.".format(channel)) - return - privlist = channel_privs(bot, channel, commandused.upper()) - dispmsg = [] - if not len(privlist): - dispmsg.append("There are no Channel " + commandused.upper() + "s for " + str(channel)) - else: - oplist = spicemanip.main(privlist, 'andlist') - dispmsg.append("Channel " + commandused.upper() + "s for " + str(channel) + " are: " + oplist) - bot.osd(dispmsg, trigger.nick, 'notice') - return - - # admin List - if commandused == 'admin': - channel = spicemanip.main(triggerargs, 1) - if not inlist(bot, channel.lower(), bot.memory['SpiceBot_Channels']['channels'].keys()): - bot.osd("Channel name {} not valid.".format(channel)) - return - if not inlist(bot, channel.lower(), bot.channels.keys()): - bot.osd("I need to be in {} to see nick privileges.".format(channel)) - return privlist = channel_privs(bot, channel, commandused.upper()) dispmsg = [] if not len(privlist): diff --git a/sopel_modules/SpiceBot_CommandsQuery/__init__.py b/sopel_modules/SpiceBot_CommandsQuery/__init__.py index a169d76..c2c2e86 100644 --- a/sopel_modules/SpiceBot_CommandsQuery/__init__.py +++ b/sopel_modules/SpiceBot_CommandsQuery/__init__.py @@ -9,4 +9,4 @@ from .CommandsQuery import * __author__ = 'Sam Zick' __email__ = 'sam@deathbybandaid.net' -__version__ = '0.1.0' +__version__ = '0.1.1' diff --git a/sopel_modules/SpiceBot_GifSearch/__init__.py b/sopel_modules/SpiceBot_GifSearch/__init__.py index 3aee2ea..841b583 100644 --- a/sopel_modules/SpiceBot_GifSearch/__init__.py +++ b/sopel_modules/SpiceBot_GifSearch/__init__.py @@ -11,4 +11,4 @@ from .gifsearch import * __author__ = 'Sam Zick' __email__ = 'sam@deathbybandaid.net' -__version__ = '0.1.0' +__version__ = '0.1.1' diff --git a/sopel_modules/SpiceBot_GifSearch/gif.py b/sopel_modules/SpiceBot_GifSearch/gif.py index a5f4724..a90f2d4 100644 --- a/sopel_modules/SpiceBot_GifSearch/gif.py +++ b/sopel_modules/SpiceBot_GifSearch/gif.py @@ -6,8 +6,6 @@ from sopel import module from .gifsearch import getGif -import sopel_modules.osd - from sopel_modules.SpiceBot_SBTools import sopel_triggerargs import spicemanip @@ -19,7 +17,7 @@ def gif_trigger(bot, trigger): while "SpiceBot_GifSearch" not in bot.memory: pass - triggerargs = sopel_triggerargs(bot, trigger) + triggerargs, triggercommand = sopel_triggerargs(bot, trigger) if triggerargs == []: return bot.osd("Please present a query to search.") diff --git a/sopel_modules/SpiceBot_GifSearch/gifapi.py b/sopel_modules/SpiceBot_GifSearch/gifapi.py index 36e0b87..9442793 100644 --- a/sopel_modules/SpiceBot_GifSearch/gifapi.py +++ b/sopel_modules/SpiceBot_GifSearch/gifapi.py @@ -6,8 +6,6 @@ from .gifsearch import getGif import sopel.module -import sopel_modules.osd - import spicemanip from sopel_modules.SpiceBot_SBTools import sopel_triggerargs @@ -19,16 +17,16 @@ def gifapi_triggers(bot, trigger): while "SpiceBot_GifSearch" not in bot.memory: pass - triggerargs, prefixcommand = sopel_triggerargs(bot, trigger, 'prefix_command') + triggerargs, triggercommand = sopel_triggerargs(bot, trigger, 'prefix_command') - if prefixcommand not in bot.memory["SpiceBot_GifSearch"]['valid_gif_api_dict'].keys(): + if triggercommand not in bot.memory["SpiceBot_GifSearch"]['valid_gif_api_dict'].keys(): return if triggerargs == []: return bot.osd("Please present a query to search.") query = spicemanip.main(triggerargs, 0) - searchdict = {"query": query, "gifsearch": prefixcommand} + searchdict = {"query": query, "gifsearch": triggercommand} gifdict = getGif(bot, searchdict) diff --git a/sopel_modules/SpiceBot_GifSearch/gifsearch.py b/sopel_modules/SpiceBot_GifSearch/gifsearch.py index 6922d29..a85b916 100644 --- a/sopel_modules/SpiceBot_GifSearch/gifsearch.py +++ b/sopel_modules/SpiceBot_GifSearch/gifsearch.py @@ -6,8 +6,6 @@ import sopel.module from sopel.tools import stderr from sopel.config.types import StaticSection, ValidatedAttribute -import sopel_modules.osd - from sopel_modules.SpiceBot_Botevents.BotEvents import set_bot_event, check_bot_events, startup_bot_event from sopel_modules.SpiceBot_CommandsQuery.CommandsQuery import commandsquery_register, commandsquery_register_type @@ -101,7 +99,7 @@ def setup_thread(bot): @sopel.module.commands('gif') def gif_trigger(bot, trigger): - triggerargs = sopel_triggerargs(bot, trigger) + triggerargs, triggercommand = sopel_triggerargs(bot, trigger) if triggerargs == []: return bot.osd("Please present a query to search.") diff --git a/sopel_modules/SpiceBot_Messaging/Messaging.py b/sopel_modules/SpiceBot_Messaging/Messaging.py new file mode 100644 index 0000000..099d356 --- /dev/null +++ b/sopel_modules/SpiceBot_Messaging/Messaging.py @@ -0,0 +1,71 @@ +# coding=utf-8 + +from __future__ import unicode_literals, absolute_import, division, print_function + +import sopel.module + +import spicemanip + +from sopel_modules.SpiceBot_SBTools import sopel_triggerargs, command_permissions_check, inlist + + +def configure(config): + pass + + +def setup(bot): + pass + + +@sopel.module.nickname_commands('action') +def bot_command_action(bot, trigger): + bot_command_process(bot, trigger) + + +@sopel.module.nickname_commands('notice') +def bot_command_notice(bot, trigger): + bot_command_process(bot, trigger) + + +@sopel.module.nickname_commands('privmsg', 'say', 'msg') +def bot_command_privmsg(bot, trigger): + bot_command_process(bot, trigger) + + +def bot_command_process(bot, trigger): + + if not command_permissions_check(bot, trigger, ['admins', 'owner']): + bot.say("I was unable to process this Bot Nick command due to privilege issues.") + return + + triggerargs, triggercommand = sopel_triggerargs(bot, trigger, 'nickname_command') + + if triggercommand in ['say', 'msg']: + triggercommand = 'privmsg' + + if not len(triggerargs): + bot.say("You must specify a channel or nick.") + return + + target = spicemanip.main(triggerargs, 1) + if (target not in ['allchans', 'allnicks'] + and not inlist(bot, target.lower(), bot.memory['SpiceBot_Channels']['channels'].keys()) + and not inlist(bot, target.lower(), bot.users)): + bot.osd("Channel/nick name {} not valid.".format(target)) + return + + triggerargs = spicemanip.main(triggerargs, '2+', 'list') + + if target == 'allchans': + targetsendlist = bot.channels.keys() + elif target == 'allnicks': + targetsendlist = bot.users + else: + targetsendlist = [target] + + botmessage = spicemanip.main(triggerargs, 0) + if not botmessage: + bot.say("You must specify a message to send.") + return + + bot.osd(botmessage, targetsendlist, triggercommand.upper()) diff --git a/sopel_modules/SpiceBot_Messaging/__init__.py b/sopel_modules/SpiceBot_Messaging/__init__.py new file mode 100644 index 0000000..816c170 --- /dev/null +++ b/sopel_modules/SpiceBot_Messaging/__init__.py @@ -0,0 +1,8 @@ +# coding=utf8 +from __future__ import unicode_literals, absolute_import, division, print_function + +from .Messaging import * + +__author__ = 'Sam Zick' +__email__ = 'sam@deathbybandaid.net' +__version__ = '0.1.1' diff --git a/sopel_modules/SpiceBot_Restart/Restart.py b/sopel_modules/SpiceBot_Restart/Restart.py index 4ed0f09..146824a 100644 --- a/sopel_modules/SpiceBot_Restart/Restart.py +++ b/sopel_modules/SpiceBot_Restart/Restart.py @@ -7,8 +7,6 @@ import sopel.module from sopel.tools import stderr from sopel.config.types import StaticSection, ValidatedAttribute -import sopel_modules.osd - from sopel_modules.SpiceBot_SBTools import service_manip diff --git a/sopel_modules/SpiceBot_SBTools/__init__.py b/sopel_modules/SpiceBot_SBTools/__init__.py index 3596aaa..d67ef4f 100644 --- a/sopel_modules/SpiceBot_SBTools/__init__.py +++ b/sopel_modules/SpiceBot_SBTools/__init__.py @@ -26,16 +26,53 @@ def sopel_triggerargs(bot, trigger, command_type='module_command'): triggerargs = spicemanip.main(trigger.args[1], 'create') triggerargs = spicemanip.main(triggerargs, 'create') - if command_type in ['module_command']: - triggerargs = spicemanip.main(triggerargs, '2+', 'list') - elif command_type in ['nickname_command']: + if command_type in ['nickname_command']: + command = spicemanip.main(triggerargs, 2).lower() triggerargs = spicemanip.main(triggerargs, '3+', 'list') - elif command_type in ['prefix_command']: - prefixcommand = spicemanip.main(triggerargs, 1).lower()[1:] + else: + command = spicemanip.main(triggerargs, 1).lower()[1:] triggerargs = spicemanip.main(triggerargs, '2+', 'list') - return triggerargs, prefixcommand - return triggerargs + return triggerargs, command + + +def bot_privs(bot, privtype): + if privtype == 'owners': + privtype = 'owner' + botpriveval = eval("bot.config.core." + privtype) + if not isinstance(botpriveval, list): + botpriveval = [botpriveval] + return botpriveval + + +def command_permissions_check(bot, trigger, privslist): + + commandrunconsensus = [] + + for botpriv in ["admins", "owner"]: + if botpriv in privslist: + botpriveval = bot_privs(bot, botpriv) + if not inlist(bot, trigger.nick, botpriveval): + commandrunconsensus.append('False') + else: + commandrunconsensus.append('True') + + if not trigger.is_privmsg: + for chanpriv in ['OP', 'HOP', 'VOICE', 'OWNER', 'ADMIN']: + if chanpriv in privslist: + chanpriveval = channel_privs(bot, trigger.sender, chanpriv) + if not inlist(bot, trigger.nick, chanpriveval): + commandrunconsensus.append('False') + else: + commandrunconsensus.append('True') + + if privslist == []: + commandrunconsensus.append('True') + + if 'True' not in commandrunconsensus: + return False + + return True """List Manipulation Functions""" diff --git a/sopel_modules/SpiceBot_StartupMonologue/StartupMonologue.py b/sopel_modules/SpiceBot_StartupMonologue/StartupMonologue.py index 7817ca6..322511f 100644 --- a/sopel_modules/SpiceBot_StartupMonologue/StartupMonologue.py +++ b/sopel_modules/SpiceBot_StartupMonologue/StartupMonologue.py @@ -5,9 +5,6 @@ from __future__ import unicode_literals, absolute_import, division, print_functi from sopel import module from sopel.tools import stderr -import sopel_modules.osd - - from sopel_modules.SpiceBot_Botevents.BotEvents import set_bot_event, check_bot_events from sopel_modules.SpiceBot_CommandsQuery.CommandsQuery import commandsquery_register diff --git a/sopel_modules/SpiceBot_StartupMonologue/__init__.py b/sopel_modules/SpiceBot_StartupMonologue/__init__.py index 70dd200..958ebb0 100644 --- a/sopel_modules/SpiceBot_StartupMonologue/__init__.py +++ b/sopel_modules/SpiceBot_StartupMonologue/__init__.py @@ -9,4 +9,4 @@ from .StartupMonologue import * __author__ = 'Sam Zick' __email__ = 'sam@deathbybandaid.net' -__version__ = '0.1.0' +__version__ = '0.1.1' diff --git a/sopel_modules/SpiceBot_Update/Update.py b/sopel_modules/SpiceBot_Update/Update.py index 489d119..06b93f0 100644 --- a/sopel_modules/SpiceBot_Update/Update.py +++ b/sopel_modules/SpiceBot_Update/Update.py @@ -7,11 +7,9 @@ import sopel.module from sopel.tools import stderr from sopel.config.types import StaticSection, ValidatedAttribute -import sopel_modules.osd - import spicemanip -from sopel_modules.SpiceBot_SBTools import service_manip, spicebot_update, sopel_triggerargs +from sopel_modules.SpiceBot_SBTools import service_manip, spicebot_update, sopel_triggerargs, command_permissions_check class SpiceBot_Update_MainSection(StaticSection): @@ -33,7 +31,11 @@ def setup(bot): @sopel.module.nickname_commands('update') def nickname_comand_chanstats(bot, trigger): - triggerargs = sopel_triggerargs(bot, trigger, 'nickname_command') + if not command_permissions_check(bot, trigger, ['admins', 'owner', 'OP', 'ADMIN', 'OWNER']): + bot.say("I was unable to process this Bot Nick command due to privilege issues.") + return + + triggerargs, triggercommand = sopel_triggerargs(bot, trigger, 'nickname_command') if not len(triggerargs): commandused = 'nodeps' @@ -46,9 +48,6 @@ def nickname_comand_chanstats(bot, trigger): triggerargs = spicemanip.main(triggerargs, '2+', 'list') - if not trigger.admin: - bot.say("You are not authorized to perform this function.") - stderr("Recieved Command to update.") bot.osd("Received command from " + trigger.nick + " to update from Github and restart. Be Back Soon!", bot.channels.keys()) diff --git a/sopel_modules/SpiceBot_osd/__init__.py b/sopel_modules/SpiceBot_osd/__init__.py new file mode 100644 index 0000000..7890905 --- /dev/null +++ b/sopel_modules/SpiceBot_osd/__init__.py @@ -0,0 +1,355 @@ +# coding=utf8 +"""Sopel OSD + +Sopel OSD is a "niche" method of displaying text in a Sopel bot +""" + +# pylama:ignore=W0611 + + +from __future__ import unicode_literals, absolute_import, division, print_function + +import sopel.bot +from sopel import tools, module +from sopel.tools import stderr, Identifier +from sopel.config.types import StaticSection, ValidatedAttribute + +import time +from collections import abc +import sys + + +__author__ = 'Sam Zick' +__email__ = 'sam@deathbybandaid.net' +__version__ = '0.1.2' + + +def configure(config): + config.define_section("MAXTARGCONFIG", MAXTARGCONFIG, validate=False) + config.MAXTARGCONFIG.configure_setting('notice', 'MAXTARG limit for NOTICE') + config.MAXTARGCONFIG.configure_setting('privmsg', 'MAXTARG limit for PRIVMSG') + + +def setup(bot): + + # Inject OSD + stderr("[Sopel-OSD] Implanting OSD function into bot.") + bot.osd = SopelOSD.osd + bot.SopelWrapper.osd = SopelOSD.SopelWrapper.osd + tools.get_available_message_bytes = ToolsOSD.get_available_message_bytes + tools.get_sendable_message_list = ToolsOSD.get_sendable_message_list + tools.get_message_recipientgroups = ToolsOSD.get_message_recipientgroups + + # overwrite default bot messaging + stderr("[Sopel-OSD] Overwrite Default Sopel messaging commands.") + bot.SopelWrapper.say = SopelOSD.SopelWrapper.say + bot.SopelWrapper.action = SopelOSD.SopelWrapper.action + bot.SopelWrapper.notice = SopelOSD.SopelWrapper.notice + bot.SopelWrapper.reply = SopelOSD.SopelWrapper.reply + + # verify config settings for server + stderr("[Sopel-OSD] Checking for config settings.") + bot.config.define_section("MAXTARGCONFIG", MAXTARGCONFIG, validate=False) + + +# RPL_ISUPPORT = '005' +@module.event('005') +@module.rule('.*') +def parse_event_005(bot, trigger): + if trigger.args[-1] != 'are supported by this server': + return + parameters = trigger.args[1:-1] + for param in parameters: + if '=' in param: + if param.startswith("TARGMAX"): + param = str(param).split('=')[1] + settings = str(param).split(',') + for setting in settings: + settingname = str(setting).split(':')[0] + if settingname.upper() in ['NOTICE', 'PRIVMSG']: + try: + value = str(setting).split(':')[1] or None + except IndexError: + value = None + if value: + if settingname.upper() == 'NOTICE': + bot.config.MAXTARGCONFIG.notice = int(value) + elif settingname.upper() == 'PRIVMSG': + bot.config.MAXTARGCONFIG.privmsg = int(value) + + +class MAXTARGCONFIG(StaticSection): + notice = ValidatedAttribute('notice', default=1) + privmsg = ValidatedAttribute('privmsg', default=1) + + +class ToolsOSD: + + def get_message_recipientgroups(bot, 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 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 = bot.config.MAXTARGCONFIG.notice + elif text_method in ['PRIVMSG', 'ACTION']: + maxtargets = bot.config.MAXTARGCONFIG.privmsg + 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(bot, recipientgroups): + """ + 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 + """ + + available_bytes = 512 + reserved_irc_bytes = 15 + available_bytes -= reserved_irc_bytes + try: + hostmaskbytes = len((bot.users.get(bot.nick).hostmask).encode('utf-8')) + except AttributeError: + hostmaskbytes = len((bot.nick).encode('utf-8')) + 12 + 63 + available_bytes -= hostmaskbytes + + groupbytes = [] + for recipients_part in recipientgroups: + groupbytes.append(len((recipients_part).encode('utf-8'))) + + max_recipients_bytes = max(groupbytes) + available_bytes -= max_recipients_bytes + + return available_bytes + + def get_sendable_message_list(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: + last_space = message.rfind(' ', 0, max_length) + if last_space == -1: + # No last space, just split where it is possible + text_list.append(message[:max_length]) + message = message[max_length:] + else: + # Split at the last best space found + text_list.append(message[:last_space]) + message = message[last_space:] + if len(message.encode('utf-8')): + text_list.append(message) + messages_list.extend(text_list) + + return messages_list + + +class SopelOSD: + + 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``. + """ + + text_method = text_method.upper() + if text_method == 'SAY' or text_method not in ['NOTICE', 'ACTION']: + text_method = 'PRIVMSG' + + recipientgroups = tools.get_message_recipientgroups(self, recipients, text_method) + available_bytes = tools.get_available_message_bytes(self, recipientgroups) + messages_list = tools.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 + + # No messages within the last 3 seconds? Go ahead! + # Otherwise, wait so it's been at least 0.8 seconds + penalty + + recipient_id = Identifier(recipientgroup) + + recipient_stack = self.stack.setdefault(recipient_id, { + 'messages': [], + 'flood_left': 4, + 'dots': 0, + # TODO + # 'flood_left': self.config.core.flood_burst_lines, + }) + recipient_stack['dots'] = 0 + + for text in messages_list: + + try: + + self.sending.acquire() + + if not recipient_stack['flood_left']: + elapsed = time.time() - recipient_stack['messages'][-1][0] + # TODO + # recipient_stack['flood_left'] = min( + # self.config.core.flood_burst_lines, + # int(elapsed) * self.config.core.flood_refill_rate) + recipient_stack['flood_left'] = min(4, int(elapsed) * 1) + + if not recipient_stack['flood_left']: + elapsed = time.time() - recipient_stack['messages'][-1][0] + penalty = float(max(0, len(text) - 50)) / 70 + # Never wait more than 2 seconds + # wait = min(self.config.core.flood_empty_wait + penalty, 2) # TODO + wait = min(0.7 + penalty, 2) + if elapsed < wait: + time.sleep(wait - elapsed) + + # Loop detection + messages = [m[1] for m in recipient_stack['messages'][-8:]] + + # If what we about to send repeated at least 5 times in the + # last 2 minutes, replace with '...' + if messages.count(text) >= 5 and elapsed < 120: + recipient_stack['dots'] += 1 + else: + recipient_stack['dots'] = 0 + + if not recipient_stack['dots'] >= 3: + + recipient_stack['flood_left'] = max(0, recipient_stack['flood_left'] - 1) + recipient_stack['messages'].append((time.time(), self.safe(text))) + recipient_stack['messages'] = recipient_stack['messages'][-10:] + + if recipient_stack['dots']: + text = '...' + if text_method == 'ACTION': + text_method = 'PRIVMSG' + 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) + + finally: + self.sending.release() + + class SopelWrapper(object): + + def osd(self, messages, recipients=None, text_method='PRIVMSG', max_messages=-1): + if recipients is None: + recipients = self._trigger.sender + self._bot.osd(self, messages, recipients, text_method, max_messages) + + def say(self, message, destination=None, max_messages=1): + if destination is None: + destination = self._trigger.sender + self._bot.osd(self, message, destination, 'PRIVMSG', 1) + # self._bot.say(message, destination, max_messages) + + def action(self, message, destination=None, max_messages=1): + if destination is None: + destination = self._trigger.sender + self._bot.osd(self, message, destination, 'ACTION', 1) + # self._bot.action(message, destination, max_messages) + + def notice(self, message, destination=None, max_messages=1): + if destination is None: + destination = self._trigger.sender + self._bot.osd(self, message, destination, 'NOTICE', 1) + # self._bot.notice(message, destination, max_messages) + + def reply(self, message, destination=None, reply_to=None, notice=False, max_messages=1): + if destination is None: + destination = self._trigger.sender + if reply_to is None: + reply_to = self._trigger.nick + message = '%s: %s' % (reply_to, message) + if notice: + self._bot.osd(self, message, destination, 'NOTICE', 1) + else: + self._bot.osd(self, message, destination, 'PRIVMSG', 1) + # self._bot.reply(message, destination, reply_to, notice, max_messages)