Skip to content

Commit

Permalink
Merge pull request #2231 from Exirel/identifier-casemapping
Browse files Browse the repository at this point in the history
irc: implement CASEMAPPING parameter for ISUPPORT
  • Loading branch information
dgw authored Jan 21, 2022
2 parents b9b827a + 5f605ea commit aecc6db
Show file tree
Hide file tree
Showing 25 changed files with 1,064 additions and 433 deletions.
9 changes: 8 additions & 1 deletion docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ sopel.tools
.. automodule:: sopel.tools
:members:
:exclude-members: iteritems, iterkeys, itervalues, raw_input
:private-members: Identifier._lower, Identifier._lower_swapped


sopel.tools.identifiers
-----------------------

.. automodule:: sopel.tools.identifiers
:members:


sopel.tools.web
---------------
Expand Down
7 changes: 4 additions & 3 deletions docs/source/plugin/bot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ second argument::

bot.say('The bot is now talking!', '#private-channel')

Instead of a string, you can use an instance of :class:`sopel.tools.Identifier`.
Instead of a string, you can use an instance of
:class:`~sopel.tools.identifiers.Identifier`.

If you want to reply to a user in a private message, you can use the trigger's
:attr:`~sopel.trigger.Trigger.nick` attribute as destination::
Expand Down Expand Up @@ -261,8 +262,8 @@ which provides the following information:
if not trigger.sender.is_nick():
# this trigger is from a channel

See :meth:`Identifier.is_nick() <sopel.tools.Identifier.is_nick>` for
more information.
See :meth:`Identifier.is_nick() <sopel.tools.identifiers.Identifier.is_nick>`
for more information.

Getting users in a channel
--------------------------
Expand Down
34 changes: 20 additions & 14 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from sopel import db, irc, logger, plugin, plugins, tools
from sopel.irc import modes
from sopel.plugins import jobs as plugin_jobs, rules as plugin_rules
from sopel.tools import deprecated, Identifier, jobs as tools_jobs
from sopel.tools import deprecated, jobs as tools_jobs
from sopel.trigger import PreTrigger, Trigger


Expand Down Expand Up @@ -81,23 +81,28 @@ def __init__(self, config, daemon=False):
self.modeparser = modes.ModeParser()
"""A mode parser used to parse ``MODE`` messages and modestrings."""

self.channels = tools.SopelIdentifierMemory()
self.channels = tools.SopelIdentifierMemory(
identifier_factory=self.make_identifier,
)
"""A map of the channels that Sopel is in.
The keys are :class:`sopel.tools.Identifier`\\s of the channel names,
and map to :class:`sopel.tools.target.Channel` objects which contain
the users in the channel and their permissions.
The keys are :class:`~sopel.tools.identifiers.Identifier`\\s of the
channel names, and map to :class:`~sopel.tools.target.Channel` objects
which contain the users in the channel and their permissions.
"""

self.users = tools.SopelIdentifierMemory()
self.users = tools.SopelIdentifierMemory(
identifier_factory=self.make_identifier,
)
"""A map of the users that Sopel is aware of.
The keys are :class:`sopel.tools.Identifier`\\s of the nicknames, and
map to :class:`sopel.tools.target.User` instances. In order for Sopel
to be aware of a user, it must share at least one mutual channel.
The keys are :class:`~sopel.tools.identifiers.Identifier`\\s of the
nicknames, and map to :class:`~sopel.tools.target.User` instances. In
order for Sopel to be aware of a user, it must share at least one
mutual channel.
"""

self.db = db.SopelDB(config)
self.db = db.SopelDB(config, identifier_factory=self.make_identifier)
"""The bot's database, as a :class:`sopel.db.SopelDB` instance."""

