Completed
Push — main ( 963be5...3918f7 )
by Jochen
04:19
created

weitersager.irc.Channel.__new__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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