Completed
Push — main ( 49d37f...de2ca0 )
by Jochen
01:40
created

weitersager.irc.create_bot()   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.3145

Importance

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