5
""" an Irc object handles the connection to the irc server .. receiving,
6
sending, connect and reconnect code.
9
__copyright__ = 'this file is in the public domain'
12
from gozerbot.utils.log import rlog
13
from gozerbot.utils.exception import handle_exception
14
from gozerbot.utils.generic import getrandomnick, toenc, fromenc, strippedtxt
15
from gozerbot.utils.generic import fix_format, splittxt, waitforqueue, uniqlist
16
from gozerbot.utils.locking import lockdec
17
from gozerbot.wait import Wait
18
from gozerbot.config import config
19
from monitor import saymonitor
20
from gozerbot.less import Less
21
from gozerbot.ignore import shouldignore
22
from gozerbot.persist.pdod import Pdod
23
from gozerbot.datadir import datadir
24
#from gozerbot.fleet import fleet
25
from gozerbot.botbase import BotBase
26
from gozerbot.threads.thr import start_new_thread, threaded
27
from gozerbot.periodical import periodical
28
from gozerbot.morphs import inputmorphs, outputmorphs
29
from ircevent import Ircevent
32
import time, thread, socket, threading, os, Queue, random
35
outlock = thread.allocate_lock()
36
outlocked = lockdec(outlock)
40
class AlreadyConnected(Exception):
42
""" already connected exception """
46
class AlreadyConnecting(Exception):
48
""" bot is already connecting exception """
54
""" the irc class, provides interface to irc related stuff. """
56
def __init__(self, name, cfg={}):
57
BotBase.__init__(self, name, cfg)
60
self.outputlock = thread.allocate_lock()
64
if not self.cfg.has_key('nolimiter'):
67
self.nolimiter = self.cfg['nolimiter']
68
self.reconnectcount = 0
72
if not self.state.has_key('alternick'):
73
self.state['alternick'] = self.cfg['alternick']
74
if not self.state.has_key('no-op'):
75
self.state['no-op'] = []
78
self.outqueues = [Queue.Queue() for i in range(10)]
79
self.tickqueue = Queue.Queue()
81
self.stopreadloop = False
82
self.stopoutloop = False
85
self.connectlock = thread.allocate_lock()
86
self.encoding = 'utf-8'
93
""" send raw text to the server. """
98
rlog(2, self.name + '.sending', txt)
101
self.lastoutput = time.time()
102
itxt = toenc(outputmorphs.do(txt), self.encoding)
103
if self.cfg.has_key('ssl') and self.cfg['ssl']:
104
self.sock.write(itxt + '\n')
106
self.sock.send(itxt[:500] + '\n')
107
except Exception, ex:
108
# check for broken pipe error .. if so ignore
109
# used for nonblocking sockets
112
if errno != 32 and errno != 9:
115
rlog(10, self.name, 'broken pipe/bad socket error .. ignoring')
117
rlog(10, self.name, "ERROR: can't send %s" % str(ex))
122
""" connect to server/port using nick. """
125
rlog(10, self.name, 'already connecting')
126
raise AlreadyConnecting()
129
rlog(10, self.name, 'already connected')
130
raise AlreadyConnected()
133
self.connecting = True
134
self.connectok.clear()
135
self.connectlock.acquire()
139
rlog(10, self.name, 'creating ipv6 socket')
140
self.oldsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
143
rlog(10, self.name, 'creating ipv4 socket')
144
self.oldsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
150
elite = self.cfg['bindhost'] or config['bindhost']
153
self.oldsock.bind((elite, 0))
154
except socket.gaierror:
155
rlog(10, self.name, "can't bind to %s" % elite)
156
# resolve the IRC server and pick a random server
159
try: socket.inet_pton(socket.AF_INET6, self.server)
160
except socket.error: pass
161
else: server = self.server
164
try: socket.inet_pton(socket.AF_INET, self.server)
165
except socket.error: pass
166
else: server = self.server
171
for item in socket.getaddrinfo(self.server, None):
172
if item[0] in [socket.AF_INET, socket.AF_INET6] and item[1] == socket.SOCK_STREAM:
174
if ip not in ips: ips.append(ip)
175
except socket.error: pass
176
else: server = random.choice(ips)
178
# do the connect .. set timeout to 30 sec upon connecting
179
rlog(10, self.name, 'connecting to %s (%s)' % (server, self.server))
180
self.oldsock.settimeout(15)
181
self.oldsock.connect((server, int(self.port)))
184
rlog(10, self.name, 'connection ok')
186
self.connected = True
189
self.fsock = self.oldsock.makefile("r")
192
self.oldsock.setblocking(self.blocking)
193
self.fsock._sock.setblocking(self.blocking)
195
# set socket time out
197
socktimeout = self.cfg['socktimeout']
201
socktimeout = float(socktimeout)
202
self.oldsock.settimeout(socktimeout)
203
self.fsock._sock.settimeout(socktimeout)
205
if self.cfg.has_key('ssl') and self.cfg['ssl']:
206
rlog(10, self.name, 'ssl enabled')
207
self.sock = socket.ssl(self.oldsock)
209
self.sock = self.oldsock
211
# try to release the outputlock
213
self.outputlock.release()
217
# start input and output loops
218
start_new_thread(self._readloop, ())
219
start_new_thread(self._outloop, ())
221
# logon and start monitor
224
self.reconnectcount = 0
230
""" loop on the socketfile. """
232
self.stopreadloop = 0
236
rlog(5, self.name, 'starting readloop')
239
while not self.stopped and not self.stopreadloop:
243
if self.cfg.has_key('ssl') and self.cfg['ssl']:
244
intxt = inputmorphs.do(self.sock.read()).split('\n')
246
intxt = inputmorphs.do(self.fsock.readline()).split('\n')
247
# if intxt == "" the other side has disconnected
248
if self.stopreadloop or self.stopped:
251
if not intxt or not intxt[0]:
255
intxt[0] = prevtxt + intxt[0]
262
rr = fromenc(r, self.encoding)
265
res = strippedtxt(rr)
267
rlog(2, self.name, res)
268
# parse txt read into an ircevent
270
ievent = Ircevent().parse(self, res)
271
except Exception, ex:
276
self.handle_ievent(ievent)
283
except socket.timeout:
284
# timeout occured .. first time send ping .. reconnect if
285
# second timeout follows
291
rlog(10, self.name, 'no pong received')
293
rlog(1, self.name, "socket timeout")
294
pingsend = self.ping()
300
except socket.sslerror, ex:
301
# timeout occured .. first time send ping .. reconnect if
302
# second timeout follows
303
if self.stopped or self.stopreadloop:
305
if not 'timed out' in str(ex):
312
rlog(10, self.name, 'no pong received')
314
rlog(1, self.name, "socket timeout")
315
pingsend = self.ping()
322
if 'temporarily' in str(ex):
325
except Exception, ex:
326
if self.stopped or self.stopreadloop:
334
# check for temp. unavailable error .. raised when using
335
# nonblocking socket .. 35 is FreeBSD 11 is Linux
336
if errno == 35 or errno == 11:
339
rlog(10, self.name, "error in readloop: %s" % msg)
343
rlog(5, self.name, 'readloop stopped')
344
self.connectok.clear()
345
self.connected = False
347
# see if we need to reconnect
354
""" get one of the outqueues. """
356
go = self.tickqueue.get()
357
for index in range(len(self.outqueues)):
358
if not self.outqueues[index].empty():
359
return self.outqueues[index]
361
def putonqueue(self, nr, *args):
363
""" put output onto one of the output queues. """
365
self.outqueues[nr].put_nowait(*args)
366
self.tickqueue.put_nowait('go')
372
rlog(5, self.name, 'starting output loop')
375
while not self.stopped and not self.stopoutloop:
376
queue = self._getqueue()
378
rlog(5, self.name, "outputsizes: %s" % self.outputsizes())
380
res = queue.get_nowait()
386
(printto, what, who, how, fromm, speed) = res
390
if not self.stopped and not self.stopoutloop and printto \
391
not in self.nicks401:
392
self.out(printto, what, who, how, fromm, speed)
396
rlog(5, self.name, 'stopping output loop')
400
""" log on to the network. """
402
# if password is provided send it
404
rlog(10, self.name ,'sending password')
405
self._raw("PASS %s" % self.password)
407
# register with irc server
408
rlog(10, self.name, 'registering with %s using nick %s' % \
409
(self.server, self.nick))
410
rlog(10, self.name, 'this may take a while')
412
# check for username and realname
413
username = self.nick or self.cfg['username']
414
realname = self.cfg['realname'] or username
418
self._raw("NICK %s" % self.nick)
422
self._raw("USER %s localhost localhost :%s" % (username, \
426
self.connectok.wait()
428
def _onconnect(self):
430
""" overload this to run after connect. """
434
def _resume(self, data, reto=None):
436
""" resume to server/port using nick. """
440
self.connectwithjoin()
447
except (TypeError, ValueError):
450
self.connecting = False # we're already connected
451
self.nick = data['nick']
452
self.orignick = self.nick
453
self.server = str(data['server'])
454
self.port = int(data['port'])
455
self.password = data['password']
456
self.ipv6 = data['ipv6']
457
self.ssl = data['ssl']
462
rlog(1, self.name, 'resuming ipv6 socket')
463
self.sock = socket.fromfd(fd , socket.AF_INET6, socket.SOCK_STREAM)
465
rlog(10, self.name, 'creating ipv6 socket')
466
self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
470
rlog(1, self.name, 'resuming ipv4 socket')
471
self.sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
473
rlog(10, self.name, 'creating ipv4 socket')
474
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
476
# do the connect .. set timeout to 30 sec upon connecting
477
rlog(10, self.name, 'resuming to ' + self.server)
478
self.sock.settimeout(30)
481
rlog(10, self.name, 'connection ok')
484
self.fsock = self.sock.makefile("r")
486
self.sock.setblocking(self.blocking)
488
# set socket time out
490
socktimeout = self.cfg['socktimeout']
494
socktimeout = float(socktimeout)
495
self.sock.settimeout(socktimeout)
498
rlog(0, self.name, 'resuming readloop')
499
start_new_thread(self._readloop, ())
500
start_new_thread(self._outloop, ())
503
self.reconnectcount = 0
505
self.connecting = False
507
# still there server?
508
self._raw('PING :RESUME %s' % str(time.time()))
510
self.connected = True
511
self.reconnectcount = 0
513
self.say(reto, 'rebooting done')
517
def _resumedata(self):
519
""" return data used for resume. """
521
fd = self.sock.fileno()
522
except AttributeError, ex:
527
'server': self.server,
529
'password': self.password,
536
def outputsizes(self):
538
""" return sizes of output queues. """
541
for q in self.outqueues:
542
result.append(q.qsize())
545
def broadcast(self, txt):
547
""" broadcast txt to all joined channels. """
549
for i in self.state['joinedchannels']:
550
self.say(i, txt, speed=-1)
554
""" save state data. """
558
def connect(self, reconnect=True):
560
""" connect to server/port using nick .. connect can timeout so catch
561
exception .. reconnect if enabled.
567
res = self._connect()
569
self.connectok.wait()
571
self.connecting = False
572
self.connected = True
573
rlog(10, self.name, 'logged on !')
574
except AlreadyConnecting:
576
except AlreadyConnected:
578
except Exception, ex:
579
self.connectlock.release()
582
rlog(10, self.name, 'connecting error: %s' % str(ex))
588
# add bot to the fleet
589
#if not fleet.byname(self.name):
591
self.connectlock.release()
596
""" shutdown the bot. """
598
rlog(10, self.name, 'shutdown')
599
self.stopoutputloop = 1
602
self.tickqueue.put_nowait('go')
604
self.connecting = False
605
self.connected = False
606
self.connectok.clear()
610
""" close the connection. """
613
if self.cfg.has_key('ssl') and self.cfg['ssl']:
614
self.oldsock.shutdown(2)
616
self.sock.shutdown(2)
620
if self.cfg.has_key('ssl') and self.cfg['ssl']:
630
""" exit the bot. """
638
""" reconnect to the irc server. """
643
# determine how many seconds to sleep
644
if self.reconnectcount > 0:
645
reconsleep = self.reconnectcount*15
646
rlog(10, self.name, 'sleeping %s seconds for reconnect' % \
648
time.sleep(reconsleep)
650
rlog(10, self.name, 'stopped.. not reconnecting')
653
rlog(10, self.name, 'already connected .. not reconnecting')
655
self.reconnectcount += 1
657
rlog(10, self.name, 'reconnecting')
658
result = self.connect()
660
except Exception, ex:
663
def handle_pong(self, ievent):
665
""" set pongcheck on received pong. """
667
rlog(1, self.name, 'received server pong')
670
def sendraw(self, txt):
672
""" send raw text to the server. """
676
rlog(2, self.name + '.sending', txt)
679
def fakein(self, txt):
681
""" do a fake ircevent. """
685
rlog(10, self.name + '.fakein', txt)
686
self.handle_ievent(Ircevent().parse(self, txt))
688
def say(self, printto, what, who=None, how='msg', fromm=None, speed=0):
690
""" say what to printto. """
692
if not printto or not what or printto in self.nicks401:
695
# if who is set add "who: " to txt
696
if not 'socket' in repr(printto):
698
what = "%s: %s" % (who, what)
701
self.putonqueue(9-speed, (printto, what, who, how, fromm, speed))
706
printto.send(what + '\n')
708
except Exception, ex :
709
if "Broken pipe" in str(ex) or "Bad file descriptor" in str(ex):
713
def out(self, printto, what, who=None, how='msg', fromm=None, speed=5):
715
""" output the first 375 chars .. put the rest into cache. """
717
# convert the data to the encoding
719
what = toenc(what.rstrip())
720
except Exception, ex:
721
rlog(10, self.name, "can't output: %s" % str(ex))
726
# split up in parts of 375 chars overflowing on word boundaries
727
txtlist = splittxt(what)
731
self.output(printto, txtlist[0], how, who, fromm)
733
# see if we need to store output in less cache
737
self.less.add(printto, txtlist[1:])
739
self.less.add(fromm, txtlist[1:])
740
size = len(txtlist) - 2
741
result = txtlist[1:2][0]
743
result += " (+%s)" % size
745
if len(txtlist) == 2:
750
self.output(printto, result, how, who, fromm)
752
def output(self, printto, what, how='msg' , who=None, fromm=None):
754
""" first output .. then call saymonitor. """
756
self.outputnolog(printto, what, how, who, fromm)
757
saymonitor.put(self.name, printto, what, who, how, fromm)
759
def outputnolog(self, printto, what, how, who=None, fromm=None):
761
""" do output to irc server .. rate limit to 3 sec. """
763
if fromm and shouldignore(fromm):
767
what = fix_format(what)
770
self.privmsg(printto, what)
771
elif how == 'notice':
772
self.notice(printto, what)
774
self.ctcp(printto, what)
775
except Exception, ex:
778
def donick(self, nick, setorig=0, save=0, whois=0):
780
""" change nick .. optionally set original nick and/or save to config. """
785
# disable auto 433 nick changing
788
# set up wait for NICK command and issue NICK
789
queue = Queue.Queue()
791
self.wait.register('NICK', self.nick[:16], queue, 12)
792
self._raw('NICK %s\n' % nick)
793
result = waitforqueue(queue, 5)
795
# reenable 433 auto nick changing
809
# save nick to state and config file
811
self.state['nick'] = nick
813
self.cfg.set('nick', nick)
817
def join(self, channel, password=None):
819
""" join channel with optional password. """
824
# do join with password
826
self._raw('JOIN %s %s' % (channel, password))
828
self.channels[channel.lower()]['key'] = password
834
self._raw('JOIN %s' % channel)
836
def part(self, channel):
838
""" leave channel. """
842
self._raw('PART %s' % channel)
845
self.state['joinedchannels'].remove(channel)
847
except (KeyError, ValueError):
852
""" send who query. """
856
self.putonqueue(6, 'WHO %s' % who.strip())
858
def names(self, channel):
860
""" send names query. """
864
self.putonqueue(6, 'NAMES %s' % channel)
866
def whois(self, who):
868
""" send whois query. """
872
self.putonqueue(6, 'WHOIS %s' % who)
874
def privmsg(self, printto, what):
876
""" send privmsg to irc server. """
878
if not printto or not what:
880
self.send('PRIVMSG %s :%s' % (printto, what))
884
""" send text to irc server. """
893
self.outputlock.acquire()
895
timetosleep = 5 - (now - self.lastoutput)
896
if timetosleep > 0 and not self.nolimiter:
897
rlog(0, self.name, 'flood protect')
898
time.sleep(timetosleep)
899
txt = toenc(strippedtxt(txt))
903
self.outputlock.release()
906
self.lastoutput = time.time()
907
except Exception, ex:
909
self.outputlock.release()
912
if not self.blocking and 'broken pipe' in str(ex).lower():
913
rlog(11, self.name, 'broken pipe error .. ignoring')
915
rlog(11, self.name, 'send error: %s' % str(ex))
919
def voice(self, channel, who):
923
if not channel or not who:
925
self.putonqueue(9, 'MODE %s +v %s' % (channel, who))
927
def doop(self, channel, who):
931
if not channel or not who:
933
self._raw('MODE %s +o %s' % (channel, who))
935
def delop(self, channel, who):
939
if not channel or not who:
941
self._raw('MODE %s -o %s' % (channel, who))
943
def quit(self, reason='http://gozerbot.org'):
945
""" send quit message. """
947
rlog(10, self.name, 'sending quit')
949
self._raw('QUIT :%s' % reason)
953
def notice(self, printto, what):
957
if not printto or not what:
959
self.send('NOTICE %s :%s' % (printto, what))
961
def ctcp(self, printto, what):
963
""" send ctcp privmsg. """
965
if not printto or not what:
967
self.send("PRIVMSG %s :\001%s\001" % (printto, what))
969
def ctcpreply(self, printto, what):
971
""" send ctcp notice. """
973
if not printto or not what:
975
self.putonqueue(2, "NOTICE %s :\001%s\001" % (printto, what))
977
def action(self, printto, what):
981
if not printto or not what:
983
self.putonqueue(9, "PRIVMSG %s :\001ACTION %s\001" % (printto, what))
985
def handle_ievent(self, ievent):
987
""" handle ircevent .. dispatch to 'handle_command' method. """
990
if ievent.cmnd == 'JOIN' or ievent.msg:
991
if ievent.nick.lower() in self.nicks401:
992
self.nicks401.remove(ievent.nick)
993
rlog(10, self.name, '%s joined .. unignoring')
994
# see if the irc object has a method to handle the ievent
995
method = getattr(self,'handle_' + ievent.cmnd.lower())
1002
except AttributeError:
1003
# no command method to handle event
1006
# see if there are wait callbacks
1007
self.wait.check(ievent)
1011
def handle_432(self, ievent):
1013
""" erroneous nick. """
1015
self.handle_433(ievent)
1017
def handle_433(self, ievent):
1019
""" handle nick already taken. """
1023
nick = ievent.arguments[1]
1024
# check for alternick
1025
alternick = self.state['alternick']
1026
if alternick and not self.nickchanged:
1027
rlog(10, self.name, 'using alternick %s' % alternick)
1028
self.donick(alternick)
1029
self.nickchanged = 1
1032
randomnick = getrandomnick()
1033
self._raw("NICK %s" % randomnick)
1034
self.nick = randomnick
1035
rlog(100, self.name, 'ALERT: nick %s already in use/unavailable .. \
1036
using randomnick %s' % (nick, randomnick))
1037
self.nickchanged = 1
1039
def handle_ping(self, ievent):
1041
""" send pong response. """
1045
self._raw('PONG :%s' % ievent.txt)
1047
def handle_001(self, ievent):
1049
""" we are connected. """
1051
self.connectok.set()
1052
self.connected = True
1053
periodical.addjob(15, 1, self.whois, self, self.nick)
1055
def handle_privmsg(self, ievent):
1057
""" check if msg is ctcp or not .. return 1 on handling. """
1059
if ievent.txt and ievent.txt[0] == '\001':
1060
self.handle_ctcp(ievent)
1063
def handle_notice(self, ievent):
1065
""" handle notice event .. check for version request. """
1067
if ievent.txt and ievent.txt.find('VERSION') != -1:
1068
self.say(ievent.nick, self.cfg['version'], None, 'notice')
1071
def handle_ctcp(self, ievent):
1073
""" handle client to client request .. version and ping. """
1075
if ievent.txt.find('VERSION') != -1:
1076
self.ctcpreply(ievent.nick, 'VERSION %s' % self.cfg['version'])
1078
if ievent.txt.find('PING') != -1:
1080
pingtime = ievent.txt.split()[1]
1081
pingtime2 = ievent.txt.split()[2]
1083
self.ctcpreply(ievent.nick, 'PING ' + pingtime + ' ' + \
1088
def handle_error(self, ievent):
1092
if ievent.txt.startswith('Closing'):
1093
rlog(10, self.name, ievent.txt)
1095
rlog(10, self.name + '.ERROR', "%s - %s" % (ievent.arguments, \
1100
""" ping the irc server. """
1102
rlog(1, self.name, 'sending ping')
1104
self.putonqueue(1, 'PING :%s' % self.server)
1106
except Exception, ex:
1107
rlog(10, self.name, "can't send ping: %s" % str(ex))
1110
def handle_401(self, ievent):
1112
""" handle 401 .. nick not available. """
1115
nick = ievent.arguments[1]
1116
if nick not in self.nicks401:
1117
rlog(10, self.name, '401 on %s .. ignoring' % nick)
1118
self.nicks401.append(nick)
1122
def handle_700(self, ievent):
1124
""" handle 700 .. encoding request of the server. """
1127
self.encoding = ievent.arguments[1]
1128
rlog(10, self.name, '700 encoding now is %s' % self.encoding)