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 = f'NICK {nick}\r\n'
 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(f'USER  {nick} 0 * :{realname}\r\n')
 67
 68        # This is my nick, i promise!
 69        ident = self.CONFIG["ident"]
 70        if ident:
 71            self.sendMsg(f'PRIVMSG nick IDENTIFY {ident}\r\n')
 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(f'JOIN {channel}\r\n')
 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 = f"PRIVMSG {channel} :{message}\r\n"
 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        res = 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        lines = None
127        try:
128            buf = self.SOCKET.recv(2048)
129            lines = self.decode_irc(buf)
130            lines = lines.split("\n")
131            buf = lines.pop()
132        except Exception as err:
133            LOG.error("Error reading incoming message %s", err)
134
135        return lines
136
137    def readincoming(self):
138        """
139        Read all files in the directory incoming, send them as a message if
140        they exists and then move the file to directory done.
141        """
142        if not os.path.isdir(self.CONFIG["dirIncoming"]):
143            return
144
145        listing = os.listdir(self.CONFIG["dirIncoming"])
146
147        for infile in listing:
148            filename = os.path.join(self.CONFIG["dirIncoming"], infile)
149
150            with open(filename, "r", encoding="UTF-8") as f:
151                for msg in f:
152                    self.sendPrivMsg(msg, self.CONFIG["channel"])
153
154            try:
155                shutil.move(filename, self.CONFIG["dirDone"])
156            except Exception:
157                LOG.warning("Failed to move %s to %s. Deleting.", filename, self.CONFIG["dirDone"])
158                os.remove(filename)
159
160    def mainLoop(self):
161        """For ever, listen and answer to incoming chats"""
162        while 1:
163            # Check in any in the incoming directory
164            self.readincoming()
165
166            for line in self.receive():
167                LOG.debug(line)
168                words = line.strip().split()
169
170                if not words:
171                    continue
172
173                self.checkIrcActions(words)
174                self.checkMarvinActions(words)
175
176    def begin(self):
177        """Start the bot"""
178        self.connectToServer()
179        self.mainLoop()
180
181    def checkIrcActions(self, words):
182        """
183        Check if Marvin should take action on any messages defined in the
184        IRC protocol.
185        """
186        if words[0] == "PING":
187            self.sendMsg(f"PONG {words[1]}\r\n")
188
189        if words[1] == 'INVITE':
190            self.sendMsg(f'JOIN {words[3]}\r\n')
191
192    def checkMarvinActions(self, words):
193        """Check if Marvin should perform any actions"""
194        if words[1] == 'PRIVMSG' and words[2] == self.CONFIG["channel"]:
195            self.MSG_LOG.debug("%s <%s>  %s",
196                               words[2],
197                               words[0].split(":")[1].split("!")[0],
198                               " ".join(words[3:]))
199
200        if words[1] == 'PRIVMSG':
201            raw = ' '.join(words[3:])
202            row = self.tokenize(raw)
203
204            if self.CONFIG["nick"] in row:
205                for action in self.ACTIONS:
206                    msg = action(row)
207                    if msg:
208                        self.sendPrivMsg(msg, words[2])
209                        break
210            else:
211                for action in self.GENERAL_ACTIONS:
212                    msg = action(row)
213                    if msg:
214                        self.sendPrivMsg(msg, words[2])
215                        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 = f'NICK {nick}\r\n'
 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(f'USER  {nick} 0 * :{realname}\r\n')
 68
 69        # This is my nick, i promise!
 70        ident = self.CONFIG["ident"]
 71        if ident:
 72            self.sendMsg(f'PRIVMSG nick IDENTIFY {ident}\r\n')
 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(f'JOIN {channel}\r\n')
 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 = f"PRIVMSG {channel} :{message}\r\n"
 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        res = None
