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()
| | | | | | | | | | | | |