self.memory = tools.SopelMemory()
Expand Down Expand Up @@ -212,9 +217,10 @@ def has_channel_privilege(self, channel, privilege) -> bool:
>>> bot.has_channel_privilege('#chan', plugin.VOICE)
True
The ``channel`` argument can be either a :class:`str` or a
:class:`sopel.tools.Identifier`, as long as Sopel joined said channel.
If the channel is unknown, a :exc:`ValueError` will be raised.
The ``channel`` argument can be either a :class:`str` or an
:class:`~sopel.tools.identifiers.Identifier`, as long as Sopel joined
said channel. If the channel is unknown, a :exc:`ValueError` will be
raised.
"""
if channel not in self.channels:
raise ValueError('Unknown channel %s' % channel)
Expand Down Expand Up @@ -987,7 +993,7 @@ def _nick_blocked(self, nick: str) -> bool:
if not bad_nick:
continue
if (re.match(bad_nick + '$', nick, re.IGNORECASE) or
Identifier(bad_nick) == nick):
self.make_identifier(bad_nick) == nick):
return True
return False

Expand Down
2 changes: 1 addition & 1 deletion sopel/config/core_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ def homedir(self):
:default: ``Sopel: https://sopel.chat/``
"""

nick = ValidatedAttribute('nick', Identifier, default=Identifier('Sopel'))
nick = ValidatedAttribute('nick', default='Sopel')
"""The nickname for the bot.
:default: ``Sopel``
Expand Down
56 changes: 38 additions & 18 deletions sopel/coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

from sopel import config, plugin
from sopel.irc import isupport, utils
from sopel.tools import events, Identifier, jobs, SopelMemory, target
from sopel.tools import events, jobs, SopelMemory, target


LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -143,7 +143,7 @@ def auth_after_register(bot):

# nickserv-based auth method needs to check for current nick
if auth_method == 'nickserv':
if bot.nick != bot.settings.core.nick:
if bot.nick != bot.make_identifier(bot.settings.core.nick):
LOGGER.warning("Sending nickserv GHOST command.")
bot.say(
'GHOST %s %s' % (bot.settings.core.nick, auth_password),
Expand Down Expand Up @@ -347,6 +347,7 @@ def handle_isupport(bot, trigger):
botmode_support = 'BOT' in bot.isupport
namesx_support = 'NAMESX' in bot.isupport
uhnames_support = 'UHNAMES' in bot.isupport
casemapping_support = 'CASEMAPPING' in bot.isupport

# parse ISUPPORT message from server
parameters = {}
Expand All @@ -367,6 +368,11 @@ def handle_isupport(bot, trigger):
if 'PREFIX' in bot.isupport:
bot.modeparser.privileges = set(bot.isupport.PREFIX.keys())

# was CASEMAPPING support status updated?
if not casemapping_support and 'CASEMAPPING' in bot.isupport:
# Re-create the bot's nick with the proper identifier+casemapping
bot.rebuild_nick()

# was BOT mode support status updated?
if not botmode_support and 'BOT' in bot.isupport:
# yes it was! set our mode unless the config overrides it
Expand Down Expand Up @@ -482,9 +488,12 @@ def handle_names(bot, trigger):
channels = re.search(r'(#\S*)', trigger.raw)
if not channels:
return
channel = Identifier(channels.group(1))
channel = bot.make_identifier(channels.group(1))
if channel not in bot.channels:
bot.channels[channel] = target.Channel(channel)
bot.channels[channel] = target.Channel(
channel,
identifier_factory=bot.make_identifier,
)

# This could probably be made flexible in the future, but I don't think
# it'd be worth it.
Expand Down Expand Up @@ -515,7 +524,7 @@ def handle_names(bot, trigger):
if prefix in name:
priv = priv | value

nick = Identifier(name.lstrip(''.join(mapping.keys())))
nick = bot.make_identifier(name.lstrip(''.join(mapping.keys())))
user = bot.users.get(nick)
if user is None:
# The username/hostname will be included in a NAMES reply only if
Expand Down Expand Up @@ -559,7 +568,7 @@ def _parse_modes(bot, args, clear=False):
modes at https://modern.ircdocs.horse/#channel-mode
"""
channel_name = Identifier(args[0])
channel_name = bot.make_identifier(args[0])
if channel_name.is_nick():
# We don't do anything with user modes
LOGGER.debug("Ignoring user modes: %r", args)
Expand Down Expand Up @@ -622,7 +631,7 @@ def _parse_modes(bot, args, clear=False):
# modeinfo.privileges contains only the valid parsed privileges
for privilege, is_added, param in modeinfo.privileges:
# User privs modes, always have a param
nick = Identifier(param)
nick = bot.make_identifier(param)
priv = channel.privileges.get(nick, 0)
value = MODE_PREFIX_PRIVILEGES[privilege]
if is_added:
Expand Down Expand Up @@ -659,7 +668,7 @@ def _parse_modes(bot, args, clear=False):
def track_nicks(bot, trigger):
"""Track nickname changes and maintain our chanops list accordingly."""
old = trigger.nick
new = Identifier(trigger)
new = bot.make_identifier(trigger)

