diff --git a/LICENSE b/LICENSE
@@ -1,6 +1,6 @@
ISC License
-Copyright (c) 2021, acidvegas <acid.vegas@acid.vegas>
+Copyright (c) 2023, 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
diff --git a/README.md b/README.md
@@ -3,17 +3,14 @@
## 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)*
## 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
-- [acid.vegas](https://acid.vegas/skeleton) *(main)*
-- [GitHub](https://github.com/acidvegas/skeleton)
-- [GitLab](https://gitlab.com/acidvegas/skeleton)
-\ No newline at end of file
+___
+
+###### Mirrors
+[acid.vegas](https://git.acid.vegas/skeleton) • [GitHub](https://github.com/acidvegas/skeleton) • [GitLab](https://gitlab.com/acidvegas/skeleton) • [SuperNETs](https://git.supernets.org/acidvegas/skeleton)
+\ No newline at end of file
diff --git a/advanced/core/bot.py b/advanced/core/bot.py
@@ -1,54 +0,0 @@
-#!/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
@@ -1,43 +0,0 @@
-#!/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
@@ -1,39 +0,0 @@
-#!/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
@@ -1,61 +0,0 @@
-#!/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
@@ -1,40 +0,0 @@
-#!/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
@@ -1,177 +1,186 @@
#!/usr/bin/env python
-# Asyncronoua IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
-# skeleton.py
-
+# Skeleton IRC bot - developed by acidvegas in python (https://git.acid.vegas/skeleton)
+import argparse
import asyncio
import logging
import logging.handlers
-import os
-import random
-import time
-
-##################################################
-
-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
-
- 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
-
-##################################################
-
-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 Command:
- def join_channel(chan, key=None):
- Command.raw(f'JOIN {chan} {key}') if key else Command.raw('JOIN ' + chan)
-
- def mode(target, mode):
- Command.raw(f'MODE {target} {mode}')
-
- def nick(new_nick):
- Command.raw('NICK ' + new_nick)
-
- def raw(data):
- Bot.writer.write(data[:510].encode('utf-8') + b'\r\n')
-
- def sendmsg(target, msg):
- Command.raw(f'PRIVMSG {target} :{msg}')
-
-##################################################
-
-class Event:
- 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)
-
- async def disconnect():
- Bot.writer.close()
- await bot.writer.wait_closed()
- asyncio.sleep(config.throttle.reconnect)
-
- def nick_in_use():
- new_nick = 'a' + str(random.randint(1000,9999))
- Command.nick(new_nick)
-
- async def handler():
- while not Bot.reader.at_eof():
- try:
- 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!')
-
-##################################################
-
-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()
-
-##################################################
+import ssl
+
+# 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: str, foreground: str, background: str='') -> str:
+ '''
+ Color a string with the specified foreground and background colors.
+
+ :param msg: The string to color.
+ :param foreground: The foreground color to use.
+ :param background: The background color to use.
+ '''
+ return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}'
+
+def ssl_ctx() -> ssl.SSLContext:
+ '''Create a SSL context for the connection.'''
+ ctx = ssl.create_default_context()
+ ctx.verify_mode = ssl.CERT_NONE # Comment out this line to verify hosts
+ #ctx.load_cert_chain('/path/to/cert', password='loldongs')
+ return ctx
+
+class Bot():
+ def __init__(self):
+ self.nickname = 'skeleton'
+ self.username = 'skelly'
+ self.realname = 'Developement Bot'
+ self.reader = None
+ self.writer = None
+
+ async def action(self, chan: str, msg: str):
+ '''
+ Send an ACTION to the IRC server.
+
+ :param chan: The channel to send the ACTION to.
+ :param msg: The message to send to the channel.
+ '''
+ await self.sendmsg(chan, f'\x01ACTION {msg}\x01')
+
+ def raw(self, data: str):
+ '''
+ Send raw data to the IRC server.
+
+ :param data: The raw data to send to the IRC server. (512 bytes max including crlf)
+ '''
+ self.writer.write(data[:510].encode('utf-8') + b'\r\n')
+
+ async def sendmsg(self, target: str, msg: str):
+ '''
+ Send a PRIVMSG to the IRC server.
+
+ :param target: The target to send the PRIVMSG to. (channel or user)
+ :param msg: The message to send to the target.
+ '''
+ await self.raw(f'PRIVMSG {target} :{msg}')
+
+ async def connect(self):
+ '''Connect to the IRC server.'''
+ while True:
+ try:
+ options = {
+ 'host' : args.server,
+ 'port' : args.port if args.port else 6697 if args.ssl else 6667,
+ 'limit' : 1024, # Buffer size in bytes (don't change this unless you know what you're doing)
+ 'ssl' : ssl_ctx() if args.ssl else None,
+ 'family' : 10 if args.ipv6 else 2, # 10 = AF_INET6 (IPv6), 2 = AF_INET (IPv4)
+ 'local_addr' : args.vhost if args.vhost else None # Can we just leave this as args.vhost?
+ }
+ self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) # 15 second timeout
+ if args.password:
+ await self.raw('PASS ' + args.password) # Rarely used, but IRCds may require this
+ await self.raw(f'USER {self.username} 0 * :{self.realname}') # These lines must be sent upon connection
+ await self.raw('NICK ' + self.nickname) # They are to identify the bot to the server
+ while not self.reader.at_eof():
+ data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 300) # 5 minute ping timeout
+ await self.handle(data.decode('utf-8').strip()) # Handle the data received from the IRC server
+ except Exception as ex:
+ logging.error(f'failed to connect to {self.server} ({ex})')
+ finally:
+ await asyncio.sleep(30) # Wait 30 seconds before reconnecting
+
+ async def handle(self, data: str):
+ '''
+ Handle the data received from the IRC server.
+
+ :param data: The data received from the IRC server.
+ '''
+ try:
+ args = data.split()
+ if data.startswith('ERROR :Closing Link:'):
+ raise Exception('BANNED')
+ if args[0] == 'PING':
+ await self.raw('PONG ' + args[1]) # Respond to the server's PING request with a PONG to prevent ping timeout
+ elif args[1] == '001': # RPL_WELCOME
+ await self.raw(f'MODE {self.nickname} +B') # Set user mode +B (Bot)
+ await self.sendmsg('NickServ', 'IDENTIFY {self.nickname} simps0nsfan420') # Identify to NickServ
+ await self.raw('OPER MrSysadmin fartsimps0n1337') # Oper up
+ await asyncio.sleep(10) # Wait 10 seconds before joining the channel (required by some IRCds to wait before JOIN)
+ await self.raw(f'JOIN {args.channel} {args.key}') # Join the channel (if no key was provided, this will still work as the key will default to an empty string)
+ elif args[1] == '433': # ERR_NICKNAMEINUSE
+ self.nickname += '_' # If the nickname is already in use, append an underscore to the end of it
+ await self.raw('NICK ' + self.nickname) # Send the new nickname to the server
+ elif args[1] == 'KICK':
+ chan = args[2]
+ kicked = args[3]
+ if kicked == self.nickname:
+ await asyncio.sleep(3)
+ await self.raw(f'JOIN {chan}')
+ elif args[1] == 'PRIVMSG':
+ ident = args[0][1:]
+ nick = args[0].split('!')[0][1:]
+ target = args[2]
+ msg = ' '.join(args[3:])[1:]
+ if target == self.nickname:
+ pass # Handle private messages here
+ if target.startswith('#'): # Channel message
+ if msg.startswith('!'):
+ if msg == '!hello':
+ self.sendmsg(chan, f'Hello {nick}!')
+ except (UnicodeDecodeError, UnicodeEncodeError):
+ pass # Some IRCds allow invalid UTF-8 characters, this is a very important exception to catch
+ except Exception as ex:
+ logging.exception(f'Unknown error has occured! ({ex})')
+
+
+def setup_logger(log_filename: str, to_file: bool = False):
+ '''
+ Set up logging to console & optionally to file.
+
+ :param log_filename: The filename of the log file
+ '''
+ sh = logging.StreamHandler()
+ sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p'))
+ if to_file:
+ fh = logging.handlers.RotatingFileHandler(log_filename+'.log', maxBytes=250000, backupCount=3, encoding='utf-8') # Max size of 250KB, 3 backups
+ fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(filename)s.%(funcName)s.%(lineno)d | %(message)s', '%Y-%m-%d %I:%M %p')) # We can be more verbose in the log file
+ logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh))
+ else:
+ logging.basicConfig(level=logging.NOTSET, handlers=(sh,))
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
+ parser = argparse.ArgumentParser(description="Connect to an IRC server.") # The arguments without -- are required arguments.
+ parser.add_argument("server", help="The IRC server address.")
+ parser.add_argument("channel", help="The IRC channel to join.")
+ parser.add_argument("--password", help="The password for the IRC server.")
+ parser.add_argument("--port", type=int, help="The port number for the IRC server.") # Port is optional, will default to 6667/6697 depending on SSL.
+ parser.add_argument("--ssl", action="store_true", help="Use SSL for the connection.")
+ parser.add_argument("--v4", action="store_true", help="Use IPv4 for the connection.")
+ parser.add_argument("--v6", action="store_true", help="Use IPv6 for the connection.")
+ parser.add_argument("--key", default="", help="The key (password) for the IRC channel, if required.")
+ parser.add_argument("--vhost", help="The VHOST to use for connection.")
+ args = parser.parse_args()
+
+ print(f"Connecting to {args.server}:{args.port} (SSL: {args.ssl}) and joining {args.channel} (Key: {args.key or 'None'})")
+
+ setup_logger('skeleton', to_file=True) # Optionally, you can log to a file, change to_file to False to disable this.
+
+ bot = Bot() # We define this here as an object so we can call it from an outside function if we need to.
+
+ asyncio.run(bot.connect())
+\ No newline at end of file
| | | | | | | |