Passed
Push — main ( 4be128...37807b )
by Jochen
01:52
created

Bot._send_custom_commands_after_welcome()   A

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.1481

Importance

Changes 0
Metric Value
cc 2
eloc 3
nop 2
dl 0
loc 4
ccs 2
cts 3
cp 0.6667
crap 2.1481
rs 10
c 0
b 0
f 0
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 List, 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
        commands: List[str],
36
        channels: Set[IrcChannel],
37
    ) -> None:
38 1
        logger.info(
39
            'Connecting to IRC server %s:%d ...', server.host, server.port
40
        )
41
42 1
        server_spec = ServerSpec(server.host, server.port, server.password)
43 1
        factory = Factory(wrapper=ssl.wrap_socket) if server.ssl else Factory()
44 1
        SingleServerIRCBot.__init__(
45
            self, [server_spec], nickname, realname, connect_factory=factory
46
        )
47
48 1
        if server.rate_limit is not None:
49
            logger.info(
50
                'IRC send rate limit set to %.2f messages per second.',
51
                server.rate_limit,
52
            )
53
            self.connection.set_rate_limit(server.rate_limit)
54
        else:
55 1
            logger.info('No IRC send rate limit set.')
56
57 1
        self.commands = commands
58
59
        # Avoid `UnicodeDecodeError` on non-UTF-8 messages.
60 1
        self.connection.buffer_class = LenientDecodingLineBuffer
61
62
        # Note: `self.channels` already exists in super class.
63 1
        self.channels_to_join = channels
64
65 1
    def start(self) -> None:
66
        """Connect to the server, in a separate thread."""
67
        start_thread(super().start, self.__class__.__name__)
68
69 1
    def get_version(self) -> str:
70
        """Return this on CTCP VERSION requests."""
71 1
        return 'Weitersager'
72
73 1
    def on_welcome(self, conn, event) -> None:
74
        """Join channels after connect."""
75 1
        logger.info(
76
            'Connected to IRC server %s:%d.', *conn.socket.getpeername()
77
        )
78
79 1
        self._send_custom_commands_after_welcome(conn)
80 1
        self._join_channels(conn)
81
82 1
    def _send_custom_commands_after_welcome(self, conn):
83
        """Send custom commands after having been welcomed by the server."""
84 1
        for command in self.commands:
85
            conn.send_raw(command)
86
87 1
    def _join_channels(self, conn):
88
        """Join the configured channels."""
89 1
        channels = sorted(self.channels_to_join)
90 1
        logger.info('Channels to join: %s', ', '.join(c.name for c in channels))
91
92 1
        for channel in channels:
93 1
            logger.info('Joining channel %s ...', channel.name)
94 1
            conn.join(channel.name, channel.password or '')
95
96 1
    def on_nicknameinuse(self, conn, event) -> None:
97
        """Choose another nickname if conflicting."""
98
        self._nickname += '_'
99
        conn.nick(self._nickname)
100
101 1
    def on_join(self, conn, event) -> None:
102
        """Successfully joined channel."""
103 1
        joined_nick = event.source.nick
104 1
        channel_name = event.target
105
106 1
        if joined_nick == self._nickname:
107 1
            logger.info('Joined IRC channel: %s', channel_name)
108 1
            irc_channel_joined.send(channel_name=channel_name)
109
110 1
    def on_badchannelkey(self, conn, event) -> None:
111
        """Channel could not be joined due to wrong password."""
112
        channel_name = event.arguments[0]
113
        logger.warning('Cannot join channel %s (bad key).', channel_name)
114
115 1
    def say(self, channel_name: str, text: str) -> None:
116
        """Say message on channel."""
117
        self.connection.privmsg(channel_name, text)
118
119
120 1
class DummyBot:
121
    """A fake bot that writes messages to STDOUT."""
122
123 1
    def __init__(self, channels: Set[IrcChannel]) -> None:
124 1
        self.channels = channels
125
126 1
    def start(self) -> None:
127
        # Fake channel joins.
128 1
        for channel in sorted(self.channels):
129 1
            irc_channel_joined.send(channel_name=channel.name)
130
131 1
    def say(self, channel_name: str, text: str) -> None:
132
        logger.debug('%s> %s', channel_name, text)
133
134 1
    def disconnect(self, msg: str) -> None:
135
        # Mimics `irc.bot.SingleServerIRCBot.disconnect`.
136 1
        logger.info('Shutting down bot ...')
137
138
139 1
def create_bot(config: IrcConfig) -> Union[Bot, DummyBot]:
140
    """Create and return an IRC bot according to the configuration."""
141 1
    if config.server is None:
142 1
        logger.info('No IRC server specified; will write to STDOUT instead.')
143 1
        return DummyBot(config.channels)
144
145 1
    return Bot(
146
        config.server,
147
        config.nickname,
148
        config.realname,
149
        config.commands,
150
        config.channels,
151
    )
152