From 4a96baa37842bbd27d977662da14d071898106eb Mon Sep 17 00:00:00 2001 From: acidvegas Date: Fri, 28 Jun 2019 02:23:40 -0400 Subject: [PATCH] Initial commit --- LICENSE | 15 ++ README.md | 12 ++ skeleton.py | 275 +++++++++++++++++++++++++++++++++ skeleton/core/config.py | 40 +++++ skeleton/core/constants.py | 229 +++++++++++++++++++++++++++ skeleton/core/database.py | 35 +++++ skeleton/core/debug.py | 81 ++++++++++ skeleton/core/functions.py | 10 ++ skeleton/core/irc.py | 283 ++++++++++++++++++++++++++++++++++ skeleton/data/cert/.gitignore | 4 + skeleton/data/logs/.gitignore | 4 + skeleton/modules/.gitignore | 4 + skeleton/skeleton.py | 24 +++ 13 files changed, 1016 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 skeleton.py create mode 100644 skeleton/core/config.py create mode 100644 skeleton/core/constants.py create mode 100644 skeleton/core/database.py create mode 100644 skeleton/core/debug.py create mode 100644 skeleton/core/functions.py create mode 100644 skeleton/core/irc.py create mode 100644 skeleton/data/cert/.gitignore create mode 100644 skeleton/data/logs/.gitignore create mode 100644 skeleton/modules/.gitignore create mode 100644 skeleton/skeleton.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b63b809 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2019, 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 +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5929ca7 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +###### 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 +- http://www.irchelp.org/protocol/rfc/ + +###### 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/skeleton.py b/skeleton.py new file mode 100644 index 0000000..2324a3d --- /dev/null +++ b/skeleton.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python +# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) + +import socket +import time +import threading + +# Configuration +_connection = {'server':'irc.server.com', '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' + +def color(msg, foreground, background=None): + if background: + return f'\x03{foreground},{background}{msg}{reset}' + else: + return f'\x03{foreground}{msg}{reset}' + +def debug(msg): + print(f'{get_time()} | [~] - {msg}') + +def error(msg, reason=None): + if reason: + print(f'{get_time()} | [!] - {msg} ({reason})') + else: + print(f'{get_time()} | [!] - {msg}') + +def error_exit(msg): + raise SystemExit(f'{get_time()} | [!] - {msg}') + +def get_time(): + return time.strftime('%I:%M:%S') + +class IRC(object): + def __init__(self): + self._queue = list() + self._sock = None + + def _run(self): + Loop._loops() + self._connect() + + 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 _ctcp(target, data): + Bot._queue.append(target, f'\001{data}\001') + + def _invite(nick, chan): + Bot._queue.append(f'INVITE {nick} {chan}') + + def _join(chan, key=None): + Bot._queue.append(f'JOIN {chan} {key}') if key else Bot._queue.append('JOIN ' + chan) + + def _mode(target, mode): + Bot._queue.append(f'MODE {target} {mode}') + + 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(_setting['channel'], _settings['key']) + + def _ctcp(nick, chan, msg): + pass + + def _disconnect(): + Bot._sock.close() + Bot._queue = list() + time.sleep(15) + Bot._connect() + + 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': + Bot._queue.append(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) + 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: + 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']) + +# 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['verify'] +Bot = IRC() +Bot._run() \ No newline at end of file diff --git a/skeleton/core/config.py b/skeleton/core/config.py new file mode 100644 index 0000000..0bb989c --- /dev/null +++ b/skeleton/core/config.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) +# config.py + +class connection: + server = 'irc.server.com' + port = 6667 + proxy = None + ipv6 = False + ssl = False + ssl_verify = False + vhost = None + channel = '#dev' + key = None + +class cert: + key = None + file = None + password = None + +class ident: + nickname = 'skeleton' + username = 'skeleton' + 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 new file mode 100644 index 0000000..3ffbdb8 --- /dev/null +++ b/skeleton/core/constants.py @@ -0,0 +1,229 @@ +#!/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 new file mode 100644 index 0000000..2d847eb --- /dev/null +++ b/skeleton/core/database.py @@ -0,0 +1,35 @@ +#!/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 new file mode 100644 index 0000000..cf48b24 --- /dev/null +++ b/skeleton/core/debug.py @@ -0,0 +1,81 @@ +#!/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 new file mode 100644 index 0000000..020acb9 --- /dev/null +++ b/skeleton/core/functions.py @@ -0,0 +1,10 @@ +#!/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 new file mode 100644 index 0000000..223310d --- /dev/null +++ b/skeleton/core/irc.py @@ -0,0 +1,283 @@ +#!/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 msg 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 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 new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/skeleton/data/cert/.gitignore @@ -0,0 +1,4 @@ +# 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 new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/skeleton/data/logs/.gitignore @@ -0,0 +1,4 @@ +# 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 new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/skeleton/modules/.gitignore @@ -0,0 +1,4 @@ +# 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 new file mode 100644 index 0000000..3eab7a2 --- /dev/null +++ b/skeleton/skeleton.py @@ -0,0 +1,24 @@ +#!/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()