Passed
Push — main ( cda80a...4be128 )
by Jochen
02:12
created

weitersager.irc.Bot.__init__()   A

Complexity

Conditions 3

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.054

Importance

Changes 0
Metric Value
cc 3
eloc 20
nop 5
dl 0
loc 31
rs 9.4
c 0
b 0
f 0
ccs 9
cts 11
cp 0.8182
crap 3.054
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
import logging
12 1
import ssl
13 1
from typing import Set, Union
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 Bot(SingleServerIRCBot):
28
    """An IRC bot to forward messages to IRC channels."""
29
30 1
    def __init__(
31
        self,
32
        server: IrcServer,
33
        nickname: str,
34
        realname: str,
35
        channels: Set[IrcChannel],
36
    ) -> None:
37 1
        logger.info(
38
            'Connecting to IRC server %s:%d ...', server.host, server.port
39
        )
40
41 1
        server_spec = ServerSpec(server.host, server.port, server.password)
42 1
        factory = Factory(wrapper=ssl.wrap_socket) if server.ssl else Factory()
43 1
        SingleServerIRCBot.__init__(
44
            self, [server_spec], nickname, realname, connect_factory=factory
45
        )
46
47 1
        if server.rate_limit is not None:
48
            logger.info(
49
                'IRC send rate limit set to %.2f messages per second.',
50
                server.rate_limit,
51
            )
52
            self.connection.set_rate_limit(server.rate_limit)
53
        else:
54 1
            logger.info('No IRC send rate limit set.')
55
56
        # Avoid `UnicodeDecodeError` on non-UTF-8 messages.
57 1
        self.connection.buffer_class = LenientDecodingLineBuffer
58
59
        # Note: `self.channels` already exists in super class.
60 1
        self.channels_to_join = channels
61
62 1
    def start(self) -> None:
63
        """Connect to the server, in a separate thread."""
64
        start_thread(super().start, self.__class__.__name__)
65
66 1
    def get_version(self) -> str:
67
        """Return this on CTCP VERSION requests."""
68 1
        return 'Weitersager'
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
        channels = sorted(self.channels_to_join)
77 1
        logger.info('Channels to join: %s', ', '.join(c.name for c in channels))
78
79 1
        for channel in channels:
80 1
            logger.info('Joining channel %s ...', channel.name)
81 1
            conn.join(channel.name, channel.password or '')
82
83 1
    def on_nicknameinuse(self, conn, event) -> None:
84
        """Choose another nickname if conflicting."""
85
        self._nickname += '_'
86
        conn.nick(self._nickname)
87
88 1
    def on_join(self, conn, event) -> None:
89
        """Successfully joined channel."""
90 1
        joined_nick = event.source.nick
91 1
        channel_name = event.target
92
93 1
        if joined_nick == self._nickname:
94 1
            logger.info('Joined IRC channel: %s', channel_name)
95 1
            irc_channel_joined.send(channel_name=channel_name)
96
97 1
    def on_badchannelkey(self, conn, event) -> None:
98
        """Channel could not be joined due to wrong password."""
99
        channel_name = event.arguments[0]
100
        logger.warning('Cannot join channel %s (bad key).', channel_name)
101
102 1
    def say(self, channel_name: str, text: str) -> None:
103
        """Say message on channel."""
104
        self.connection.privmsg(channel_name, text)
105
106
107 1
class DummyBot:
108
    """A fake bot that writes messages to STDOUT."""
109
110 1
    def __init__(self, channels: Set[IrcChannel]) -> None:
111 1
        self.channels = channels
112
113 1
    def start(self) -> None:
114
        # Fake channel joins.
115 1
        for channel in sorted(self.channels):
116 1
            irc_channel_joined.send(channel_name=channel.name)
117
118 1
    def say(self, channel_name: str, text: str) -> None:
119
        logger.debug('%s> %s', channel_name, text)
120
121 1
    def disconnect(self, msg: str) -> None:
122
        # Mimics `irc.bot.SingleServerIRCBot.disconnect`.
123 1
        logger.info('Shutting down bot ...')
124
125
126 1
def create_bot(config: IrcConfig) -> Union[Bot, DummyBot]:
127
    """Create and return an IRC bot according to the configuration."""
128 1
    if config.server is None:
129 1
        logger.info('No IRC server specified; will write to STDOUT instead.')
130 1
        return DummyBot(config.channels)
131
132
    return Bot(config.server, config.nickname, config.realname, config.channels)
133