syslog2irc.irc.DummyBot.start()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
crap 2
1
"""
2
syslog2irc.irc
3
~~~~~~~~~~~~~~
4
5
Internet Relay Chat
6
7
:Copyright: 2007-2021 Jochen Kupperschmidt
8
:License: MIT, see LICENSE for details.
9
"""
10
11 1
from __future__ import annotations
12 1
from dataclasses import dataclass
13 1
import logging
14 1
import ssl
15 1
from typing import Optional, Union
16
17 1
from irc.bot import ServerSpec, SingleServerIRCBot
18 1
from irc.connection import Factory
19
20 1
from .signals import irc_channel_joined
21 1
from .util import start_thread
22
23
24 1
logger = logging.getLogger(__name__)
25
26
27 1
@dataclass(frozen=True)
28
class IrcServer:
29
    """An IRC server."""
30
31 1
    host: str
32 1
    port: int = 6667
33 1
    ssl: bool = False
34 1
    password: Optional[str] = None
35 1
    rate_limit: Optional[float] = None
36
37
38 1
@dataclass(frozen=True, order=True)
39
class IrcChannel:
40
    """An IRC channel with optional password."""
41
42 1
    name: str
43 1
    password: Optional[str] = None
44
45
46 1
@dataclass(frozen=True)
47
class IrcConfig:
48
    """An IRC bot configuration."""
49
50 1
    server: Optional[IrcServer]
51 1
    nickname: str
52 1
    realname: str
53 1
    commands: list[str]
54 1
    channels: set[IrcChannel]
55
56
57 1
class Bot(SingleServerIRCBot):
58
    """An IRC bot to forward messages to IRC channels."""
59
60 1
    def __init__(
61
        self,
62
        server: IrcServer,
63
        nickname: str,
64
        realname: str,
65
        commands: list[str],
66
        channels: set[IrcChannel],
67
    ) -> None:
68 1
        logger.info(
69
            'Connecting to IRC server %s:%d ...', server.host, server.port
70
        )
71
72 1
        server_spec = ServerSpec(server.host, server.port, server.password)
73 1
        factory = Factory(wrapper=ssl.wrap_socket) if server.ssl else Factory()
74 1
        SingleServerIRCBot.__init__(
75
            self, [server_spec], nickname, realname, connect_factory=factory
76
        )
77
78 1
        if server.rate_limit is not None:
79
            logger.info(
80
                'IRC send rate limit set to %.2f messages per second.',
81
                server.rate_limit,
82
            )
83
            self.connection.set_rate_limit(server.rate_limit)
84
        else:
85 1
            logger.info('No IRC send rate limit set.')
86
87 1
        self.commands = commands
88
89
        # Note: `self.channels` already exists in super class.
90 1
        self.channels_to_join = channels
91
92 1
    def start(self) -> None:
93
        """Connect to the server, in a separate thread."""
94
        start_thread(super().start, self.__class__.__name__)
95
96 1
    def get_version(self) -> str:
97
        """Return this on CTCP VERSION requests."""
98 1
        return 'syslog2IRC'
99
100 1
    def on_welcome(self, conn, event) -> None:
101
        """Join channels after connect."""
102 1
        logger.info(
103
            'Connected to IRC server %s:%d.', *conn.socket.getpeername()
104
        )
105
106 1
        self._send_custom_commands_after_welcome(conn)
107 1
        self._join_channels(conn)
108
109 1
    def _send_custom_commands_after_welcome(self, conn):
110
        """Send custom commands after having been welcomed by the server."""
111 1
        for command in self.commands:
112
            conn.send_raw(command)
113
114 1
    def _join_channels(self, conn):
115
        """Join the configured channels."""
116 1
        channels = sorted(self.channels_to_join)
117 1
        logger.info('Channels to join: %s', ', '.join(c.name for c in channels))
118
119 1
        for channel in channels:
120 1
            logger.info('Joining channel %s ...', channel.name)
121 1
            conn.join(channel.name, channel.password or '')
122
123 1
    def on_nicknameinuse(self, conn, event) -> None:
124
        """Choose another nickname if conflicting."""
125
        self._nickname += '_'
126
        conn.nick(self._nickname)
127
128 1
    def on_join(self, conn, event) -> None:
129
        """Successfully joined channel."""
130 1
        joined_nick = event.source.nick
131 1
        channel_name = event.target
132
133 1
        if joined_nick == self._nickname:
134 1
            logger.info('Joined IRC channel: %s', channel_name)
135 1
            irc_channel_joined.send(channel_name=channel_name)
136
137 1
    def on_badchannelkey(self, conn, event) -> None:
138
        """Channel could not be joined due to wrong password."""
139
        channel_name = event.arguments[0]
140
        logger.warning('Cannot join channel %s (bad key).', channel_name)
141
142 1
    def say(self, channel_name: str, text: str) -> None:
143
        """Say message on channel."""
144
        self.connection.privmsg(channel_name, text)
145
146
147 1
class DummyBot:
148
    """A fake bot that writes messages to STDOUT."""
149
150 1
    def __init__(self, channels: set[IrcChannel]) -> None:
151 1
        self.channels = channels
152
153 1
    def start(self) -> None:
154
        # Fake channel joins.
155 1
        for channel in sorted(self.channels):
156 1
            irc_channel_joined.send(channel_name=channel.name)
157
158 1
    def say(self, channel_name: str, text: str) -> None:
159
        logger.debug('%s> %s', channel_name, text)
160
161 1
    def disconnect(self, msg: str) -> None:
162
        # Mimics `irc.bot.SingleServerIRCBot.disconnect`.
163 1
        logger.info('Shutting down bot ...')
164
165
166 1
def create_bot(config: IrcConfig) -> Union[Bot, DummyBot]:
167
    """Create and return an IRC bot according to the configuration."""
168 1
    if config.server is None:
169 1
        logger.info('No IRC server specified; will write to STDOUT instead.')
170 1
        return DummyBot(config.channels)
171
172 1
    return Bot(
173
        config.server,
174
        config.nickname,
175
        config.realname,
176
        config.commands,
177
        config.channels,
178
    )
179