def get_rep(bot, target): return bot.db.get_nick_value(Identifier(target), 'rep_score')
def blocks(bot, trigger): """Manage Sopel's blocking features. https://github.com/sopel-irc/sopel/wiki/Making-Sopel-ignore-people """ if not trigger.admin: return STRINGS = { "success_del": "Successfully deleted block: %s", "success_add": "Successfully added block: %s", "no_nick": "No matching nick block found for: %s", "no_host": "No matching hostmask block found for: %s", "invalid": "Invalid format for %s a block. Try: .blocks add (nick|hostmask) sopel", "invalid_display": "Invalid input for displaying blocks.", "nonelisted": "No %s listed in the blocklist.", 'huh': "I could not figure out what you wanted to do.", } masks = set(s for s in bot.config.core.host_blocks if s != '') nicks = set(Identifier(nick) for nick in bot.config.core.nick_blocks if nick != '') text = trigger.group().split() if len(text) == 3 and text[1] == "list": if text[2] == "hostmask": if len(masks) > 0: blocked = ', '.join(unicode(mask) for mask in masks) bot.say("Blocked hostmasks: {}".format(blocked)) else: bot.reply(STRINGS['nonelisted'] % ('hostmasks')) elif text[2] == "nick": if len(nicks) > 0: blocked = ', '.join(unicode(nick) for nick in nicks) bot.say("Blocked nicks: {}".format(blocked)) else: bot.reply(STRINGS['nonelisted'] % ('nicks')) else: bot.reply(STRINGS['invalid_display']) elif len(text) == 4 and text[1] == "add": if text[2] == "nick": nicks.add(text[3]) bot.config.core.nick_blocks = nicks bot.config.save() elif text[2] == "hostmask": masks.add(text[3].lower()) bot.config.core.host_blocks = list(masks) else: bot.reply(STRINGS['invalid'] % ("adding")) return bot.reply(STRINGS['success_add'] % (text[3])) elif len(text) == 4 and text[1] == "del": if text[2] == "nick": if Identifier(text[3]) not in nicks: bot.reply(STRINGS['no_nick'] % (text[3])) return nicks.remove(Identifier(text[3])) bot.config.core.nick_blocks = [unicode(n) for n in nicks] bot.config.save() bot.reply(STRINGS['success_del'] % (text[3])) elif text[2] == "hostmask": mask = text[3].lower() if mask not in masks: bot.reply(STRINGS['no_host'] % (text[3])) return masks.remove(mask) bot.config.core.host_blocks = [unicode(m) for m in masks] bot.config.save() bot.reply(STRINGS['success_del'] % (text[3])) else: bot.reply(STRINGS['invalid'] % ("deleting")) return else: bot.reply(STRINGS['huh'])
class CoreSection(StaticSection): """The config section used for configuring the bot itself.""" admins = ListAttribute('admins') """The list of people (other than the owner) who can administer the bot""" admin_accounts = ListAttribute('admin_accounts') """The list of accounts (other than the owner's) who can administer the bot. This should not be set for networks that do not support IRCv3 account capabilities.""" alias_nicks = ListAttribute('alias_nicks') """List of alternate names recognized as the bot's nick for $nick and $nickname regex substitutions""" auth_method = ChoiceAttribute( 'auth_method', choices=['nickserv', 'authserv', 'Q', 'sasl', 'server', 'userserv']) """Simple method to authenticate with the server. Can be ``nickserv``, ``authserv``, ``Q``, ``sasl``, or ``server`` or ``userserv``. This allows only a single authentication method; to use both a server-based authentication method as well as a nick-based authentication method, see ``server_auth_method`` and ``nick_auth_method``. If this is specified, both ``server_auth_method`` and ``nick_auth_method`` will be ignored. """ auth_password = ValidatedAttribute('auth_password') """The password to use to authenticate with the server.""" auth_target = ValidatedAttribute('auth_target') """The user to use for nickserv authentication, or the SASL mechanism. May not apply, depending on ``auth_method``. Defaults to NickServ for nickserv auth, and PLAIN for SASL auth.""" auth_username = ValidatedAttribute('auth_username') """The username/account to use to authenticate with the server. May not apply, depending on ``auth_method``.""" auto_url_schemes = ListAttribute('auto_url_schemes', strip=True, default=['http', 'https', 'ftp']) """List of URL schemes that will trigger URL callbacks. Used by the URL callbacks feature; see :func:`sopel.module.url` decorator for plugins. The default value allows ``http``, ``https``, and ``ftp``. """ bind_host = ValidatedAttribute('bind_host') """Bind the connection to a specific IP""" ca_certs = FilenameAttribute('ca_certs', default=_find_certs()) """The path of the CA certs pem file""" channels = ListAttribute('channels') """List of channels for the bot to join when it connects""" db_type = ChoiceAttribute('db_type', choices=[ 'sqlite', 'mysql', 'postgres', 'mssql', 'oracle', 'firebird', 'sybase' ], default='sqlite') """The type of database to use for Sopel's database. mysql - pip install mysql-python (Python 2) or pip install mysqlclient (Python 3) postgres - pip install psycopg2 mssql - pip install pymssql See https://docs.sqlalchemy.org/en/latest/dialects/ for a full list of dialects""" db_filename = ValidatedAttribute('db_filename') """The filename for Sopel's database. (SQLite only)""" db_driver = ValidatedAttribute('db_driver') """The driver for Sopel's database. This is optional, but can be specified if user wants to use a different driver https://docs.sqlalchemy.org/en/latest/core/engines.html""" db_user = ValidatedAttribute('db_user') """The user for Sopel's database.""" db_pass = ValidatedAttribute('db_pass') """The password for Sopel's database.""" db_host = ValidatedAttribute('db_host') """The host for Sopel's database.""" db_port = ValidatedAttribute('db_port') """The port for Sopel's database.""" db_name = ValidatedAttribute('db_name') """The name of Sopel's database.""" default_time_format = ValidatedAttribute('default_time_format', default='%Y-%m-%d - %T%Z') """The default format to use for time in messages.""" default_timezone = ValidatedAttribute('default_timezone', default='UTC') """The default timezone to use for time in messages.""" enable = ListAttribute('enable') """A whitelist of the only modules you want to enable.""" exclude = ListAttribute('exclude') """A list of modules which should not be loaded.""" extra = ListAttribute('extra') """A list of other directories you'd like to include modules from.""" help_prefix = ValidatedAttribute('help_prefix', default='.') """The prefix to use in help""" @property def homedir(self): """The directory in which various files are stored at runtime. By default, this is the same directory as the config. It can not be changed at runtime. """ return self._parent.homedir host = ValidatedAttribute('host', default='irc.dftba.net') """The server to connect to.""" host_blocks = ListAttribute('host_blocks') """A list of hostmasks which Sopel should ignore. Regular expression syntax is used""" log_raw = ValidatedAttribute('log_raw', bool, default=False) """Whether a log of raw lines as sent and received should be kept.""" logdir = FilenameAttribute('logdir', directory=True, default='logs') """Directory in which to place logs.""" logging_channel = ValidatedAttribute('logging_channel', Identifier) """The channel to send logging messages to.""" logging_channel_datefmt = ValidatedAttribute('logging_channel_datefmt') """The logging format string to use for timestamps in IRC channel logs. If not specified, this falls back to using ``logging_datefmt``. """ logging_channel_format = ValidatedAttribute('logging_channel_format') """The logging format string to use in IRC channel logs. If not specified, this falls back to using ``logging_format``. """ logging_channel_level = ChoiceAttribute( 'logging_channel_level', ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], 'WARNING') """The lowest severity of logs to display in IRC channel logs. If not specified, this falls back to using ``logging_level``. """ logging_datefmt = ValidatedAttribute('logging_datefmt') """The logging format string to use for timestamps in logs. If not specified, the datefmt is not provided, and logging will use the Python default. """ logging_format = ValidatedAttribute('logging_format') """The logging format string to use for logs. If not specified, the format is not provided, and logging will use the Python default. """ logging_level = ChoiceAttribute( 'logging_level', ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], 'WARNING') """The lowest severity of logs to display. If not specified, this defaults to WARNING. """ modes = ValidatedAttribute('modes', default='B') """User modes to be set on connection.""" name = ValidatedAttribute('name', default='Sopel: https://sopel.chat') """The "real name" of your bot for WHOIS responses.""" nick = ValidatedAttribute('nick', Identifier, default=Identifier('Sopel')) """The nickname for the bot""" nick_auth_method = ChoiceAttribute( 'nick_auth_method', choices=['nickserv', 'authserv', 'Q', 'userserv']) """The nick authentication method. Can be ``nickserv``, ``authserv``, ``Q``, or ``userserv``. """ nick_auth_password = ValidatedAttribute('nick_auth_password') """The password to use to authenticate your nick.""" nick_auth_target = ValidatedAttribute('nick_auth_target') """The target user for nick authentication. May not apply, depending on ``nick_auth_method``. Defaults to NickServ for nickserv, and UserServ for userserv. """ nick_auth_username = ValidatedAttribute('nick_auth_username') """The username/account to use for nick authentication. May not apply, depending on ``nick_auth_method``. Defaults to the value of ``nick``. """ nick_blocks = ListAttribute('nick_blocks') """A list of nicks which Sopel should ignore. Regular expression syntax is used.""" not_configured = ValidatedAttribute('not_configured', bool, default=False) """For package maintainers. Not used in normal configurations. This allows software packages to install a default config file, with this set to true, so that the bot will not run until it has been properly configured.""" owner = ValidatedAttribute('owner', default=NO_DEFAULT) """The IRC name of the owner of the bot.""" owner_account = ValidatedAttribute('owner_account') """The services account name of the owner of the bot. This should only be set on networks which support IRCv3 account capabilities. """ pid_dir = FilenameAttribute('pid_dir', directory=True, default='.') """The directory in which to put the file Sopel uses to track its process ID. You probably do not need to change this unless you're managing Sopel with systemd or similar.""" port = ValidatedAttribute('port', int, default=6667) """The port to connect on.""" prefix = ValidatedAttribute('prefix', default='\\.') """The prefix to add to the beginning of commands. It is a regular expression (so the default, ``\\.``, means commands start with a period), though using capturing groups will create problems.""" reply_errors = ValidatedAttribute('reply_errors', bool, default=True) """Whether to message the sender of a message that triggered an error with the exception.""" server_auth_method = ChoiceAttribute('server_auth_method', choices=['sasl', 'server']) """The server authentication method. Can be ``sasl`` or ``server``. """ server_auth_password = ValidatedAttribute('server_auth_password') """The password to use to authenticate with the server.""" server_auth_sasl_mech = ValidatedAttribute('server_auth_sasl_mech') """The SASL mechanism. Defaults to PLAIN. """ server_auth_username = ValidatedAttribute('server_auth_username') """The username/account to use to authenticate with the server.""" throttle_join = ValidatedAttribute('throttle_join', int) """Slow down the initial join of channels to prevent getting kicked. Sopel will only join this many channels at a time, sleeping for a second between each batch. This is unnecessary on most networks.""" timeout = ValidatedAttribute('timeout', int, default=120) """The amount of time acceptable between pings before timing out.""" use_ssl = ValidatedAttribute('use_ssl', bool, default=False) """Whether to use a SSL secured connection.""" user = ValidatedAttribute('user', default='sopel') """The "user" for your bot (the part before the @ in the hostname).""" verify_ssl = ValidatedAttribute('verify_ssl', bool, default=True) """Whether to require a trusted SSL certificate for SSL connections.""" flood_burst_lines = ValidatedAttribute('flood_burst_lines', int, default=4) """How many messages can be sent in burst mode.""" flood_empty_wait = ValidatedAttribute('flood_empty_wait', float, default=0.7) """How long to wait between sending messages when not in burst mode, in seconds.""" flood_refill_rate = ValidatedAttribute('flood_refill_rate', int, default=1) """How quickly burst mode recovers, in messages per second."""
def del_voice(self, channel, name): self.voices[channel].discard(Identifier(name))
def nick(): return Identifier('Sopel')
def add_halfop(self, channel, name): if isinstance(name, Identifier): self.halfplus[channel].add(name) else: self.halfplus[channel].add(Identifier(name))
def del_op(self, channel, name): self.ops[channel].discard(Identifier(name))
def bot(sopel, pretrigger): bot = MockSopelWrapper(sopel, pretrigger) bot.channels[Identifier('#Sopel')].privileges[Identifier( 'Foo')] = module.VOICE return bot
def pretrigger_pm(): line = ':[email protected] PRIVMSG Sopel :Hello, world' return PreTrigger(Identifier('Foo'), line)
def getPredicate(self, predicate, nick, nick_id=None): if not nick_id: nick = Identifier(nick) nick_id = botusers.get_nick_id(nick, True) self.aiml_kernel.getPredicate(predicate, nick_id)
def save_nick_session(self, nick, nick_id=None): if not nick_id: nick = Identifier(nick) nick_id = botusers.get_nick_id(nick, True) sessionData = self.aiml_kernel.getSessionData(nick_id) botdb.set_nick_value(nick, 'botai', sessionData)
def reset_channel_value(self, channel, key): """Resets the value for a given key to be associated with a channel.""" channel = Identifier(channel).lower() self.execute( 'DELETE FROM channel_values WHERE channel = ? AND key = ?', [channel, key])
def reset_nick_value(self, nick, key): """Resets the value for a given key to be associated with the nick.""" nick = Identifier(nick) nick_id = self.get_nick_id(nick) self.execute('DELETE FROM nick_values WHERE nick_id = ? AND key = ?', [nick_id, key])
def set_rep(bot, caller, target, newrep): bot.db.set_nick_value(Identifier(target), 'rep_score', newrep) bot.db.set_nick_value(Identifier(caller), 'rep_used', time.time())
def say(self, text, recipient, max_messages=1): """Send ``text`` as a PRIVMSG to ``recipient``. 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, this will attempt to send the entire ``text`` in one message. If the text is too long for the server, it may be truncated. If ``max_messages`` is given, the ``text`` will be split into at most that many messages, each no more than 400 bytes. The split is made at the last space character before the 400th byte, or at the 400th byte if no such space exists. If the ``text`` is too long to fit into the specified number of messages using the above splitting, the final message will contain the entire remainder, which may be truncated by the server. """ excess = '' if not isinstance(text, unicode): # Make sure we are dealing with unicode string text = text.decode('utf-8') if max_messages > 1: # Manage multi-line only when needed text, excess = tools.get_sendable_message(text) try: self.sending.acquire() # No messages within the last 3 seconds? Go ahead! # Otherwise, wait so it's been at least 0.8 seconds + penalty recipient_id = Identifier(recipient) if recipient_id not in self.stack: self.stack[recipient_id] = [] elif self.stack[recipient_id]: elapsed = time.time() - self.stack[recipient_id][-1][0] if elapsed < 3: penalty = float(max(0, len(text) - 40)) / 70 wait = min(0.8 + penalty, 2) # Never wait more than 2 seconds if elapsed < wait: time.sleep(wait - elapsed) # Loop detection messages = [m[1] for m in self.stack[recipient_id][-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: text = '...' if messages.count('...') >= 3: # If we said '...' 3 times, discard message return self.write(('PRIVMSG', recipient), text) self.stack[recipient_id].append((time.time(), self.safe(text))) self.stack[recipient_id] = self.stack[recipient_id][-10:] finally: self.sending.release() # Now that we've sent the first part, we need to send the rest. Doing # this recursively seems easier to me than iteratively if excess: self.msg(recipient, excess, max_messages - 1)
def trigger_owner(bot): line = ':[email protected] PRIVMSG #Sopel :Hello, world' return Trigger(bot.config, PreTrigger(Identifier('Bar'), line), None)
def msg(self, recipient, text, max_messages=1): # We're arbitrarily saying that the max is 400 bytes of text when # messages will be split. Otherwise, we'd have to acocunt for the bot's # hostmask, which is hard. max_text_length = 400 # Encode to bytes, for propper length calculation if isinstance(text, unicode): encoded_text = text.encode('utf-8') else: encoded_text = text excess = '' if max_messages > 1 and len(encoded_text) > max_text_length: last_space = encoded_text.rfind(' '.encode('utf-8'), 0, max_text_length) if last_space == -1: excess = encoded_text[max_text_length:] encoded_text = encoded_text[:max_text_length] else: excess = encoded_text[last_space + 1:] encoded_text = encoded_text[:last_space] # We'll then send the excess at the end # Back to unicode again, so we don't screw things up later. text = encoded_text.decode('utf-8') try: self.sending.acquire() # No messages within the last 3 seconds? Go ahead! # Otherwise, wait so it's been at least 0.8 seconds + penalty recipient_id = Identifier(recipient) if recipient_id not in self.stack: self.stack[recipient_id] = [] elif self.stack[recipient_id]: elapsed = time.time() - self.stack[recipient_id][-1][0] if elapsed < 3: penalty = float(max(0, len(text) - 50)) / 70 wait = 0.7 + penalty if elapsed < wait: time.sleep(wait - elapsed) # Loop detection messages = [m[1] for m in self.stack[recipient_id][-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: text = '...' if messages.count('...') >= 3: # If we said '...' 3 times, discard message return self.write(('PRIVMSG', recipient), text) self.stack[recipient_id].append((time.time(), self.safe(text))) self.stack[recipient_id] = self.stack[recipient_id][-10:] finally: self.sending.release() # Now that we've sent the first part, we need to send the rest. Doing # this recursively seems easier to me than iteratively if excess: self.msg(recipient, excess, max_messages - 1)
def findandreplace(bot, trigger): # Don't bother in PM if trigger.is_privmsg: return # Correcting other person vs self. rnick = Identifier(trigger.group('nick') or trigger.nick) # only do something if there is conversation to work with history = bot.memory['find_lines'].get(trigger.sender, {}).get(rnick, None) if not history: return sep = trigger.group('sep') old = trigger.group('old').replace('\\%s' % sep, sep) new = trigger.group('new') me = False # /me command flags = trigger.group('flags') or '' # only clean/format the new string if it's non-empty if new: new = bold(new.replace('\\%s' % sep, sep)) # If g flag is given, replace all. Otherwise, replace once. if 'g' in flags: count = -1 else: count = 1 # repl is a dynamically defined function which performs the substitution. # i flag turns off case sensitivity. re.U turns on unicode replacement. if 'i' in flags: regex = re.compile(re.escape(old), re.U | re.I) def repl(s): return re.sub(regex, new, s, count == 1) else: def repl(s): return s.replace(old, new, count) # Look back through the user's lines in the channel until you find a line # where the replacement works new_phrase = None for line in history: if line.startswith("\x01ACTION"): me = True # /me command line = line[8:] else: me = False replaced = repl(line) if replaced != line: # we are done new_phrase = replaced break if not new_phrase: return # Didn't find anything # Save the new "edited" message. action = (me and '\x01ACTION ') or '' # If /me message, prepend \x01ACTION history.appendleft(action + new_phrase) # history is in most-recent-first order # output if not me: new_phrase = 'meant to say: %s' % new_phrase if trigger.group(1): phrase = '%s thinks %s %s' % (trigger.nick, rnick, new_phrase) else: phrase = '%s %s' % (trigger.nick, new_phrase) bot.say(phrase)
def add_voice(self, channel, name): if isinstance(name, Identifier): self.voices[channel].add(name) else: self.voices[channel].add(Identifier(name))
def sotd(bot, trigger): if (trigger.group(2)): res = mysql(action='select') if trigger.group(2).strip() == res[2].strip(): return bot.say("Duplicate, song not added!") match = re.match( r'[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}(youtube|youtu|sc0tt|soundcloud|bandcamp|nicovideo)\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)', trigger.group(2), re.I) if match: bksongname = "" name = Identifier(trigger.nick) link = match.group(0) domain = urlparse(link) if (domain.netloc == 'youtu.be' or domain.netloc == 'www.youtu.be'): yt, bksongname = fetch_yt_video_info(bot, domain.path[1:]) if yt == None: return bot.say("Please do not .sotd a live channel") song = yt['title'] bksongname = bksongname + ".mp3" elif (domain.netloc == 'youtube.com' or domain.netloc == 'www.youtube.com'): yt, bksongname = fetch_yt_video_info(bot, domain.query[2:]) if yt == None: return bot.say("Please do not .sotd a live channel") song = yt['title'] bksongname = bksongname + ".mp3" elif (domain.netloc == 'soundcloud.com' or domain.netloc == 'www.soundcloud.com'): song, bksongname = soundcloudinfo(link) elif (domain.netloc == 'i.sc0tt.net'): bksongname = ''.join( random.choice(string.ascii_lowercase) for i in range(10)) + ".mp3" filename = "/path/to/songs/folder/{0}".format(bksongname) response = requests.head(link) if (int(response.headers['Content-Length']) < 26214400): try: r = requests.get(link, stream=True) with open(filename, 'wb') as x: shutil.copyfileobj(r.raw, x) a = EasyID3(filename) song = a['artist'][0] + " - " + a['title'][0] except: song = "" else: song = "" elif ('bandcamp.com' in domain.netloc): try: bs = BeautifulSoup(requests.get(link).content) title = bs.findAll( 'h2', {"class": "trackTitle"})[0].text.strip() artist = bs.findAll('meta', {"itemprop": "name"})[0].get('content') song = "{0} - {1}".format(artist, title) except: song = "" elif (domain.netloc == 'nicovideo.jp' or domain.netloc == 'www.nicovideo.jp'): id = domain.path[7:] nico = Nicovideo() nico.append(id) title = nico._video[id].title else: song = "" sdate = datetime.datetime.now() mysql(name, link, song, sdate, bksongname, 'insert') return bot.say("Song saved.") else: return bot.say("Enter a valid link.") else: res = mysql(action='select') bot.say("Last SotD: {0} - {1}".format(res[2], res[3]))
def del_halfop(self, channel, name): self.halfplus[channel].discard(Identifier(name))
def trigger_runstatus(bot, trigger, botcom): # Bots can't run commands if Identifier(trigger.nick) == bot.nick: return False # Allow permissions for enabling and disabling commands via hyphenargs if botcom.dict["adminswitch"]: if botusers.command_permissions_check(bot, trigger, ['admins', 'owner', 'OP', 'ADMIN', 'OWNER']): return True else: botmessagelog.messagelog_error(botcom.dict["log_id"], "The admin switch (-a) is for use by authorized nicks ONLY.") return False # Stop here if not registered or not identified if bot.config.SpiceBot_regnick.regnick: # not registered if str(trigger.nick).lower() not in [x.lower() for x in botusers.dict["registered"]]: message = "The " + str(botcom.dict["comtext"]) + " command requires you to be registered with IRC services. Registering may take a few minutes to process with the bot." return trigger_cant_run(bot, trigger, botcom, message) # registered nick, but not identified else: nick_id = botusers.whois_ident(trigger.nick) if nick_id not in botusers.dict["identified"]: message = "Your nickname appears to be registered with IRC services. However, you have not identified. Identifying may take a few minutes to process with the bot." return trigger_cant_run(bot, trigger, botcom, message) # if botcom.dict["hyphen_arg"]: # return False if not trigger.is_privmsg: if str(trigger.sender).lower() in [x.lower() for x in botcom.dict["dict"]["hardcoded_channel_block"]]: message = "The " + str(botcom.dict["comtext"]) + " command cannot be used in " + str(trigger.sender) + " because it is hardcoded not to." return trigger_cant_run(bot, trigger, botcom, message) if botcom.dict["runcount"] > 1: # check channel multirun blocks if not trigger.is_privmsg: channel_disabled_list = botcommands.get_commands_disabled(str(trigger.sender), "multirun") if botcom.dict["realcomref"] in list(channel_disabled_list.keys()): reason = channel_disabled_list[botcom.dict["realcomref"]]["reason"] timestamp = channel_disabled_list[botcom.dict["realcomref"]]["timestamp"] bywhom = channel_disabled_list[botcom.dict["realcomref"]]["disabledby"] message = "The " + str(botcom.dict["comtext"]) + " command multirun usage was disabled by " + bywhom + " for " + str(trigger.sender) + " at " + str(timestamp) + " for the following reason: " + str(reason) return trigger_cant_run(bot, trigger, botcom, message) # don't run commands that are disabled for specific users nick_disabled_list = botcommands.get_commands_disabled(str(trigger.nick), "multirun") if botcom.dict["realcomref"] in list(nick_disabled_list.keys()): bywhom = nick_disabled_list[botcom.dict["realcomref"]]["disabledby"] if botusers.ID(bywhom) != botusers.ID(trigger.nick): reason = nick_disabled_list[botcom.dict["realcomref"]]["reason"] timestamp = nick_disabled_list[botcom.dict["realcomref"]]["timestamp"] message = "The " + str(botcom.dict["comtext"]) + " command was multirun unsage disabled by " + bywhom + " for " + str(trigger.sender) + " at " + str(timestamp) + " for the following reason: " + str(reason) return trigger_cant_run(bot, trigger, botcom, message) else: botcommands.unset_command_disabled(botcom.dict["realcomref"], trigger.nick, "multirun") botmessagelog.messagelog_error(botcom.dict["log_id"], botcom.dict["comtext"] + " multirun is now enabled for " + str(trigger.nick)) # don't run commands that are disabled in channels if not trigger.is_privmsg: channel_disabled_list = botcommands.get_commands_disabled(str(trigger.sender)) if botcom.dict["realcomref"] in list(channel_disabled_list.keys()): reason = channel_disabled_list[botcom.dict["realcomref"]]["reason"] timestamp = channel_disabled_list[botcom.dict["realcomref"]]["timestamp"] bywhom = channel_disabled_list[botcom.dict["realcomref"]]["disabledby"] message = "The " + str(botcom.dict["comtext"]) + " command was disabled by " + bywhom + " for " + str(trigger.sender) + " at " + str(timestamp) + " for the following reason: " + str(reason) return trigger_cant_run(bot, trigger, botcom, message) # don't run commands that are disabled for specific users nick_disabled_list = botcommands.get_commands_disabled(str(trigger.nick)) if botcom.dict["realcomref"] in list(nick_disabled_list.keys()): bywhom = nick_disabled_list[botcom.dict["realcomref"]]["disabledby"] if botusers.ID(bywhom) != botusers.ID(trigger.nick): reason = nick_disabled_list[botcom.dict["realcomref"]]["reason"] timestamp = nick_disabled_list[botcom.dict["realcomref"]]["timestamp"] message = "The " + str(botcom.dict["comtext"]) + " command was disabled by " + bywhom + " for " + str(trigger.sender) + " at " + str(timestamp) + " for the following reason: " + str(reason) return trigger_cant_run(bot, trigger, botcom, message) else: botcommands.unset_command_disabled(botcom.dict["realcomref"], trigger.nick) botmessagelog.messagelog_error(botcom.dict["log_id"], botcom.dict["comtext"] + " is now enabled for " + str(trigger.nick)) return True
def set_channel_value(self, channel, key, value): channel = Identifier(channel).lower() value = json.dumps(value, ensure_ascii=False) self.execute('INSERT OR REPLACE INTO channel_values VALUES (?, ?, ?)', [channel, key, value])
def say(self, text, recipient, max_messages=1): """Send ``text`` as a PRIVMSG to ``recipient``. 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, this will attempt to send the entire ``text`` in one message. If the text is too long for the server, it may be truncated. If ``max_messages`` is given, the ``text`` will be split into at most that many messages, each no more than 400 bytes. The split is made at the last space character before the 400th byte, or at the 400th byte if no such space exists. If the ``text`` is too long to fit into the specified number of messages using the above splitting, the final message will contain the entire remainder, which may be truncated by the server. """ # We're arbitrarily saying that the max is 400 bytes of text when # messages will be split. Otherwise, we'd have to acocunt for the bot's # hostmask, which is hard. max_text_length = 400 # Encode to bytes, for propper length calculation if isinstance(text, unicode): encoded_text = text.encode('utf-8') else: encoded_text = text excess = '' if max_messages > 1 and len(encoded_text) > max_text_length: last_space = encoded_text.rfind(' '.encode('utf-8'), 0, max_text_length) if last_space == -1: excess = encoded_text[max_text_length:] encoded_text = encoded_text[:max_text_length] else: excess = encoded_text[last_space + 1:] encoded_text = encoded_text[:last_space] # We'll then send the excess at the end # Back to unicode again, so we don't screw things up later. text = encoded_text.decode('utf-8') try: self.sending.acquire() # No messages within the last 3 seconds? Go ahead! # Otherwise, wait so it's been at least 0.8 seconds + penalty recipient_id = Identifier(recipient) if recipient_id not in self.stack: self.stack[recipient_id] = [] elif self.stack[recipient_id]: elapsed = time.time() - self.stack[recipient_id][-1][0] if elapsed < 3: penalty = float(max(0, len(text) - 40)) / 70 wait = 0.8 + penalty if elapsed < wait: time.sleep(wait - elapsed) # Loop detection messages = [m[1] for m in self.stack[recipient_id][-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: text = '...' if messages.count('...') >= 3: # If we said '...' 3 times, discard message return self.write(('PRIVMSG', recipient), text) self.stack[recipient_id].append((time.time(), self.safe(text))) self.stack[recipient_id] = self.stack[recipient_id][-10:] finally: self.sending.release() # Now that we've sent the first part, we need to send the rest. Doing # this recursively seems easier to me than iteratively if excess: self.msg(recipient, excess, max_messages - 1)
def track_kick(bot, trigger): nick = Identifier(trigger.args[1]) channel = trigger.sender _remove_from_channel(bot, nick, channel)
def findandreplace(bot, trigger): # Don't bother in PM if trigger.is_privmsg: return # Correcting other person vs self. rnick = Identifier(trigger.group(1) or trigger.nick) search_dict = bot.memory['find_lines'] # only do something if there is conversation to work with if trigger.sender not in search_dict: return if Identifier(rnick) not in search_dict[trigger.sender]: return #TODO rest[0] is find, rest[1] is replace. These should be made variables of #their own at some point. rest = [trigger.group(2), trigger.group(3)] rest[0] = rest[0].replace(r'\/', '/') rest[1] = rest[1].replace(r'\/', '/') me = False # /me command flags = (trigger.group(4) or '') # If g flag is given, replace all. Otherwise, replace once. if 'g' in flags: count = -1 else: count = 1 # repl is a lambda function which performs the substitution. i flag turns # off case sensitivity. re.U turns on unicode replacement. if 'i' in flags: regex = re.compile(re.escape(rest[0]), re.U | re.I) repl = lambda s: re.sub(regex, rest[1], s, count == 1) else: repl = lambda s: s.replace(rest[0], rest[1], count) # Look back through the user's lines in the channel until you find a line # where the replacement works new_phrase = None for line in reversed(search_dict[trigger.sender][rnick]): if line.startswith("\x01ACTION"): me = True # /me command line = line[8:] else: me = False new_phrase = repl(line) if new_phrase != line: # we are done break if not new_phrase or new_phrase == line: return # Didn't find anything # Save the new "edited" message. action = (me and '\x01ACTION ') or '' # If /me message, prepend \x01ACTION templist = search_dict[trigger.sender][rnick] templist.append(action + new_phrase) search_dict[trigger.sender][rnick] = templist bot.memory['find_lines'] = search_dict # output if not me: new_phrase = '%s to say: %s' % (bold('meant'), new_phrase) if trigger.group(1): phrase = '%s thinks %s %s' % (trigger.nick, rnick, new_phrase) else: phrase = '%s %s' % (trigger.nick, new_phrase) bot.say(phrase)
class CoreSection(StaticSection): """The config section used for configuring the bot itself. .. important:: All **Required** values must be specified, or Sopel will fail to start. .. note:: You can use the command ``sopel configure`` to generate a config file with the minimal required options. """ admins = ListAttribute('admins') """The list of people (other than the owner) who can administer the bot. Example: .. code-block:: ini admin = YourFavAdmin TheOtherAdmin YetAnotherRockstarAdmin """ admin_accounts = ListAttribute('admin_accounts') """The list of admin accounts other than the owner's. Each account is allowed to administer the bot and can perform commands that are restricted to admins. Example: .. code-block:: ini admin_accounts = favadmin otheradm yetanotherone .. important:: This should not be set for networks that do not support IRCv3 account capabilities. In that case, use :attr:`admins` instead. """ alias_nicks = ListAttribute('alias_nicks') """List of alternate names users may call the bot. These aliases are used along with the bot's nick for ``$nick`` and ``$nickname`` regex substitutions. For example, a bot named "William" (its :attr:`nick`) could have these aliases: .. code-block:: ini alias_nicks = Bill Will Liam This would then allow both "William: Hi!" and "Bill: Hi!" to work with :func:`~sopel.plugin.nickname_command`. """ auth_method = ChoiceAttribute( 'auth_method', choices=['nickserv', 'authserv', 'Q', 'sasl', 'server', 'userserv']) """Simple method to authenticate with the server. Can be one of ``nickserv``, ``authserv``, ``Q``, ``sasl``, ``server``, or ``userserv``. This allows only a single authentication method; to use both a server-based authentication method *and* a nick-based authentication method, see :attr:`server_auth_method` and :attr:`nick_auth_method`. For more information about these methods, see :ref:`Authentication`. .. note:: If this is specified, :attr:`nick_auth_method` will be ignored, and this value will override :attr:`server_auth_method`. """ auth_password = SecretAttribute('auth_password') """The password to use to authenticate with the :attr:`auth_method`. See :ref:`Authentication`. """ auth_target = ValidatedAttribute('auth_target') """Target for authentication. :default: * ``NickServ`` if using the ``nickserv`` :attr:`auth_method` * ``PLAIN`` if using the ``sasl`` :attr:`auth_method` The nickname of the NickServ service, or the name of the desired SASL mechanism, if :attr:`auth_method` is set to one of these methods. This value is otherwise ignored. See :ref:`Authentication`. """ auth_username = ValidatedAttribute('auth_username') """The user/account name to use when authenticating. May not apply, depending on :attr:`auth_method`. See :ref:`Authentication`. """ auto_url_schemes = ListAttribute('auto_url_schemes', strip=True, default=URL_DEFAULT_SCHEMES) """List of URL schemes that will trigger URL callbacks. :default: ``['http', 'https', 'ftp']`` Used by the URL callbacks feature to call plugins when links are posted in chat; see the :func:`sopel.plugin.url` decorator. The default value allows ``http``, ``https``, and ``ftp``. It is equivalent to this configuration example: .. code-block:: ini auto_url_schemes = http https ftp """ bind_host = ValidatedAttribute('bind_host') """Bind the connection to a specific IP. :default: ``0.0.0.0`` (all interfaces) This is equivalent to the default value: .. code-block:: ini bind_host = 0.0.0.0 """ ca_certs = FilenameAttribute('ca_certs', default=_find_certs()) """The path to the CA certs ``.pem`` file. Example: .. code-block:: ini ca_certs = /etc/ssl/certs/ca-certificates.crt If not specified, Sopel will try to find the certificate trust store itself from a set of known locations. If the given value is not an absolute path, it will be interpreted relative to the directory containing the config file with which Sopel was started. """ channels = ListAttribute('channels') """List of channels for the bot to join when it connects. If a channel key needs to be provided, separate it from the channel name with a space: .. code-block:: ini channels = "#channel" "#logs" &rare_prefix_channel "#private password" .. important:: If you edit the config file manually, make sure to wrap each line starting with a ``#`` in double quotes, as shown in the example above. An unquoted ``#`` denotes a comment, which will be ignored by Sopel's configuration parser. """ commands_on_connect = ListAttribute('commands_on_connect') """A list of commands to send upon successful connection to the IRC server. Each line is a message that will be sent to the server once connected, in the order they are defined: .. code-block:: ini commands_on_connect = PRIVMSG [email protected] :AUTH my_username MyPassword,@#$%! PRIVMSG MyOwner :I'm here! ``$nickname`` can be used in a command as a placeholder, and will be replaced with the bot's :attr:`nick`. For example, if the bot's nick is ``Sopel``, ``MODE $nickname +Xxw`` will be expanded to ``MODE Sopel +Xxw``. .. versionadded:: 7.0 """ db_driver = ValidatedAttribute('db_driver') """The driver to use for connecting to the database. This is optional, but can be specified if user wants to use a different driver than the default for the chosen :attr:`db_type`. .. seealso:: Refer to :ref:`SQLAlchemy's documentation <engines_toplevel>` for more information. """ db_filename = ValidatedAttribute('db_filename') """The filename for Sopel's database. Used only for SQLite. Ignored for all other :attr:`db_type` values. """ db_host = ValidatedAttribute('db_host') """The host for Sopel's database. Ignored when using SQLite. """ db_name = ValidatedAttribute('db_name') """The name of Sopel's database. Ignored when using SQLite. """ db_pass = SecretAttribute('db_pass') """The password for Sopel's database. Ignored when using SQLite. """ db_port = ValidatedAttribute('db_port') """The port for Sopel's database. Ignored when using SQLite. """ db_type = ChoiceAttribute('db_type', choices=[ 'sqlite', 'mysql', 'postgres', 'mssql', 'oracle', 'firebird', 'sybase' ], default='sqlite') """The type of database Sopel should connect to. :default: ``sqlite`` (part of Python's standard library) The full list of values Sopel recognizes is: * ``firebird`` * ``mssql`` * ``mysql`` * ``oracle`` * ``postgres`` * ``sqlite`` * ``sybase`` Here are the additional PyPI packages you may need to install to use one of the most commonly requested alternatives: mysql ``pip install mysql-python`` (Python 2) ``pip install mysqlclient`` (Python 3) postgres ``pip install psycopg2`` mssql ``pip install pymssql`` This is equivalent to the default value: .. code-block:: ini db_type = sqlite .. seealso:: Refer to :ref:`SQLAlchemy's documentation <dialect_toplevel>` for more information about the different dialects it supports. .. note:: Plugins originally written for Sopel 6.x and older *might* not work correctly with ``db_type``\\s other than ``sqlite``. """ db_user = ValidatedAttribute('db_user') """The user for Sopel's database. Ignored when using SQLite. """ default_time_format = ValidatedAttribute('default_time_format', default='%Y-%m-%d - %T%Z') """The default format to use for time in messages. :default: ``%Y-%m-%d - %T%Z`` Used when plugins format times with :func:`sopel.tools.time.format_time`. This is equivalent to the default value: .. code-block:: ini default_time_format = %Y-%m-%d - %T%Z .. seealso:: Time format reference is available in the documentation for Python's :func:`time.strftime` function. """ default_timezone = ValidatedAttribute('default_timezone', default='UTC') """The default timezone to use for time in messages. :default: ``UTC`` .. highlight:: ini Used when plugins format times with :func:`sopel.tools.time.format_time`. For example, to make Sopel fall back on British time:: default_timezone = Europe/London And this is equivalent to the default value:: default_timezone = UTC """ enable = ListAttribute('enable') """A list of the only plugins you want to enable. .. highlight:: ini If set, Sopel will *only* load the plugins named here. All other available plugins will be ignored:: enable = url xkcd help In that case, only the ``url``, ``xkcd``, and ``help`` plugins will be enabled and loaded by Sopel. To load *all* available plugins, clear this setting by removing it, or by making it empty:: enable = To disable only a few plugins, see :attr:`exclude`. .. seealso:: The :ref:`Plugins` chapter for an overview of all plugin-related settings. """ exclude = ListAttribute('exclude') """A list of plugins which should not be loaded. .. highlight:: ini If set, Sopel will load all available plugins *except* those named here:: exclude = url calc meetbot In that case, ``url``, ``calc``, and ``meetbot`` will be excluded, and they won't be loaded by Sopel. A plugin named both here and in :attr:`enable` **will not** be loaded; :attr:`exclude` takes priority. .. seealso:: The :ref:`Plugins` chapter for an overview of all plugin-related settings. """ extra = ListAttribute('extra') """A list of other directories in which to search for plugin files. Example: .. code-block:: ini extra = /home/myuser/custom-sopel-plugins/ /usr/local/lib/ad-hoc-plugins/ .. seealso:: The :ref:`Plugins` chapter for an overview of all plugin-related settings. """ flood_burst_lines = ValidatedAttribute('flood_burst_lines', int, default=4) """How many messages can be sent in burst mode. :default: ``4`` This is equivalent to the default value: .. code-block:: ini flood_burst_lines = 4 .. seealso:: The :ref:`Flood Prevention` chapter to learn what each flood-related setting does. .. versionadded:: 7.0 """ flood_empty_wait = ValidatedAttribute('flood_empty_wait', float, default=0.7) """How long to wait between sending messages when not in burst mode, in seconds. :default: ``0.7`` This is equivalent to the default value: .. code-block:: ini flood_empty_wait = 0.7 .. seealso:: The :ref:`Flood Prevention` chapter to learn what each flood-related setting does. .. versionadded:: 7.0 """ flood_max_wait = ValidatedAttribute('flood_max_wait', float, default=2) """How much time to wait at most when flood protection kicks in. :default: ``2`` This is equivalent to the default value: .. code-block:: ini flood_max_wait = 2 .. seealso:: The :ref:`Flood Prevention` chapter to learn what each flood-related setting does. .. versionadded:: 7.1 """ flood_penalty_ratio = ValidatedAttribute('flood_penalty_ratio', float, default=1.4) """Ratio of the message length used to compute the added wait penalty. :default: ``1.4`` Messages longer than :attr:`flood_text_length` will get an added wait penalty (in seconds) that will be computed like this:: overflow = max(0, (len(text) - flood_text_length)) rate = flood_text_length * flood_penalty_ratio penalty = overflow / rate .. note:: If the penalty ratio is 0, this penalty will be disabled. This is equivalent to the default value: .. code-block:: ini flood_penalty_ratio = 1.4 .. seealso:: The :ref:`Flood Prevention` chapter to learn what each flood-related setting does. .. versionadded:: 7.1 """ flood_refill_rate = ValidatedAttribute('flood_refill_rate', int, default=1) """How quickly burst mode recovers, in messages per second. :default: ``1`` This is equivalent to the default value: .. code-block:: ini flood_refill_rate = 1 .. seealso:: The :ref:`Flood Prevention` chapter to learn what each flood-related setting does. .. versionadded:: 7.0 """ flood_text_length = ValidatedAttribute('flood_text_length', int, default=50) """Length of text at which an extra wait penalty is added. :default: ``50`` Messages longer than this (in bytes) get an added wait penalty if the flood protection limit is reached. This is equivalent to the default value: .. code-block:: ini flood_text_length = 50 .. seealso:: The :ref:`Flood Prevention` chapter to learn what each flood-related setting does. .. versionadded:: 7.1 """ help_prefix = ValidatedAttribute('help_prefix', default=COMMAND_DEFAULT_HELP_PREFIX) """The prefix to use in help output. :default: ``.`` This is equivalent to the default value: .. code-block:: ini help_prefix = . If :attr:`prefix` is changed from the default, this setting **must** be updated to reflect the prefix your bot will actually respond to, or the built-in ``help`` functionality will provide incorrect example usage. """ @property def homedir(self): """The directory in which various files are stored at runtime. By default, this is the same directory as the config file. It cannot be changed at runtime. """ return self._parent.homedir host = ValidatedAttribute('host', default='chat.freenode.net') """The IRC server to connect to. :default: ``chat.freenode.net`` **Required**: .. code-block:: ini host = chat.freenode.net """ host_blocks = ListAttribute('host_blocks') """A list of hostnames which Sopel should ignore. Messages from any user whose connection hostname matches one of these values will be ignored. :ref:`Regular expression syntax <re-syntax>` is supported, so remember to escape special characters: .. code-block:: ini host_blocks = (.+\\.)*domain\\.com .. seealso:: The :attr:`nick_blocks` list can be used to block users by their nick. .. note:: We are working on a better block system; see `issue #1355`__ for more information and update. .. __: https://github.com/sopel-irc/sopel/issues/1355 """ log_raw = ValidatedAttribute('log_raw', bool, default=False) """Whether a log of raw lines as sent and received should be kept. :default: ``no`` To enable this logging: .. code-block:: ini log_raw = yes .. seealso:: The :ref:`Raw Logs` chapter. """ logdir = FilenameAttribute('logdir', directory=True, default='logs') """Directory in which to place logs. :default: ``logs`` If the given value is not an absolute path, it will be interpreted relative to the directory containing the config file with which Sopel was started. .. seealso:: The :ref:`Logging` chapter. """ logging_channel = ValidatedAttribute('logging_channel', Identifier) """The channel to send logging messages to. .. seealso:: The :ref:`Log to a Channel` chapter. """ logging_channel_datefmt = ValidatedAttribute('logging_channel_datefmt') """The format string to use for timestamps in IRC channel logs. If not specified, this falls back to using :attr:`logging_datefmt`. .. seealso:: Time format reference is available in the documentation for Python's :func:`time.strftime` function. For more information about logging, see :ref:`Log to a Channel`. .. versionadded:: 7.0 """ logging_channel_format = ValidatedAttribute('logging_channel_format') """The logging format string to use in IRC channel logs. If not specified, this falls back to using :attr:`logging_format`. .. seealso:: The :ref:`Log to a Channel` chapter. .. versionadded:: 7.0 """ logging_channel_level = ChoiceAttribute( 'logging_channel_level', ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], 'WARNING') """The lowest severity of logs to display in IRC channel logs. If not specified, this falls back to using :attr:`logging_level`. .. seealso:: The :ref:`Log to a Channel` chapter. .. versionadded:: 7.0 """ logging_datefmt = ValidatedAttribute('logging_datefmt') """The format string to use for timestamps in logs. If not set, the ``datefmt`` argument is not provided, and :mod:`logging` will use the Python default. .. seealso:: Time format reference is available in the documentation for Python's :func:`time.strftime` function. .. versionadded:: 7.0 """ logging_format = ValidatedAttribute( 'logging_format', default='[%(asctime)s] %(name)-20s %(levelname)-8s - %(message)s') """The logging format string to use for logs. :default: ``[%(asctime)s] %(name)-20s %(levelname)-8s - %(message)s`` The default log line format will output the timestamp, the package that generated the log line, the log level of the line, and (finally) the actual message. For example:: [2019-10-21 12:47:44,272] sopel.irc INFO - Connected. This is equivalent to the default value: .. code-block:: ini logging_format = [%(asctime)s] %(name)-20s %(levelname)-8s - %(message)s .. seealso:: Python's logging format documentation: :ref:`logrecord-attributes` .. versionadded:: 7.0 """ logging_level = ChoiceAttribute( 'logging_level', ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], 'INFO') """The lowest severity of logs to display. :default: ``INFO`` Valid values sorted by increasing verbosity: * ``CRITICAL`` * ``ERROR`` * ``WARNING`` * ``INFO`` * ``DEBUG`` For example to log only at WARNING level and above: .. code-block:: ini logging_level = WARNING """ modes = ValidatedAttribute('modes', default='B') """User modes to be set on connection. :default: ``B`` Include only the mode letters; this value is automatically prefixed with ``+`` before Sopel sends the MODE command to IRC. """ name = ValidatedAttribute('name', default='Sopel: https://sopel.chat/') """The "real name" of your bot for ``WHOIS`` responses. :default: ``Sopel: https://sopel.chat/`` """ nick = ValidatedAttribute('nick', Identifier, default=Identifier('Sopel')) """The nickname for the bot. :default: ``Sopel`` **Required**: .. code-block:: ini nick = Sopel """ nick_auth_method = ChoiceAttribute( 'nick_auth_method', choices=['nickserv', 'authserv', 'Q', 'userserv']) """The nick authentication method. Can be one of ``nickserv``, ``authserv``, ``Q``, or ``userserv``. .. seealso:: The :ref:`Authentication` chapter for more details. .. versionadded:: 7.0 """ nick_auth_password = SecretAttribute('nick_auth_password') """The password to use to authenticate the bot's nick. .. seealso:: The :ref:`Authentication` chapter for more details. .. versionadded:: 7.0 """ nick_auth_target = ValidatedAttribute('nick_auth_target') """The target user for nick authentication. :default: ``NickServ`` for ``nickserv`` authentication; ``UserServ`` for ``userserv`` authentication May not apply, depending on the chosen :attr:`nick_auth_method`. .. seealso:: The :ref:`Authentication` chapter for more details. .. versionadded:: 7.0 """ nick_auth_username = ValidatedAttribute('nick_auth_username') """The username/account to use for nick authentication. :default: the value of :attr:`nick` May not apply, depending on the chosen :attr:`nick_auth_method`. .. seealso:: The :ref:`Authentication` chapter for more details. .. versionadded:: 7.0 """ nick_blocks = ListAttribute('nick_blocks') """A list of nicks which Sopel should ignore. Messages from any user whose nickname matches one of these values will be ignored. :ref:`Regular expression syntax <re-syntax>` is supported, so remember to escape special characters: .. code-block:: ini nick_blocks = ExactNick _*RegexMatch_* .. seealso:: The :attr:`host_blocks` list can be used to block users by their host. .. note:: We are working on a better block system; see `issue #1355`__ for more information and update. .. __: https://github.com/sopel-irc/sopel/issues/1355 """ not_configured = ValidatedAttribute('not_configured', bool, default=False) """For package maintainers. Not used in normal configurations. :default: ``False`` This allows software packages to install a default config file, with this option set to ``True``, so that commands to start, stop, or restart the bot won't work until the bot has been properly configured. """ owner = ValidatedAttribute('owner', default=NO_DEFAULT) """The IRC name of the owner of the bot. **Required** even if :attr:`owner_account` is set. """ owner_account = ValidatedAttribute('owner_account') """The services account name of the owner of the bot. This should only be set on networks which support IRCv3 account capabilities. """ pid_dir = FilenameAttribute('pid_dir', directory=True, default='.') """The directory in which to put the file Sopel uses to track its process ID. :default: ``.`` If the given value is not an absolute path, it will be interpreted relative to the directory containing the config file with which Sopel was started. You probably do not need to change this unless you're managing Sopel with ``systemd`` or similar. """ port = ValidatedAttribute('port', int, default=6667) """The port to connect on. :default: ``6667`` normally; ``6697`` if :attr:`use_ssl` is ``True`` .. highlight:: ini **Required**:: port = 6667 And usually when SSL is enabled:: port = 6697 use_ssl = yes """ prefix = ValidatedAttribute('prefix', default=COMMAND_DEFAULT_PREFIX) """The prefix to add to the beginning of commands as a regular expression. :default: ``\\.`` .. highlight:: ini **Required**:: prefix = \\. With the default value, users will invoke commands like this: .. code-block:: irc <nick> .help Since it's a regular expression, you can use multiple prefixes:: prefix = \\.|\\? .. important:: As the prefix is a regular expression, don't forget to escape it when necessary. It is not recommended to use capturing groups, as it **will** create problems with argument parsing for commands. .. note:: Remember to change the :attr:`help_prefix` value accordingly:: prefix = \\? help_prefix = ? In that example, users will invoke commands like this: .. code-block:: irc <nick> ?help xkcd <Sopel> ?xkcd - Finds an xkcd comic strip <Sopel> Takes one of 3 inputs: [...] """ reply_errors = ValidatedAttribute('reply_errors', bool, default=True) """Whether to reply to the sender of a message that triggered an error. :default: ``True`` If ``True``, Sopel will send information about the triggered exception to the sender of the message that caused the error. If ``False``, Sopel will only log the error and will appear to fail silently from the triggering IRC user's perspective. """ server_auth_method = ChoiceAttribute('server_auth_method', choices=['sasl', 'server']) """The server authentication method. Can be ``sasl`` or ``server``. .. versionadded:: 7.0 """ server_auth_password = SecretAttribute('server_auth_password') """The password to use to authenticate with the server. .. versionadded:: 7.0 """ server_auth_sasl_mech = ValidatedAttribute('server_auth_sasl_mech') """The SASL mechanism. :default: ``PLAIN`` .. versionadded:: 7.0 """ server_auth_username = ValidatedAttribute('server_auth_username') """The username/account to use to authenticate with the server. .. versionadded:: 7.0 """ throttle_join = ValidatedAttribute('throttle_join', int, default=0) """Slow down the initial join of channels to prevent getting kicked. :default: ``0`` Sopel will only join this many channels at a time, sleeping for a second between each batch to avoid getting kicked for joining too quickly. This is unnecessary on most networks. If not set, or set to 0, Sopel won't slow down the initial join. In this example, Sopel will try to join 4 channels at a time: .. code-block:: ini throttle_join = 4 .. seealso:: :attr:`throttle_wait` controls Sopel's waiting time between joining batches of channels. """ throttle_wait = ValidatedAttribute('throttle_wait', int, default=1) """Time in seconds Sopel waits between joining batches of channels. :default: ``1`` In this example: .. code-block:: ini throttle_wait = 5 throttle_join = 2 Sopel will join 2 channels every 5s. If :attr:`throttle_join` is ``0``, this setting has no effect. .. seealso:: :attr:`throttle_join` controls channel batch size. """ timeout = ValidatedAttribute('timeout', int, default=120) """The number of seconds acceptable between pings before timing out. :default: ``120`` You can change the timeout like this: .. code-block:: ini # increase to 200 seconds timeout = 200 """ use_ssl = ValidatedAttribute('use_ssl', bool, default=False) """Whether to use a SSL/TLS encrypted connection. :default: ``False`` Example with SSL on: .. code-block:: ini use_ssl = yes """ user = ValidatedAttribute('user', default='sopel') """The "user" for your bot (the part before the ``@`` in the hostname). :default: ``sopel`` **Required**: .. code-block:: ini user = sopel """ verify_ssl = ValidatedAttribute('verify_ssl', bool, default=True) """Whether to require a trusted certificate for encrypted connections.
def track_modes(bot, trigger): """Track usermode changes and keep our lists of ops up to date.""" channel = Identifier(trigger.args[0]) if channel.is_nick(): # We can just ignore MODE messages that appear to be for a user/nick. # TODO: `Identifier.is_nick()` doesn't handle CHANTYPES from ISUPPORT # numeric (005); it still uses a hard-coded list of channel prefixes. return # Relevant format: MODE <channel> *( ( "-" / "+" ) *<modes> *<modeparams> ) if len(trigger.args) < 3: # If the MODE message appears to be for a channel, we need at least # [channel, mode, nickname] to do anything useful. LOGGER.debug("Received an apparently useless MODE message: {}".format( trigger.raw)) return # Our old MODE parsing code checked if any of the args was empty. # Somewhere around here would be a good place to re-implement that if it's # actually necessary to guard against some non-compliant IRCd. But for now # let's just log malformed lines to the debug log. if not all(trigger.args): LOGGER.debug( "The server sent a possibly malformed MODE message: {}".format( trigger.raw)) # From here on, we will make a (possibly dangerous) assumption that the # received MODE message is more-or-less compliant modestring = trigger.args[1] nicks = [Identifier(nick) for nick in trigger.args[2:]] mapping = { "v": module.VOICE, "h": module.HALFOP, "o": module.OP, "a": module.ADMIN, "q": module.OWNER, "y": module.OPER, "Y": module.OPER, } # Parse modes before doing anything else modes = [] sign = '' for char in modestring: # There was a comment claiming IRC allows e.g. MODE +aB-c foo, but it # doesn't seem to appear in any RFCs. But modern.ircdocs.horse shows # it, so we'll leave in the extra parsing for now. if char in '+-': sign = char elif char in mapping: # Filter out unexpected modes and hope they don't have parameters modes.append(sign + char) # Try to map modes to arguments, after sanity-checking if len(modes) != len(nicks) or not all([nick.is_nick() for nick in nicks]): # Something fucky happening, like unusual batching of non-privilege # modes together with the ones we expect. Way easier to just re-WHO # than try to account for non-standard parameter-taking modes. LOGGER.debug('Sending WHO for channel: %s', channel) _send_who(bot, channel) return for (mode, nick) in zip(modes, nicks): priv = bot.channels[channel].privileges.get(nick, 0) # Log a warning if the two privilege-tracking data structures # get out of sync. That should never happen. # This is a good place to verify that bot.channels is doing # what it's supposed to do before ultimately removing the old, # deprecated bot.privileges structure completely. ppriv = bot.privileges[channel].get(nick, 0) if priv != ppriv: LOGGER.warning("Privilege data error! Please share Sopel's" "raw log with the developers, if enabled. " "(Expected {} == {} for {} in {}.)".format( priv, ppriv, nick, channel)) value = mapping.get(mode[1]) if value is not None: if mode[0] == '+': priv = priv | value else: priv = priv & ~value bot.privileges[channel][nick] = priv bot.channels[channel].privileges[nick] = priv
class CoreSection(StaticSection): """The config section used for configuring the bot itself.""" admins = ListAttribute('admins') """The list of people (other than the owner) who can administer the bot""" admin_accounts = ListAttribute('admin_accounts') """The list of accounts (other than the owner's) who can administer the bot. This should not be set for networks that do not support IRCv3 account capabilities.""" auth_method = ChoiceAttribute( 'auth_method', choices=['nickserv', 'authserv', 'userserv', 'Q', 'sasl', 'server']) """The method to use to authenticate with the server. Can be ``nickserv``, ``authserv``, ``userserv``, ``Q``, ``sasl``, or ``server``.""" auth_password = ValidatedAttribute('auth_password') """The password to use to authenticate with the server.""" auth_target = ValidatedAttribute('auth_target') """The user to use for nickserv authentication, or the SASL mechanism. May not apply, depending on ``auth_method``. Defaults to NickServ for nickserv auth, and PLAIN for SASL auth.""" auth_username = ValidatedAttribute('auth_username') """The username/account to use to authenticate with the server. May not apply, depending on ``auth_method``.""" bind_host = ValidatedAttribute('bind_host') """Bind the connection to a specific IP""" ca_certs = FilenameAttribute('ca_certs', default=_find_certs()) """The path of the CA certs pem file""" channels = ListAttribute('channels') """List of channels for the bot to join when it connects""" db_filename = ValidatedAttribute('db_filename') """The filename for Sopel's database.""" default_time_format = ValidatedAttribute('default_time_format', default='%Y-%m-%d - %T%Z') """The default format to use for time in messages.""" default_timezone = ValidatedAttribute('default_timezone') """The default timezone to use for time in messages.""" enable = ListAttribute('enable') """A whitelist of the only modules you want to enable.""" exclude = ListAttribute('exclude') """A list of modules which should not be loaded.""" extra = ListAttribute('extra') """A list of other directories you'd like to include modules from.""" help_prefix = ValidatedAttribute('help_prefix', default='.') """The prefix to use in help""" @property def homedir(self): """The directory in which various files are stored at runtime. By default, this is the same directory as the config. It can not be changed at runtime. """ return self._parent.homedir host = ValidatedAttribute('host', default='irc.dftba.net') """The server to connect to.""" host_blocks = ListAttribute('host_blocks') """A list of hostmasks which Sopel should ignore. Regular expression syntax is used""" log_raw = ValidatedAttribute('log_raw', bool, default=True) """Whether a log of raw lines as sent and recieved should be kept.""" logdir = FilenameAttribute('logdir', directory=True, default='logs') """Directory in which to place logs.""" logging_channel = ValidatedAttribute('logging_channel', Identifier) """The channel to send logging messages to.""" logging_level = ChoiceAttribute( 'logging_level', ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], 'WARNING') """The lowest severity of logs to display.""" modes = ValidatedAttribute('modes', default='B') """User modes to be set on connection.""" name = ValidatedAttribute('name', default='Sopel: http://sopel.chat') """The "real name" of your bot for WHOIS responses.""" nick = ValidatedAttribute('nick', Identifier, default=Identifier('Sopel')) """The nickname for the bot""" nick_blocks = ListAttribute('nick_blocks') """A list of nicks which Sopel should ignore. Regular expression syntax is used.""" not_configured = ValidatedAttribute('not_configured', bool, default=False) """For package maintainers. Not used in normal configurations. This allows software packages to install a default config file, with this set to true, so that the bot will not run until it has been properly configured.""" owner = ValidatedAttribute('owner', default=NO_DEFAULT) """The IRC name of the owner of the bot.""" owner_account = ValidatedAttribute('owner_account') """The services account name of the owner of the bot. This should only be set on networks which support IRCv3 account capabilities. """ pid_dir = FilenameAttribute('pid_dir', directory=True, default='.') """The directory in which to put the file Sopel uses to track its process ID. You probably do not need to change this unless you're managing Sopel with systemd or similar.""" port = ValidatedAttribute('port', int, default=6667) """The port to connect on.""" prefix = ValidatedAttribute('prefix', default='\.') """The prefix to add to the beginning of commands. It is a regular expression (so the default, ``\.``, means commands start with a period), though using capturing groups will create problems.""" reply_errors = ValidatedAttribute('reply_errors', bool, default=True) """Whether to message the sender of a message that triggered an error with the exception.""" throttle_join = ValidatedAttribute('throttle_join', int) """Slow down the initial join of channels to prevent getting kicked. Sopel will only join this many channels at a time, sleeping for a second between each batch. This is unnecessary on most networks.""" timeout = ValidatedAttribute('timeout', int, default=120) """The amount of time acceptable between pings before timing out.""" use_ssl = ValidatedAttribute('use_ssl', bool, default=False) """Whether to use a SSL secured connection.""" user = ValidatedAttribute('user', default='sopel') """The "user" for your bot (the part before the @ in the hostname).""" verify_ssl = ValidatedAttribute('verify_ssl', bool, default=True) """Whether to require a trusted SSL certificate for SSL connections."""
def luv_h8_cmd(bot, trigger): if not trigger.group(3): bot.reply("No user specified.") return target = Identifier(trigger.group(3)) luv_h8(bot, trigger, target, trigger.group(1))