Exemplo n.º 1
0
    def get_channel_slug(self, chan: str) -> str:
        """Return the case-normalized representation of ``channel``.

        :param channel: the channel name to normalize, with prefix (required)
        :return: the case-normalized channel name (or "slug" representation)

        This is useful to make sure that a channel name is stored consistently
        in both the bot's own database and third-party plugins'
        databases/files, without regard for variation in case between
        different clients and/or servers on the network.
        """
        slug = self.make_identifier(chan).lower()
        session = self.ssession()
        try:
            count = session.query(ChannelValues) \
                .filter(ChannelValues.channel == slug) \
                .count()

            if count == 0:
                # see if it needs case-mapping migration
                old_rows = session.query(ChannelValues) \
                    .filter(ChannelValues.channel == Identifier._lower_swapped(chan))
                old_count = old_rows.count()
                if old_count > 0:
                    # it does!
                    old_rows.update({ChannelValues.channel: slug})
                    session.commit()

            return slug
        except SQLAlchemyError:
            session.rollback()
            raise
        finally:
            self.ssession.remove()
Exemplo n.º 2
0
Arquivo: db.py Projeto: dgw/sopel
    def get_nick_id(self, nick: str, create: bool = False) -> int:
        """Return the internal identifier for a given nick.

        :param nick: the nickname for which to fetch an ID
        :param create: whether to create an ID if one does not exist
                       (set to ``False`` by default)
        :raise ValueError: if no ID exists for the given ``nick`` and ``create``
                           is set to ``False``
        :raise ~sqlalchemy.exc.SQLAlchemyError: if there is a database error

        The nick ID is shared across all of a user's aliases, assuming their
        nicks have been grouped together.

        .. versionchanged:: 8.0

            The ``create`` parameter is now ``False`` by default.

        .. seealso::

            Alias/group management functions: :meth:`alias_nick`,
            :meth:`unalias_nick`, :meth:`merge_nick_groups`, and
            :meth:`forget_nick_group`.

        """
        slug = self.make_identifier(nick).lower()
        with self.session() as session:
            nickname = session.execute(
                select(Nicknames).where(
                    Nicknames.slug == slug)).scalar_one_or_none()

            if nickname is None:
                # see if it needs case-mapping migration
                nickname = session.execute(
                    select(Nicknames).where(
                        Nicknames.slug == Identifier._lower_swapped(
                            nick))).scalar_one_or_none()

                if nickname is not None:
                    # it does!
                    nickname.slug = slug
                    session.commit()

            if nickname is None:  # "is /* still */ None", if Python had inline comments
                if not create:
                    raise ValueError('No ID exists for the given nick')
                # Generate a new ID
                nick_id = NickIDs()
                session.add(nick_id)
                session.commit()

                # Create a new Nickname
                nickname = Nicknames(
                    nick_id=nick_id.nick_id,
                    slug=slug,
                    canonical=nick,
                )
                session.add(nickname)
                session.commit()
            return nickname.nick_id
Exemplo n.º 3
0
Arquivo: db.py Projeto: dgw/sopel
    def get_channel_slug(self, chan: str) -> str:
        """Return the case-normalized representation of ``channel``.

        :param channel: the channel name to normalize, with prefix (required)
        :return: the case-normalized channel name (or "slug" representation)

        This is useful to make sure that a channel name is stored consistently
        in both the bot's own database and third-party plugins'
        databases/files, without regard for variation in case between
        different clients and/or servers on the network.
        """
        slug = self.make_identifier(chan).lower()

        with self.session() as session:
            # Always migrate from old casemapping
            session.execute(
                update(ChannelValues).where(
                    ChannelValues.channel == Identifier._lower_swapped(
                        chan)).values(channel=slug).execution_options(
                            synchronize_session="fetch"))
            session.commit()

        return slug
