4
\$\begingroup\$

I started learning Python by making an IRC bot, as it took some pains in another language. I've improved it now over time. As it involves networking, I'd also like some comments on that side.

# -*- coding: utf-8 -*-

import socket
import os
import importlib

plugins = []

class Bot_core(object):
    def __init__(self, 
                 server_url = 'chat.freenode.net',
                 port = 6667,
                 name = 'appinvBot',
                 owners = ['appinv'],
                 password = '',
                 friends = ['haruno'],
                 autojoin_channels = ['##bottestingmu']
                 ):
        self.server_url = server_url
        self.port = port
        self.name = name
        self.owners = owners
        self.password = password
        self.autojoin_channels = autojoin_channels
        self.friends = friends

        '''
        NORMAL ATTRIBUTES
        '''
        self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.isListenOn = 1
        dom = self.server_url.split('.')
        self.domain = '.'.join(dom[-2:])
        self.sp_command = 'hbot'
        self.plugins = []

    '''
    STRINGS
    '''
    def set_nick_command(self):
        return 'NICK ' + self.name + '\r\n'

    def present_command(self):
        return 'USER '+self.name+' '+self.name+' '+self.name+' : '+self.name+' IRC\r\n'

    def identify_command(self):
        return 'msg NickServ identify ' + self.password + ' \r\n'

    def join_channel_command(self, channel):
        return 'JOIN ' + channel + ' \r\n'

    def specific_send_command(self, target, msg):
        return "PRIVMSG "+ target +" :"+ msg +"\r\n"

    def pong_return(self):
        return 'PONG \r\n'

    def info(self, s):
        def return_it(x):
            if x == None:
                return ''
            else:
                return x
        try:
            prefix = ''
            trailing = []
            address = ''
            if not s:
               print("Empty line.")
            if s[0] == ':':
                prefix, s = s[1:].split(' ', 1)
            if s.find(' :') != -1:
                s, trailing = s.split(' :', 1)
                args = s.split()
                args.append(trailing)
            else:
                args = s.split()
            command = args.pop(0)
            if '#' in args[0]:
                address = args[0]
            else:
                address = prefix.split('!~')[0]
            # return prefix, command, args, address
            return {
                    'prefix':return_it(prefix),
                    'command':return_it(command),
                    'args':['' if e is None else e for e in args],
                    'address':return_it(address)
                    }
        except Exception as e:
            print('woops',e)

    '''
    MESSAGE UTIL
    '''
    def send(self, msg):
        self.irc.send(bytes( msg, "UTF-8")) 

    def send_target(self, target, msg):
        self.send(self.specific_send_command(target, msg))

    def join(self, channel):
        self.send(self.join_channel_command(channel))

    '''
    BOT UTIL
    '''

    def load_plugins(self, list_to_add):
        try:
            to_load = []
            with open('PLUGINS.conf', 'r') as f:
                to_load = f.read().split('\n')
                to_load = list(filter(lambda x: x != '', to_load))
            for file in to_load:
                module = importlib.import_module('plugins.'+file)
                Plugin = getattr(module, 'Plugin')
                obj = Plugin()
                list_to_add.append(obj)
        except ModuleNotFoundError as e:
            print('module not found', e)

    def methods(self):
        return {
                'send_raw':self.send,
                'send':self.send_target,
                'join':self.join
                }

    def run_plugins(self, listfrom, incoming):
        for plugin in listfrom:
            plugin.run(incoming, self.methods(), self.info(incoming))

    '''
    MESSAGE PARSING
    '''
    def core_commands_parse(self, incoming):

        '''
        PLUGINS
        '''
        self.run_plugins(self.plugins, incoming)


    '''
    BOT IRC FUNCTIONS
    '''
    def connect(self):
            self.irc.connect((self.server_url, self.port))

    def identify(self):
        self.send(self.identify_command())

    def greet(self):
        self.send(self.set_nick_command())
        self.send(self.present_command())
        for channel in self.autojoin_channels:
            self.send(self.join_channel_command(channel))

    def pull(self):
        while self.isListenOn:
            try :
                data = self.irc.recv(2048)
                raw_msg = data.decode("UTF-8")
                msg = raw_msg.strip('\n\r')
                self.stay_alive(msg)
                self.core_commands_parse(msg)
                print(
                """***
{}                
                   """.format(msg))
                if len(data) == 0:
                    try:
                        self.irc.close()
                        self.registered_run()
                    except Exception as e:
                        print(e)
            except Exception as e:
                print(e)


    # all in one for registered bot
    def registered_run(self):
        self.connect()
        self.identify()
        self.greet()
        self.load_plugins(self.plugins)
        self.pull()

    def unregistered_run(self):
        self.connect()
        self.greet()
        self.load_plugins(plugins)
        self.pull()

    '''
    ONGOING REQUIREMENT/S
    '''
    def stay_alive(self, incoming):
        if 'ping' in incoming.lower():
            part = incoming.split(':')
            if self.domain in part[1]:
                self.send(self.pong_return())
                print(''' 
                      ***** message *****
                      ping detected from
                      {}
                      *******************
                      '''.format(part[1]))
                self.irc.recv(2048).decode("UTF-8")

