Passed
Push — main ( 281afb...25807a )
by Jochen
01:30
created

weitersager.irc.create_bot()   A

Complexity

Conditions 3

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 9.561

Importance

Changes 0
Metric Value
cc 3
eloc 15
nop 2
dl 0
loc 20
ccs 1
cts 10
cp 0.1
crap 9.561
rs 9.65
c 0
b 0
f 0
1
"""
2
weitersager.irc
3
~~~~~~~~~~~~~~~
4
5
Internet Relay Chat
6
7
:Copyright: 2007-2020 Jochen Kupperschmidt
8
:License: MIT, see LICENSE for details.
9
"""
10
11 1
from irc.bot import ServerSpec, SingleServerIRCBot
12 1
from jaraco.stream.buffer import LenientDecodingLineBuffer
13
14 1
from .signals import channel_joined, shutdown_requested
15 1
from .util import log, start_thread
16
17
18 1
class Bot(SingleServerIRCBot):
19
    """An IRC bot to forward messages to IRC channels."""
20
21 1
    def __init__(
22
        self,
23
        server,
24
        nickname,
25
        realname,
26
        channels,
27
        *,
28
        shutdown_predicate=None,
29
    ):
30
        log('Connecting to IRC server {0.host}:{0.port:d} ...', server)
31
32
        server_spec = ServerSpec(server.host, server.port, server.password)
33
        SingleServerIRCBot.__init__(self, [server_spec], nickname, realname)
34
35
        if server.rate_limit is not None:
36
            self.connection.set_rate_limit(server.rate_limit)
37
38
        # Avoid `UnicodeDecodeError` on non-UTF-8 messages.
39
        self.connection.buffer_class = LenientDecodingLineBuffer
40
41
        # Note: `self.channels` already exists in super class.
42
        self.channels_to_join = channels
43
44
        self.shutdown_predicate = shutdown_predicate
45
46 1
    def start(self):
47
        """Connect to the server, in a separate thread."""
48
        start_thread(super().start, self.__class__.__name__)
49
50 1
    def get_version(self):
51
        return 'Weitersager'
52
53 1
    def on_welcome(self, conn, event):
54
        """Join channels after connect."""
55
        log('Connected to {}:{:d}.', *conn.socket.getpeername())
56
57
        channel_names = sorted(c.name for c in self.channels_to_join)
58
        log('Channels to join: {}', ', '.join(channel_names))
59
60
        for channel in self.channels_to_join:
61
            log('Joining channel {} ...', channel.name)
62
            conn.join(channel.name, channel.password or '')
63
64 1
    def on_nicknameinuse(self, conn, event):
65
        """Choose another nickname if conflicting."""
66
        self._nickname += '_'
67
        conn.nick(self._nickname)
68
69 1
    def on_join(self, conn, event):
70
        """Successfully joined channel."""
71
        joined_nick = event.source.nick
72
        channel_name = event.target
73
74
        if joined_nick == self._nickname:
75
            log('Joined IRC channel: {}', channel_name)
76
            channel_joined.send(channel_name=channel_name)
77
78 1
    def on_badchannelkey(self, conn, event):
79
        """Channel could not be joined due to wrong password."""
80
        channel = event.arguments[0]
81
        log('Cannot join channel {} (bad key).', channel)
82
83 1
    def on_privmsg(self, conn, event):
84
        """React on private messages."""
85
        nickmask = event.source
86
        text = event.arguments[0]
87
        if self.shutdown_predicate and self.shutdown_predicate(nickmask, text):
88
            self.shutdown(nickmask)
89
90 1
    def shutdown(self, nickmask):
91
        """Shut the bot down."""
92
        log('Shutdown requested by {}.', nickmask)
93
        shutdown_requested.send()
94
        self.die('Shutting down.')  # Joins IRC bot thread.
95
96 1
    def say(self, sender, *, channel_name=None, text=None):
97
        """Say message on channel."""
98
        self.connection.privmsg(channel_name, text)
99
100
101 1
class DummyBot:
102
103 1
    def __init__(self, server, nickname, realname, channels, **options):
104
        self.channels = channels
105
106 1
    def start(self):
107
        # Fake channel joins.
108
        for channel in self.channels:
109
            channel_joined.send(channel_name=channel.name)
110
111 1
    def say(self, sender, *, channel_name=None, text=None):
112
        log('{}> {}', channel_name, text)
113
114
115 1
def create_bot(config, **options):
116
    """Create and return an IRC bot according to the configuration."""
117
    if config.server:
118
        bot_class = Bot
119
120
        rate_limit = config.server.rate_limit
121
        if rate_limit is not None:
122
            log('IRC send rate limit set to {:.2f} messages per second.', rate_limit)
123
        else:
124
            log('No IRC send rate limit set.')
125
    else:
126
        log('No IRC server specified; will write to STDOUT instead.')
127
        bot_class = DummyBot
128
129
    return bot_class(
130
        config.server,
131
        config.nickname,
132
        config.realname,
133
        config.channels,
134
        **options,
135
    )
136
137
138 1
def default_shutdown_predicate(nickmask, text):
139
    """Determine if this is a valid shutdown request."""
140
    return text == 'shutdown!'
141