mirror of
https://git.wownero.com/wownero/tippero.git
synced 2024-08-15 00:33:14 +00:00
503 lines
15 KiB
Python
503 lines
15 KiB
Python
#!/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 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]
|
|
chan = parts[2]
|
|
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]
|
|
exidx = text.find('!')
|
|
if exidx != -1 and len(text)>exidx+1 and text[exidx+1] in string.ascii_letters:
|
|
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))
|
|
|