diff --git a/LICENSE b/LICENSE index b63b809..4c8b212 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2019, acidvegas +Copyright (c) 2021, acidvegas Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -12,4 +12,4 @@ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index 5929ca7..834967b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ -###### Requirements +# skeleton +> asyncronous bot skeleton for the internet relay chat protocol + +## Requirements * [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python)* * [PySocks](https://pypi.python.org/pypi/PySocks) *(**Optional:** For using the `proxy` setting)* -###### IRC RCF Reference +## Information +The repository comes with 2 skeletons. A simple, single-file skeleton for basic bots & an advanced structured skeleton for more complex bots. + +This is just a basic structure to help setup a bot. The bots have no use by default. It is asyncronous, can log to file, handle basic I/O, flood control, etc. + +## IRC RCF Reference - http://www.irchelp.org/protocol/rfc/ -###### Mirrors +## Mirrors - [acid.vegas](https://acid.vegas/skeleton) *(main)* -- [SuperNETs](https://git.supernets.org/acidvegas/skeleton) - [GitHub](https://github.com/acidvegas/skeleton) - [GitLab](https://gitlab.com/acidvegas/skeleton) \ No newline at end of file diff --git a/advanced/core/bot.py b/advanced/core/bot.py new file mode 100644 index 0000000..49bc0b0 --- /dev/null +++ b/advanced/core/bot.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# Asyncronous IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) +# bot.py + +import asyncio +import logging + +import config + +from commands import Command +from events import Event + +def ssl_ctx(): + import ssl + ctx = ssl.create_default_context() + if not config.connection.ssl_verify: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + if config.cert.file: + ctx.load_cert_chain(config.cert.file, password=config.cert.password) + return ctx + +class IrcBot: + def __init__(self): + self.options = { + 'host' : config.connection.server, + 'port' : config.connection.port, + 'limit' : 1024, + 'ssl' : ssl_ctx() if config.connection.ssl else None, + 'family' : 10 if config.connection.ipv6 else 2, + 'local_addr' : (config.connection.vhost, 0) if config.connection.vhost else None + } + self.reader = None + self.writer = None + + async def run(self): + try: + self.reader, self.writer = await asyncio.open_connection(**self.options, timeout=config.throttle.timeout) + except Exception as ex: + logging.exception('Failed to connect to IRC server!') + else: + try: + await Command(Bot).register(config.ident.nickname, config.ident.username, config.ident.realname, config.login.network) + while not self.reader.at_eof(): + data = await self.reader.readline() + Event(Bot).handle(data.decode('utf-8').strip()) + except (UnicodeDecodeError, UnicodeEncodeError): + pass + except Exception as ex: + logging.exception('Unknown error has occured!') + finally: + Event.disconnect() + +Bot = IrcBot() \ No newline at end of file diff --git a/advanced/core/commands.py b/advanced/core/commands.py new file mode 100644 index 0000000..2e96d76 --- /dev/null +++ b/advanced/core/commands.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# Asyncronous IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) +# commands.py + +class Command: + def __init__(self, bot): + self.Bot = bot + + def action(self, target, msg): + self.sendmsg(target, f'\x01ACTION {msg}\x01') + + def join_channel(self, chan, key=None): + self.raw(f'JOIN {chan} {key}') if key else raw('JOIN ' + chan) + + def mode(self, target, mode): + self.raw(f'MODE {target} {mode}') + + def nick(self, new_nick): + self.raw('NICK ' + new_nick) + + def notice(self, target, msg): + self.raw(f'NOTICE {target} :{msg}') + + def part_channel(self, chan, msg=None): + self.raw(f'PART {chan} {msg}') if msg else raw('PART ' + chan) + + def quit(self, msg=None): + self.raw('QUIT :' + msg) if msg else raw('QUIT') + + def raw(self, data): + self.Bot.writer.write(data[:510].encode('utf-8') + b'\r\n') + + def register(self, nickname, username, realname, password=None): + if password: + self.raw('PASS ' + password) + self.raw('NICK ' + nickname) + self.raw(f'USER {username} 0 * :{realname}') + + def sendmsg(self, target, msg): + self.raw(f'PRIVMSG {target} :{msg}') + + def topic(self, chan, data): + self.raw(f'TOPIC {chan} :{text}') \ No newline at end of file diff --git a/advanced/core/config.py b/advanced/core/config.py new file mode 100644 index 0000000..caaf7a3 --- /dev/null +++ b/advanced/core/config.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# Asyncronous IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) +# config.py + +class connection: + server = 'irc.server.com' + port = 6667 + ipv6 = False + ssl = False + ssl_verify = False + vhost = None + channel = '#dev' + key = None + modes = None + +class cert: + file = None + password = None + +class ident: + nickname = 'skeleton' + username = 'skeleton' + realname = 'acid.vegas/skeleton' + +class login: + network = None + nickserv = None + operator = None + +class settings: + admin = 'nick!user@host' # Must be in nick!user@host format (Wildcards accepted) + log = False + +class throttle: + command = 3 + message = 0.5 + reconnect = 15 + rejoin = 5 + timeout = 15 \ No newline at end of file diff --git a/advanced/core/events.py b/advanced/core/events.py new file mode 100644 index 0000000..afa6a79 --- /dev/null +++ b/advanced/core/events.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# Asyncronous IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/asyncirc) +# events.py + +import asyncio +import logging + +import config + +class Event: + def __init__(self, bot): + self.Bot = bot + + def connect(self): + if config.settings.modes: + Commands.raw(f'MODE {config.ident.nickname} +{config.settings.modes}') + if config.login.nickserv: + Commands.sendmsg('NickServ', f'IDENTIFY {config.ident.nickname} {config.login.nickserv}') + if config.login.operator: + Commands.raw(f'OPER {config.ident.username} {config.login.operator}') + Commands.join_channel(config.connection.channel, config.connection.key) + + async def disconnect(self): + self.writer.close() + await self.writer.wait_closed() + asyncio.sleep(config.throttle.reconnect) + + def join_channel(self): + pass + + def kick(self): + pass + + def invite(self): + pass + + def message(self): + pass + + def nick_in_use(self): + new_nick = 'a' + str(random.randint(1000,9999)) + Command.nick(new_nick) + + def part_channel(self): + pass + + def private_message(self): + pass + + def quit(self): + pass + + async def handler(self, data): + logging.info(data) + args = data.split() + if args[0] == 'PING': + self.raw('PONG ' + args[1][1:]) + elif args[1] == '001': #RPL_WELCOME + self.connect() + elif args[1] == '433': #ERR_NICKNAMEINUSE + self.nick_in_use() diff --git a/advanced/skeleton.py b/advanced/skeleton.py new file mode 100644 index 0000000..c1442a8 --- /dev/null +++ b/advanced/skeleton.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# Asyncronous IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) +# skeleton.py + +import asyncio +import logging +import logging.handlers +import os +import sys + +sys.dont_write_bytecode = True +os.chdir(os.path.dirname(__file__) or '.') +sys.path += ('core','modules') + +import config + +if not os.path.exists('logs'): + os.makedirs('logs') +sh = logging.StreamHandler() +sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p')) +if config.settings.log: + fh = logging.handlers.RotatingFileHandler('logs/debug.log', maxBytes=250000, backupCount=7, encoding='utf-8') + fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(filename)s.%(funcName)s.%(lineno)d | %(message)s', '%Y-%m-%d %I:%M %p')) + logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh)) + del fh +else: + logging.basicConfig(level=logging.NOTSET, handlers=(sh,)) +del sh + +print('#'*56) +print('#{:^54}#'.format('')) +print('#{:^54}#'.format('Asyncronous IRC Bot Skeleton')) +print('#{:^54}#'.format('Developed by acidvegas in Python')) +print('#{:^54}#'.format('https://acid.vegas/skeleton')) +print('#{:^54}#'.format('')) +print('#'*56) + +from bot import Bot + +asyncio.run(Bot.run()) \ No newline at end of file diff --git a/skeleton.py b/skeleton.py index 9a90047..df7feb9 100644 --- a/skeleton.py +++ b/skeleton.py @@ -1,276 +1,177 @@ #!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) +# Asyncronoua IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) +# skeleton.py -import socket +import asyncio +import logging +import logging.handlers +import os +import random import time -import threading -# Configuration -_connection = {'server':'irc.supernets.org', 'port':6697, 'proxy':None, 'ssl':True, 'ssl_verify':False, 'ipv6':False, 'vhost':None} -_cert = {'file':None, 'key':None, 'password':None} -_ident = {'nickname':'DevBot', 'username':'dev', 'realname':'acid.vegas/skeleton'} -_login = {'nickserv':None, 'network':None, 'operator':None} -_settings = {'channel':'#dev', 'key':None, 'modes':None, 'throttle':1} +################################################## -# 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' +class config: + class connection: + server = 'irc.supernets.org' + port = 6697 + ipv6 = False + ssl = True + ssl_verify = False + vhost = None + channel = '#dev' + key = None + modes = None -def color(msg, foreground, background=None): - if background: - return f'\x03{foreground},{background}{msg}{reset}' - else: - return f'\x03{foreground}{msg}{reset}' + class cert: + file = None + password = None -def debug(msg): - print(f'{get_time()} | [~] - {msg}') + class ident: + nickname = 'skeleton' + username = 'skeleton' + realname = 'acid.vegas/skeleton' -def error(msg, reason=None): - if reason: - print(f'{get_time()} | [!] - {msg} ({reason})') - else: - print(f'{get_time()} | [!] - {msg}') + class login: + network = None + nickserv = None + operator = None -def error_exit(msg): - raise SystemExit(f'{get_time()} | [!] - {msg}') + class settings: + admin = 'nick!user@host' # Must be in nick!user@host format (Wildcards accepted) + log = False -def get_time(): - return time.strftime('%I:%M:%S') + class throttle: + command = 3 + message = 0.5 + reconnect = 15 + rejoin = 5 + timeout = 15 -class IRC(object): - def __init__(self): - self._queue = list() - self._sock = None +################################################## - def _run(self): - Loop._loops() - self._connect() +def ssl_ctx(): + import ssl + ctx = ssl.create_default_context() + if not config.connection.ssl_verify: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + if config.cert.file: + ctx.load_cert_chain(config.cert.file, password=config.cert.password) + return ctx - def _connect(self): - try: - self._create_socket() - self._sock.connect((_connection['server'], _connection['port'])) - self._register() - except socket.error as ex: - error('Failed to connect to IRC server.', ex) - Event._disconnect() - else: - self._listen() - - def _create_socket(self): - family = socket.AF_INET6 if _connection['ipv6'] else socket.AF_INET - if _connection['proxy']: - proxy_server, proxy_port = _connection['proxy'].split(':') - self._sock = socks.socksocket(family, socket.SOCK_STREAM) - self._sock.setblocking(0) - self._sock.settimeout(15) - self._sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port)) - else: - self._sock = socket.socket(family, socket.SOCK_STREAM) - if _connection['vhost']: - self._sock.bind((_connection['vhost'], 0)) - if _connection['ssl']: - ctx = ssl.SSLContext() - if _cert['file']: - ctx.load_cert_chain(_cert['file'], _cert['key'], _cert['password']) - if _connection['ssl_verify']: - ctx.verify_mode = ssl.CERT_REQUIRED - ctx.load_default_certs() - else: - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - self._sock = ctx.wrap_socket(self._sock) - - def _listen(self): - while True: - try: - data = self._sock.recv(1024).decode('utf-8') - for line in (line for line in data.split('\r\n') if len(line.split()) >= 2): - debug(line) - Event._handle(line) - except (UnicodeDecodeError,UnicodeEncodeError): - pass - #except Exception as ex: - # error('Unexpected error occured.', ex) - # break - Event._disconnect() - - def _register(self): - if _login['network']: - Bot._queue.append('PASS ' + _login['network']) - Bot._queue.append('USER {0} 0 * :{1}'.format(_ident['username'], _ident['realname'])) - Bot._queue.append('NICK ' + _ident['nickname']) +################################################## class Command: - def _action(target, msg): - Bot._queue.append(chan, f'\x01ACTION {msg}\x01') + def join_channel(chan, key=None): + Command.raw(f'JOIN {chan} {key}') if key else Command.raw('JOIN ' + chan) - def _ctcp(target, data): - Bot._queue.append(target, f'\001{data}\001') + def mode(target, mode): + Command.raw(f'MODE {target} {mode}') - def _invite(nick, chan): - Bot._queue.append(f'INVITE {nick} {chan}') + def nick(new_nick): + Command.raw('NICK ' + new_nick) - def _join(chan, key=None): - Bot._queue.append(f'JOIN {chan} {key}') if key else Bot._queue.append('JOIN ' + chan) + def raw(data): + Bot.writer.write(data[:510].encode('utf-8') + b'\r\n') - def _mode(target, mode): - Bot._queue.append(f'MODE {target} {mode}') + def sendmsg(target, msg): + Command.raw(f'PRIVMSG {target} :{msg}') - def _nick(nick): - Bot._queue.append('NICK ' + nick) - - def _notice(target, msg): - Bot._queue.append(f'NOTICE {target} :{msg}') - - def _part(chan, msg=None): - Bot._queue.append(f'PART {chan} {msg}') if msg else Bot._queue.append('PART ' + chan) - - def _quit(msg=None): - Bot._queue.append('QUIT :' + msg) if msg else Bot._queue.append('QUIT') - - def _raw(data): - Bot._sock.send(bytes(data[:510] + '\r\n', 'utf-8')) - - def _sendmsg(target, msg): - Bot._queue.append(f'PRIVMSG {target} :{msg}') - - def _topic(chan, text): - Bot._queue.append(f'TOPIC {chan} :{text}') +################################################## class Event: - def _connect(): - if _settings['modes']: - Command._mode(_ident['nickname'], '+' + _settings['modes']) - if _login['nickserv']: - Command._sendmsg('NickServ', 'IDENTIFY {0} {1}'.format(_ident['nickname'], _login['nickserv'])) - if _login['operator']: - Bot._queue.append('OPER {0} {1}'.format(_ident['username'], _login['operator'])) - Command._join(_settings['channel'], _settings['key']) + def connect(): + if config.connection.modes: + Command.raw(f'MODE {config.ident.nickname} +{config.connection.modes}') + if config.login.nickserv: + Command.sendmsg('NickServ', f'IDENTIFY {config.ident.nickname} {config.login.nickserv}') + if config.login.operator: + Command.raw(f'OPER {config.ident.username} {config.login.operator}') + Command.join_channel(config.connection.channel, config.connection.key) - def _ctcp(nick, chan, msg): - pass + async def disconnect(): + Bot.writer.close() + await bot.writer.wait_closed() + asyncio.sleep(config.throttle.reconnect) - def _disconnect(): - Bot._sock.close() - Bot._queue = list() - time.sleep(15) - Bot._connect() + def nick_in_use(): + new_nick = 'a' + str(random.randint(1000,9999)) + Command.nick(new_nick) - def _invite(nick, chan): - pass - - def _join(nick, chan): - pass - - def _kick(nick, chan, kicked): - if kicked == _ident['nickname'] and chan == _settings['channel']: - time.sleep(3) - Command.join(chan, _Settings['key']) - - def _message(nick, chan, msg): - if msg == '!test': - Command._sendmsg(chan, 'It Works!') - - def _nick_in_use(): - error_exit('The bot is already running or nick is in use!') - - def _part(nick, chan): - pass - - def _private(nick, msg): - pass - - def _quit(nick): - pass - - def _handle(data): - args = data.split() - if data.startswith('ERROR :Closing Link:'): - raise Exception('Connection has closed.') - elif data.startswith('ERROR :Reconnecting too fast, throttled.'): - raise Exception('Connection has closed. (throttled)') - elif args[0] == 'PING': - Command._raw('PONG ' + args[1][1:]) - elif args[1] == '001': - Event._connect() - elif args[1] == '433': - Event._nick_in_use() - elif args[1] == 'INVITE': - nick = args[0].split('!')[0][1:] - chan = args[3][1:] - Event._invite(nick, chan) - elif args[1] == 'JOIN': - nick = args[0].split('!')[0][1:] - chan = args[2][1:] - Event._join(nick, chan) - Command._raw('WHOIS sniff') - elif args[1] == 'KICK': - nick = args[0].split('!')[0][1:] - chan = args[2] - kicked = args[3] - Event._kick(nick, chan, kicked) - elif args[1] == 'PART': - nick = args[0].split('!')[0][1:] - chan = args[2] - Event._part(nick, chan) - elif args[1] == 'PRIVMSG': - #ident = args[0][1:] - nick = args[0].split('!')[0][1:] - chan = args[2] - msg = ' '.join(args[3:])[1:] - if msg.startswith('\001'): - Event._ctcp(nick, chan, msg) - elif chan == _ident['nickname']: - Event._private(nick, msg) - else: - Event._message(nick, chan, msg) - elif args[1] == 'QUIT': - nick = args[0].split('!')[0][1:] - Event._quit(nick) - -class Loop: - def _loops(): - threading.Thread(target=Loop._queue).start() - - def _queue(): - while True: + async def handler(): + while not Bot.reader.at_eof(): try: - if Bot._queue: - Command._raw(Bot._queue.pop(0)) - except Exception as ex: - error('Error occured in the queue handler!', ex) - finally: - time.sleep(_settings['throttle']) + data = await Bot.reader.readline() + data = data.decode('utf-8').strip() + logging.info(data) + args = data.split() + if data.startswith('ERROR :Closing Link:'): + raise Exception('Connection has closed.') + elif data.startswith('ERROR :Reconnecting too fast, throttled.'): + raise Exception('Connection has closed. (throttled)') + elif args[0] == 'PING': + Command.raw('PONG ' + args[1][1:]) + elif args[1] == '001': #RPL_WELCOME + Event.connect() + elif args[1] == '433': #ERR_NICKNAMEINUSE + Event.nick_in_use() + elif args[1] == 'KICK': + pass # handle kick + except (UnicodeDecodeError, UnicodeEncodeError): + pass + except: + logging.exception('Unknown error has occured!') -# Main -if _connection['proxy']: - try: - import socks - except ImportError: - error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)') -if _connection['ssl']: - import ssl -else: - del _cert, _connection['ssl_verify'] -Bot = IRC() -Bot._run() \ No newline at end of file +################################################## + +class IrcBot: + def __init__(self): + self.options = { + 'host' : config.connection.server, + 'port' : config.connection.port, + 'limit' : 1024, + 'ssl' : ssl_ctx() if config.connection.ssl else None, + 'family' : socket.AF_INET6 if config.connection.ipv6 else socket.AF_INET, + 'local_addr' : (config.connection.vhost, 0) if config.connection.vhost else None + } + self.reader, self.writer = (None, None) + + async def connect(self): + try: + self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**self.options), timeout=config.throttle.timeout) + if config.login.network: + Command.raw('PASS ' + config.login.network) + Command.raw(f'USER {config.ident.username} 0 * :{config.ident.realname}') + Command.raw('NICK ' + config.ident.nickname) + except: + logging.exception('Failed to connect to IRC server!') + else: + await Event.handler() + +################################################## + +if __name__ == '__main__': + if not os.path.exists('logs'): + os.makedirs('logs') + sh = logging.StreamHandler() + sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p')) + if config.settings.log: + fh = logging.handlers.RotatingFileHandler('logs/debug.log', maxBytes=250000, backupCount=7, encoding='utf-8') + fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(filename)s.%(funcName)s.%(lineno)d | %(message)s', '%Y-%m-%d %I:%M %p')) + logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh)) + del fh,sh + else: + logging.basicConfig(level=logging.NOTSET, handlers=(sh,)) + del sh + + print('#'*56) + print('#{:^54}#'.format('')) + print('#{:^54}#'.format('Asyncronous IRC Bot Skeleton')) + print('#{:^54}#'.format('Developed by acidvegas in Python')) + print('#{:^54}#'.format('https://acid.vegas/skeleton')) + print('#{:^54}#'.format('')) + print('#'*56) + + Bot = IrcBot() + asyncio.run(Bot.connect()) \ No newline at end of file diff --git a/skeleton/core/config.py b/skeleton/core/config.py deleted file mode 100644 index 8b44202..0000000 --- a/skeleton/core/config.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) -# config.py - -class connection: - server = 'irc.supernets.org' - port = 6697 - proxy = None - ipv6 = False - ssl = True - ssl_verify = False - vhost = None - channel = '#dev' - key = None - -class cert: - key = None - file = None - password = None - -class ident: - nickname = 'DevBot' - username = 'devbot' - realname = 'acid.vegas/skeleton' - -class login: - network = None - nickserv = None - operator = None - -class throttle: - command = 3 - reconnect = 10 - rejoin = 3 - -class settings: - admin = 'nick!user@host.name' # Must be in nick!user@host format (Can use wildcards here) - cmd_char = '!' - log = False - modes = None diff --git a/skeleton/core/constants.py b/skeleton/core/constants.py deleted file mode 100644 index 3ffbdb8..0000000 --- a/skeleton/core/constants.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) -# constants.py - -# Control Characters -bold = '\x02' -color = '\x03' -italic = '\x1D' -underline = '\x1F' -reverse = '\x16' -reset = '\x0f' - -# Color Codes -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' - -# Events -PASS = 'PASS' -NICK = 'NICK' -USER = 'USER' -OPER = 'OPER' -MODE = 'MODE' -SERVICE = 'SERVICE' -QUIT = 'QUIT' -SQUIT = 'SQUIT' -JOIN = 'JOIN' -PART = 'PART' -TOPIC = 'TOPIC' -NAMES = 'NAMES' -LIST = 'LIST' -INVITE = 'INVITE' -KICK = 'KICK' -PRIVMSG = 'PRIVMSG' -NOTICE = 'NOTICE' -MOTD = 'MOTD' -LUSERS = 'LUSERS' -VERSION = 'VERSION' -STATS = 'STATS' -LINKS = 'LINKS' -TIME = 'TIME' -CONNECT = 'CONNECT' -TRACE = 'TRACE' -ADMIN = 'ADMIN' -INFO = 'INFO' -SERVLIST = 'SERVLIST' -SQUERY = 'SQUERY' -WHO = 'WHO' -WHOIS = 'WHOIS' -WHOWAS = 'WHOWAS' -KILL = 'KILL' -PING = 'PING' -PONG = 'PONG' -ERROR = 'ERROR' -AWAY = 'AWAY' -REHASH = 'REHASH' -DIE = 'DIE' -RESTART = 'RESTART' -SUMMON = 'SUMMON' -USERS = 'USERS' -WALLOPS = 'WALLOPS' -USERHOST = 'USERHOST' -ISON = 'ISON' - -# Event Numerics -RPL_WELCOME = '001' -RPL_YOURHOST = '002' -RPL_CREATED = '003' -RPL_MYINFO = '004' -RPL_ISUPPORT = '005' -RPL_TRACELINK = '200' -RPL_TRACECONNECTING = '201' -RPL_TRACEHANDSHAKE = '202' -RPL_TRACEUNKNOWN = '203' -RPL_TRACEOPERATOR = '204' -RPL_TRACEUSER = '205' -RPL_TRACESERVER = '206' -RPL_TRACESERVICE = '207' -RPL_TRACENEWTYPE = '208' -RPL_TRACECLASS = '209' -RPL_STATSLINKINFO = '211' -RPL_STATSCOMMANDS = '212' -RPL_STATSCLINE = '213' -RPL_STATSILINE = '215' -RPL_STATSKLINE = '216' -RPL_STATSYLINE = '218' -RPL_ENDOFSTATS = '219' -RPL_UMODEIS = '221' -RPL_SERVLIST = '234' -RPL_SERVLISTEND = '235' -RPL_STATSLLINE = '241' -RPL_STATSUPTIME = '242' -RPL_STATSOLINE = '243' -RPL_STATSHLINE = '244' -RPL_LUSERCLIENT = '251' -RPL_LUSEROP = '252' -RPL_LUSERUNKNOWN = '253' -RPL_LUSERCHANNELS = '254' -RPL_LUSERME = '255' -RPL_ADMINME = '256' -RPL_ADMINLOC1 = '257' -RPL_ADMINLOC2 = '258' -RPL_ADMINEMAIL = '259' -RPL_TRACELOG = '261' -RPL_TRYAGAIN = '263' -RPL_NONE = '300' -RPL_AWAY = '301' -RPL_USERHOST = '302' -RPL_ISON = '303' -RPL_UNAWAY = '305' -RPL_NOWAWAY = '306' -RPL_WHOISUSER = '311' -RPL_WHOISSERVER = '312' -RPL_WHOISOPERATOR = '313' -RPL_WHOWASUSER = '314' -RPL_ENDOFWHO = '315' -RPL_WHOISIDLE = '317' -RPL_ENDOFWHOIS = '318' -RPL_WHOISCHANNELS = '319' -RPL_LIST = '322' -RPL_LISTEND = '323' -RPL_CHANNELMODEIS = '324' -RPL_NOTOPIC = '331' -RPL_TOPIC = '332' -RPL_INVITING = '341' -RPL_INVITELIST = '346' -RPL_ENDOFINVITELIST = '347' -RPL_EXCEPTLIST = '348' -RPL_ENDOFEXCEPTLIST = '349' -RPL_VERSION = '351' -RPL_WHOREPLY = '352' -RPL_NAMREPLY = '353' -RPL_LINKS = '364' -RPL_ENDOFLINKS = '365' -RPL_ENDOFNAMES = '366' -RPL_BANLIST = '367' -RPL_ENDOFBANLIST = '368' -RPL_ENDOFWHOWAS = '369' -RPL_INFO = '371' -RPL_MOTD = '372' -RPL_ENDOFINFO = '374' -RPL_MOTDSTART = '375' -RPL_ENDOFMOTD = '376' -RPL_YOUREOPER = '381' -RPL_REHASHING = '382' -RPL_YOURESERVICE = '383' -RPL_TIME = '391' -RPL_USERSSTART = '392' -RPL_USERS = '393' -RPL_ENDOFUSERS = '394' -RPL_NOUSERS = '395' -ERR_NOSUCHNICK = '401' -ERR_NOSUCHSERVER = '402' -ERR_NOSUCHCHANNEL = '403' -ERR_CANNOTSENDTOCHAN = '404' -ERR_TOOMANYCHANNELS = '405' -ERR_WASNOSUCHNICK = '406' -ERR_TOOMANYTARGETS = '407' -ERR_NOSUCHSERVICE = '408' -ERR_NOORIGIN = '409' -ERR_NORECIPIENT = '411' -ERR_NOTEXTTOSEND = '412' -ERR_NOTOPLEVEL = '413' -ERR_WILDTOPLEVEL = '414' -ERR_BADMASK = '415' -ERR_UNKNOWNCOMMAND = '421' -ERR_NOMOTD = '422' -ERR_NOADMININFO = '423' -ERR_FILEERROR = '424' -ERR_NONICKNAMEGIVEN = '431' -ERR_ERRONEUSNICKNAME = '432' -ERR_NICKNAMEINUSE = '433' -ERR_NICKCOLLISION = '436' -ERR_USERNOTINCHANNEL = '441' -ERR_NOTONCHANNEL = '442' -ERR_USERONCHANNEL = '443' -ERR_NOLOGIN = '444' -ERR_SUMMONDISABLED = '445' -ERR_USERSDISABLED = '446' -ERR_NOTREGISTERED = '451' -ERR_NEEDMOREPARAMS = '461' -ERR_ALREADYREGISTRED = '462' -ERR_NOPERMFORHOST = '463' -ERR_PASSWDMISMATCH = '464' -ERR_YOUREBANNEDCREEP = '465' -ERR_KEYSET = '467' -ERR_CHANNELISFULL = '471' -ERR_UNKNOWNMODE = '472' -ERR_INVITEONLYCHAN = '473' -ERR_BANNEDFROMCHAN = '474' -ERR_BADCHANNELKEY = '475' -ERR_BADCHANMASK = '476' -ERR_BANLISTFULL = '478' -ERR_NOPRIVILEGES = '481' -ERR_CHANOPRIVSNEEDED = '482' -ERR_CANTKILLSERVER = '483' -ERR_UNIQOPRIVSNEEDED = '485' -ERR_NOOPERHOST = '491' -ERR_UMODEUNKNOWNFLAG = '501' -ERR_USERSDONTMATCH = '502' -RPL_STARTTLS = '670' -ERR_STARTTLS = '691' -RPL_MONONLINE = '730' -RPL_MONOFFLINE = '731' -RPL_MONLIST = '732' -RPL_ENDOFMONLIST = '733' -ERR_MONLISTFULL = '734' -RPL_LOGGEDIN = '900' -RPL_LOGGEDOUT = '901' -ERR_NICKLOCKED = '902' -RPL_SASLSUCCESS = '903' -ERR_SASLFAIL = '904' -ERR_SASLTOOLONG = '905' -ERR_SASLABORTED = '906' -ERR_SASLALREADY = '907' -RPL_SASLMECHS = '908' diff --git a/skeleton/core/database.py b/skeleton/core/database.py deleted file mode 100644 index 2d847eb..0000000 --- a/skeleton/core/database.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) -# database.py - -import os -import re -import sqlite3 - -# Globals -db = sqlite3.connect(os.path.join('data', 'bot.db'), check_same_thread=False) -sql = db.cursor() - -def check(): - tables = sql.execute('SELECT name FROM sqlite_master WHERE type=\'table\'').fetchall() - if not len(tables): - sql.execute('CREATE TABLE IGNORE (IDENT TEXT NOT NULL);') - db.commit() - -class Ignore: - def add(ident): - sql.execute('INSERT INTO IGNORE (IDENT) VALUES (?)', (ident,)) - db.commit() - - def check(ident): - for ignored_ident in Ignore.read(): - if re.compile(ignored_ident.replace('*','.*')).search(ident): - return True - return False - - def read(): - return list(item[0] for item in sql.execute('SELECT IDENT FROM IGNORE ORDER BY IDENT ASC').fetchall()) - - def remove(ident): - sql.execute('DELETE FROM IGNORE WHERE IDENT=?', (ident,)) - db.commit() diff --git a/skeleton/core/debug.py b/skeleton/core/debug.py deleted file mode 100644 index cf48b24..0000000 --- a/skeleton/core/debug.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) -# debug.py - -import ctypes -import logging -import os -import sys -import time - -from logging.handlers import RotatingFileHandler - -import config - -def check_libs(): - if config.connection.proxy: - try: - import socks - except ImportError: - error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)') - -def check_privileges(): - if check_windows(): - if ctypes.windll.shell32.IsUserAnAdmin() != 0: - return True - else: - return False - else: - if os.getuid() == 0 or os.geteuid() == 0: - return True - else: - return False - -def check_version(major): - if sys.version_info.major == major: - return True - else: - return False - -def check_windows(): - if os.name == 'nt': - return True - else: - return False - -def clear(): - if check_windows(): - os.system('cls') - else: - os.system('clear') - -def error(msg, reason=None): - if reason: - logging.debug(f'[!] - {msg} ({reason})') - else: - logging.debug('[!] - ' + msg) - -def error_exit(msg): - raise SystemExit('[!] - ' + msg) - -def info(): - clear() - logging.debug('#'*56) - logging.debug('#{0}#'.format(''.center(54))) - logging.debug('#{0}#'.format('IRC Bot Skeleton'.center(54))) - logging.debug('#{0}#'.format('Developed by acidvegas in Python'.center(54))) - logging.debug('#{0}#'.format('https://git.acid.vegas/skeleton'.center(54))) - logging.debug('#{0}#'.format(''.center(54))) - logging.debug('#'*56) - -def irc(msg): - logging.debug('[~] - ' + msg) - -def setup_logger(): - stream_handler = logging.StreamHandler(sys.stdout) - if config.settings.log: - log_file = os.path.join(os.path.join('data','logs'), 'bot.log') - file_handler = RotatingFileHandler(log_file, maxBytes=256000, backupCount=3) - logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(file_handler,stream_handler)) - else: - logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(stream_handler,)) diff --git a/skeleton/core/functions.py b/skeleton/core/functions.py deleted file mode 100644 index 020acb9..0000000 --- a/skeleton/core/functions.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) -# functions.py - -import re - -import config - -def is_admin(ident): - return re.compile(config.settings.admin.replace('*','.*')).search(ident) diff --git a/skeleton/core/irc.py b/skeleton/core/irc.py deleted file mode 100644 index 695139e..0000000 --- a/skeleton/core/irc.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) -# irc.py - -import socket -import time - -import config -import constants -import database -import debug -import functions - -# Load optional modules -if config.connection.ssl: - import ssl -if config.connection.proxy: - try: - import sock - except ImportError: - debug.error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)') # Required for proxy support. - -def color(msg, foreground, background=None): - if background: - return f'\x03{foreground},{background}{msg}{constants.reset}' - else: - return f'\x03{foreground}{msg}{constants.reset}' - -class IRC(object): - def __init__(self): - self.last = 0 - self.slow = False - self.sock = None - self.status = True - - def connect(self): - try: - self.create_socket() - self.sock.connect((config.connection.server, config.connection.port)) - self.register() - except socket.error as ex: - debug.error('Failed to connect to IRC server.', ex) - Events.disconnect() - else: - self.listen() - - def create_socket(self): - family = socket.AF_INET6 if config.connection.ipv6 else socket.AF_INET - if config.connection.proxy: - proxy_server, proxy_port = config.connection.proxy.split(':') - self.sock = socks.socksocket(family, socket.SOCK_STREAM) - self.sock.setblocking(0) - self.sock.settimeout(15) - self.sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port)) - else: - self.sock = socket.socket(family, socket.SOCK_STREAM) - if config.connection.vhost: - self.sock.bind((config.connection.vhost, 0)) - if config.connection.ssl: - ctx = ssl.SSLContext() - if config.cert.file: - ctx.load_cert_chain(config.cert.file, config.cert.key, config.cert.password) - if config.connection.ssl_verify: - ctx.verify_mode = ssl.CERT_REQUIRED - ctx.load_default_certs() - else: - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - self.sock = ctx.wrap_socket(self.sock) - - def listen(self): - while True: - try: - data = self.sock.recv(2048).decode('utf-8') - for line in (line for line in data.split('\r\n') if line): - debug.irc(line) - if len(line.split()) >= 2: - Events.handle(line) - except (UnicodeDecodeError,UnicodeEncodeError): - pass - except Exception as ex: - debug.error('Unexpected error occured.', ex) - break - Events.disconnect() - - def register(self): - if config.login.network: - Commands.raw('PASS ' + config.login.network) - Commands.raw(f'USER {config.ident.username} 0 * :{config.ident.realname}') - Commands.nick(config.ident.nickname) - - - -class Commands: - def action(chan, msg): - Commands.sendmsg(chan, f'\x01ACTION {msg}\x01') - - def ctcp(target, data): - Commands.sendmsg(target, f'\001{data}\001') - - def error(target, data, reason=None): - if reason: - Commands.sendmsg(target, '[{0}] {1} {2}'.format(color('!', constants.red), data, color('({0})'.format(reason), constants.grey))) - else: - Commands.sendmsg(target, '[{0}] {1}'.format(color('!', constants.red), data)) - - def identify(nick, password): - Commands.sendmsg('nickserv', f'identify {nick} {password}') - - def invite(nick, chan): - Commands.raw(f'INVITE {nick} {chan}') - - def join_channel(chan, key=None): - Commands.raw(f'JOIN {chan} {key}') if key else Commands.raw('JOIN ' + chan) - - def mode(target, mode): - Commands.raw(f'MODE {target} {mode}') - - def nick(nick): - Commands.raw('NICK ' + nick) - - def notice(target, msg): - Commands.raw(f'NOTICE {target} :{msg}') - - def oper(user, password): - Commands.raw(f'OPER {user} {password}') - - def part(chan, msg=None): - Commands.raw(f'PART {chan} {msg}') if msg else Commands.raw('PART ' + chan) - - def quit(msg=None): - Commands.raw('QUIT :' + msg) if msg else Commands.raw('QUIT') - - def raw(msg): - Bot.sock.send(bytes(msg + '\r\n', 'utf-8')) - - def sendmsg(target, msg): - Commands.raw(f'PRIVMSG {target} :{msg}') - - def topic(chan, text): - Commands.raw(f'TOPIC {chan} :{text}') - - - -class Events: - def connect(): - if config.settings.modes: - Commands.mode(config.ident.nickname, '+' + config.settings.modes) - if config.login.nickserv: - Commands.identify(config.ident.nickname, config.login.nickserv) - if config.login.operator: - Commands.oper(config.ident.username, config.login.operator) - Commands.join_channel(config.connection.channel, config.connection.key) - - def ctcp(nick, chan, msg): - pass - - def disconnect(): - Bot.sock.close() - time.sleep(config.throttle.reconnect) - Bot.connect() - - def invite(nick, chan): - if nick == config.ident.nickname and chan == config.connection.channe: - Commands.join_channel(config.connection.channel, config.connection.key) - - def join_channel(nick, chan): - pass - - def kick(nick, chan, kicked): - if kicked == config.ident.nickname and chan == config.connection.channel: - time.sleep(config.throttle.rejoin) - Commands.join_channel(chan, config.connection.key) - - def message(nick, ident, chan, msg): - try: - if chan == config.connection.channel and Bot.status: - if msg.startswith(config.settings.cmd_char): - if not database.Ignore.check(ident): - if time.time() - Bot.last < config.throttle.command and not functions.is_admin(ident): - if not Bot.slow: - Commands.sendmsg(chan, color('Slow down nerd!', constants.red)) - Bot.slow = True - elif Bot.status or functions.is_admin(ident): - Bot.slow = False - args = msg.split() - if msg == 'test': - while True: - Commands.raw('WHO') - time.sleep(0.5) - ''' - if len(args) == 1: - if cmd == 'test': - Commands.sendmsg(chan, 'It works!') - elif len(args) >= 2: - if cmd == 'echo': - Commands.sendmsg(chan, args) - ''' - Bot.last = time.time() - except Exception as ex: - Commands.error(chan, 'Command threw an exception.', ex) - - def nick_in_use(): - debug.error('The bot is already running or nick is in use.') - - def part(nick, chan): - pass - - def private(nick, ident, msg): - if functions.is_admin(ident): - args = msg.split() - if msg == '.ignore': - ignores = database.Ignore.read() - if ignores: - Commands.sendmsg(nick, '[{0}]'.format(color('Ignore List', constants.purple))) - for user in ignores: - Commands.sendmsg(nick, color(user, constants.yellow)) - Commands.sendmsg(nick, '{0} {1}'.format(color('Total:', constants.light_blue), color(len(ignores), constants.grey))) - else: - Commands.error(nick, 'Ignore list is empty!') - elif msg == '.off': - Bot.status = False - Commands.sendmsg(nick, color('OFF', constants.red)) - elif msg == '.on': - Bot.status = True - Commands.sendmsg(nick, color('ON', constants.green)) - elif len(args) == 3: - if args[0] == '.ignore': - if args[1] == 'add': - user_ident = args[2] - if user_ident not in database.Ignore.hosts(): - database.Ignore.add(nickname, user_ident) - Commands.sendmsg(nick, 'Ident {0} to the ignore list.'.format(color('added', constants.green))) - else: - Commands.error(nick, 'Ident is already on the ignore list.') - elif args[1] == 'del': - user_ident = args[2] - if user_ident in database.Ignore.hosts(): - database.Ignore.remove(user_ident) - Commands.sendmsg(nick, 'Ident {0} from the ignore list.'.format(color('removed', constants.red))) - else: - Commands.error(nick, 'Ident does not exist in the ignore list.') - - def quit(nick): - pass - - def handle(data): - args = data.split() - if data.startswith('ERROR :Closing Link:'): - raise Exception('Connection has closed.') - elif args[0] == 'PING': - Commands.raw('PONG ' + args[1][1:]) - elif args[1] == constants.RPL_WELCOME: - Events.connect() - elif args[1] == constants.ERR_NICKNAMEINUSE: - Events.nick_in_use() - elif args[1] == constants.INVITE and len(args) == 4: - nick = args[0].split('!')[0][1:] - chan = args[3][1:] - Events.invite(nick, chan) - elif args[1] == constants.JOIN and len(args) == 3: - nick = args[0].split('!')[0][1:] - chan = args[2][1:] - Events.join_channel(nick, chan) - elif args[1] == constants.KICK and len(args) >= 4: - nick = args[0].split('!')[0][1:] - chan = args[2] - kicked = args[3] - Events.kick(nick, chan, kicked) - elif args[1] == constants.PART and len(args) >= 3: - nick = args[0].split('!')[0][1:] - chan = args[2] - Events.part(nick, chan) - elif args[1] == constants.PRIVMSG and len(args) >= 4: - nick = args[0].split('!')[0][1:] - ident = args[0].split('!')[1] - chan = args[2] - msg = data.split(f'{args[0]} PRIVMSG {chan} :')[1] - if msg.startswith('\001'): - Events.ctcp(nick, chan, msg) - elif chan == config.ident.nickname: - Events.private(nick, ident, msg) - else: - Events.message(nick, ident, chan, msg) - elif args[1] == constants.QUIT: - nick = args[0].split('!')[0][1:] - Events.quit(nick) - -Bot = IRC() diff --git a/skeleton/data/cert/.gitignore b/skeleton/data/cert/.gitignore deleted file mode 100644 index 86d0cb2..0000000 --- a/skeleton/data/cert/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/skeleton/data/logs/.gitignore b/skeleton/data/logs/.gitignore deleted file mode 100644 index 86d0cb2..0000000 --- a/skeleton/data/logs/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/skeleton/modules/.gitignore b/skeleton/modules/.gitignore deleted file mode 100644 index 86d0cb2..0000000 --- a/skeleton/modules/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/skeleton/skeleton.py b/skeleton/skeleton.py deleted file mode 100644 index 3eab7a2..0000000 --- a/skeleton/skeleton.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) -# skeleton.py - -import os -import sys - -sys.dont_write_bytecode = True -os.chdir(sys.path[0] or '.') -sys.path += ('core','modules') - -import debug - -debug.setup_logger() -debug.info() -if not debug.check_version(3): - debug.error_exit('Python 3 is required!') -if debug.check_privileges(): - debug.error_exit('Do not run as admin/root!') -debug.check_libs() -import database -database.check() -import irc -irc.Bot.connect()