Passed
Pull Request — master (#72)
by
unknown
04:03
created

irc_bot   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Test Coverage

Coverage 21.01%

Importance

Changes 0
Metric Value
eloc 156
dl 0
loc 249
ccs 29
cts 138
cp 0.2101
rs 8.72
c 0
b 0
f 0
wmc 46

13 Methods

Rating   Name   Duplication   Size   Complexity  
B IrcBot.readincoming() 0 22 6
A IrcBot.ircLogWriteToFile() 0 4 2
A IrcBot.checkIrcActions() 0 10 3
A IrcBot.__init__() 0 21 1
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 7 2
A IrcBot.ircLogAppend() 0 12 3
B IrcBot.decode_irc() 0 27 6
B IrcBot.connectToServer() 0 40 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
97
        msg = "PRIVMSG {CHANNEL} :{MSG}\r\n".format(CHANNEL=channel, MSG=message)
98
        self.sendMsg(msg)
99
100 1
    def sendMsg(self, msg):
101
        """Send and occasionally print the message sent"""
102
        LOG.debug("SEND: %s", msg.rstrip("\r\n"))
103
        self.SOCKET.send(msg.encode())
104
105 1
    def decode_irc(self, raw, preferred_encs=None):
106
        """
107
        Do character detection.
108
        You can send preferred encodings as a list through preferred_encs.
109
        http://stackoverflow.com/questions/938870/python-irc-bot-and-encoding-issue
110
        """
111
        if preferred_encs is None:
112
            preferred_encs = ["UTF-8", "CP1252", "ISO-8859-1"]
113
114
        changed = False
115
        enc = None
116
        for enc in preferred_encs:
117
            try:
118
                res = raw.decode(enc)
119
                changed = True
120
                break
121
            except Exception:
122
                pass
123
124
        if not changed:
125
            try:
126
                enc = chardet.detect(raw)['encoding']
127
                res = raw.decode(enc)
128
            except Exception:
129
                res = raw.decode(enc, 'ignore')
130
131
        return res
0 ignored issues
show
introduced by
The variable res does not seem to be defined in case the for loop on line 116 is not entered. Are you sure this can never be the case?
Loading history...
132
133 1
    def receive(self):
134
        """Read incoming message and guess encoding"""
135
        try:
136
            buf = self.SOCKET.recv(2048)
137
            lines = self.decode_irc(buf)
138
            lines = lines.split("\n")
139
            buf = lines.pop()
140
        except Exception as err:
141
            LOG.error("Error reading incoming message %s", err)
142
143
        return lines
0 ignored issues
show
introduced by
The variable lines does not seem to be defined for all execution paths.
Loading history...
144
145 1
    def ircLogAppend(self, line=None, user=None, message=None):
146
        """Read incoming message and guess encoding"""
147
        if not user:
148
            user = re.search(r"(?<=:)\w+", line[0]).group(0)
149
150
        if not message:
151
            message = ' '.join(line[3:]).lstrip(':')
152
153
        self.IRCLOG.append({
154
            'time': datetime.now().strftime("%H:%M").rjust(5),
155
            'user': user,
156
            'msg': message
157
        })
158
159 1
    def ircLogWriteToFile(self):
160
        """Write IRClog to file"""
161
        with open(self.CONFIG["irclogfile"], 'w', encoding="UTF-8") as f:
162
            json.dump(list(self.IRCLOG), f, indent=2)
163
164 1
    def readincoming(self):
165
        """
166
        Read all files in the directory incoming, send them as a message if
167
        they exists and then move the file to directory done.
168
        """
169
        if not os.path.isdir(self.CONFIG["dirIncoming"]):
170
            return
171
172
        listing = os.listdir(self.CONFIG["dirIncoming"])
173
174
        for infile in listing:
175
            filename = os.path.join(self.CONFIG["dirIncoming"], infile)
176
177
            with open(filename, "r", encoding="UTF-8") as f:
178
                for msg in f:
179
                    self.sendPrivMsg(msg, self.CONFIG["channel"])
180
181
            try:
182
                shutil.move(filename, self.CONFIG["dirDone"])
183
            except Exception:
184
                LOG.warning("Failed to move %s to %s. Deleting.", filename, self.CONFIG["dirDone"])
185
                os.remove(filename)
186
187 1
    def mainLoop(self):
188
        """For ever, listen and answer to incoming chats"""
189
        self.IRCLOG = deque([], self.CONFIG["irclogmax"])
190
191
        while 1:
192
            # Write irclog
193
            self.ircLogWriteToFile()
194
195
            # Check in any in the incoming directory
196
            self.readincoming()
197
198
            for line in self.receive():
199
                LOG.debug(line)
200
                words = line.strip().split()
201
202
                if not words:
203
                    continue
204
205
                self.checkIrcActions(words)
206
                self.checkMarvinActions(words)
207
208 1
    def begin(self):
209
        """Start the bot"""
210
        self.connectToServer()
211
        self.mainLoop()
212
213 1
    def checkIrcActions(self, words):
214
        """
215
        Check if Marvin should take action on any messages defined in the
216
        IRC protocol.
217
        """
218
        if words[0] == "PING":
219
            self.sendMsg("PONG {ARG}\r\n".format(ARG=words[1]))
220
221
        if words[1] == 'INVITE':
222
            self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=words[3]))
223
224 1
    def checkMarvinActions(self, words):
225
        """Check if Marvin should perform any actions"""
226
        if words[1] == 'PRIVMSG' and words[2] == self.CONFIG["channel"]:
227
            self.MSG_LOG.debug("%s <%s>  %s",
228
                               words[2],
229
                               words[0].split(":")[1].split("!")[0],
230
                               " ".join(words[3:]))
231
            self.ircLogAppend(words)
232
233
        if words[1] == 'PRIVMSG':
234
            raw = ' '.join(words[3:])
235
            row = self.tokenize(raw)
236
237
            if self.CONFIG["nick"] in row:
238
                for action in self.ACTIONS:
239
                    msg = action(row)
240
                    if msg:
241
                        self.sendPrivMsg(msg, words[2])
242
                        break
243
            else:
244
                for action in self.GENERAL_ACTIONS:
245
                    msg = action(row)
246
                    if msg:
247
                        self.sendPrivMsg(msg, words[2])
248
                        break
249