Passed
Pull Request — master (#73)
by
unknown
03:11
created

irc_bot   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Test Coverage

Coverage 20.86%

Importance

Changes 0
Metric Value
eloc 157
dl 0
loc 250
ccs 29
cts 139
cp 0.2086
rs 8.72
c 0
b 0
f 0
wmc 46

13 Methods

Rating   Name   Duplication   Size   Complexity  
A IrcBot.__init__() 0 21 1
B IrcBot.connectToServer() 0 40 6
B IrcBot.readincoming() 0 22 6
A IrcBot.ircLogWriteToFile() 0 4 2
A IrcBot.checkIrcActions() 0 10 3
A IrcBot.sendMsg() 0 4 1
A IrcBot.mainLoop() 0 20 4
A IrcBot.receive() 0 11 2
C IrcBot.checkMarvinActions() 0 25 9
A IrcBot.sendPrivMsg() 0 8 2
A IrcBot.ircLogAppend() 0 12 3
B IrcBot.decode_irc() 0 27 6
A IrcBot.begin() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like irc_bot often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#! /usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
4
"""
5
Module for the IRC bot.
6
7
Connecting, sending and receiving messages and doing custom actions.
8
9
Keeping a log and reading incoming material.
10
"""
11 1
from collections import deque
12 1
from datetime import datetime
13 1
import json
14 1
import logging
15 1
import os
16 1
import re
17 1
import shutil
18 1
import socket
19
20 1
import chardet
21
22 1
from bot import Bot
23
24 1
LOG = logging.getLogger("bot")
25
26 1
class IrcBot(Bot):
27
    """Bot implementing the IRC protocol"""
28 1
    def __init__(self):
29 1
        super().__init__()
30 1
        self.CONFIG = {
31
            "server": None,
32
            "port": 6667,
33
            "channel": None,
34
            "nick": "marvin",
35
            "realname": "Marvin The All Mighty dbwebb-bot",
36
            "ident": None,
37
            "irclogfile": "irclog.txt",
38
            "irclogmax": 20,
39
            "dirIncoming": "incoming",
40
            "dirDone": "done",
41
            "lastfm": None,
42
        }
43
44
        # Socket for IRC server
45 1
        self.SOCKET = None
46
47
        # Keep a log of the latest messages
48 1
        self.IRCLOG = None
49
50
51 1
    def connectToServer(self):
52
        """Connect to the IRC Server"""
53
54
        # Create the socket  & Connect to the server
55
        server = self.CONFIG["server"]
56
        port = self.CONFIG["port"]
57
58
        if server and port:
59
            self.SOCKET = socket.socket()
60
            LOG.info("Connecting: %s:%d", server, port)
61
            self.SOCKET.connect((server, port))
62
        else:
63
            LOG.error("Failed to connect, missing server or port in configuration.")
64
            return
65
66
        # Send the nick to server
67
        nick = self.CONFIG["nick"]
68
        if nick:
69
            msg = 'NICK {NICK}\r\n'.format(NICK=nick)
70
            self.sendMsg(msg)
71
        else:
72
            LOG.info("Ignore sending nick, missing nick in configuration.")
73
74
        # Present yourself
75
        realname = self.CONFIG["realname"]
76
        self.sendMsg('USER  {NICK} 0 * :{REALNAME}\r\n'.format(NICK=nick, REALNAME=realname))
77
78
        # This is my nick, i promise!
79
        ident = self.CONFIG["ident"]
80
        if ident:
81
            self.sendMsg('PRIVMSG nick IDENTIFY {IDENT}\r\n'.format(IDENT=ident))
82
        else:
83
            LOG.info("Ignore identifying with password, ident is not set.")
84
85
        # Join a channel
86
        channel = self.CONFIG["channel"]
87
        if channel:
88
            self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=channel))
89
        else:
90
            LOG.info("Ignore joining channel, missing channel name in configuration.")
91
92 1
    def sendPrivMsg(self, message, channel):
93
        """Send and log a PRIV message"""
94
        if channel == self.CONFIG["channel"]:
95
            self.ircLogAppend(user=self.CONFIG["nick"].ljust(8), message=message)
96
            self.MSG_LOG.debug("%s <%s>  %s", channel, self.CONFIG["nick"], message)
97
98
        msg = "PRIVMSG {CHANNEL} :{MSG}\r\n".format(CHANNEL=channel, MSG=message)
99
        self.sendMsg(msg)
100
101 1
    def sendMsg(self, msg):
102
        """Send and occasionally print the message sent"""
103
        LOG.debug("SEND: %s", msg.rstrip("\r\n"))
104
        self.SOCKET.send(msg.encode())
105
106 1
    def decode_irc(self, raw, preferred_encs=None):
107
        """
108
        Do character detection.
109
        You can send preferred encodings as a list through preferred_encs.
110
        http://stackoverflow.com/questions/938870/python-irc-bot-and-encoding-issue
111
        """
