skeleton

- bot skeleton for irc
git clone git://git.acid.vegas/skeleton.git
Log | Files | Refs | Archive | README | LICENSE

commit 4a96baa37842bbd27d977662da14d071898106eb
Author: acidvegas <acid.vegas@acid.vegas>
Date: Fri, 28 Jun 2019 02:23:40 -0400

Initial commit

Diffstat:
ALICENSE | 16++++++++++++++++
AREADME.md | 13+++++++++++++
Askeleton.py | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Askeleton/core/config.py | 40++++++++++++++++++++++++++++++++++++++++
Askeleton/core/constants.py | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Askeleton/core/database.py | 35+++++++++++++++++++++++++++++++++++
Askeleton/core/debug.py | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Askeleton/core/functions.py | 10++++++++++
Askeleton/core/irc.py | 283+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Askeleton/data/cert/.gitignore | 5+++++
Askeleton/data/logs/.gitignore | 5+++++
Askeleton/modules/.gitignore | 5+++++
Askeleton/skeleton.py | 24++++++++++++++++++++++++

13 files changed, 1022 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2019, acidvegas <acid.vegas@acid.vegas>
+
+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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()