You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

516 lines
16 KiB

#!/bin/python
#
# Cryptonote tipbot - IRC routines
# Copyright 2014 moneromooo
# Inspired by "Simple Python IRC bot" by berend
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import sys
import socket
import ssl
import select
import time
import string
import base64
import re
import tipbot.config as config
from tipbot.log import log, log_error, log_warn, log_info, log_log
irc_line_delay = 0
irc = None
sslirc = None
irc_password = ""
irc_min_send_delay = 0.05 # seconds
irc_max_send_delay = 1.1 # seconds
last_ping_time = time.time()
last_send_time = 0
current_send_delay = irc_min_send_delay
irc_network = None
irc_port = None
irc_name = None
irc_quitting = False
userstable=dict()
registered_users=set()
def log_IRCRECV(msg):
log("IRCRECV",msg)
def log_IRCSEND(msg):
log("IRCSEND",msg)
def SendIRC(msg):
global last_send_time, current_send_delay
t = time.time()
dt = t - last_send_time
if dt < current_send_delay:
time.sleep (current_send_delay - dt)
current_send_delay = current_send_delay * 1.5
if current_send_delay > irc_max_send_delay:
current_send_delay = irc_max_send_delay
else:
while dt > current_send_delay * 1.5:
dt = dt - current_send_delay
current_send_delay = current_send_delay / 1.5
if current_send_delay < irc_min_send_delay:
current_send_delay = irc_min_send_delay
break
log_IRCSEND(msg)
irc_send(msg + '\r\n')
last_send_time = time.time()
def irc_recv(size,flags=None):
if config.irc_use_ssl:
return sslirc.read(size)
else:
return irc.recv(size,flags)
def irc_send(data):
if config.irc_use_ssl:
return sslirc.write(data)
else:
return irc.send(data)
def connect_to_irc(network,port,name,password,delay):
global irc
global sslirc
global irc_line_delay
global irc_network
global irc_port
global irc_line_delay
global irc_name
global irc_password
irc_network=network
irc_port=port
irc_name=name
irc_line_delay = delay
irc_password=password
log_info('Connecting to IRC at %s:%u' % (network, port))
try:
irc = socket.socket ( socket.AF_INET, socket.SOCK_STREAM )
if config.irc_use_ssl:
try:
raise RuntimeError('')
irc_ssl_context = ssl.create_default_context()
sslirc = irc_ssl_context.wrap_socket(irc, network)
sslirc.connect ( ( network, port ) )
except Exception,e:
log_warn('Failed to create SSL context, using fallback code')
irc.connect ( ( network, port ) )
sslirc = socket.ssl(irc)
except Exception, e:
log_error( 'Error initializing IRC: %s' % str(e))
exit()
log_IRCRECV(irc_recv(4096))
if config.irc_use_sasl:
SendIRC('CAP REQ :sasl')
else:
SendIRC ( 'PASS *********')
SendIRC ( 'NICK %s' % name)
SendIRC ( 'USER %s %s %s :%s' % (name, name, name, name))
return irc
def reconnect_to_irc():
connect_to_irc(irc_network,irc_port,irc_name,irc_password,irc_line_delay)
def SendTo(where,msg):
SendIRC ('PRIVMSG ' + where + ' : ' + msg)
def Join(chan):
SendIRC ( 'JOIN ' + chan)
def Part(chan):
SendIRC ( 'PART ' + chan)
def Quit(msg):
global irc_quitting
irc_quitting = True
SendIRC ( 'QUIT%s' % msg)
def Who(chan):
userstable[chan] = dict()
SendIRC ( 'WHO ' + chan)
def GetNick(data): # Return Nickname
nick = data.split('!')[0]
nick = nick.replace(':', ' ')
nick = nick.replace(' ', '')
nick = nick.strip(' \t\n\r')
return nick
def GetSendTo(nick,chan):
if chan[0] == '#':
return chan
return nick
def UpdateLastActiveTime(chan,nick):
if chan[0] != '#':
return
if not chan in userstable:
log_error("UpdateLastActiveTime: %s spoke in %s, but %s not found in users table" % (nick, chan, chan))
userstable[chan] = dict()
if not nick in userstable[chan]:
log_error("UpdateLastActiveTime: %s spoke in %s, but was not found in that channel's users table" % (nick, chan))
userstable[chan][nick] = None
userstable[chan][nick] = time.time()
def GetTimeSinceActive(chan,nick):
if not chan in userstable:
log_error("GetTimeSinceActive: channel %s not found in users table" % chan)
return None
if not nick in userstable[chan]:
log_error("GetTimeSinceActive: %s not found in channel %s's users table" % (nick, chan))
return None
t = userstable[chan][nick]
if t == None:
return None
dt = time.time() - t
if dt < 0:
log_error("GetTimeSinceActive: %s active in %s in the future" % (nick, chan))
return None
return dt
def GetActiveNicks(chan,seconds):
nicks = []
if not chan in userstable:
return []
now = time.time()
for nick in userstable[chan]:
t = userstable[chan][nick]
if t == None:
continue
dt = now - t
if dt < 0:
log_error("GetActiveNicks: %s active in %s in the future" % (nick, chan))
continue
if dt < seconds:
nicks.append(nick)
return nicks
def GetUsersTable():
return userstable
def IsAcceptableCommandPrefix(s):
s=s.strip()
log_log('checking whether %s is an acceptable command prefix' % s)
if s=="":
return True
if re.match("%s[\t ]*[:,]?$"%config.tipbot_name, s):
return True
return False
#def Op(to_op, chan):
# SendIRC( 'MODE ' + chan + ' +o: ' + to_op)
#
#def DeOp(to_deop, chan):
# SendIRC( 'MODE ' + chan + ' -o: ' + to_deop)
#
#def Voice(to_v, chan):
# SendIRC( 'MODE ' + chan + ' +v: ' + to_v)
#
#def DeVoice(to_dv, chan):
# SendIRC( 'MODE ' + chan + ' -v: ' + to_dv)
buffered_data = ""
def GetIRCLine():
global buffered_data
idx = buffered_data.find("\n")
if idx == -1:
try:
(r,w,x)=select.select([irc.fileno()],[],[],1)
if irc.fileno() in r:
newdata=irc_recv(4096,socket.MSG_DONTWAIT)
else:
newdata = None
if irc.fileno() in x:
log_error('getline: IRC socket in exception set')
newdata = None
except Exception,e:
log_error('getline: Exception: %s' % str(e))
# Broken pipe when we get kicked for spam
if str(e).find("Broken pipe") != -1:
raise
newdata = None
if newdata == None:
return None
buffered_data+=newdata
idx = buffered_data.find("\n")
if idx == -1:
ret = buffered_data
buffered_data = ""
return ret
ret = buffered_data[0:idx+1]
buffered_data = buffered_data[idx+1:]
return ret
def IRCLoop(on_idle,on_identified,on_command):
global userstable
global registered_users
global last_ping_time
while True:
action = None
try:
data = GetIRCLine()
except Exception,e:
log_warn('Exception from GetIRCLine, we were probably disconnected, reconnecting in 5 seconds')
time.sleep(5)
last_ping_time = time.time()
reconnect_to_irc()
continue
# All that must be done even when nothing from IRC - data may be None here
on_idle()
if data == None:
if time.time() - last_ping_time > config.irc_timeout_seconds:
log_warn('%s seconds without PING, reconnecting in 5 seconds' % config.irc_timeout_seconds)
time.sleep(5)
last_ping_time = time.time()
reconnect_to_irc()
continue
data = data.strip("\r\n")
log_IRCRECV(data)
# consider any IRC data as a ping
last_ping_time = time.time()
if data.find ( config.irc_welcome_line ) != -1:
userstable = dict()
registered_users.clear()
if not config.irc_use_sasl:
SendTo("nickserv", "IDENTIFY %s" % irc_password)
for chan in config.irc_channels:
Join(chan)
#ScanWho(None,[chan])
if data.find ( 'PING' ) == 0:
log_log('Got PING, replying PONG')
last_ping_time = time.time()
SendIRC ( 'PONG ' + data.split() [ 1 ])
continue
if data.startswith('AUTHENTICATE +'):
if config.irc_use_sasl:
authstring = config.irc_sasl_name + chr(0) + config.irc_sasl_name + chr(0) + irc_password
SendIRC('AUTHENTICATE %s' % base64.b64encode(authstring))
else:
log_warn('Got AUTHENTICATE while not using SASL')
if data.find('ERROR :Closing Link:') == 0:
if irc_quitting:
log_info('IRC stopped, bye')
break
log_warn('We were kicked from IRC, reconnecting in 5 seconds')
time.sleep(5)
last_ping_time = time.time()
reconnect_to_irc()
continue
#--------------------------- Action check --------------------------------#
if data.find(':') == -1:
continue
try:
cparts = data.lstrip(':').split(' :')
if len(cparts) == 0:
continue
#if len(cparts) >= 9:
# idx_colon = data.find(':',1)
# idx_space = data.find(' ')
# if idx_space and idx_colon < idx_space and re.search("@([0-9a-fA-F]+:){7}[0-9a-fA-F]+", data):
# log_info('Found IPv6 address in non-text, restructuring')
# idx = data.rfind(':')
# cparts = [ cparts[0], "".join(cparts[1:]) ]
if len(cparts) >= 2:
text = cparts[1]
else:
text = ""
parts = cparts[0].split(' ')
who = parts[0]
action = parts[1]
if len(parts) >= 3:
chan = parts[2]
else:
chan = None
except Exception, e:
log_error('main parser: Exception, continuing: %s' % str(e))
continue
if action == None:
continue
#print 'cparts: ', str(cparts)
#print 'parts: ', str(parts)
#print 'text: ', text
#print 'who: ', who
#print 'action: ', action
#print 'chan: ', chan
# if data.find('#') != -1:
# action = data.split('#')[0]
# action = action.split(' ')[1]
# if data.find('NICK') != -1:
# if data.find('#') == -1:
# action = 'NICK'
#----------------------------- Actions -----------------------------------#
try:
if action == 'CAP':
if parts[2] == '*' and parts[3] == 'ACK':
log_info('CAP ACK received from server')
SendIRC('AUTHENTICATE PLAIN')
elif parts[2] == '*' and parts[3] == 'NAK':
log_info('CAP NAK received from server')
log_error('Failed to negotiate SASL')
exit()
else:
log_warn('Unknown CAP line received from server: %s' % data)
if action == 'NOTICE':
if text.find ('throttled due to flooding') >= 0:
log_warn('Flood protection kicked in, outgoing messages lost')
if who == "NickServ!NickServ@services.":
#if text.find('Information on ') != -1:
# ns_nick = text.split(' ')[2].strip("\002")
# print 'NickServ says %s is registered' % ns_nick
# PerformNextAction(ns_nick, True)
#elif text.find(' is not registered') != -1:
# ns_nick = text.split(' ')[0].strip("\002")
# print 'NickServ says %s is not registered' % ns_nick
# PerformNextAction(ns_nick, False)
if text.find(' ACC ') != -1:
stext = text.split(' ')
ns_nick = stext[0]
ns_acc = stext[1]
ns_status = stext[2]
if ns_acc == "ACC":
if ns_status == "3":
log_info('NickServ says %s is identified' % ns_nick)
on_identified(ns_nick, True)
else:
log_info('NickServ says %s is not identified' % ns_nick)
on_identified(ns_nick, False)
else:
log_error('ACC line not as expected...')
elif action == '903':
log_info('SASL authentication success')
SendIRC('CAP END')
elif action in ['902', '904', '905', '906']:
log_error('SASL authentication failed (%s)' % action)
elif action == '352':
try:
who_chan = parts[3]
who_chan_user = parts[7]
if not who_chan_user in userstable[who_chan]:
userstable[who_chan][who_chan_user] = None
log_log("New list of users in %s: %s" % (who_chan, str(userstable[who_chan].keys())))
except Exception,e:
log_error('Failed to parse "352" line: %s: %s' % (data, str(e)))
elif action == '353':
try:
who_chan = parts[4]
who_chan_users = cparts[1].split(" ")
log_info('who_chan: %s' % str(who_chan))
log_info('who_chan_users: %s' % str(who_chan_users))
for who_chan_user in who_chan_users:
if not who_chan_user in userstable[who_chan]:
if who_chan_user[0] == "@":
who_chan_user = who_chan_user[1:]
userstable[who_chan][who_chan_user] = None
log_log("New list of users in %s: %s" % (who_chan, str(userstable[who_chan].keys())))
except Exception,e:
log_error('Failed to parse "353" line: %s: %s' % (data, str(e)))
elif action == 'PRIVMSG':
UpdateLastActiveTime(chan,GetNick(who))
# resplit to avoid splitting text that contains ':'
text = data.split(':',2)[2]
text=text.strip()
exidx = text.find('!')
if exidx != -1 and len(text)>exidx+1 and text[exidx+1] in string.ascii_letters and IsAcceptableCommandPrefix(text[:exidx]):
cmd = text.split('!')[1]
cmd = cmd.split(' ')
cmd[0] = cmd[0].strip(' \t\n\r')
log_log('Found command from %s: "%s" in channel "%s"' % (who, str(cmd), str(chan)))
#if cmd[0] == 'join':
# Join('#' + cmd[1])
#elif cmd[0] == 'part':
# Part('#' + cmd[1])
on_command(cmd,chan,who)
elif action == 'JOIN':
nick = GetNick(who)
log_info('%s joined the channel' % nick)
if not chan in userstable:
userstable[chan] = dict()
if nick in userstable[chan]:
log_warn('%s joined, but already in %s' % (nick, chan))
else:
userstable[chan][nick] = None
log_log("New list of users in %s: %s" % (chan, str(userstable[chan].keys())))
elif action == 'PART':
nick = GetNick(who)
log_info('%s left the channel' % nick)
if not nick in userstable[chan]:
log_warn('%s left, but was not in %s' % (nick, chan))
else:
del userstable[chan][nick]
log_log("New list of users in %s: %s" % (chan, str(userstable[chan].keys())))
elif action == 'QUIT':
nick = GetNick(who)
log_info('%s quit' % nick)
removed_list = ""
for chan in userstable:
log_log("Checking in %s" % chan)
if nick in userstable[chan]:
removed_list = removed_list + " " + chan
del userstable[chan][nick]
log_log("New list of users in %s: %s" % (chan, str(userstable[chan].keys())))
elif action == 'KICK':
nick = parts[3]
log_info('%s was kicked' % nick)
removed_list = ""
for chan in userstable:
log_log("Checking in %s" % chan)
if nick in userstable[chan]:
removed_list = removed_list + " " + chan
del userstable[chan][nick]
log_log("New list of users in %s: %s" % (chan, str(userstable[chan].keys())))
elif action == 'NICK':
nick = GetNick(who)
new_nick = cparts[len(cparts)-1]
log_info('%s renamed to %s' % (nick, new_nick))
for c in userstable:
log_log('checking %s' % c)
if nick in userstable[c]:
del userstable[c][nick]
if new_nick in userstable[c]:
log_warn('%s is the new name of %s, but was already in %s' % (new_nick, nick, c))
else:
userstable[c][new_nick] = None
log_log("New list of users in %s: %s" % (c, str(userstable[c].keys())))
except Exception,e:
log_error('Exception in top level action processing: %s' % str(e))