Passed
Push — master ( 1b94f8...858664 )
by Mikael
04:13 queued 10s
created

irc_bot   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Test Coverage

Coverage 18.18%

Importance

Changes 0
Metric Value
eloc 134
dl 0
loc 214
ccs 22
cts 121
cp 0.1818
rs 9.1199
c 0
b 0
f 0
wmc 41

11 Methods

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