# Give debug message, and PM the owner, if the bot's own nick changes.
if old == bot.nick and new != bot.nick:
Expand Down Expand Up @@ -705,7 +714,7 @@ def track_part(bot, trigger):
@plugin.priority('medium')
def track_kick(bot, trigger):
"""Track users kicked from channels."""
nick = Identifier(trigger.args[1])
nick = bot.make_identifier(trigger.args[1])
channel = trigger.sender
_remove_from_channel(bot, nick, channel)
LOGGER.info(
Expand Down Expand Up @@ -748,7 +757,9 @@ def _send_who(bot, channel):
# We might be on an old network, but we still care about keeping our
# user list updated
bot.write(['WHO', channel])
bot.channels[Identifier(channel)].last_who = datetime.datetime.utcnow()

channel_id = bot.make_identifier(channel)
bot.channels[channel_id].last_who = datetime.datetime.utcnow()


@plugin.interval(30)
Expand Down Expand Up @@ -793,7 +804,10 @@ def track_join(bot, trigger):

# is it a new channel?
if channel not in bot.channels:
bot.channels[channel] = target.Channel(channel)
bot.channels[channel] = target.Channel(
channel,
identifier_factory=bot.make_identifier,
)

# did *we* just join?
if trigger.nick == bot.nick:
Expand Down Expand Up @@ -836,7 +850,8 @@ def track_quit(bot, trigger):

LOGGER.info("User quit: %s", trigger.nick)

if trigger.nick == bot.settings.core.nick and trigger.nick != bot.nick:
configured_nick = bot.make_identifier(bot.settings.core.nick)
if trigger.nick == configured_nick and trigger.nick != bot.nick:
# old nick is now available, let's change nick again
bot.change_current_nick(bot.settings.core.nick)
auth_after_register(bot)
Expand Down Expand Up @@ -1229,7 +1244,7 @@ def blocks(bot, trigger):
}

masks = set(s for s in bot.config.core.host_blocks if s != '')
nicks = set(Identifier(nick)
nicks = set(bot.make_identifier(nick)
for nick in bot.config.core.nick_blocks
if nick != '')
text = trigger.group().split()
Expand Down Expand Up @@ -1266,10 +1281,11 @@ def blocks(bot, trigger):

elif len(text) == 4 and text[1] == "del":
if text[2] == "nick":
if Identifier(text[3]) not in nicks:
nick = bot.make_identifier(text[3])
if nick not in nicks:
bot.reply(STRINGS['no_nick'] % (text[3]))
return
nicks.remove(Identifier(text[3]))
nicks.remove(nick)
bot.config.core.nick_blocks = [str(n) for n in nicks]
bot.config.save()
bot.reply(STRINGS['success_del'] % (text[3]))
Expand Down Expand Up @@ -1352,8 +1368,8 @@ def recv_whox(bot, trigger):


def _record_who(bot, channel, user, host, nick, account=None, away=None, modes=None):
nick = Identifier(nick)
channel = Identifier(channel)
nick = bot.make_identifier(nick)
channel = bot.make_identifier(channel)
if nick not in bot.users:
usr = target.User(nick, user, host)
bot.users[nick] = usr
Expand Down Expand Up @@ -1383,7 +1399,11 @@ def _record_who(bot, channel, user, host, nick, account=None, away=None, modes=N
for c in modes:
priv = priv | mapping[c]
if channel not in bot.channels:
bot.channels[channel] = target.Channel(channel)
bot.channels[channel] = target.Channel(
channel,
identifier_factory=bot.make_identifier,
)

bot.channels[channel].add_user(usr, privs=priv)


Expand Down
Loading

0 comments on commit aecc6db

Please sign in to comment.