from config import config import time import ssl import argparse import logging import logging.handlers import asyncio import importlib import time import sys import functions from functions import shop, buyweapon, buyammo, buybandages, shoot, reload, ammo, heal, revive, getmoney, getbandages, removeweapon, createuser, profile, removeuser, setlevel, createuser, punch, checkexist import sqlite3 # Formatting Control Characters / Color Codes bold = '\x02' italic = '\x1D' underline = '\x1F' reverse = '\x16' reset = '\x0f' white = '00' black = '01' blue = '02' green = '03' red = '04' brown = '05' purple = '06' orange = '07' yellow = '08' light_green = '09' cyan = '10' light_cyan = '11' light_blue = '12' pink = '13' grey = '14' light_grey = '15' conn = sqlite3.connect('data.db') c = conn.cursor() def color(msg: str, foreground: str, background: str='') -> str: ''' Color a string with the specified foreground and background colors. :param msg: The string to color. :param foreground: The foreground color to use. :param background: The background color to use. ''' return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}' def ssl_ctx() -> ssl.SSLContext: '''Create a SSL context for the connection.''' ctx = ssl.create_default_context() #ctx.verify_mode = ssl.CERT_NONE # Comment out this line to verify hosts #ctx.load_cert_chain('/path/to/cert', password='loldongs') return ctx class Bot(object): def __init__(self): self.nickname = config.irc.nickname self.username = config.irc.username self.realname = config.irc.realname self.channel = config.irc.channel self.channelkey = config.irc.channelkey self.reader = None self.writer = None #createtable() async def action(self, chan: str, msg: str): ''' Send an ACTION to the IRC server. :param chan: The channel to send the ACTION to. :param msg: The message to send to the channel. ''' await self.sendmsg(chan, f'\x01ACTION {msg}\x01') async def raw(self, data: str): ''' Send raw data to the IRC server. :param data: The raw data to send to the IRC server. (512 bytes max including crlf) ''' self.writer.write(data[:510].encode('utf-8') + b'\r\n') async def sendmsg(self, target: str, msg: str): ''' Send a PRIVMSG to the IRC server. :param target: The target to send the PRIVMSG to. (channel or user) :param msg: The message to send to the target. ''' try: await self.raw(f'PRIVMSG {target} :{msg}') time.sleep(config.throttle.msg) except: await bot.sendmsg(config.irc.channel, "Slow down homie!!") async def connect(self): '''Connect to the IRC server.''' while True: try: options = { 'host' : config.irc.server, 'port' : config.irc.port if config.irc.port else 6697 if config.irc.ssl else 6667, 'limit' : 1024, # Buffer size in bytes (don't change this unless you know what you're doing) 'ssl' : ssl_ctx() if config.irc.ssl else None, 'family' : 2, # 10 = AF_INET6 (IPv6), 2 = AF_INET (IPv4) 'local_addr' : None # Can we just leave this as args.vhost? } self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) # 15 second timeout if config.irc.password: await self.raw('PASS ' + config.irc.password) # Rarely used, but IRCds may require this await self.raw(f'USER {self.username} 0 * :{self.realname}') # These lines must be sent upon connection await self.raw('NICK ' + self.nickname) # They are to identify the bot to the server while not self.reader.at_eof(): data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 300) # 5 minute ping timeout await self.handle(data.decode('utf-8').strip()) # Handle the data received from the IRC server except Exception as ex: logging.error(f'failed to connect to {config.irc.server} ({str(ex)})') finally: await asyncio.sleep(30) # Wait 30 seconds before reconnecting async def handle(self, data: str): ''' Handle the data received from the IRC server. :param data: The data received from the IRC server. ''' try: logging.info(data) args = data.split() if data.startswith('ERROR :Closing Link:'): raise Exception('Cannot Connect') if args[0] == 'PING': await self.raw('PONG ' + args[1]) # Respond to the server's PING request with a PONG to prevent ping timeout elif args[1] == '001': # RPL_WELCOME await self.raw(f'MODE {self.nickname} +B') # Set user mode +B (Bot) await asyncio.sleep(10) # Wait 10 seconds before joining the channel (required by some IRCds to wait before JOIN) await self.raw(f'JOIN {self.channel} {self.channelkey}') elif args[1] == '433': # ERR_NICKNAMEINUSE self.nickname += '_' # If the nickname is already in use, append an underscore to the end of it await self.raw('NICK ' + self.nickname) # Send the new nickname to the server elif args[1] == 'KICK': chan = args[2] kicked = args[3] if kicked == self.nickname: await asyncio.sleep(3) await self.raw(f'JOIN {chan}') elif args[1] == 'PRIVMSG': ident = args[0][1:] nick = args[0].split('!')[0][1:].lower() target = args[2] msg = ' '.join(args[3:])[1:] arguments = msg.split() bandageamount = '0' if target == self.nickname: pass # Handle private messages here if target.startswith('#'): # Channel message if msg.startswith('!'): try: if time.time() - config.throttle.last < config.throttle.cmd and config.throttle.lastnick == nick: if not config.throttle.slow: config.throttle.slow = True await bot.sendmsg(config.irc.channel, color("Slow down homie!!", red)) else: config.throttle.slow = False config.throttle.lastnick = nick if arguments[0] == '!hug': await bot.sendmsg(target, f'[XOXO Hugger9000... {nick} hugs {arguments[1]}]') if arguments[0] == '!admins': for i in config.irc.admins: await bot.sendmsg(target, f'[Admins: ' + color(i, red) + ']') if arguments[0] == '!shop': await bot.sendmsg(config.irc.channel, f'[Shop Accessories]') await bot.sendmsg(config.irc.channel, f'[!buy weapon - purchase a 3-round burst gun (note: weapon has a quality of 300 trigger limit)]') await bot.sendmsg(config.irc.channel, f'[!buy ammo - buy ammunition (+60 rounds) for your weapon]') await bot.sendmsg(config.irc.channel, f'[!buy bandages ]') if arguments[0] == '!buy' and arguments[1] != None: if arguments[1] == 'weapon': value = await functions.buyweapon(nick) await bot.sendmsg(config.irc.channel, f'{value}') elif arguments[1] == 'ammo': value = await functions.buyammo(nick) await bot.sendmsg(config.irc.channel, f'{value}') elif arguments[1] == 'bandages': if len(arguments) <= 2 or int(arguments[2]) == 0: await bot.sendmsg(config.irc.channel, '[You must specify amount of bandages (greater than 0) to purchase]') else: value = await buybandages(nick, int(arguments[2])) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!test': value = await functions.testfunction(nick) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!shoot': await functions.shoot(arguments[1].lower(), nick) #await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!reload': value = await functions.reload(nick) if value != None: await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!ammo': ammovalue = await ammo(nick) await bot.sendmsg(config.irc.channel, f'{ammovalue}') if arguments[0] == '!heal': value = await functions.heal(nick) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!revive': value = await functions.revive(nick) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!help': await bot.sendmsg(config.irc.channel, '[Command List]') await bot.sendmsg(config.irc.channel, '[!help - shows commands]') await bot.sendmsg(config.irc.channel, '[!register - Register your player]') await bot.sendmsg(config.irc.channel, '[!profile - Shows Profile Stats [!profile username]') await bot.sendmsg(config.irc.channel, '[!punch - Fight [!punch username]') await bot.sendmsg(config.irc.channel, '[!shoot - Shoot if you own a weapon]') await bot.sendmsg(config.irc.channel, '[!bank - Returns Bank Balance]') await bot.sendmsg(config.irc.channel, '[!buy [weapon, ammo, bandages ]') await bot.sendmsg(config.irc.channel, '[!reload - Reloads Weapon]') await bot.sendmsg(config.irc.channel, '[!ammo - Show Ammunition Amounts]') await bot.sendmsg(config.irc.channel, '[!bandages - Shows Bandage Amounts]') await bot.sendmsg(config.irc.channel, '[!revive - Brings you back to health if dead]') await bot.sendmsg(config.irc.channel, '[!heal - Use bandages to regain health]') await bot.sendmsg(config.irc.channel, ' ') await bot.sendmsg(config.irc.channel, '[Admin Command List]') await bot.sendmsg(config.irc.channel, '[!setlevel - !setlevel username Level (1 to 5)]') await bot.sendmsg(config.irc.channel, '[!adduser - Force create a user]') await bot.sendmsg(config.irc.channel, '[!remove - Removes a player]') await bot.sendmsg(config.irc.channel, '[!removeweapon - Remove user weapon]') if arguments[0] == '!bank': value = await functions.getmoney(nick) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!bandages': value = await functions.getbandages(nick) await bot.sendmsg(config.irc.channel, f'{value}') if nick in config.irc.admins: #await self.sendmsg(target, f'{nick} is an ' + color('Admin!', red)) if arguments[0] == '!removeweapon': value = await functions.removeweapon(nick) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!adduser': name = arguments[1].lower() c.execute(f"SELECT rowid FROM users WHERE name = (:name)", {'name': name}) data=c.fetchone() if data is None: await bot.sendmsg(config.irc.channel, '[Registering Player: %s]'%name) await createuser(name) await profile(name) else: await bot.sendmsg(config.irc.channel, f'{color("[Player already exists!]", red)}') if arguments[0] == '!remove': logging.debug('remove user') name = arguments[1].lower() c.execute(f"SELECT rowid FROM users WHERE name= (:name)", {'name': name}) data=c.fetchone() if data != None: await bot.sendmsg(config.irc.channel, f'[Removing {color(name, red)} from database]') await functions.removeuser(name) else: await bot.sendmsg(config.irc.channel, f'[User does not exist]') if arguments[0] == '!setlevel': value = await functions.setlevel(arguments[1].lower(), arguments[2]) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!register': c.execute(f"SELECT rowid FROM users WHERE name = (:name)", {'name': nick}) data=c.fetchone() if data is None: await bot.sendmsg(config.irc.channel, f'[Registering Player: %s]'%nick) await createuser(nick) await profile(nick) else: await bot.sendmsg(config.irc.channel, f'{color("[Player already exists!]", red)}') if arguments[0] == '!profile': try: value = await profile(arguments[1].lower()) await bot.sendmsg(config.irc.channel, f'{value}') except: value = await profile(nick) await bot.sendmsg(config.irc.channel, f'{value}') if arguments[0] == '!punch': await functions.punch(arguments[1].lower(), nick) config.throttle.last = time.time() except Exception as ex: if time.time() - config.throttle.last < config.throttle.cmd: if not config.throttle.slow: await bot.sendmsg(config.irc.channel, color('Slow down homie!', red)) config.throttle.slow = True config.throttle.last = time.time() #except (UnicodeDecodeError, UnicodeEncodeError): except (UnicodeDecodeError, UnicodeEncodeError): pass # Some IRCds allow invalid UTF-8 characters, this is a very important exception to catch except Exception as ex: logging.exception(f'Unknown error has occured! ({ex})') def setup_logger(log_filename: str, to_file: bool = False): ''' Set up logging to console & optionally to file. :param log_filename: The filename of the log file ''' sh = logging.StreamHandler() sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p')) if to_file: fh = logging.handlers.RotatingFileHandler(log_filename+'.log', maxBytes=250000, backupCount=3, encoding='utf-8') # Max size of 250KB, 3 backups fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(filename)s.%(funcName)s.%(lineno)d | %(message)s', '%Y-%m-%d %I:%M %p')) # We can be more verbose in the log file logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh)) else: logging.basicConfig(level=logging.NOTSET, handlers=(sh,)) bot = Bot() def run(): functions.createtable() setup_logger('skeleton', to_file=True) # Optionally, you can log to a file, change to_file to False to disable this. asyncio.run(bot.connect())