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

View File

@ -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)
___
###### 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)

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
# 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
# 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()
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)
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 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:
class Bot():
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)
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:
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()
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.
if __name__ == '__main__':
if not os.path.exists('logs'):
os.makedirs('logs')
: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 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'))
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))
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)
if __name__ == '__main__':
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()
Bot = IrcBot()
asyncio.run(Bot.connect())
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())