108        for enc in preferred_encs:
109            try:
110                res = raw.decode(enc)
111                changed = True
112                break
113            except Exception:
114                pass
115
116        if not changed:
117            try:
118                enc = chardet.detect(raw)['encoding']
119                res = raw.decode(enc)
120            except Exception:
121                res = raw.decode(enc, 'ignore')
122
123        return res
124
125    def receive(self):
126        """Read incoming message and guess encoding"""
127        lines = None
128        try:
129            buf = self.SOCKET.recv(2048)
130            lines = self.decode_irc(buf)
131            lines = lines.split("\n")
132            buf = lines.pop()
133        except Exception as err:
134            LOG.error("Error reading incoming message %s", err)
135
136        return lines
137
138    def readincoming(self):
139        """
140        Read all files in the directory incoming, send them as a message if
141        they exists and then move the file to directory done.
142        """
143        if not os.path.isdir(self.CONFIG["dirIncoming"]):
144            return
145
146        listing = os.listdir(self.CONFIG["dirIncoming"])
147
148        for infile in listing:
149            filename = os.path.join(self.CONFIG["dirIncoming"], infile)
150
151            with open(filename, "r", encoding="UTF-8") as f:
152                for msg in f:
153                    self.sendPrivMsg(msg, self.CONFIG["channel"])
154
155            try:
156                shutil.move(filename, self.CONFIG["dirDone"])
157            except Exception:
158                LOG.warning("Failed to move %s to %s. Deleting.", filename, self.CONFIG["dirDone"])
159                os.remove(filename)
160
161    def mainLoop(self):
162        """For ever, listen and answer to incoming chats"""
163        while 1:
164            # Check in any in the incoming directory
165            self.readincoming()
166
167            for line in self.receive():
168                LOG.debug(line)
169                words = line.strip().split()
170
171                if not words:
172                    continue
173
174                self.checkIrcActions(words)
175                self.checkMarvinActions(words)
176
177    def begin(self):
178        """Start the bot"""
179        self.connectToServer()
180        self.mainLoop()
181
182    def checkIrcActions(self, words):
183        """
184        Check if Marvin should take action on any messages defined in the
185        IRC protocol.
186        """
187        if words[0] == "PING":
188            self.sendMsg(f"PONG {words[1]}\r\n")
189
190        if words[1] == 'INVITE':
191            self.sendMsg(f'JOIN {words[3]}\r\n')
192
193    def checkMarvinActions(self, words):
194        """Check if Marvin should perform any actions"""
195        if words[1] == 'PRIVMSG' and words[2] == self.CONFIG["channel"]:
196            self.MSG_LOG.debug("%s <%s>  %s",
197                               words[2],
198                               words[0].split(":")[1].split("!")[0],
199                               " ".join(words[3:]))
200
201        if words[1] == 'PRIVMSG':
202            raw = ' '.join(words[3:])
203            row = self.tokenize(raw)
204
205            if self.CONFIG["nick"] in row:
206                for action in self.ACTIONS:
207                    msg = action(row)
208                    if msg:
209                        self.sendPrivMsg(msg, words[2])
210                        break
211            else:
212                for action in self.GENERAL_ACTIONS:
213                    msg = action(row)
214                    if msg:
215                        self.sendPrivMsg(msg, words[2])
216                        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 = f'NICK {nick}\r\n'
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(f'USER  {nick} 0 * :{realname}\r\n')
68
69        # This is my nick, i promise!
70        ident = self.CONFIG["ident"]
71        if ident:
72            self.sendMsg(f'PRIVMSG nick IDENTIFY {ident}\r\n')
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(f'JOIN {channel}\r\n')
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 = f"PRIVMSG {channel} :{message}\r\n"
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        res = None
108        for enc in preferred_encs:
109            try:
110                res = raw.decode(enc)
111                changed = True
112                break
113            except Exception:
114                pass
115
116        if not changed:
117            try:
118                enc = chardet.detect(raw)['encoding']
119                res = raw.decode(enc)
120            except Exception:
121                res = raw.decode(enc, 'ignore')
122
123        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):
125    def receive(self):
126        """Read incoming message and guess encoding"""
127        lines = None
128        try:
129            buf = self.SOCKET.recv(2048)
130            lines = self.decode_irc(buf)
131            lines = lines.split("\n")
132            buf = lines.pop()
133        except Exception as err:
134            LOG.error("Error reading incoming message %s", err)
135
136        return lines

Read incoming message and guess encoding

def readincoming(self):
138    def readincoming(self):
139        """
140        Read all files in the directory incoming, send them as a message if
141        they exists and then move the file to directory done.
142        """
143        if not os.path.isdir(self.CONFIG["dirIncoming"]):
144            return
145
146        listing = os.listdir(self.CONFIG["dirIncoming"])
147
148        for infile in listing:
149            filename = os.path.join(self.CONFIG["dirIncoming"], infile)
150
151            with open(filename, "r", encoding="UTF-8") as f:
152                for msg in f:
153                    self.sendPrivMsg(msg, self.CONFIG["channel"])
154
155            try:
156                shutil.move(filename, self.CONFIG["dirDone"])
157            except Exception:
158                LOG.warning("Failed to move %s to %s. Deleting.", filename, self.CONFIG["dirDone"])
159                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):
161    def mainLoop(self):
162        """For ever, listen and answer to incoming chats"""
163        while 1:
164            # Check in any in the incoming directory
165            self.readincoming()
166
167            for line in self.receive():
168                LOG.debug(line)
169                words = line.strip().split()
170
171                if not words:
172                    continue
173
174                self.checkIrcActions(words)
175                self.checkMarvinActions(words)

For ever, listen and answer to incoming chats

def begin(self):
177    def begin(self):
178        """Start the bot"""
179        self.connectToServer()
180        self.mainLoop()

Start the bot

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

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

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

Check if Marvin should perform any actions