Source code for fmn.sender.irc

# SPDX-FileCopyrightText: Contributors to the Fedora Project
#
# SPDX-License-Identifier: MIT

import asyncio
import logging
from urllib.parse import urlparse

from irc.client import ServerConnectionError
from irc.client_aio import AioSimpleIRCClient
from irc.connection import AioFactory

from .handler import Handler, HandlerError

log = logging.getLogger(__name__)


[docs] class IRCHandler(Handler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._client = IRCClient()
[docs] async def setup(self): irc_url = urlparse(self._config["irc_url"]) await self._client.connect( irc_url.hostname, irc_url.port, irc_url.username, password=irc_url.password, connect_factory=AioFactory(ssl=(irc_url.scheme == "ircs")), ) log.debug("IRC connection established")
[docs] async def stop(self): log.debug("Stopping IRC handler...") await self._client.disconnect()
@property def closed(self): return self._client.closed
[docs] async def handle(self, message): log.info("Sending messsage to %s: %s", message["to"], message["message"]) await self._client.privmsg(message["to"], message["message"])
[docs] class IRCClient(AioSimpleIRCClient): _shutdown_message = "FMN is shutting down" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._connection_future = None self._loop = asyncio.get_event_loop() self.closed = self._loop.create_future() self._set_logged_in = None
[docs] async def connect(self, *args, **kwargs): self._connection_future = asyncio.Future() await self.connection.connect(*args, **kwargs) try: await self._connection_future except asyncio.exceptions.CancelledError: self.connection.disconnect("Connection cancelled") raise except ServerConnectionError as e: message = e.args[0] self.closed.set_result(message) raise HandlerError(f"the handler could not connect: {message}") from e
[docs] async def privmsg(self, *args, **kwargs): # This is not async yet. return self.connection.privmsg(*args, **kwargs)
[docs] async def disconnect(self): if self.connection.connected: return self.connection.disconnect(self._shutdown_message)
def _cancel_or_close(self, message): """Cancel or close the futures on shutdown. Cancel the ``_connection_future`` if it's not done yet, otherwise set the ``closed`` future to signal shutdown. Args: message (str): The message for the cancellation or the closed future result. """ if self._connection_future.done(): self.closed.set_result(message) else: # This will disconnect and set the closed future self._connection_future.cancel(message)
[docs] def on_disconnect(self, connection, event): message = event.arguments[0] if message != self._shutdown_message: self._cancel_or_close(message)
[docs] def on_error(self, connection, event): message = event.arguments[0] if event.arguments else event.target self._cancel_or_close(message)
[docs] def on_nicknameinuse(self, connection, event): message = f"{event.arguments[0]}: {event.arguments[1]}" self._connection_future.set_exception(ServerConnectionError(message))
[docs] def on_loggedin(self, connection, event): # Not supported by the IRC library yet, so we get ``self.on_900()``. if self._set_logged_in is not None: self._set_logged_in.cancel() self._connection_future.set_result(connection)
[docs] def on_900(self, connection, event): # When logged in. # See IRCv3: https://ircv3.net/specs/extensions/sasl-3.1.html#numerics-used-by-this-extension return self.on_loggedin(connection, event)
[docs] def on_privnotice(self, connection, event): """Set the connection as ready in a few seconds. This is necessary because libera.chat does not always send us the ``900 LOGGED_IN`` response. We must thus setup a delayed call to flag the connection as ready, and cancel it if the proper response arrives. See https://github.com/fedora-infra/fmn/issues/884 """ if event.source != "NickServ!NickServ@services.libera.chat": return try: message = event.arguments[0] except IndexError: return if not message.startswith("You are now identified for"): return self._set_logged_in = self._loop.call_later(5, self.on_loggedin, connection, event)