Compare commits

..

No commits in common. "3ffff551db78bfc68bef6d64af575b4795a462ed" and "4af50c243c17afd64bbd29b600bfeffe40b36e9a" have entirely different histories.

5 changed files with 150 additions and 509 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
*.log

View File

@ -1,6 +1,6 @@
ISC License ISC License
Copyright (c) 2024, 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

@ -1,17 +1,16 @@
# 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 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. 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.
A skeleton in Python & Golang *(beta)* are in this repository. ## IRC RCF Reference
- http://www.irchelp.org/protocol/rfc/
Join **#dev** on **irc.supernets.org** for help building IRC bots frm scratch! ___
###### References ###### Mirrors
- **RFC1459** - [Internet Relay Chat Protocol](https://raw.githubusercontent.com/internet-relay-chat/archive/master/rfc/rfc1459.txt) [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)
- **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)

View File

@ -1,15 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
# irc bot skeleton - developed by acidvegas in python (https://git.acid.vegas/skeleton) # Skeleton IRC bot - 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'
@ -34,212 +29,158 @@ 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.
def color(msg: str, foreground: str, background: str = None) -> str: :param msg: The string to color.
''' :param foreground: The foreground color to use.
Color a string with the specified foreground and background colors. :param background: The background color to use.
'''
:param msg: The string to color. return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}'
: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.
async def action(self, chan: str, msg: str): :param chan: The channel to send the ACTION to.
''' :param msg: The message to send to the channel.
Send an ACTION to the IRC server. '''
await self.sendmsg(chan, f'\x01ACTION {msg}\x01')
:param chan: The channel to send the ACTION to. def raw(self, data: str):
: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 raw(self, data: str): async def sendmsg(self, target: str, msg: str):
''' '''
Send raw data to the IRC server. Send a PRIVMSG to the IRC server.
:param data: The raw data to send to the IRC server. (512 bytes max including crlf) :param target: The target to send the PRIVMSG to. (channel or user)
''' :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 sendmsg(self, target: str, msg: str): async def handle(self, data: str):
''' '''
Send a PRIVMSG to the IRC server. Handle the data received from the IRC server.
:param target: The target to send the PRIVMSG to. (channel or user) :param data: The data received from the IRC server.
:param msg: The message to send to the target. '''
''' try:
await self.raw(f'PRIVMSG {target} :{msg}') args = data.split()
if data.startswith('ERROR :Closing Link:'):
raise Exception('BANNED')
async def connect(self): if args[0] == 'PING':
'''Connect to the IRC server.''' await self.raw('PONG ' + args[1]) # Respond to the server's PING request with a PONG to prevent ping timeout
while True: elif args[1] == '001': # RPL_WELCOME
try: await self.raw(f'MODE {self.nickname} +B') # Set user mode +B (Bot)
options = { await self.sendmsg('NickServ', 'IDENTIFY {self.nickname} simps0nsfan420') # Identify to NickServ
'host' : args.server, await self.raw('OPER MrSysadmin fartsimps0n1337') # Oper up
'port' : args.port if args.port else 6697 if args.ssl else 6667, await asyncio.sleep(10) # Wait 10 seconds before joining the channel (required by some IRCds to wait before JOIN)
'limit' : 1024, # Buffer size in bytes (don't change this unless you know what you're doing) 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)
'ssl' : ssl_ctx() if args.ssl else None, elif args[1] == '433': # ERR_NICKNAMEINUSE
'family' : 10 if args.v6 else 2, # 10 = AF_INET6 (IPv6), 2 = AF_INET (IPv4) self.nickname += '_' # If the nickname is already in use, append an underscore to the end of it
'local_addr' : args.vhost if args.vhost else None # Can we just leave this as args.vhost? await self.raw('NICK ' + self.nickname) # Send the new nickname to the server
} elif args[1] == 'KICK':
self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) # 15 second timeout chan = args[2]
if args.password: kicked = args[3]
await self.raw('PASS ' + args.password) # Rarely used, but IRCds may require this if kicked == self.nickname:
await self.raw(f'USER {self.username} 0 * :{self.realname}') # These lines must be sent upon connection await asyncio.sleep(3)
await self.raw('NICK ' + self.nickname) # They are to identify the bot to the server await self.raw(f'JOIN {chan}')
while not self.reader.at_eof(): elif args[1] == 'PRIVMSG':
data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 300) # 5 minute ping timeout ident = args[0][1:]
await self.handle(data.decode('utf-8').strip()) # Handle the data received from the IRC server nick = args[0].split('!')[0][1:]
except Exception as ex: target = args[2]
logging.error(f'failed to connect to {args.server} ({str(ex)})') msg = ' '.join(args[3:])[1:]
finally: if target == self.nickname:
await asyncio.sleep(30) # Wait 30 seconds before reconnecting pass # Handle private messages here
if target.startswith('#'): # Channel message
if msg.startswith('!'):
async def eventPRIVMSG(self, data: str): if msg == '!hello':
''' self.sendmsg(chan, f'Hello {nick}! Do you like ' + color('colors?', green))
Handle the PRIVMSG event. except (UnicodeDecodeError, UnicodeEncodeError):
pass # Some IRCds allow invalid UTF-8 characters, this is a very important exception to catch
:param data: The data received from the IRC server. except Exception as ex:
''' 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
View File

@ -1,298 +0,0 @@
// 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)
}
}