112
        if preferred_encs is None:
113
            preferred_encs = ["UTF-8", "CP1252", "ISO-8859-1"]
114
115
        changed = False
116
        enc = None
117
        for enc in preferred_encs:
118
            try:
119
                res = raw.decode(enc)
120
                changed = True
121
                break
122
            except Exception:
123
                pass
124
125
        if not changed:
126
            try:
127
                enc = chardet.detect(raw)['encoding']
128
                res = raw.decode(enc)
129
            except Exception:
130
                res = raw.decode(enc, 'ignore')
131
132
        return res
0 ignored issues
show
introduced by
The variable res does not seem to be defined in case the for loop on line 117 is not entered. Are you sure this can never be the case?
Loading history...
133
134 1
    def receive(self):
135
        """Read incoming message and guess encoding"""
136
        try:
137
            buf = self.SOCKET.recv(2048)
138
            lines = self.decode_irc(buf)
139
            lines = lines.split("\n")
140
            buf = lines.pop()
141
        except Exception as err:
142
            LOG.error("Error reading incoming message %s", err)
143
144
        return lines
0 ignored issues
show
introduced by
The variable lines does not seem to be defined for all execution paths.
Loading history...
145
146 1
    def ircLogAppend(self, line=None, user=None, message=None):
147
        """Read incoming message and guess encoding"""
148
        if not user:
149
            user = re.search(r"(?<=:)\w+", line[0]).group(0)
150
151
        if not message:
152
            message = ' '.join(line[3:]).lstrip(':')
153
154
        self.IRCLOG.append({
155
            'time': datetime.now().strftime("%H:%M").rjust(5),
156
            'user': user,
157
            'msg': message
158
        })
159
160 1
    def ircLogWriteToFile(self):
161
        """Write IRClog to file"""
162
        with open(self.CONFIG["irclogfile"], 'w', encoding="UTF-8") as f:
163
            json.dump(list(self.IRCLOG), f, indent=2)
164
165 1
    def readincoming(self):
166
        """
167
        Read all files in the directory incoming, send them as a message if
168
        they exists and then move the file to directory done.
169
        """
170
        if not os.path.isdir(self.CONFIG["dirIncoming"]):
171
            return
172
173
        listing = os.listdir(self.CONFIG["dirIncoming"])
174
175
        for infile in listing:
176
            filename = os.path.join(self.CONFIG["dirIncoming"], infile)
177
178
            with open(filename, "r", encoding="UTF-8") as f:
179
                for msg in f:
180
                    self.sendPrivMsg(msg, self.CONFIG["channel"])
181
182
            try:
183
                shutil.move(filename, self.CONFIG["dirDone"])
184
            except Exception:
185
                LOG.warning("Failed to move %s to %s. Deleting.", filename, self.CONFIG["dirDone"])
186
                os.remove(filename)
187
188 1
    def mainLoop(self):
189
        """For ever, listen and answer to incoming chats"""
190
        self.IRCLOG = deque([], self.CONFIG["irclogmax"])
191
192
        while 1:
193
            # Write irclog
194
            self.ircLogWriteToFile()
195
196
            # Check in any in the incoming directory
197
            self.readincoming()
198
199
            for line in self.receive():
200
                LOG.debug(line)
201
                words = line.strip().split()
202
203
                if not words:
204
                    continue
205
206
                self.checkIrcActions(words)
207
                self.checkMarvinActions(words)
208
209 1
    def begin(self):
210
        """Start the bot"""
211
        self.connectToServer()
212
        self.mainLoop()
213
214 1
    def checkIrcActions(self, words):
215
        """
216
        Check if Marvin should take action on any messages defined in the
217
        IRC protocol.
218
        """
219
        if words[0] == "PING":
220
            self.sendMsg("PONG {ARG}\r\n".format(ARG=words[1]))
221
222
        if words[1] == 'INVITE':
223
            self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=words[3]))
224
225 1
    def checkMarvinActions(self, words):
226
        """Check if Marvin should perform any actions"""
227
        if words[1] == 'PRIVMSG' and words[2] == self.CONFIG["channel"]:
228
            self.MSG_LOG.debug("%s <%s>  %s",
229
                               words[2],
230
                               words[0].split(":")[1].split("!")[0],
231
                               " ".join(words[3:]))
232
            self.ircLogAppend(words)
233
234
        if words[1] == 'PRIVMSG':
235
            raw = ' '.join(words[3:])
236
            row = self.tokenize(raw)
237
238
            if self.CONFIG["nick"] in row:
239
                for action in self.ACTIONS:
240
                    msg = action(row)
241
                    if msg:
242
                        self.sendPrivMsg(msg, words[2])
243
                        break
244
            else:
245
                for action in self.GENERAL_ACTIONS:
246
                    msg = action(row)
247
                    if msg:
248
                        self.sendPrivMsg(msg, words[2])
249
                        break
250