Passed
Push — main ( 392e9b...3378a3 )
by Jochen
04:26
created

weitersager.irc.create_announcer()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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