Passed
Push — main ( aae69a...152cdf )
by Jochen
02:03
created

syslog2irc.irc.Bot.on_nicknameinuse()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.2963

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 4
ccs 1
cts 3
cp 0.3333
rs 10
c 0
b 0
f 0
cc 1
nop 3
crap 1.2963
1
"""
2
syslog2irc.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 dataclasses import dataclass
12 1
import logging
13 1
from ssl import wrap_socket as ssl_wrap_socket
14 1
from typing import List, Optional, Set, Union
15
16 1
from irc.bot import ServerSpec, SingleServerIRCBot
17 1
from irc.connection import Factory
18
19 1
from .signals import irc_channel_joined
20 1
from .util import start_thread
21
22
23 1
logger = logging.getLogger(__name__)
24
25
26 1
@dataclass(frozen=True)
27
class IrcServer:
28
    """An IRC server."""
29
30 1
    host: str
31 1
    port: int = 6667
32 1
    ssl: bool = False
33 1
    password: Optional[str] = None
34 1
    rate_limit: Optional[float] = None
35
36
37 1
@dataclass(frozen=True, order=True)
38
class IrcChannel:
39
    """An IRC channel with optional password."""
40
41 1
    name: str
42 1
    password: Optional[str] = None
43
44
45 1
@dataclass(frozen=True)
46
class IrcConfig:
47
    """An IRC bot configuration."""
48
49 1
    server: Optional[IrcServer]
50 1
    nickname: str
51 1
    realname: str
52 1
    commands: List[str]
53 1
    channels: Set[IrcChannel]
54
55
56 1
class Bot(SingleServerIRCBot):
57
    """An IRC bot to forward messages to IRC channels."""
58
59 1
    def __init__(
60
        self,
61
        server: IrcServer,
62
        nickname: str,
63
        realname: str,
64
        commands: List[str],
65
        channels: Set[IrcChannel],
66
    ) -> None:
67 1
        logger.info(
68
            'Connecting to IRC server %s:%d ...', server.host, server.port
69
        )
70
71 1
        server_spec = ServerSpec(server.host, server.port, server.password)
72 1
        factory = Factory(wrapper=ssl_wrap_socket) if server.ssl else Factory()
73 1
        SingleServerIRCBot.__init__(
74
            self, [server_spec], nickname, realname, connect_factory=factory
75
        )
76
77 1
        if server.rate_limit is not None:
78
            logger.info(
79
                'IRC send rate limit set to %.2f messages per second.',
80
                server.rate_limit,
81
            )
82
            self.connection.set_rate_limit(server.rate_limit)
83
        else:
84 1
            logger.info('No IRC send rate limit set.')
85
86 1
        self.commands = commands
87
88
        # Note: `self.channels` already exists in super class.
89 1
        self.channels_to_join = channels
90
91 1
    def start(self) -> None:
92
        """Connect to the server, in a separate thread."""
93
        start_thread(super().start, self.__class__.__name__)
94
95 1
    def get_version(self) -> str:
96
        """Return this on CTCP VERSION requests."""
97 1
        return 'syslog2IRC'
98
99 1
    def on_welcome(self, conn, event) -> None:
100
        """Join channels after connect."""
101 1
        logger.info(
102
            'Connected to IRC server %s:%d.', *conn.socket.getpeername()
103
        )
104
105 1
        self._send_custom_commands_after_welcome(conn)
106 1
        self._join_channels(conn)
107
108 1
    def _send_custom_commands_after_welcome(self, conn):
109
        """Send custom commands after having been welcomed by the server."""
110 1
        for command in self.commands:
111
            conn.send_raw(command)
112
113 1
    def _join_channels(self, conn):
114
        """Join the configured channels."""
115 1
        channels = sorted(self.channels_to_join)
116 1
        logger.info('Channels to join: %s', ', '.join(c.name for c in channels))
117
118 1
        for channel in channels:
119 1
            logger.info('Joining channel %s ...', channel.name)
120 1
            conn.join(channel.name, channel.password or '')
121
122 1
    def on_nicknameinuse(self, conn, event) -> None:
123
        """Choose another nickname if conflicting."""
124
        self._nickname += '_'
125
        conn.nick(self._nickname)
126
127 1
    def on_join(self, conn, event) -> None:
128
        """Successfully joined channel."""
129 1
        joined_nick = event.source.nick
130 1
        channel_name = event.target
131
132 1
        if joined_nick == self._nickname:
133 1
            logger.info('Joined IRC channel: %s', channel_name)
134 1
            irc_channel_joined.send(channel_name=channel_name)
135
136 1
    def on_badchannelkey(self, conn, event) -> None:
137
        """Channel could not be joined due to wrong password."""
138
        channel_name = event.arguments[0]
139
        logger.info('Cannot join channel %s (bad key).', channel_name)
140
141 1
    def say(self, channel_name: str, text: str) -> None:
142
        """Say message on channel."""
143
        self.connection.privmsg(channel_name, text)
144
145
146 1
class DummyBot:
147
    """A fake bot that writes messages to STDOUT."""
148
149 1
    def __init__(self, channels: Set[IrcChannel]) -> None:
150 1
        self.channels = channels
151
152 1
    def start(self) -> None:
153
        # Fake channel joins.
154 1
        for channel in sorted(self.channels):
155 1
            irc_channel_joined.send(channel_name=channel.name)
156
157 1
    def say(self, channel_name: str, text: str) -> None:
158
        logger.info('%s> %s', channel_name, text)
159
160 1
    def disconnect(self, msg: str) -> None:
161
        # Mimics `irc.bot.SingleServerIRCBot.disconnect`.
162 1
        logger.info('Shutting down bot ...')
163
164
165 1
def create_bot(config: IrcConfig) -> Union[Bot, DummyBot]:
166
    """Create and return an IRC bot according to the configuration."""
167 1
    if config.server is None:
168 1
        logger.info('No IRC server specified; will write to STDOUT instead.')
169 1
        return DummyBot(config.channels)
170
171 1
    return Bot(
172
        config.server,
173
        config.nickname,
174
        config.realname,
175
        config.commands,
176
        config.channels,
177
    )
178