Passed
Push — main ( fedced...62aa48 )
by Jochen
01:57
created

syslog2irc.irc.DummyBot.disconnect()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
crap 1
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 Any, 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)
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
    channels: Set[IrcChannel]
53
54
55 1
class Bot(SingleServerIRCBot):
56
    """An IRC bot to forward messages to IRC channels."""
57
58 1
    def __init__(
59
        self,
60
        server: IrcServer,
61
        nickname: str,
62
        realname: str,
63
        channels: Set[IrcChannel],
64
    ) -> None:
65 1
        logger.info(
66
            'Connecting to IRC server %s:%d ...', server.host, server.port
67
        )
68
69 1
        server_spec = ServerSpec(server.host, server.port, server.password)
70 1
        factory = Factory(wrapper=ssl_wrap_socket) if server.ssl else Factory()
71 1
        SingleServerIRCBot.__init__(
72
            self, [server_spec], nickname, realname, connect_factory=factory
73
        )
74
75 1
        if server.rate_limit is not None:
76
            logger.info(
77
                'IRC send rate limit set to %.2f messages per second.',
78
                server.rate_limit,
79
            )
80
            self.connection.set_rate_limit(server.rate_limit)
81
        else:
82 1
            logger.info('No IRC send rate limit set.')
83
84
        # Note: `self.channels` already exists in super class.
85 1
        self.channels_to_join = channels
86
87 1
    def start(self) -> None:
88
        """Connect to the server, in a separate thread."""
89
        start_thread(super().start, self.__class__.__name__)
90
91 1
    def get_version(self) -> str:
92
        """Return this on CTCP VERSION requests."""
93 1
        return 'syslog2IRC'
94
95 1
    def on_welcome(self, conn, event) -> None:
96
        """Join channels after connect."""
97 1
        logger.info('Connected to %s:%d.', *conn.socket.getpeername())
98
99 1
        channels = _sort_channels_by_name(self.channels_to_join)
100 1
        logger.info('Channels to join: %s', ', '.join(c.name for c in channels))
101
102 1
        for channel in channels:
103 1
            logger.info('Joining channel %s ...', channel.name)
104 1
            conn.join(channel.name, channel.password or '')
105
106 1
    def on_nicknameinuse(self, conn, event) -> None:
107
        """Choose another nickname if conflicting."""
108
        self._nickname += '_'
109
        conn.nick(self._nickname)
110
111 1
    def on_join(self, conn, event) -> None:
112
        """Successfully joined channel."""
113 1
        joined_nick = event.source.nick
114 1
        channel_name = event.target
115
116 1
        if joined_nick == self._nickname:
117 1
            logger.info('Joined IRC channel: %s', channel_name)
118 1
            irc_channel_joined.send(channel_name=channel_name)
119
120 1
    def on_badchannelkey(self, conn, event) -> None:
121
        """Channel could not be joined due to wrong password."""
122
        channel_name = event.arguments[0]
123
        logger.info('Cannot join channel %s (bad key).', channel_name)
124
125 1
    def say(
126
        self,
127
        sender: Optional[Any],
128
        *,
129
        channel_name: Optional[str] = None,
130
        text: Optional[str] = None,
131
    ) -> None:
132
        """Say message on channel."""
133
        self.connection.privmsg(channel_name, text)
134
135
136 1
class DummyBot:
137
    """A fake bot that writes messages to STDOUT."""
138
139 1
    def __init__(self, channels: Set[IrcChannel]) -> None:
140 1
        self.channels = channels
141
142 1
    def start(self) -> None:
143
        # Fake channel joins.
144 1
        for channel in _sort_channels_by_name(self.channels):
145 1
            irc_channel_joined.send(channel_name=channel.name)
146
147 1
    def say(
148
        self,
149
        sender: Optional[Any],
150
        *,
151
        channel_name: Optional[str] = None,
152
        text: Optional[str] = None,
153
    ) -> None:
154
        logger.info('%s> %s', channel_name, text)
155
156 1
    def disconnect(self, msg: str) -> None:
157
        # Mimics `irc.bot.SingleServerIRCBot.disconnect`.
158 1
        logger.info('Shutting down bot ...')
159
160
161 1
def _sort_channels_by_name(channels: Set[IrcChannel]) -> List[IrcChannel]:
162 1
    return list(sorted(channels, key=lambda c: c.name))
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
    return Bot(config.server, config.nickname, config.realname, config.channels)
172