Passed
Push — main ( 3dc1e3...b0391b )
by Jochen
03:34
created

weitersager.irc   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Test Coverage

Coverage 85.71%

Importance

Changes 0
Metric Value
wmc 27
eloc 101
dl 0
loc 185
ccs 72
cts 84
cp 0.8571
rs 10
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A Announcer.start() 0 2 1
A Announcer.shutdown() 0 2 1
A Announcer.announce() 0 3 1
A IrcAnnouncer.start() 0 9 1
A IrcAnnouncer._join_channels() 0 8 2
A IrcAnnouncer._on_welcome() 0 8 1
A Bot.on_nicknameinuse() 0 4 1
A Bot.get_version() 0 3 1
A DummyAnnouncer.announce() 0 3 1
A IrcAnnouncer.announce() 0 3 1
A IrcAnnouncer._send_commands() 0 4 2
A Bot.on_badchannelkey() 0 4 1
A Bot.on_join() 0 8 2
A DummyAnnouncer.start() 0 5 2
A IrcAnnouncer.shutdown() 0 3 1
A IrcAnnouncer.__init__() 0 14 1
A DummyAnnouncer.__init__() 0 2 1

3 Functions

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