# zbot/drivers/irc.py
#
#
""" IRC bot. """
## IMPORTS
from zbot import Bot, Object, RemoteDisconnect, TargetMissing, kernel, handle_loop
from zbot.utils import error, split_txt, dispatch, resolve
import collections
import threading
import logging
import _thread
import socket
import time
import sys
import os
import re
## irc_parse function
[docs]def irc_parse(obj, *args, **kwargs):
rawstr = str(args[0])
self = obj
self.servermsg = False
splitted = re.split('\s+', rawstr)
if not rawstr[0] == ':': self.servermsg = True
self.prefix = splitted[0]
if self.servermsg: self.etype = self.prefix
else: self.etype = splitted[1]
try:
nickuser = self.prefix.split('!')
self.origin = nickuser[1]
self.nick = nickuser[0][1:]
except IndexError: self.origin = self.prefix ; self.servermsg = True
if self.etype in pfc:
self.arguments = splitted[2:pfc[self.etype]+2]
txtsplit = re.split('\s+', rawstr, pfc[self.etype]+2)
self.txt = txtsplit[-1]
else: self.arguments = splitted[2:]
if self.arguments: self.target = self.arguments[0]
self.postfix = ' '.join(self.arguments)
if not "txt" in self: self.txt = rawstr.rsplit(":")[-1]
if self.txt.startswith(":"): self.txt = self.txt[1:]
if not "channel" in self:
for c in self.arguments + [self.txt, ]:
if c.startswith("#"): self.channel = c
if self.servermsg:
self.origin = self.origin[1:-1]
self.channel = self.origin
if not "origin" in self: self.origin = self.channel
if not "target" in self: self.target = self.channel or self.origin
return obj
## IRCBot
[docs]class IRCBot(Bot):
marker = "\r\n"
cc = "."
def __init__(self, *args, **kwargs):
Bot.__init__(self, *args, **kwargs)
self.connected = threading.Event()
self.register("250", self._onconnect)
self.register("001", self._onconnect)
self.register("513", self.handle_513)
self.register("433", self.handle_433)
self.register("PING", self.handle_ping)
self.register("INVITE", self.handle_invite)
self.register("PRIVMSG", self.handle_privmsg)
self._lock = _thread.allocate_lock()
self._buffer = []
self._lastline = ""
self.encoding = "utf-8"
self.server = self.server or "localhost"
self.port = self.port or 6667
self.nick = self.nick or "zbot"
self.name = self.name or self.etype
if not self.channels: self.channels = []
if self.channel and self.channel not in self.channels: self.channels.append(self.channel)
def _raw(self, txt):
logging.warn("> %s" % txt)
if not txt.endswith(self.marker): txt += self.marker
itxt = bytes(txt, self.encoding)
if self.outer: o = self.outer ; o.write(itxt) ; o.write("\n") ; o.flush() ; return
if 'ssl' in self and self.ssl: self.sock.write(itxt)
else: self.sock.send(itxt[:512])
def _connect(self):
self.stopped = False
if self.ipv6: self.oldsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else: self.oldsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server = self.bind()
logging.warn('connect %s - %s (%s)' % (self.server, self.port, self.name))
self.oldsock.settimeout(60)
self.oldsock.connect((self.server, int(str(self.port or 6667))))
self.blocking = 1
self.oldsock.setblocking(self.blocking)
logging.warn('connection made to %s (%s)' % (self.server, self.name))
self.fsock = self.oldsock.makefile("r")
if self.blocking:
socktimeout = 301.0
self.oldsock.settimeout(socktimeout)
if 'ssl' in self and self['ssl']: self.sock = socket.ssl(self.oldsock)
else: self.sock = self.oldsock
self.connecttime = time.time()
return True
def _onconnect(self, *args, **kwargs):
logging.warn("logged on !!")
if "onconnect" in self: time.sleep(2) ; self._raw(self.onconnect)
if "servermodes" in self: self._raw("MODE %s %s" % (self.nick, self.servermodes))
self.join_channels(*args, **kwargs)
self.ready()
def _dodcc(self, event, s):
s.send(bytes('Welcome to ZBOT ' + event.nick + " !!\n", self.encoding))
_thread.start_new_thread(self._dccloop, (event, s))
def _dccloop(self, event, s):
sockfile = s.makefile('rw')
s.setblocking(True)
while 1:
try:
res = sockfile.readline()
if not res: break
res = res.rstrip()
logging.warn("< DCC %s" % res)
o = Object(_target=self, txt=res)
o.outer = sockfile
self.put(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(self, event):
try:
addr = event.args[3] ; port = event.args[4][:-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
self._dodcc(event, s)
[docs] def get_one(self, *args, **kwargs):
if not self._buffer: self.read_some()
line = self._buffer.pop(0)
e = Object()
e._target = self
o = irc_parse(e, line.rstrip())
return o
[docs] def read_some(self, *args, **kwargs):
try:
if "ssl" in self and self.ssl: inbytes = self.sock.read()
else: inbytes = self.sock.recv(512)
except Exception as ex: error()
txt = str(inbytes, self.encoding)
if txt == "": raise RemoteDisconnect()
self._lastline += txt
splitted = self._lastline.split(self.marker)
for s in splitted[:-1]:
logging.warn("< %s" % s)
self._buffer.append(s)
self._lastline = splitted[-1]
[docs] def send(self, txt): self._raw(txt)
[docs] def bind(self):
server = self.server
try: self.oldsock.bind((server, 0))
except socket.error:
if not server:
try: socket.inet_pton(socket.AF_INET6, self.server)
except socket.error: pass
else: server = self.server
if not server:
try: socket.inet_pton(socket.AF_INET, self.server)
except socket.error: pass
else: server = self.server
if not server:
ips = []
try:
for item in socket.getaddrinfo(self.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(self):
if "password" in self: self._raw("PASS %s" % self.password)
logging.warn('logging in on %s - this may take a while' % self.server)
self._raw("NICK %s" % self.nick or "zbot")
self._raw("USER %s localhost %s :%s" % (self.username or "zbot", self.server or "localhost", self.realname or "zbot"))
[docs] def say(self, channel, txt):
for text in txt.split("\n"):
for line in split_txt(text):
if line: self.privmsg(channel, line)
[docs] def join_channels(self, *args, **kwargs):
for channel in self.channels: self.join(channel)
def broadcast(self, txt):
for i in self.channels: self.say(i, txt)
[docs] def connect(self, reconnect=True):
res = self._connect()
if res: time.sleep(1) ; self.logon()
return res
[docs] def close(self):
if 'ssl' in self and self['ssl']: self.oldsock.shutdown(2)
else: self.sock.shutdown(2)
if 'ssl' in self and self['ssl']: self.oldsock.close()
else: self.sock.close()
self.fsock.close()
[docs] def handle_ping(self, event):
self.pongcheck = True
self.pong()
[docs] def handle_433(self, event): self.donick(event.arguments[1] + "_")
[docs] def handle_invite(self, event): self.join(event.channel)
[docs] def handle_privmsg(self, event):
if event.txt.startswith("\001DCC"): self._dccconnect(event) ; return
if event.txt and event.txt[0] == self.cc: event.is_cmnd = True ; resolve(event)
[docs] def handle_513(self, event): self._raw("PONG %s" % event.arguments[6])
[docs] def handle_ctcp(self, event):
if event.txt and event.txt[0] == self.cc: resolve(event)
[docs] def donick(self, nick, setorig=False, save=False, whois=False):
nick = nick[:16]
self.send('NICK %s\n' % nick)
[docs] def join(self, channel, password=None):
if password: self._raw('JOIN %s %s' % (channel, password))
else: self._raw('JOIN %s' % channel)
[docs] def part(self, channel):
self.send('PART %s' % channel)
if channel in self.channels: self.channels.remove(channel) ; self.save()
[docs] def who(self, who): self.send('WHO %s' % who.strip())
[docs] def names(self, channel): self.send('NAMES %s' % channel)
[docs] def whois(self, who): self.send('WHOIS %s' % who)
[docs] def privmsg(self, printto, what): self.send('PRIVMSG %s :%s' % (printto, what))
[docs] def voice(self, channel, who): self.send('MODE %s +v %s' % (channel, who))
[docs] def doop(self, channel, who): self.send('MODE %s +o %s' % (channel, who))
[docs] def delop(self, channel, who): self.send('MODE %s -o %s' % (channel, who))
[docs] def quit(self, reason='https://pikacode.com/milla/zbot'): self.send('QUIT :%s' % reason)
[docs] def notice(self, printto, what): self.send('NOTICE %s :%s' % (printto, what))
[docs] def ctcp(self, printto, what): self.send("PRIVMSG %s :\001%s\001" % (printto, what))
[docs] def ctcpreply(self, printto, what): self.send("NOTICE %s :\001%s\001" % (printto, what))
[docs] def action(self, printto, what, event=None, *args, **kwargs): self.send("PRIVMSG %s :\001ACTION %s\001" % (printto, what))
[docs] def getchannelmode(self, channel): self.send('MODE %s' % channel)
[docs] def settopic(self, channel, txt): self.send('TOPIC %s :%s' % (channel, txt))
[docs] def ping(self, *args, **kwargs): self.send('PING :%s' % self.server)
[docs] def pong(self, *args, **kwargs): self.send('PONG :%s' % self.server)
[docs] def broadcast(self, txt):
for i in self.channels: self.say(i, txt)
## 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