forked from platypush/platypush
396 lines
14 KiB
Python
396 lines
14 KiB
Python
import os
|
|
from typing import Sequence, Dict, Tuple, Union, Optional
|
|
|
|
from platypush.plugins import RunnablePlugin, action
|
|
from platypush.schemas.irc import IRCServerSchema, IRCServerStatusSchema, IRCChannelSchema
|
|
|
|
from ._bot import IRCBot
|
|
from .. import ChatPlugin
|
|
|
|
|
|
class ChatIrcPlugin(RunnablePlugin, ChatPlugin):
|
|
"""
|
|
IRC integration.
|
|
|
|
This plugin allows you to easily create IRC bots with custom logic that reacts to IRC events
|
|
and interact with IRC sessions.
|
|
|
|
Triggers:
|
|
|
|
* :class:`platypush.message.event.irc.IRCChannelJoinEvent` when a user joins a channel.
|
|
* :class:`platypush.message.event.irc.IRCChannelKickEvent` when a user is kicked from a channel.
|
|
* :class:`platypush.message.event.irc.IRCModeEvent` when a user/channel mode change event occurs.
|
|
* :class:`platypush.message.event.irc.IRCPartEvent` when a user parts a channel.
|
|
* :class:`platypush.message.event.irc.IRCQuitEvent` when a user quits.
|
|
* :class:`platypush.message.event.irc.IRCNickChangeEvent` when a user nick changes.
|
|
* :class:`platypush.message.event.irc.IRCConnectEvent` when the bot connects to a server.
|
|
* :class:`platypush.message.event.irc.IRCDisconnectEvent` when the bot disconnects from a server.
|
|
* :class:`platypush.message.event.irc.IRCPrivateMessageEvent` when a private message is received.
|
|
* :class:`platypush.message.event.irc.IRCPublicMessageEvent` when a public message is received.
|
|
* :class:`platypush.message.event.irc.IRCDCCRequestEvent` when a DCC connection request is received.
|
|
* :class:`platypush.message.event.irc.IRCDCCMessageEvent` when a DCC message is received.
|
|
* :class:`platypush.message.event.irc.IRCCTCPMessageEvent` when a CTCP message is received.
|
|
* :class:`platypush.message.event.irc.IRCDCCFileRequestEvent` when a DCC file request is received.
|
|
* :class:`platypush.message.event.irc.IRCDCCFileRecvCompletedEvent` when a DCC file download is completed.
|
|
* :class:`platypush.message.event.irc.IRCDCCFileRecvCancelledEvent` when a DCC file download is cancelled.
|
|
* :class:`platypush.message.event.irc.IRCDCCFileSendCompletedEvent` when a DCC file upload is completed.
|
|
* :class:`platypush.message.event.irc.IRCDCCFileSendCancelledEvent` when a DCC file upload is cancelled.
|
|
|
|
Requires:
|
|
|
|
* **irc** (``pip install irc``)
|
|
|
|
"""
|
|
|
|
def __init__(self, servers: Sequence[dict], **kwargs):
|
|
"""
|
|
:param servers: List of servers/channels that the bot will automatically connect/join.
|
|
"""
|
|
super().__init__(**kwargs)
|
|
try:
|
|
self._bots: Dict[Tuple[str, int], IRCBot] = {
|
|
(server_conf['server'], server_conf['port']): IRCBot(**server_conf)
|
|
for server_conf in IRCServerSchema().load(servers, many=True)
|
|
}
|
|
except Exception as e:
|
|
self.logger.warning(f'Could not load IRC server configuration: {e}')
|
|
self.logger.exception(e)
|
|
raise e
|
|
|
|
@property
|
|
def _bots_by_server(self) -> Dict[str, IRCBot]:
|
|
return {
|
|
bot.server: bot
|
|
for srv, bot in self._bots.items()
|
|
}
|
|
|
|
@property
|
|
def _bots_by_server_and_port(self) -> Dict[Tuple[str, int], IRCBot]:
|
|
return {
|
|
(bot.server, bot.port): bot
|
|
for srv, bot in self._bots.items()
|
|
}
|
|
|
|
@property
|
|
def _bots_by_alias(self) -> Dict[str, IRCBot]:
|
|
return {
|
|
bot.alias: bot
|
|
for srv, bot in self._bots.items()
|
|
if bot.alias
|
|
}
|
|
|
|
def main(self):
|
|
self._connect()
|
|
self._should_stop.wait()
|
|
|
|
def _connect(self):
|
|
for srv, bot in self._bots.items():
|
|
self.logger.info(f'Connecting to IRC server {srv}')
|
|
bot.start()
|
|
|
|
def stop(self):
|
|
for srv, bot in self._bots.items():
|
|
self.logger.info(f'Disconnecting from IRC server {srv}')
|
|
try:
|
|
bot.stop(bot.stop_message or 'Application stopped')
|
|
except Exception as e:
|
|
self.logger.warning(f'Error while stopping connection to {srv}: {e}')
|
|
|
|
super().stop()
|
|
|
|
def _get_bot(self, server: Union[str, Tuple[str, int]]) -> IRCBot:
|
|
if isinstance(server, (tuple, list, set)):
|
|
bot = self._bots_by_server_and_port[tuple(server)]
|
|
else:
|
|
bot = self._bots_by_alias.get(server, self._bots_by_server.get(server))
|
|
|
|
assert bot, f'Bot connection to {server} not found'
|
|
return bot
|
|
|
|
@action
|
|
def send_file(
|
|
self, file: str, server: Union[str, Tuple[str, int]], nick: str, bind_address: Optional[str] = None
|
|
):
|
|
"""
|
|
Send a file to an IRC user over DCC connection.
|
|
Note that passive connections are currently not supported.
|
|
|
|
:param file: Path of the file that should be transferred.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param nick: Target IRC nick.
|
|
:param bind_address: DCC listen bind address (default: any).
|
|
"""
|
|
file = os.path.expanduser(file)
|
|
assert os.path.isfile(file), f'{file} is not a regular file'
|
|
bot = self._get_bot(server)
|
|
bot.dcc_file_transfer(file=file, nick=nick, bind_address=bind_address)
|
|
|
|
@action
|
|
def send_message(
|
|
self, text: str, server: Union[str, Tuple[str, int]], target: Union[str, Sequence[str]]
|
|
):
|
|
"""
|
|
Send a message to a channel or a nick.
|
|
|
|
:param text: Message content.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param target: Message target (nick or channel). If it's a list then the message will be sent
|
|
to multiple targets.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
method = (
|
|
bot.connection.privmsg if isinstance(target, str)
|
|
else bot.connection.privmsg_many
|
|
)
|
|
method(target, text)
|
|
|
|
@action
|
|
def send_notice(
|
|
self, text: str, server: Union[str, Tuple[str, int]], target: str
|
|
):
|
|
"""
|
|
Send a notice to a channel or a nick.
|
|
|
|
:param text: Message content.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param target: Message target (nick or channel).
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.notice(target, text)
|
|
|
|
@action
|
|
def servers(self) -> Sequence[dict]:
|
|
"""
|
|
Get information about the connected servers.
|
|
|
|
:return: .. schema:: irc.IRCServerStatusSchema(many=True)
|
|
"""
|
|
bots = self._bots_by_server.values()
|
|
return IRCServerStatusSchema().dump(
|
|
{
|
|
'server': bot.server,
|
|
'port': bot.port,
|
|
'alias': bot.alias,
|
|
'real_name': bot.connection.get_server_name(),
|
|
'nickname': bot.connection.get_nickname(),
|
|
'is_connected': bot.connection.is_connected(),
|
|
'connected_channels': bot.channels.keys(),
|
|
}
|
|
for bot in bots
|
|
)
|
|
|
|
@action
|
|
def channel(self, server: Union[str, Tuple[str, int]], channel: str) -> dict:
|
|
"""
|
|
Get information about a connected channel.
|
|
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param channel:
|
|
:return: .. schema:: irc.IRCChannelSchema
|
|
"""
|
|
bot = self._get_bot(server)
|
|
channel_name = channel
|
|
channel = bot.channels.get(channel)
|
|
assert channel, f'Not connected to channel {channel}'
|
|
return IRCChannelSchema().dump({
|
|
'is_invite_only': channel.is_invite_only(),
|
|
'is_moderated': channel.is_moderated(),
|
|
'is_protected': channel.is_protected(),
|
|
'is_secret': channel.is_secret(),
|
|
'name': channel_name,
|
|
'modes': channel.modes,
|
|
'opers': list(channel.opers()),
|
|
'owners': channel.owners(),
|
|
'users': list(channel.users()),
|
|
'voiced': list(channel.voiced()),
|
|
})
|
|
|
|
@action
|
|
def send_ctcp_message(
|
|
self, ctcp_type: str, body: str, server: Union[str, Tuple[str, int]], target: str
|
|
):
|
|
"""
|
|
Send a CTCP message to a target.
|
|
|
|
:param ctcp_type: CTCP message type.
|
|
:param body: Message content.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param target: Message target.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.ctcp(ctcp_type, target, body)
|
|
|
|
@action
|
|
def send_ctcp_reply(
|
|
self, body: str, server: Union[str, Tuple[str, int]], target: str
|
|
):
|
|
"""
|
|
Send a CTCP REPLY command.
|
|
|
|
:param body: Message content.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param target: Message target.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.ctcp_reply(target, body)
|
|
|
|
@action
|
|
def disconnect(self, server: Union[str, Tuple[str, int]], message: Optional[str] = None):
|
|
"""
|
|
Disconnect from a server.
|
|
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param message: Disconnect message (default: configured ``stop_message``.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.disconnect(message or bot.stop_message)
|
|
|
|
@action
|
|
def invite(
|
|
self, nick: str, channel: str, server: Union[str, Tuple[str, int]]
|
|
):
|
|
"""
|
|
Invite a nick to a channel.
|
|
|
|
:param nick: Target IRC nick.
|
|
:param channel: Target IRC channel.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.invite(nick, channel)
|
|
|
|
@action
|
|
def join(self, channel: str, server: Union[str, Tuple[str, int]]):
|
|
"""
|
|
Join a channel.
|
|
|
|
:param channel: Target IRC channel.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.join(channel)
|
|
|
|
@action
|
|
def kick(
|
|
self, nick: str, channel: str, server: Union[str, Tuple[str, int]], reason: Optional[str] = None
|
|
):
|
|
"""
|
|
Kick a nick from a channel.
|
|
|
|
:param nick: Target IRC nick.
|
|
:param channel: Target IRC channel.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param reason: Kick reason.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.kick(channel, nick, reason)
|
|
|
|
@action
|
|
def mode(
|
|
self, target: str, command: str, server: Union[str, Tuple[str, int]]
|
|
):
|
|
"""
|
|
Send a MODE command on the selected target.
|
|
|
|
:param target: IRC target.
|
|
:param command: Mode command.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.mode(target, command)
|
|
|
|
@action
|
|
def set_nick(self, nick: str, server: Union[str, Tuple[str, int]]):
|
|
"""
|
|
Set the IRC nick.
|
|
|
|
:param nick: New IRC nick.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.nick(nick)
|
|
|
|
@action
|
|
def oper(self, nick: str, password: str, server: Union[str, Tuple[str, int]]):
|
|
"""
|
|
Send an OPER command.
|
|
|
|
:param nick: IRC nick.
|
|
:param password: Nick password.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.oper(nick, password)
|
|
|
|
@action
|
|
def part(
|
|
self, channel: Union[str, Sequence[str]], server: Union[str, Tuple[str, int]],
|
|
message: Optional[str] = None
|
|
):
|
|
"""
|
|
Parts/exits a channel.
|
|
|
|
:param channel: IRC channel (or list of channels).
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param message: Optional part message (default: same as the bot's ``stop_message``).
|
|
"""
|
|
bot = self._get_bot(server)
|
|
channels = [channel] if isinstance(channel, str) else channel
|
|
bot.connection.part(channels=channels, message=message or bot.stop_message)
|
|
|
|
@action
|
|
def quit(
|
|
self, server: Union[str, Tuple[str, int]], message: Optional[str] = None
|
|
):
|
|
"""
|
|
Send a QUIT command.
|
|
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param message: Optional quit message (default: same as the bot's ``stop_message``).
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.quit(message=message or bot.stop_message)
|
|
|
|
@action
|
|
def send_raw(self, message: str, server: Union[str, Tuple[str, int]]):
|
|
"""
|
|
Send a raw IRC message to a connected server.
|
|
|
|
:param message: IRC message.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
bot.connection.send_raw(message)
|
|
|
|
@action
|
|
def topic(self, channel: str, server: Union[str, Tuple[str, int]], topic: Optional[str] = None) -> str:
|
|
"""
|
|
Get/set the topic of an IRC channel.
|
|
|
|
:param channel: IRC channel.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
:param topic: If specified, then set the new topic as channel topic.
|
|
Otherwise, just return the current channel topic.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
with bot.event_queue('currenttopic') as evt_queue:
|
|
bot.connection.topic(channel, topic)
|
|
evt = evt_queue.get(block=True, timeout=bot.response_timeout)
|
|
return evt.arguments[1]
|
|
|
|
@action
|
|
def whois(self, target: str, server: Union[str, Tuple[str, int]]):
|
|
"""
|
|
Send a WHOIS command for a target.
|
|
|
|
:param target: IRC target.
|
|
:param server: IRC server, identified either by ``alias`` or ``(server, port)`` tuple.
|
|
"""
|
|
bot = self._get_bot(server)
|
|
with bot.event_queue('whoisuser') as evt_queue:
|
|
bot.connection.whois([target])
|
|
evt = evt_queue.get(block=True, timeout=bot.response_timeout)
|
|
return evt.arguments
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|