x = Bot_core(); x.registered_run()
\$\endgroup\$
2
  • 1
    \$\begingroup\$ which python version are you building this in? \$\endgroup\$
    – hjpotter92
    Aug 6, 2018 at 4:18
  • \$\begingroup\$ i am building this in python 3.4+ \$\endgroup\$ Aug 6, 2018 at 7:57

1 Answer 1

3
\$\begingroup\$

I have the following points to make on the code:
- No logging
- No external config file for the settings (Open/Close Principal violation)
- Creating resources in the init instead of in a separate section (no re-use of common functions should the user attempt to connect to multiple IRC servers)
- I don't see any validation of config/data items. Users could have the expected strings or they could introduce some buffer overflow execution code. Always validate user input.
- String concatenation and formatting - don't use "+" to join strings and data together, use f-strings or .format()
- You have using long if else statements instead of ternary

            if x == None:
                return ''
            else:
                return x

should be:

            return "" if not x else x

(also, no need to do == None as that is assumed).

  • You have many huge try/except blocks, reduce this to only the specific lines which could throw an exception and catch that specific exception.
  • There are many huge if: if: if: statements instead of reducing each if statement into a separate function (Single Responsibility Principal)
  • Your Exceptions printing to the screen instead of logging, you should not interrupt the UX - handle the error gracefully and continue on or warn the user you need to exit the program due to an error.
  • Plugins: There is no validation when loading the plugin. It could be malicious or faulty. You need to wrap loading the plugin in a try/except, and you need to run some checks on the plugin to determine validity. Like not trusting user input, you need to validate plugins so they don't crash the bot.
  • As hinted at before, you have plugin methods talking directly to send function, however there is no validation on what they're sending through the bot. Ensure you validate the input into .send(cmd) before transmitting it else you could unwittingly make a DDOS bot.
  • The connection function has no validation routine either, you should validate if the input is a valid domain name or ip address on loading (as an example, use regex). Also, you should validate if there is a valid listening port 'out there' before assuming the remainder of the data for that particular IRC Server is valid. Handle faults and errors gracefully in the UX.

Finally... Comments in capitals (please, no) or comments stating the obvious (unnecessary) or comments that don't correlate to the code. Your code needs to explain what it's doing, you should only need comments when the code is difficult (like some math code), and it should state "this is the sieve of eratosthenes" or some other comment which is the "Why" or "What", and not the "how" - the code is the "how" (I hope that makes sense?).

Also, when your code gets bigger, it might be an idea to export all the plugin code to a separate .py file named 'plugins.py' and import that into your main. Speaking of main, you're missing the entry point. Have a look at a few other examples on Code Review.

Good Luck!

\$\endgroup\$
3
  • \$\begingroup\$ thank you very much, very thorough. as for the plugins, see the complete architecture here : here \$\endgroup\$ Aug 6, 2018 at 8:06
  • 2
    \$\begingroup\$ watch out ! if x == None is NOT the reverse ofif not x ; the latter is also true if x is False, 0, or the empty string. A more accurate translation is return '' if x is None else x. \$\endgroup\$
    – pjz
    Aug 6, 2018 at 17:39
  • 2
    \$\begingroup\$ True, I was thinking of return x if x else "" but my fingers tried to keep it as close as the OPs writing. Also, x == None you shouldn't do equality statements against None (forgot to add that in there too), it's always is. \$\endgroup\$
    – C. Harley
    Aug 7, 2018 at 6:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.