# bard/bots/irc.py
#
#
""" basic package for the program. """
__copyright__ = "Copyright 2015, B.H.J Thate"
## IMPORTS
from bard.dispatcher import DISPATCHER
from bard.runtime import kernel, fleet
from bard.object import Object
from bard.bot import BOT
from bard.utils import error, split_txt, txt_parse
from bard.defines import BLA, GREEN, ENDC
from bard.errors import RemoteDisconnect
from bard.defaults import irc
import threading
import logging
import _thread
import socket
import time
import re
## IRC
[docs]class IRC(BOT):
""" IRC bot. """
marker = "\r\n"
cc = "."
def __init__(zelf, *args, **kwargs):
BOT.__init__(zelf, *args, **kwargs)
zelf.connected = threading.Event()
zelf._handlers = DISPATCHER()
zelf._handlers.register("004", zelf._onconnect)
zelf._handlers.register("513", zelf.handle_513)
zelf._handlers.register("433", zelf.handle_433)
zelf._handlers.register("366", zelf.handle_366)
zelf._handlers.register("PING", zelf.handle_ping)
zelf._handlers.register("PONG", zelf.handle_pong)
zelf._handlers.register("QUIT", zelf.handle_quit)
zelf._handlers.register("INVITE", zelf.handle_invite)
zelf._handlers.register("PRIVMSG", zelf.handle_privmsg)
zelf._handlers.register("NOTICE", zelf.handle_notice)
zelf._handlers.register("JOIN", zelf.handle_join)
zelf._lock = _thread.allocate_lock()
zelf._buffer = []
zelf._lastline = ""
zelf.channels = []
zelf.encoding = "utf-8"
if "realname" not in zelf: zelf.realname = "bard"
if "username" not in zelf: zelf.username = "bard"
if "server" not in zelf: zelf.server = "localhost"
if "port" not in zelf: zelf.port = 6667
if "nick" not in zelf: zelf.nick = "bard"
if "channel" in zelf and zelf.channel not in zelf.channels: zelf.channels.append(zelf.channel)
def _raw(zelf, txt):
if not txt.endswith(zelf.marker): txt += zelf.marker
txt = txt[:512]
txt = bytes(txt, "utf-8")
logging.debug("> %s/raw %s" % (zelf.type, txt))
try:
if 'ssl' in zelf and zelf.ssl: zelf._sock.write(txt)
else: zelf._sock.send(txt)
except BrokenPipeError: zelf._connect()
def _connect(zelf):
zelf.stopped = False
logging.warn("! %s/connect to %s" % (zelf.type, zelf.server))
if "ipv6" in zelf: zelf._oldsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else: zelf._oldsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
zelf.server = zelf.bind()
zelf._oldsock.settimeout(60)
zelf._oldsock.connect((zelf.server, int(str(zelf.port or 6667))))
zelf.blocking = 1
zelf._oldsock.setblocking(zelf.blocking)
zelf.fsock = zelf._oldsock.makefile("r")
if "blocking" in zelf: zelf._oldsock.settimeout(301.0)
if 'ssl' in zelf and zelf['ssl']: zelf._sock = socket.ssl(zelf._oldsock)
else: zelf._sock = zelf._oldsock
return True
def _onconnect(zelf, *args, **kwargs):
if "onconnect" in zelf: time.sleep(0.5) ; zelf._raw(zelf.onconnect)
if "servermodes" in zelf: zelf._raw("MODE %s %s" % (zelf.nick, zelf.servermodes))
zelf.join_channels()
def _dodcc(zelf, event, s):
s.send(bytes('Welcome to BARD ' + event.nick + " !!\n", zelf.encoding))
kernel.workers.put(zelf._dccloop, event, s)
def _dccloop(zelf, event, s):
sockfile = s.makefile('rw')
s.setblocking(True)
while 1:
try:
res = sockfile.readline()
if not res: break
res = res.rstrip()
logging.info("< %s/DCC %s" % (zelf.type, res.strip()))
o = Object(_target=zelf, txt=res)
o.outer = sockfile
o.origin = event.origin
BOT.handle_event(zelf, o)
except socket.timeout: time.sleep(0.01)
except socket.error as ex:
if ex.errno in [EAGAIN, ]: continue
else: raise
except Exception as ex: error()
sockfile.close()
def _dccconnect(zelf, event):
try:
addr = event.parsed.args[2] ; port = event.parsed.args[3][:-1]
port = int(port)
if re.search(':', addr): s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((addr, port))
except Exception as ex: error() ; return
zelf._dodcc(event, s)
[docs] def start(zelf, *args, **kwargs):
zelf.connect()
BOT.start(zelf, *args, **kwargs)
[docs] def parse(zelf, *args, **kwargs):
""" parse a string into an IRC event. """
rawstr = str(args[0])
obj = Object()
obj._target = zelf
obj.servermsg = False
splitted = re.split('\s+', rawstr)
if not rawstr[0] == ':': obj.servermsg = True
obj.prefix = splitted[0]
if obj.servermsg: obj.type = obj.prefix
else: obj.type = splitted[1]
try:
nickuser = obj.prefix.split('!')
obj.origin = nickuser[1]
obj.nick = nickuser[0][1:]
except IndexError: obj.origin = obj.prefix ; obj.servermsg = True
if obj.type in pfc:
obj.arguments = splitted[2:pfc[obj.type]+2]
txtsplit = re.split('\s+', rawstr, pfc[obj.type]+2)
obj.txt = txtsplit[-1]
else: obj.arguments = splitted[2:]
if obj.arguments: obj.target = obj.arguments[0]
obj.postfix = ' '.join(obj.arguments)
if not "txt" in obj: obj.txt = rawstr.rsplit(":")[-1]
if obj.txt.startswith(":"): obj.txt = obj.txt[1:]
if not "channel" in obj:
for c in obj.arguments + [obj.txt, ]:
if c.startswith("#"): obj.channel = c
if obj.servermsg:
obj.origin = obj.origin[1:-1]
obj.channel = obj.origin
if not "origin" in obj: obj.origin = obj.channel
return obj
[docs] def event(zelf, *args, **kwargs):
if not zelf._buffer: zelf.read_some()
line = zelf._buffer.pop(0)
event = zelf.parse(line.rstrip())
event.cc = zelf.cc
return event
[docs] def handle_event(zelf, *args, **kwargs):
event = args[0]
if event.type not in zelf._handlers: return
for handler in zelf._handlers[event.type]: zelf._handlers.put(handler.func, *args, **kwargs)
[docs] def read_some(zelf, *args, **kwargs):
if "ssl" in zelf and zelf.ssl: inbytes = zelf._sock.read()
else: inbytes = zelf._sock.recv(512)
txt = str(inbytes, zelf.encoding)
if txt == "": raise RemoteDisconnect()
zelf._lastline += txt
splitted = zelf._lastline.split(zelf.marker)
for s in splitted[:-1]: logging.debug("< %s/read %s" % (zelf.type, s.strip())) ; zelf._buffer.append(s)
zelf._lastline = splitted[-1]
[docs] def send(zelf, txt): zelf._raw(txt) ; time.sleep(3.0)
[docs] def bind(zelf):
server = zelf.server
try: zelf._oldsock.bind((server, 0))
except socket.error:
if not server:
try: socket.inet_pton(socket.AF_INET6, zelf.server)
except socket.error: pass
else: server = zelf.server
if not server:
try: socket.inet_pton(socket.AF_INET, zelf.server)
except socket.error: pass
else: server = zelf.server
if not server:
ips = []
try:
for item in socket.getaddrinfo(zelf.server, None):
if item[0] in [socket.AF_INET, socket.AF_INET6] and item[1] == socket.SOCK_STREAM:
ip = item[4][0]
if ip not in ips: ips.append(ip)
except socket.error: pass
else: server = random.choice(ips)
return server
[docs] def logon(zelf):
if "password" in zelf: zelf._raw("PASS %s" % zelf.password)
zelf._raw("NICK %s" % zelf.nick or "bard")
zelf._raw("USER %s localhost %s :%s" % (zelf.username or "bard", zelf.server or "localhost", zelf.realname))
[docs] def say(zelf, *args, **kwargs):
channel = args[0]
txt = args[1]
txt_list = split_txt(txt)
for txt in txt_list:
time.sleep(0.1)
for t in txt.split("\n"): zelf.privmsg(channel, t)
[docs] def connect(zelf, reconnect=True):
res = None
while 1:
try: res = zelf._connect() ; time.sleep(1) ; zelf.logon() ; break
except Exception as ex: error()
time.sleep(10)
zelf.ready()
return res
[docs] def exit(zelf): zelf.quit()
[docs] def close(zelf):
if 'ssl' in zelf and zelf['ssl']: zelf.oldsock.shutdown(1) ; zelf.oldsock.close()
else: zelf._sock.shutdown(1) ; zelf._sock.close()
zelf.fsock.close()
[docs] def handle_join(zelf, *args, **kwargs):
event = args[0]
if event.channel not in zelf.channels: zelf.channels.append(event.channel)
[docs] def handle_notice(zelf, *args, **kwargs): pass
[docs] def handle_ping(zelf, *args, **kwargs): zelf.pongcheck = True ; zelf.pong()
[docs] def handle_pong(zelf, *args, **kwargs): pass
[docs] def handle_433(zelf, *args, **kwargs):
event = args[0]
zelf.donick(event.parsed.args[1] + "_")
[docs] def handle_quit(zelf, *args, **kwargs):
event = args[0]
if "Ping timeout" in event.txt and event.nick == zelf.nick: zelf.connect()
[docs] def handle_366(zelf, *args, **kwargs): logging.warn("! %s/logged on %s" % (zelf.type, zelf.server))
[docs] def handle_invite(zelf, *args, **kwargs):
event = args[0]
zelf.join(event.channel)
[docs] def handle_privmsg(zelf, *args, **kwargs):
event = args[0]
if event.txt.startswith("\001DCC"): zelf._dccconnect(event) ; return
if event.txt: BOT.handle_event(zelf, event)
[docs] def handle_513(zelf, *args, **kwargs):
event = args[0]
zelf._raw("PONG %s" % event.arguments[6])
[docs] def handle_ctcp(zelf, event):
if event.txt and event.txt[0] == zelf.cc: resolve(event)
[docs] def donick(zelf, nick, setorig=False, save=False, whois=False): zelf.send('NICK %s\n' % nick[:16])
[docs] def join(zelf, channel, password=None):
if password: zelf._raw('JOIN %s %s' % (channel, password))
else: zelf._raw('JOIN %s' % channel)
[docs] def part(zelf, channel):
zelf.send('PART %s' % channel)
if channel in zelf.channels: zelf.channels.remove(channel) ; zelf.save()
[docs] def who(zelf, who): zelf.send('WHO %s' % who.strip())
[docs] def names(zelf, channel): zelf.send('NAMES %s' % channel)
[docs] def whois(zelf, who): zelf.send('WHOIS %s' % who)
[docs] def privmsg(zelf, printto, txt): zelf.send('PRIVMSG %s :%s' % (printto, txt))
[docs] def voice(zelf, channel, who): zelf.send('MODE %s +v %s' % (channel, who))
[docs] def doop(zelf, channel, who): zelf.send('MODE %s +o %s' % (channel, who))
[docs] def delop(zelf, channel, who): zelf.send('MODE %s -o %s' % (channel, who))
[docs] def quit(zelf, reason='https://pikacode.com/bthate/bard'): zelf.send('QUIT :%s' % reason)
[docs] def notice(zelf, printto, what): zelf.send('NOTICE %s :%s' % (printto, what))
[docs] def ctcp(zelf, printto, what): zelf.send("PRIVMSG %s :\001%s\001" % (printto, what))
[docs] def ctcpreply(zelf, printto, what): zelf.send("NOTICE %s :\001%s\001" % (printto, what))
[docs] def action(zelf, printto, what, event=None, *args, **kwargs): zelf.send("PRIVMSG %s :\001ACTION %s\001" % (printto, what))
[docs] def getchannelmode(zelf, channel): zelf.send('MODE %s' % channel)
[docs] def settopic(zelf, channel, txt): zelf.send('TOPIC %s :%s' % (channel, txt))
[docs] def ping(zelf, *args, **kwargs): zelf.send('PING :%s' % zelf.server)
[docs] def pong(zelf, *args, **kwargs): zelf.send('PONG :%s' % zelf.server)
[docs]def init(*args, **kwargs):
ncfg = kernel.last("cfg", "irc")
if not ncfg: ncfg = irc ; ncfg.save()
bot = IRC(**ncfg)
fleet.append(bot)
kernel.put(bot.start)
## POSTFIXCOUNT - how many arguments used in a IRC message
pfc = {}
pfc['NICK'] = 0
pfc['QUIT'] = 0
pfc['SQUIT'] = 1
pfc['JOIN'] = 0
pfc['PART'] = 1
pfc['TOPIC'] = 1
pfc['KICK'] = 2
pfc['PRIVMSG'] = 1
pfc['NOTICE'] = 1
pfc['SQUERY'] = 1
pfc['PING'] = 0
pfc['ERROR'] = 0
pfc['AWAY'] = 0
pfc['WALLOPS'] = 0
pfc['INVITE'] = 1
pfc['001'] = 1
pfc['002'] = 1
pfc['003'] = 1
pfc['004'] = 4
pfc['005'] = 15
pfc['302'] = 1
pfc['303'] = 1
pfc['301'] = 2
pfc['305'] = 1
pfc['306'] = 1
pfc['311'] = 5
pfc['312'] = 3
pfc['313'] = 2
pfc['317'] = 3
pfc['318'] = 2
pfc['319'] = 2
pfc['314'] = 5
pfc['369'] = 2
pfc['322'] = 3
pfc['323'] = 1
pfc['325'] = 3
pfc['324'] = 4
pfc['331'] = 2
pfc['332'] = 2
pfc['341'] = 3
pfc['342'] = 2
pfc['346'] = 3
pfc['347'] = 2
pfc['348'] = 3
pfc['349'] = 2
pfc['351'] = 3
pfc['352'] = 7
pfc['315'] = 2
pfc['353'] = 3
pfc['366'] = 2
pfc['364'] = 3
pfc['365'] = 2
pfc['367'] = 2
pfc['368'] = 2
pfc['371'] = 1
pfc['374'] = 1
pfc['375'] = 1
pfc['372'] = 1
pfc['376'] = 1
pfc['378'] = 2
pfc['381'] = 1
pfc['382'] = 2
pfc['383'] = 5
pfc['391'] = 2
pfc['392'] = 1
pfc['393'] = 1
pfc['394'] = 1
pfc['395'] = 1
pfc['262'] = 3
pfc['242'] = 1
pfc['235'] = 3
pfc['250'] = 1
pfc['251'] = 1
pfc['252'] = 2
pfc['253'] = 2
pfc['254'] = 2
pfc['255'] = 1
pfc['256'] = 2
pfc['257'] = 1
pfc['258'] = 1
pfc['259'] = 1
pfc['263'] = 2
pfc['265'] = 1
pfc['266'] = 1
pfc['401'] = 2
pfc['402'] = 2
pfc['403'] = 2
pfc['404'] = 2
pfc['405'] = 2
pfc['406'] = 2
pfc['407'] = 2
pfc['408'] = 2
pfc['409'] = 1
pfc['411'] = 1
pfc['412'] = 1
pfc['413'] = 2
pfc['414'] = 2
pfc['415'] = 2
pfc['421'] = 2
pfc['422'] = 1
pfc['423'] = 2
pfc['424'] = 1
pfc['431'] = 1
pfc['432'] = 2
pfc['433'] = 2
pfc['436'] = 2
pfc['437'] = 2
pfc['441'] = 3
pfc['442'] = 2
pfc['443'] = 3
pfc['444'] = 2
pfc['445'] = 1
pfc['446'] = 1
pfc['451'] = 1
pfc['461'] = 2
pfc['462'] = 1
pfc['463'] = 1
pfc['464'] = 1
pfc['465'] = 1
pfc['467'] = 2
pfc['471'] = 2
pfc['472'] = 2
pfc['473'] = 2
pfc['474'] = 2
pfc['475'] = 2
pfc['476'] = 2
pfc['477'] = 2
pfc['478'] = 3
pfc['481'] = 1
pfc['482'] = 2
pfc['483'] = 1
pfc['484'] = 1
pfc['485'] = 1
pfc['491'] = 1
pfc['501'] = 1
pfc['502'] = 1
pfc['700'] = 2