Completed
Push — main ( 25807a...2ed45a )
by Jochen
01:37
created

weitersager.irc   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 151
Duplicated Lines 0 %

Test Coverage

Coverage 34.33%

Importance

Changes 0
Metric Value
eloc 89
dl 0
loc 151
ccs 23
cts 67
cp 0.3433
rs 10
c 0
b 0
f 0
wmc 22

13 Methods

Rating   Name   Duplication   Size   Complexity  
A Bot.start() 0 3 1
A Bot.on_nicknameinuse() 0 4 1
A Bot.get_version() 0 2 1
A DummyBot.__init__() 0 2 1
A DummyBot.start() 0 4 2
A Bot.__init__() 0 30 2
A Bot.on_welcome() 0 10 2
A Bot.on_privmsg() 0 6 3
A Bot.shutdown() 0 5 1
A Bot.on_badchannelkey() 0 4 1
A Bot.on_join() 0 8 2
A DummyBot.say() 0 4 1
A Bot.say() 0 9 1

2 Functions

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