Compare commits
10 Commits
4af50c243c
...
3ffff551db
Author | SHA1 | Date | |
---|---|---|---|
3ffff551db | |||
bafe01a091 | |||
0df93b8f80 | |||
4e675ed26e | |||
5899410cd8 | |||
d11efff33e | |||
2560ba8496 | |||
038a09177f | |||
1910f37173 | |||
a4efb1ad08 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.log
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
ISC License
|
ISC License
|
||||||
|
|
||||||
Copyright (c) 2023, acidvegas <acid.vegas@acid.vegas>
|
Copyright (c) 2024, 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
|
||||||
|
25
README.md
25
README.md
@ -1,16 +1,17 @@
|
|||||||
# skeleton
|
|
||||||
> asyncronous bot skeleton for the internet relay chat protocol
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
* [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python)*
|
|
||||||
|
|
||||||
## Information
|
## Information
|
||||||
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 a basic skeleton for building your own bots for Internet Relay Chat *(IRC)* usage. It is asyncronous, can log to file, handle basic I/O, flood control, etc.
|
||||||
|
|
||||||
## IRC RCF Reference
|
A skeleton in Python & Golang *(beta)* are in this repository.
|
||||||
- http://www.irchelp.org/protocol/rfc/
|
|
||||||
|
|
||||||
___
|
Join **#dev** on **irc.supernets.org** for help building IRC bots frm scratch!
|
||||||
|
|
||||||
###### Mirrors
|
###### References
|
||||||
[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)
|
- **RFC1459** - [Internet Relay Chat Protocol](https://raw.githubusercontent.com/internet-relay-chat/archive/master/rfc/rfc1459.txt)
|
||||||
|
- **RFC2810** - [Internet Relay Chat: Architecture](https://raw.githubusercontent.com/internet-relay-chat/archive/master/rfc/rfc2810.txt)
|
||||||
|
- **RFC2811** - [Internet Relay Chat: Channel Management](https://raw.githubusercontent.com/internet-relay-chat/archive/master/rfc/rfc2811.txt)
|
||||||
|
- **RFC2812** - [Internet Relay Chat: Client Protocol](https://raw.githubusercontent.com/internet-relay-chat/archive/master/rfc/rfc2812.txt)
|
||||||
|
- **RFC2813** - [Internet Relay Chat: Server Protocol](https://raw.githubusercontent.com/internet-relay-chat/archive/master/rfc/rfc2813.txt)
|
||||||
|
- **RFC7194** - [Default Port for Internet Relay Chat (IRC) via TLS/SSL](https://raw.githubusercontent.com/internet-relay-chat/archive/master/rfc/rfc7194.txt)
|
||||||
|
- [Numerics & Events](https://raw.githubusercontent.com/internet-relay-chat/archive/master/numerics.txt)
|
||||||
|
|
||||||
|
###### Mirrors for this repository: [acid.vegas](https://git.acid.vegas/skeleton) • [SuperNETs](https://git.supernets.org/acidvegas/skeleton) • [GitHub](https://github.com/acidvegas/skeleton) • [GitLab](https://gitlab.com/acidvegas/skeleton) • [Codeberg](https://codeberg.org/acidvegas/skeleton)
|
||||||
|
325
skeleton.py
325
skeleton.py
@ -1,10 +1,15 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# Skeleton IRC bot - developed by acidvegas in python (https://git.acid.vegas/skeleton)
|
# irc bot skeleton - developed by acidvegas in python (https://git.acid.vegas/skeleton)
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import ssl
|
import ssl
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
cmd_flood = 3 # Delay between bot command usage in seconds (In this case, anything prefixed with a ! is a command)
|
||||||
|
|
||||||
# Formatting Control Characters / Color Codes
|
# Formatting Control Characters / Color Codes
|
||||||
bold = '\x02'
|
bold = '\x02'
|
||||||
@ -29,158 +34,212 @@ pink = '13'
|
|||||||
grey = '14'
|
grey = '14'
|
||||||
light_grey = '15'
|
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.
|
def color(msg: str, foreground: str, background: str = None) -> str:
|
||||||
:param foreground: The foreground color to use.
|
'''
|
||||||
:param background: The background color to use.
|
Color a string with the specified foreground and background colors.
|
||||||
'''
|
|
||||||
return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}'
|
: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(verify: bool = False, cert_path: str = None, cert_pass: str = None) -> ssl.SSLContext:
|
||||||
|
'''
|
||||||
|
Create a SSL context for the connection.
|
||||||
|
|
||||||
|
:param verify: Verify the SSL certificate.
|
||||||
|
:param cert_path: The path to the SSL certificate.
|
||||||
|
:param cert_pass: The password for the SSL certificate.
|
||||||
|
'''
|
||||||
|
ctx = ssl.create_default_context() if verify else ssl._create_unverified_context()
|
||||||
|
if cert_path:
|
||||||
|
ctx.load_cert_chain(cert_path) if not cert_pass else ctx.load_cert_chain(cert_path, cert_pass)
|
||||||
|
return ctx
|
||||||
|
|
||||||
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():
|
class Bot():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.nickname = 'skeleton'
|
self.nickname = 'skeleton'
|
||||||
self.username = 'skelly'
|
self.username = 'skelly'
|
||||||
self.realname = 'Developement Bot'
|
self.realname = 'Developement Bot'
|
||||||
self.reader = None
|
self.reader = None
|
||||||
self.writer = None
|
self.writer = None
|
||||||
|
self.last = time.time()
|
||||||
|
|
||||||
async def action(self, chan: str, msg: str):
|
|
||||||
'''
|
|
||||||
Send an ACTION to the IRC server.
|
|
||||||
|
|
||||||
:param chan: The channel to send the ACTION to.
|
async def action(self, chan: str, msg: str):
|
||||||
:param msg: The message to send to the channel.
|
'''
|
||||||
'''
|
Send an ACTION to the IRC server.
|
||||||
await self.sendmsg(chan, f'\x01ACTION {msg}\x01')
|
|
||||||
|
|
||||||
def raw(self, data: str):
|
:param chan: The channel to send the ACTION to.
|
||||||
'''
|
:param msg: The message to send to the channel.
|
||||||
Send raw data to the IRC server.
|
'''
|
||||||
|
await self.sendmsg(chan, f'\x01ACTION {msg}\x01')
|
||||||
|
|
||||||
: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):
|
async def raw(self, data: str):
|
||||||
'''
|
'''
|
||||||
Send a PRIVMSG to the IRC server.
|
Send raw data to the IRC server.
|
||||||
|
|
||||||
:param target: The target to send the PRIVMSG to. (channel or user)
|
:param data: The raw data to send to the IRC server. (512 bytes max including crlf)
|
||||||
:param msg: The message to send to the target.
|
'''
|
||||||
'''
|
self.writer.write(data[:510].encode('utf-8') + b'\r\n')
|
||||||
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):
|
async def sendmsg(self, target: str, msg: str):
|
||||||
'''
|
'''
|
||||||
Handle the data received from the IRC server.
|
Send a PRIVMSG to the IRC server.
|
||||||
|
|
||||||
:param data: The data received from the IRC server.
|
:param target: The target to send the PRIVMSG to. (channel or user)
|
||||||
'''
|
:param msg: The message to send to the target.
|
||||||
try:
|
'''
|
||||||
args = data.split()
|
await self.raw(f'PRIVMSG {target} :{msg}')
|
||||||
if data.startswith('ERROR :Closing Link:'):
|
|
||||||
raise Exception('BANNED')
|
|
||||||
if args[0] == 'PING':
|
async def connect(self):
|
||||||
await self.raw('PONG ' + args[1]) # Respond to the server's PING request with a PONG to prevent ping timeout
|
'''Connect to the IRC server.'''
|
||||||
elif args[1] == '001': # RPL_WELCOME
|
while True:
|
||||||
await self.raw(f'MODE {self.nickname} +B') # Set user mode +B (Bot)
|
try:
|
||||||
await self.sendmsg('NickServ', 'IDENTIFY {self.nickname} simps0nsfan420') # Identify to NickServ
|
options = {
|
||||||
await self.raw('OPER MrSysadmin fartsimps0n1337') # Oper up
|
'host' : args.server,
|
||||||
await asyncio.sleep(10) # Wait 10 seconds before joining the channel (required by some IRCds to wait before JOIN)
|
'port' : args.port if args.port else 6697 if args.ssl else 6667,
|
||||||
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)
|
'limit' : 1024, # Buffer size in bytes (don't change this unless you know what you're doing)
|
||||||
elif args[1] == '433': # ERR_NICKNAMEINUSE
|
'ssl' : ssl_ctx() if args.ssl else None,
|
||||||
self.nickname += '_' # If the nickname is already in use, append an underscore to the end of it
|
'family' : 10 if args.v6 else 2, # 10 = AF_INET6 (IPv6), 2 = AF_INET (IPv4)
|
||||||
await self.raw('NICK ' + self.nickname) # Send the new nickname to the server
|
'local_addr' : args.vhost if args.vhost else None # Can we just leave this as args.vhost?
|
||||||
elif args[1] == 'KICK':
|
}
|
||||||
chan = args[2]
|
self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) # 15 second timeout
|
||||||
kicked = args[3]
|
if args.password:
|
||||||
if kicked == self.nickname:
|
await self.raw('PASS ' + args.password) # Rarely used, but IRCds may require this
|
||||||
await asyncio.sleep(3)
|
await self.raw(f'USER {self.username} 0 * :{self.realname}') # These lines must be sent upon connection
|
||||||
await self.raw(f'JOIN {chan}')
|
await self.raw('NICK ' + self.nickname) # They are to identify the bot to the server
|
||||||
elif args[1] == 'PRIVMSG':
|
while not self.reader.at_eof():
|
||||||
ident = args[0][1:]
|
data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 300) # 5 minute ping timeout
|
||||||
nick = args[0].split('!')[0][1:]
|
await self.handle(data.decode('utf-8').strip()) # Handle the data received from the IRC server
|
||||||
target = args[2]
|
except Exception as ex:
|
||||||
msg = ' '.join(args[3:])[1:]
|
logging.error(f'failed to connect to {args.server} ({str(ex)})')
|
||||||
if target == self.nickname:
|
finally:
|
||||||
pass # Handle private messages here
|
await asyncio.sleep(30) # Wait 30 seconds before reconnecting
|
||||||
if target.startswith('#'): # Channel message
|
|
||||||
if msg.startswith('!'):
|
|
||||||
if msg == '!hello':
|
async def eventPRIVMSG(self, data: str):
|
||||||
self.sendmsg(chan, f'Hello {nick}! Do you like ' + color('colors?', green))
|
'''
|
||||||
except (UnicodeDecodeError, UnicodeEncodeError):
|
Handle the PRIVMSG event.
|
||||||
pass # Some IRCds allow invalid UTF-8 characters, this is a very important exception to catch
|
|
||||||
except Exception as ex:
|
:param data: The data received from the IRC server.
|
||||||
logging.exception(f'Unknown error has occured! ({ex})')
|
'''
|
||||||
|
parts = data.split()
|
||||||
|
ident = parts[0][1:] # nick!user@host
|
||||||
|
nick = parts[0].split('!')[0][1:] # Nickname of the user who sent the message
|
||||||
|
target = parts[2] # Channel or user (us) the message was sent to
|
||||||
|
msg = ' '.join(parts[3:])[1:]
|
||||||
|
if target == self.nickname: # Handle private messages
|
||||||
|
if ident == 'acidvegas!stillfree@big.dick.acid.vegas': # Admin only command based on ident
|
||||||
|
if msg.startswith('!raw') and len(msg.split()) > 1: # Only allow !raw if there is some data
|
||||||
|
option = ' '.join(msg.split()[1:]) # Everything after !raw is stored here
|
||||||
|
await self.raw(option) # Send raw data to the server FROM the bot
|
||||||
|
else:
|
||||||
|
await self.sendmsg(nick, 'Do NOT message me!') # Let's ignore anyone PM'ing the bot that isn't the admin
|
||||||
|
if target.startswith('#'): # Handle channel messages
|
||||||
|
if msg.startswith('!'):
|
||||||
|
if time.time() - self.last < cmd_flood: # Prevent command flooding
|
||||||
|
if not self.slow: # The self.slow variable is used so that a warning is only issued one time
|
||||||
|
self.slow = True
|
||||||
|
await self.sendmsg(target, color('Slow down nerd!', red))
|
||||||
|
else: # Once we confirm the user isn't command flooding, we can handle the commands
|
||||||
|
self.slow = False
|
||||||
|
if msg == '!help':
|
||||||
|
await self.action(target, 'explodes')
|
||||||
|
elif msg == '!ping':
|
||||||
|
await self.sendmsg(target, 'Pong!')
|
||||||
|
elif msg.startswith('!say') and len(msg.split()) > 1: # Only allow !say if there is something to say
|
||||||
|
option = ' '.join(msg.split()[1:]) # Everything after !say is stored here
|
||||||
|
await self.sendmsg(target, option)
|
||||||
|
self.last = time.time() # Update the last command time if it starts with ! character to prevent command flooding
|
||||||
|
|
||||||
|
|
||||||
|
async def handle(self, data: str):
|
||||||
|
'''
|
||||||
|
Handle the data received from the IRC server.
|
||||||
|
|
||||||
|
:param data: The data received from the IRC server.
|
||||||
|
'''
|
||||||
|
logging.info(data)
|
||||||
|
try:
|
||||||
|
parts = data.split()
|
||||||
|
if data.startswith('ERROR :Closing Link:'):
|
||||||
|
raise Exception('BANNED')
|
||||||
|
if parts[0] == 'PING':
|
||||||
|
await self.raw('PONG ' + parts[1]) # Respond to the server's PING request with a PONG to prevent ping timeout
|
||||||
|
elif parts[1] == '001': # RPL_WELCOME
|
||||||
|
await self.raw(f'MODE {self.nickname} +B') # Set user mode +B (Bot)
|
||||||
|
await self.sendmsg('NickServ', f'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)
|
||||||
|
if parts.key:
|
||||||
|
await self.raw(f'JOIN {args.channel} {args.key}') # Join the channel with the key
|
||||||
|
else:
|
||||||
|
await self.raw(f'JOIN {args.channel}')
|
||||||
|
elif parts[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 parts[1] == 'INVITE':
|
||||||
|
target = parts[2]
|
||||||
|
chan = parts[3][1:]
|
||||||
|
if target == self.nickname: # If we were invited to a channel, join it
|
||||||
|
await self.raw(f'JOIN {chan}')
|
||||||
|
elif parts[1] == 'KICK':
|
||||||
|
chan = parts[2]
|
||||||
|
kicked = parts[3]
|
||||||
|
if kicked == self.nickname: # If we were kicked from the channel, rejoin it after 3 seconds
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
await self.raw(f'JOIN {chan}')
|
||||||
|
elif parts[1] == 'PRIVMSG':
|
||||||
|
await self.eventPRIVMSG(data) # We put this in a separate function since it will likely be the most used/handled event
|
||||||
|
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):
|
def setup_logger(log_filename: str, to_file: bool = False):
|
||||||
'''
|
'''
|
||||||
Set up logging to console & optionally to file.
|
Set up logging to console & optionally to file.
|
||||||
|
|
||||||
|
:param log_filename: The filename of the log file
|
||||||
|
:param to_file: Whether or not to log to a 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,))
|
||||||
|
|
||||||
|
|
||||||
: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 __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description="Connect to an IRC server.") # The arguments without -- are required arguments.
|
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("server", help="The IRC server address.")
|
||||||
parser.add_argument("channel", help="The IRC channel to join.")
|
parser.add_argument("channel", help="The IRC channel to join.")
|
||||||
parser.add_argument("--password", help="The password for the IRC server.")
|
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("--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("--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("--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("--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("--key", default="", help="The key (password) for the IRC channel, if required.")
|
||||||
parser.add_argument("--vhost", help="The VHOST to use for connection.")
|
parser.add_argument("--vhost", help="The VHOST to use for connection.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
print(f"Connecting to {args.server}:{args.port} (SSL: {args.ssl}) and joining {args.channel} (Key: {args.key or 'None'})")
|
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.
|
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.
|
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())
|
asyncio.run(bot.connect())
|
||||||
|
298
skelly.go
Normal file
298
skelly.go
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
// irc bot skeleton - developed by acidvegas in golang (https://git.acid.vegas/skeleton)
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IRC color & control codes
|
||||||
|
const (
|
||||||
|
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"
|
||||||
|
lightGreen = "09"
|
||||||
|
cyan = "10"
|
||||||
|
lightCyan = "11"
|
||||||
|
lightBlue = "12"
|
||||||
|
pink = "13"
|
||||||
|
grey = "14"
|
||||||
|
lightGrey = "15"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Connection settings
|
||||||
|
server string
|
||||||
|
port int
|
||||||
|
channel string
|
||||||
|
key string
|
||||||
|
password string
|
||||||
|
ipv4 bool
|
||||||
|
ipv6 bool
|
||||||
|
vhost string
|
||||||
|
|
||||||
|
// SSL settings
|
||||||
|
useSSL bool
|
||||||
|
sslVerify bool
|
||||||
|
sslCert string
|
||||||
|
sslPass string
|
||||||
|
|
||||||
|
// Bot settings
|
||||||
|
nick string
|
||||||
|
user string
|
||||||
|
real string
|
||||||
|
nickserv string
|
||||||
|
operserv string
|
||||||
|
mode string
|
||||||
|
flood int
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&server, "server", "", "The IRC server address.")
|
||||||
|
flag.IntVar(&port, "port", 6667, "The port number for the IRC server.")
|
||||||
|
flag.StringVar(&channel, "channel", "", "The IRC channel to join.")
|
||||||
|
flag.StringVar(&key, "key", "", "The key (password) for the IRC channel, if required.")
|
||||||
|
flag.StringVar(&password, "password", "", "The password for the IRC server.")
|
||||||
|
flag.BoolVar(&ipv4, "v4", false, "Use IPv4 for the connection.")
|
||||||
|
flag.BoolVar(&ipv6, "v6", false, "Use IPv6 for the connection.")
|
||||||
|
flag.StringVar(&vhost, "vhost", "", "The VHOST to use for connection.")
|
||||||
|
flag.BoolVar(&useSSL, "ssl", false, "Use SSL for the connection.")
|
||||||
|
flag.BoolVar(&sslVerify, "ssl-verify", false, "Verify SSL certificates.")
|
||||||
|
flag.StringVar(&sslCert, "ssl-cert", "", "The SSL certificate to use for the connection.")
|
||||||
|
flag.StringVar(&sslPass, "ssl-pass", "", "The SSL certificate password.")
|
||||||
|
flag.StringVar(&nick, "nick", "skelly", "The nickname to use for the bot.")
|
||||||
|
flag.StringVar(&user, "user", "skelly", "The username to use for the bot.")
|
||||||
|
flag.StringVar(&real, "real", "Development Bot", "The realname to use for the bot.")
|
||||||
|
flag.StringVar(&mode, "mode", "+B", "The mode to set on the bot's nickname.")
|
||||||
|
flag.StringVar(&nickserv, "nickserv", "", "The password for the bot's nickname to be identified with NickServ.")
|
||||||
|
flag.StringVar(&operserv, "operserv", "", "The password for the bot's nickname to be identified with OperServ.")
|
||||||
|
flag.IntVar(&flood, "flood", 3, "Delay between command usage.")
|
||||||
|
flag.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func logfmt(option string, message string) string {
|
||||||
|
switch option {
|
||||||
|
case "DEBUG":
|
||||||
|
return fmt.Sprintf("\033[95m%s\033[0m [\033[95mDEBUG\033[0m] %s", getnow(), message)
|
||||||
|
case "ERROR":
|
||||||
|
return fmt.Sprintf("\033[95m%s\033[0m [\033[31mERROR\033[0m] %s", getnow(), message)
|
||||||
|
case "SEND":
|
||||||
|
return fmt.Sprintf("\033[95m%s\033[0m [\033[92mSEND\033[0m] %s", getnow(), message)
|
||||||
|
case "RECV":
|
||||||
|
return fmt.Sprintf("\033[95m%s\033[0m [\033[96mRECV\033[0m] %s", getnow(), message)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\033[95m%s\033[0m [\033[95mDEBUG\033[0m] %s", getnow(), message) // This should never happen
|
||||||
|
}
|
||||||
|
|
||||||
|
func color(msg string, foreground string, background string) string {
|
||||||
|
if background != "" {
|
||||||
|
return fmt.Sprintf("\x03%s,%s%s%s", foreground, background, msg, reset)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\x03%s%s%s", foreground, msg, reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bot struct {
|
||||||
|
nickname string
|
||||||
|
username string
|
||||||
|
realname string
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
writer *bufio.Writer
|
||||||
|
last time.Time
|
||||||
|
slow bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Skeleton() *Bot {
|
||||||
|
return &Bot{
|
||||||
|
nickname: "skeleton",
|
||||||
|
username: "skelly",
|
||||||
|
realname: "Development Bot",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *Bot) Connect() error {
|
||||||
|
address := fmt.Sprintf("%s:%d", server, port)
|
||||||
|
|
||||||
|
var networkType string
|
||||||
|
switch {
|
||||||
|
case ipv4:
|
||||||
|
networkType = "tcp4"
|
||||||
|
case ipv6:
|
||||||
|
networkType = "tcp6"
|
||||||
|
default:
|
||||||
|
networkType = "tcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialer net.Dialer
|
||||||
|
|
||||||
|
if vhost != "" {
|
||||||
|
localAddr, err := net.ResolveTCPAddr(networkType, vhost+":0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve local address: %w", err)
|
||||||
|
}
|
||||||
|
dialer.LocalAddr = localAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if useSSL {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: !sslVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sslCert != "" {
|
||||||
|
var cert tls.Certificate
|
||||||
|
cert, err = tls.LoadX509KeyPair(sslCert, sslPass)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load SSL certificate: %w", err)
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.conn, err = tls.DialWithDialer(&dialer, networkType, address, tlsConfig)
|
||||||
|
} else {
|
||||||
|
bot.conn, err = dialer.Dial(networkType, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to dial: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.reader = bufio.NewReader(bot.conn)
|
||||||
|
bot.writer = bufio.NewWriter(bot.conn)
|
||||||
|
|
||||||
|
if password != "" {
|
||||||
|
bot.raw("PASS " + password)
|
||||||
|
}
|
||||||
|
bot.raw(fmt.Sprintf("USER %s 0 * :%s", user, real))
|
||||||
|
bot.raw("NICK " + nick)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *Bot) raw(data string) {
|
||||||
|
if bot.writer != nil {
|
||||||
|
bot.writer.WriteString(data + "\r\n")
|
||||||
|
bot.writer.Flush()
|
||||||
|
if strings.Split(data, " ")[0] == "PONG" {
|
||||||
|
fmt.Println(logfmt("SEND", "\033[93m"+data+"\033[0m"))
|
||||||
|
} else {
|
||||||
|
fmt.Println(logfmt("SEND", data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *Bot) sendMsg(target string, msg string) {
|
||||||
|
bot.raw(fmt.Sprintf("PRIVMSG %s :%s", target, msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *Bot) handle(data string) {
|
||||||
|
parts := strings.Fields(data)
|
||||||
|
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[0] != "PING" {
|
||||||
|
parts[1] = "\033[38;5;141m" + parts[1] + "\033[0m"
|
||||||
|
}
|
||||||
|
coloredData := strings.Join(parts, " ")
|
||||||
|
fmt.Println(logfmt("RECV", coloredData))
|
||||||
|
|
||||||
|
parts = strings.Fields(data)
|
||||||
|
if parts[0] == "PING" {
|
||||||
|
bot.raw("PONG " + parts[1])
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
command := parts[1]
|
||||||
|
switch command {
|
||||||
|
case "001": // RPL_WELCOME
|
||||||
|
bot.raw("MODE " + nick + " " + mode)
|
||||||
|
|
||||||
|
if nickserv != "" {
|
||||||
|
bot.raw("PRIVMSG NickServ :IDENTIFY " + nickserv)
|
||||||
|
}
|
||||||
|
|
||||||
|
if operserv != "" {
|
||||||
|
bot.raw("OPER " + nick + " " + operserv)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
if key != "" {
|
||||||
|
bot.raw("JOIN " + channel + " " + key)
|
||||||
|
} else {
|
||||||
|
bot.raw("JOIN " + channel)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case "PRIVMSG":
|
||||||
|
bot.eventPrivMsg(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getnow() string {
|
||||||
|
return time.Now().Format("03:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *Bot) eventPrivMsg(data string) {
|
||||||
|
parts := strings.Split(data, " ")
|
||||||
|
ident := strings.TrimPrefix(parts[0], ":")
|
||||||
|
nick := strings.Split(ident, "!")[0]
|
||||||
|
target := parts[2]
|
||||||
|
msg := strings.Join(parts[3:], " ")[1:]
|
||||||
|
|
||||||
|
if target == bot.nickname {
|
||||||
|
// Private message handling
|
||||||
|
} else if strings.HasPrefix(target, "#") {
|
||||||
|
if target == channel {
|
||||||
|
if msg == "!test" {
|
||||||
|
bot.sendMsg(channel, nick+": Test successful!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
for {
|
||||||
|
fmt.Printf("\033[90m%s\033[0m [\033[95mDEBUG\033[0m] Connecting to %s:%d and joining %s\n", getnow(), server, port, channel)
|
||||||
|
|
||||||
|
bot := Skeleton()
|
||||||
|
err := bot.Connect()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("\033[90m%s\033[0m [\033[31mERROR\033[0m]\033[93m Failed to connect to server! Retrying in 15 seconds... \033[90m(%v)\033[0m", getnow(), err)
|
||||||
|
} else {
|
||||||
|
for {
|
||||||
|
line, _, err := bot.reader.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("\033[90m%s\033[0m [\033[31mERROR\033[0m]\033[93m Lost connection to server! Retrying in 15 seconds... \033[90m(%v)\033[0m", getnow(), err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bot.handle(string(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bot.conn != nil {
|
||||||
|
bot.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user