irc_bot

Module for the IRC bot.

Connecting, sending and receiving messages and doing custom actions.

Keeping a log and reading incoming material.

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

Bot implementing the IRC protocol

CONFIG
SOCKET
def connectToServer(self):
42    def connectToServer(self):
43        """Connect to the IRC Server"""
44
45        # Create the socket  & Connect to the server
46        server = self.CONFIG["server"]
47        port = self.CONFIG["port"]
48
49        if server and port:
50            self.SOCKET = socket.socket()
51            LOG.info("Connecting: %s:%d", server, port)
52            self.SOCKET.connect((server, port))
53        else:
54            LOG.error("Failed to connect, missing server or port in configuration.")
55            return
56
57        # Send the nick to server
58        nick = self.CONFIG["nick"]
59        if nick:
60            msg = 'NICK {NICK}\r\n'.format(NICK=nick)
61            self.sendMsg(msg)
62        else:
63            LOG.info("Ignore sending nick, missing nick in configuration.")
64
65        # Present yourself
66        realname = self.CONFIG["realname"]
67        self.sendMsg('USER  {NICK} 0 * :{REALNAME}\r\n'.format(NICK=nick, REALNAME=realname))
68
69        # This is my nick, i promise!
70        ident = self.CONFIG["ident"]
71        if ident:
72            self.sendMsg('PRIVMSG nick IDENTIFY {IDENT}\r\n'.format(IDENT=ident))
73        else:
74            LOG.info("Ignore identifying with password, ident is not set.")
75
76        # Join a channel
77        channel = self.CONFIG["channel"]
78        if channel:
79            self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=channel))
80        else:
81            LOG.info("Ignore joining channel, missing channel name in configuration.")

Connect to the IRC Server

def sendPrivMsg(self, message, channel):
83    def sendPrivMsg(self, message, channel):
84        """Send and log a PRIV message"""
85        if channel == self.CONFIG["channel"]:
86            self.MSG_LOG.debug("%s <%s>  %s", channel, self.CONFIG["nick"], message)
87
88        msg = "PRIVMSG {CHANNEL} :{MSG}\r\n".format(CHANNEL=channel, MSG=message)
89        self.sendMsg(msg)

Send and log a PRIV message

def sendMsg(self, msg):
91    def sendMsg(self, msg):
92        """Send and occasionally print the message sent"""
93        LOG.debug("SEND: %s", msg.rstrip("\r\n"))
94        self.SOCKET.send(msg.encode())

Send and occasionally print the message sent

def decode_irc(self, raw, preferred_encs=None):
 96    def decode_irc(self, raw, preferred_encs=None):
 97        """
 98        Do character detection.
 99        You can send preferred encodings as a list through preferred_encs.
100        http://stackoverflow.com/questions/938870/python-irc-bot-and-encoding-issue
101        """
102        if preferred_encs is None:
103            preferred_encs = ["UTF-8", "CP1252", "ISO-8859-1"]
104
105        changed = False
106        enc = None
107        for enc in preferred_encs:
108            try:
109                res = raw.decode(enc)
110                changed = True
111                break
112            except Exception:
113                pass
114
115        if not changed:
116            try:
117                enc = chardet.detect(raw)['encoding']
118                res = raw.decode(enc)
119            except Exception:
120                res = raw.decode(enc, 'ignore')
121
122        return res

Do character detection. You can send preferred encodings as a list through preferred_encs. http://stackoverflow.com/questions/938870/python-irc-bot-and-encoding-issue

def receive(self):
124    def receive(self):
125        """Read incoming message and guess encoding"""
126        try:
127            buf = self.SOCKET.recv(2048)
128            lines = self.decode_irc(buf)
129            lines = lines.split("\n")
130            buf = lines.pop()
131        except Exception as err:
132            LOG.error("Error reading incoming message %s", err)
133
134        return lines

Read incoming message and guess encoding

def readincoming(self):
136    def readincoming(self):
137        """
138        Read all files in the directory incoming, send them as a message if
139        they exists and then move the file to directory done.
140        """
141        if not os.path.isdir(self.CONFIG["dirIncoming"]):
142            return
143
144        listing = os.listdir(self.CONFIG["dirIncoming"])
145
146        for infile in listing:
147            filename = os.path.join(self.CONFIG["dirIncoming"], infile)
148
149            with open(filename, "r", encoding="UTF-8") as f:
150                for msg in f:
151                    self.sendPrivMsg(msg, self.CONFIG["channel"])
152
153            try:
154                shutil.move(filename, self.CONFIG["dirDone"])
155            except Exception:
156                LOG.warning("Failed to move %s to %s. Deleting.", filename, self.CONFIG["dirDone"])
157                os.remove(filename)

Read all files in the directory incoming, send them as a message if they exists and then move the file to directory done.

def mainLoop(self):
159    def mainLoop(self):
160        """For ever, listen and answer to incoming chats"""
161        while 1:
162            # Check in any in the incoming directory
163            self.readincoming()
164
165            for line in self.receive():
166                LOG.debug(line)
167                words = line.strip().split()
168
169                if not words:
170                    continue
171
172                self.checkIrcActions(words)
173                self.checkMarvinActions(words)

For ever, listen and answer to incoming chats

def begin(self):
175    def begin(self):
176        """Start the bot"""
177        self.connectToServer()
178        self.mainLoop()

Start the bot

def checkIrcActions(self, words):
180    def checkIrcActions(self, words):
181        """
182        Check if Marvin should take action on any messages defined in the
183        IRC protocol.
184        """
185        if words[0] == "PING":
186            self.sendMsg("PONG {ARG}\r\n".format(ARG=words[1]))
187
188        if words[1] == 'INVITE':
189            self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=words[3]))

Check if Marvin should take action on any messages defined in the IRC protocol.

def checkMarvinActions(self, words):
191    def checkMarvinActions(self, words):
192        """Check if Marvin should perform any actions"""
193        if words[1] == 'PRIVMSG' and words[2] == self.CONFIG["channel"]:
194            self.MSG_LOG.debug("%s <%s>  %s",
195                               words[2],
196                               words[0].split(":")[1].split("!")[0],
197                               " ".join(words[3:]))
198
199        if words[1] == 'PRIVMSG':
200            raw = ' '.join(words[3:])
201            row = self.tokenize(raw)
202
203            if self.CONFIG["nick"] in row:
204                for action in self.ACTIONS:
205                    msg = action(row)
206                    if msg:
207                        self.sendPrivMsg(msg, words[2])
208                        break
209            else:
210                for action in self.GENERAL_ACTIONS:
211                    msg = action(row)
212                    if msg:
213                        self.sendPrivMsg(msg, words[2])
214                        break

Check if Marvin should perform any actions