Simplified, asyncio bot revamped for vortex with commenting

This commit is contained in:
Dionysus 2023-10-02 23:32:37 -04:00
parent 35d810d89b
commit b20e882809
Signed by: acidvegas
GPG Key ID: EF4B922DB85DC9DE
8 changed files with 170 additions and 401 deletions

View File

@ -1,6 +1,6 @@
ISC License 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 Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

View File

@ -3,17 +3,14 @@
## Requirements ## Requirements
* [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python)* * [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 ## 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. 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 ## IRC RCF Reference
- http://www.irchelp.org/protocol/rfc/ - http://www.irchelp.org/protocol/rfc/
## Mirrors ___
- [acid.vegas](https://acid.vegas/skeleton) *(main)*
- [GitHub](https://github.com/acidvegas/skeleton) ###### Mirrors
- [GitLab](https://gitlab.com/acidvegas/skeleton) [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)

View File

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

View File

@ -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}')

View File

@ -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

View File

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

View File

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

View File

@ -1,177 +1,186 @@
#!/usr/bin/env python #!/usr/bin/env python
# Asyncronoua IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton) # Skeleton IRC bot - developed by acidvegas in python (https://git.acid.vegas/skeleton)
# skeleton.py import argparse
import asyncio import asyncio
import logging import logging
import logging.handlers 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 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 = ssl.create_default_context()
if not config.connection.ssl_verify: ctx.verify_mode = ssl.CERT_NONE # Comment out this line to verify hosts
ctx.check_hostname = False #ctx.load_cert_chain('/path/to/cert', password='loldongs')
ctx.verify_mode = ssl.CERT_NONE
if config.cert.file:
ctx.load_cert_chain(config.cert.file, password=config.cert.password)
return ctx return ctx
################################################## class Bot():
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): def __init__(self):
self.options = { self.nickname = 'skeleton'
'host' : config.connection.server, self.username = 'skelly'
'port' : config.connection.port, self.realname = 'Developement Bot'
'limit' : 1024, self.reader = None
'ssl' : ssl_ctx() if config.connection.ssl else None, self.writer = 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 async def action(self, chan: str, msg: str):
} '''
self.reader, self.writer = (None, None) 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): async def connect(self):
'''Connect to the IRC server.'''
while True:
try: try:
self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**self.options), timeout=config.throttle.timeout) options = {
if config.login.network: 'host' : args.server,
Command.raw('PASS ' + config.login.network) 'port' : args.port if args.port else 6697 if args.ssl else 6667,
Command.raw(f'USER {config.ident.username} 0 * :{config.ident.realname}') 'limit' : 1024, # Buffer size in bytes (don't change this unless you know what you're doing)
Command.raw('NICK ' + config.ident.nickname) 'ssl' : ssl_ctx() if args.ssl else None,
except: 'family' : 10 if args.ipv6 else 2, # 10 = AF_INET6 (IPv6), 2 = AF_INET (IPv4)
logging.exception('Failed to connect to IRC server!') 'local_addr' : args.vhost if args.vhost else None # Can we just leave this as args.vhost?
else: }
await Event.handler() 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.
if __name__ == '__main__': :param data: The data received from the IRC server.
if not os.path.exists('logs'): '''
os.makedirs('logs') 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 = logging.StreamHandler()
sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p')) sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p'))
if config.settings.log: if to_file:
fh = logging.handlers.RotatingFileHandler('logs/debug.log', maxBytes=250000, backupCount=7, encoding='utf-8') 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')) 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)) logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh))
del fh,sh
else: else:
logging.basicConfig(level=logging.NOTSET, handlers=(sh,)) logging.basicConfig(level=logging.NOTSET, handlers=(sh,))
del sh
print('#'*56) if __name__ == '__main__':
print('#{:^54}#'.format('')) parser = argparse.ArgumentParser(description="Connect to an IRC server.") # The arguments without -- are required arguments.
print('#{:^54}#'.format('Asyncronous IRC Bot Skeleton')) parser.add_argument("server", help="The IRC server address.")
print('#{:^54}#'.format('Developed by acidvegas in Python')) parser.add_argument("channel", help="The IRC channel to join.")
print('#{:^54}#'.format('https://acid.vegas/skeleton')) parser.add_argument("--password", help="The password for the IRC server.")
print('#{:^54}#'.format('')) 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.
print('#'*56) 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()
Bot = IrcBot() print(f"Connecting to {args.server}:{args.port} (SSL: {args.ssl}) and joining {args.channel} (Key: {args.key or 'None'})")
asyncio.run(Bot.connect())
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())