Passed
Push — main ( 37807b...392e9b )
by Jochen
04:24
created

weitersager.irc   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 152
Duplicated Lines 0 %

Test Coverage

Coverage 85.71%

Importance

Changes 0
Metric Value
wmc 22
eloc 87
dl 0
loc 152
ccs 60
cts 70
cp 0.8571
rs 10
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A Bot.start() 0 3 1
A Bot.on_nicknameinuse() 0 4 1
A Bot.get_version() 0 3 1
A DummyBot.__init__() 0 2 1
A DummyBot.start() 0 4 2
A Bot.__init__() 0 34 3
A Bot.on_welcome() 0 8 1
A DummyBot.disconnect() 0 3 1
A Bot.on_badchannelkey() 0 4 1
A Bot.on_join() 0 8 2
A Bot._send_custom_commands_after_welcome() 0 4 2
A DummyBot.say() 0 2 1
A Bot.say() 0 3 1
A Bot._join_channels() 0 8 2

1 Function

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