Exemplo n.º 4
0
    def __init__(
        self,
        own_nick: identifiers.Identifier,
        line: str,
        url_schemes: Optional[Sequence] = None,
        identifier_factory: IdentifierFactory = identifiers.Identifier,
    ):
        self.make_identifier = identifier_factory
        line = line.strip('\r\n')
        self.line: str = line
        self.urls: Tuple[str, ...] = tuple()
        self.plain: str = ''
        self.ctcp: Optional[str] = None

        # Break off IRCv3 message tags, if present
        self.tags: Dict[str, Optional[str]] = {}
        if line.startswith('@'):
            tagstring, line = line.split(' ', 1)
            for raw_tag in tagstring[1:].split(';'):
                tag = raw_tag.split('=', 1)
                if len(tag) > 1:
                    self.tags[tag[0]] = tag[1]
                else:
                    self.tags[tag[0]] = None

        # Client time or server time
        self.time = datetime.datetime.utcnow().replace(
            tzinfo=datetime.timezone.utc
        )
        if 'time' in self.tags:
            # ensure "time" is a string (typecheck)
            tag_time = self.tags['time'] or ''
            try:
                self.time = datetime.datetime.strptime(
                    tag_time,
                    "%Y-%m-%dT%H:%M:%S.%fZ",
                ).replace(tzinfo=datetime.timezone.utc)
            except ValueError:
                pass  # Server isn't conforming to spec, ignore the server-time

        # Grabs hostmask from line.
        # Example: line = ':Sopel!foo@bar PRIVMSG #sopel :foobar!'
        #          print(hostmask)  # Sopel!foo@bar
        # All lines start with ":" except PING.
        self.hostmask: Optional[str]
        if line.startswith(':'):
            self.hostmask, line = line[1:].split(' ', 1)
        else:
            self.hostmask = None

        # Parses the line into a list of arguments.
        # Some events like MODE don't have a secondary string argument, i.e. no ' :' inside the line.
        # Example 1:  line = ':nick!ident@domain PRIVMSG #sopel :foo bar!'
        #             print(text)    # 'foo bar!'
        #             print(argstr)  # ':nick!ident@domain PRIVMSG #sopel'
        #             print(args)    # [':nick!ident@domain', 'PRIVMSG', '#sopel', 'foo bar!']
        # Example 2:  line = 'irc.libera.chat MODE Sopel +i'
        #             print(text)    # '+i'
        #             print(args)    # ['irc.libera.chat', 'MODE', 'Sopel', '+i']
        if ' :' in line:
            argstr, self.text = line.split(' :', 1)
            self.args = argstr.split(' ')
            self.args.append(self.text)
        else:
            self.args = line.split(' ')
            self.text = self.args[-1]

        self.event = self.args[0]
        self.args = self.args[1:]

        # The regex will always match any string, even an empty one
        components_match = cast(
            Match, PreTrigger.component_regex.match(self.hostmask or ''))
        nick, self.user, self.host = components_match.groups()
        self.nick: identifiers.Identifier = self.make_identifier(nick)

        # If we have arguments, the first one is the sender
        # Unless it's a QUIT event
        target: Optional[identifiers.Identifier] = None

        if self.args and self.event != 'QUIT':
            target = self.make_identifier(self.args[0])

            # Unless we're messaging the bot directly, in which case that
            # second arg will be our bot's name.
            if target.lower() == own_nick.lower():
                target = self.nick

        self.sender = target

        # Parse CTCP
        if self.event == 'PRIVMSG' or self.event == 'NOTICE':
            ctcp_match = PreTrigger.ctcp_regex.match(self.args[-1])
            if ctcp_match is not None:
                ctcp, message = ctcp_match.groups()
                self.ctcp = ctcp
                self.args[-1] = message or ''

            # Search URLs after CTCP parsing
            self.urls = tuple(
                web.search_urls(self.args[-1], schemes=url_schemes))

        # Populate account from extended-join messages
        if self.event == 'JOIN' and len(self.args) == 3:
            # Account is the second arg `...JOIN #Sopel account :realname`
            self.tags['account'] = self.args[1]

        # get plain text message
        if self.args:
            self.plain = formatting.plain(self.args[-1])