weitersager.irc._set_rate_limit()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

Changes 0
Metric Value
cc 2
eloc 7
nop 2
dl 0
loc 10
rs 10
c 0
b 0
f 0
ccs 3
cts 5
cp 0.6
crap 2.2559
1
"""
2
weitersager.irc
3
~~~~~~~~~~~~~~~
4
5
Internet Relay Chat
6
7
:Copyright: 2007-2025 Jochen Kupperschmidt
8
:License: MIT, see LICENSE for details.
9
"""
10
11 1
from __future__ import annotations
12 1
import logging
13 1
import ssl
14
15 1
from irc.bot import ServerSpec, SingleServerIRCBot
16 1
from irc.connection import Factory
17 1
from jaraco.stream.buffer import LenientDecodingLineBuffer
18
19 1
from .config import IrcChannel, IrcConfig, IrcServer
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
class Announcer:
28
    """An announcer."""
29
30 1
    def start(self) -> None:
31
        """Start the announcer."""
32
33 1
    def announce(self, channel_name: str, text: str) -> None:
34
        """Announce a message."""
35
        raise NotImplementedError
36
37 1
    def shutdown(self) -> None:
38
        """Shut the announcer down."""
39
40
41 1
class IrcAnnouncer(Announcer):
42
    """An announcer that writes messages to IRC."""
43
44 1
    def __init__(
45
        self,
46
        server: IrcServer,
47
        nickname: str,
48
        realname: str,
49
        commands: list[str],
50
        channels: set[IrcChannel],
51
    ) -> None:
52 1
        self.server = server
53 1
        self.commands = commands
54 1
        self.channels = channels
55
56 1
        self.bot = _create_bot(server, nickname, realname)
57 1
        self.bot.on_welcome = self._on_welcome
58
59 1
    def start(self) -> None:
60
        """Connect to the server, in a separate thread."""
61
        logger.info(
62
            'Connecting to IRC server %s:%d ...',
63
            self.server.host,
64
            self.server.port,
65
        )
66
67
        start_thread(self.bot.start)
68
69 1
    def _on_welcome(self, conn, event) -> None:
70
        """Join channels after connect."""
71 1
        logger.info(
72
            'Connected to IRC server %s:%d.', *conn.socket.getpeername()
73
        )
74
75 1
        self._send_commands(conn)
76 1
        self._join_channels(conn)
77
78 1
    def _send_commands(self, conn):
79
        """Send custom commands after having been welcomed by the server."""
80 1
        for command in self.commands:
81
            conn.send_raw(command)
82
83 1
    def _join_channels(self, conn):
84
        """Join the configured channels."""
85 1
        channels = sorted(self.channels)
86 1
        logger.info('Channels to join: %s', ', '.join(c.name for c in channels))
87
88 1
        for channel in channels:
89 1
            logger.info('Joining channel %s ...', channel.name)
90 1
            conn.join(channel.name, channel.password or '')
91
92 1
    def announce(self, channel_name: str, text: str) -> None:
93
        """Announce a message."""
94
        self.bot.connection.privmsg(channel_name, text)
95
96 1
    def shutdown(self) -> None:
97
        """Shut the announcer down."""
98 1
        self.bot.disconnect('Bye.')
99
100
101 1
class Bot(SingleServerIRCBot):
102
    """An IRC bot to forward messages to IRC channels."""
103
104 1
    def get_version(self) -> str:
105
        """Return this on CTCP VERSION requests."""
106 1
        return 'Weitersager'
107
108 1
    def on_nicknameinuse(self, conn, event) -> None:
109
        """Choose another nickname if conflicting."""
110
        self._nickname += '_'
111
        conn.nick(self._nickname)
112
113 1
    def on_join(self, conn, event) -> None:
114
        """Successfully joined channel."""
115 1
        joined_nick = event.source.nick
116 1
        channel_name = event.target
117
118 1
        if joined_nick == self._nickname:
119 1
            logger.info('Joined IRC channel: %s', channel_name)
120 1
            irc_channel_joined.send(channel_name=channel_name)
121
122 1
    def on_badchannelkey(self, conn, event) -> None:
123
        """Channel could not be joined due to wrong password."""
124
        channel_name = event.arguments[0]
125
        logger.warning('Cannot join channel %s (bad key).', channel_name)
126
127
128 1
def _create_bot(server: IrcServer, nickname: str, realname: str) -> Bot:
129
    """Create a bot."""
130 1
    server_spec = ServerSpec(server.host, server.port, server.password)
131 1
    factory = Factory(wrapper=ssl.wrap_socket) if server.ssl else Factory()
132
133 1
    bot = Bot([server_spec], nickname, realname, connect_factory=factory)
134
135 1
    _set_rate_limit(bot.connection, server.rate_limit)
136
137
    # Avoid `UnicodeDecodeError` on non-UTF-8 messages.
138 1
    bot.connection.buffer_class = LenientDecodingLineBuffer
139
140 1
    return bot
141
142
143 1
def _set_rate_limit(connection, rate_limit: float | None) -> None:
144
    """Set rate limit."""
145 1
    if rate_limit is not None:
146
        logger.info(
147
            'IRC send rate limit set to %.2f messages per second.',
148
            rate_limit,
149
        )
150
        connection.set_rate_limit(rate_limit)
151
    else:
152 1
        logger.info('No IRC send rate limit set.')
153
154
155 1
class DummyAnnouncer(Announcer):
156
    """An announcer that writes messages to STDOUT."""
157
158 1
    def __init__(self, channels: set[IrcChannel]) -> None:
159 1
        self.channels = channels
160
161 1
    def start(self) -> None:
162
        """Start the announcer."""
163
        # Fake channel joins.
164 1
        for channel in sorted(self.channels):
165 1
            irc_channel_joined.send(channel_name=channel.name)
166
167 1
    def announce(self, channel_name: str, text: str) -> None:
168
        """Announce a message."""
169
        logger.debug('%s> %s', channel_name, text)
170
171
172 1
def create_announcer(config: IrcConfig) -> Announcer:
173
    """Create an announcer."""
174 1
    if config.server is None:
175 1
        logger.info('No IRC server specified; will write to STDOUT instead.')
176 1
        return DummyAnnouncer(config.channels)
177
178 1
    return IrcAnnouncer(
179
        config.server,
180
        config.nickname,
181
        config.realname,
182
        config.commands,
183
        config.channels,
184
    )
185