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.
389 lines
11 KiB
389 lines
11 KiB
import xmpp, os, sys, time, traceback, stream_ft
|
|
import threading, errno, select
|
|
import util, datetime
|
|
|
|
from config import *
|
|
|
|
VERSION = "0.2"
|
|
IGNORE_FIRST = 10
|
|
|
|
last_connect = 0
|
|
reconnect = False
|
|
commands = dict()
|
|
commands_private = dict()
|
|
group_listeners = []
|
|
private_listeners = []
|
|
status_listeners = []
|
|
call_ats = dict()
|
|
call_ats_id = 0
|
|
join_time = time.time()+60*60*24*365 #1 year in the future
|
|
|
|
users = dict()
|
|
|
|
STATUS_JOIN, STATUS_ONLINE, STATUS_OFFLINE, STATUS_RENAME, STATUS_XAWAY, STATUS_AWAY, STATUS_UNKNOWN = range(7)
|
|
SI, JINGLE = range(2)
|
|
|
|
def getUsers():
|
|
"""Return a dictionary of every user in the group, the values are status codes."""
|
|
return users
|
|
|
|
def addCommand(name,help,func):
|
|
"""Add a groupchat command.
|
|
|
|
@param name -- The name of the command, if the command keyword
|
|
"!name" is used at the start of a message the given
|
|
handler will be called
|
|
@param help -- Help string displayed on with the !help command
|
|
@param func -- Handler of the form func(msg,args) where msg is the
|
|
xmpppy representation of the message sent and args is
|
|
a list of arguments"""
|
|
commands[name]=(help,func)
|
|
|
|
def addPrivateCommand(name,help,func):
|
|
"""Add a private chat command.
|
|
|
|
@param name -- The name of the command, if the command keyword
|
|
"!name" is used at the start of a message the given
|
|
handler will be called
|
|
@param help -- Help string displayed on with the !help command
|
|
@param func -- Handler of the form func(msg,args) where msg is the
|
|
xmpppy representation of the message sent and args is
|
|
a list of arguments"""
|
|
commands_private[name]=(help,func)
|
|
|
|
def addGroupListener(func):
|
|
"""Add a groupchat listener.
|
|
|
|
@param func -- Handler of the form func(from,msg) where both arguments
|
|
are unicode strings"""
|
|
group_listeners.append(func)
|
|
|
|
def addPrivateListener(func):
|
|
"""Adds a private chat listener.
|
|
|
|
@param func -- Handler of the form func(from,msg) where both arguments
|
|
are unicode strings"""
|
|
private_listeners.append(func)
|
|
|
|
def addStatusListener(func):
|
|
"""Adds a user status listener.
|
|
|
|
@param func -- Handler of the form func(from,msg,status) where first two
|
|
arguments are unicode strings and status is one of STATUS_*
|
|
constants"""
|
|
status_listeners.append(func)
|
|
|
|
def sendMsg(to,body,mtype=None):
|
|
"""Send a message to the server.
|
|
|
|
@param to -- Recipient of the message
|
|
@param body -- Message body
|
|
@param mtype -- Type of the message, (e.g. "groupchat" for group
|
|
messages)"""
|
|
mymsg = xmpp.protocol.Message(body=body)
|
|
mymsg.setTo(to)
|
|
if mtype!=None:
|
|
mymsg.setType(mtype)
|
|
cl.send(mymsg)
|
|
|
|
def sendStyledMsg(to,body,mtype=None,style=""):
|
|
"""Send a CSS-styled message to the server.
|
|
|
|
@param to -- Recipient of the message
|
|
@param body -- Message body
|
|
@param mtype -- Type of the message, (e.g. "groupchat" for group
|
|
messages)
|
|
@param style -- Inline CSS used to format the message"""
|
|
mymsg = xmpp.protocol.Message(body=body)
|
|
mymsg.setTo(to)
|
|
if mtype!=None:
|
|
mymsg.setType(mtype)
|
|
c=mymsg.addChild('html', namespace='http://jabber.org/protocol/xhtml-im')
|
|
c=c.addChild('body',namespace='http://www.w3.org/1999/xhtml')
|
|
c=c.addChild('span',payload=body,attrs={'style':style})
|
|
cl.send(mymsg)
|
|
|
|
def sendMonoMsg(to,body,mtype=None):
|
|
"""Send a mono-spaced styled message to the server (e.g. ASCII art).
|
|
|
|
@param to -- Recipient of the message
|
|
@param body -- Message body
|
|
@param mtype -- Type of the message, (e.g. "groupchat" for group
|
|
messages)"""
|
|
mymsg = xmpp.protocol.Message(body=body)
|
|
mymsg.setTo(to)
|
|
mymsg.setType(mtype)
|
|
c=mymsg.addChild('html', namespace='http://jabber.org/protocol/xhtml-im')
|
|
c=c.addChild('body',namespace='http://www.w3.org/1999/xhtml')
|
|
c=c.addChild('span', attrs={'style':'font-family: Courier New, monospace;'})
|
|
for part in body.split('\n'):
|
|
c=c.addChild('pre',payload=part,attrs={'style':'background-color: #ffffff;'})
|
|
c=c.addChild('br')
|
|
cl.send(mymsg)
|
|
|
|
def __help(msg,args):
|
|
sendMsg(ROOM,"Available commands:",'groupchat')
|
|
for cmd in commands:
|
|
sendMsg(ROOM," !"+cmd+" - "+commands[cmd][0],'groupchat')
|
|
sendMsg(ROOM,"Available private chat commands:",'groupchat')
|
|
for cmd in commands_private:
|
|
sendMsg(ROOM," !"+cmd+" - "+commands_private[cmd][0],'groupchat')
|
|
|
|
def __help2(msg,args):
|
|
sendMsg(msg.getFrom(),"Available commands:")
|
|
for cmd in commands:
|
|
sendMsg(msg.getFrom()," !"+cmd+" - "+commands[cmd][0])
|
|
sendMsg(msg.getFrom(),"Available private chat commands:")
|
|
for cmd in commands_private:
|
|
sendMsg(msg.getFrom()," !"+cmd+" - "+commands_private[cmd][0])
|
|
|
|
def __messageCB(conn,msg):
|
|
if msg.getType() == "groupchat":
|
|
body = unicode(msg.getBody())
|
|
mfrom = msg.getFrom()
|
|
|
|
for cmd in commands:
|
|
if body.startswith("!"+cmd) and not room_nick in unicode(mfrom):
|
|
args = util.split_args(body)
|
|
commands[cmd][1](msg,args)
|
|
for listener in group_listeners:
|
|
listener( mfrom , msg )
|
|
|
|
if msg.getType() == "chat":
|
|
if msg.getBody() != None:
|
|
body = unicode(msg.getBody())
|
|
mfrom = msg.getFrom()
|
|
for cmd in commands_private:
|
|
if body.startswith("!"+cmd) and not room_nick in unicode(mfrom):
|
|
args = util.split_args(body)
|
|
commands_private[cmd][1](msg,args)
|
|
for listener in private_listeners:
|
|
listener( mfrom, msg )
|
|
raise xmpp.NodeProcessed
|
|
|
|
def __presenceCB(conn,msg):
|
|
global reconnect
|
|
if msg.getType()=='unavailable':
|
|
nick=msg.getNick()
|
|
print room_nick
|
|
if nick==None:
|
|
if msg.getFrom() in users:
|
|
del users[msg.getFrom()]
|
|
#Disconnect
|
|
if not msg.getFrom() in users:
|
|
for listener in status_listeners:
|
|
listener(msg.getFrom(),msg,STATUS_OFFLINE)
|
|
elif nick==NAME:
|
|
print "--got kicked"
|
|
reconnect = True
|
|
else:
|
|
if msg.getFrom() in users:
|
|
del users[msg.getFrom()]
|
|
#Rename
|
|
if not msg.getFrom() in users:
|
|
for listener in status_listeners:
|
|
listener(msg.getFrom(),msg,STATUS_RENAME)
|
|
else:
|
|
show = msg.getShow()
|
|
if show==None:
|
|
if not msg.getFrom() in users:
|
|
for listener in status_listeners:
|
|
listener(msg.getFrom(),msg,STATUS_JOIN)
|
|
|
|
users[msg.getFrom()] = STATUS_ONLINE
|
|
#Online
|
|
else:
|
|
#print msg.getFrom(),"is now",show
|
|
if not msg.getFrom() in users:
|
|
for listener in status_listeners:
|
|
listener(msg.getFrom(),msg,STATUS_JOIN)
|
|
if show=="away":
|
|
users[msg.getFrom()] = STATUS_AWAY
|
|
elif show=="xa":
|
|
users[msg.getFrom()] = STATUS_XAWAY
|
|
else:
|
|
users[msg.getFrom()] = STATUS_UNKNOWN
|
|
# status change
|
|
|
|
status = STATUS_OFFLINE
|
|
if msg.getFrom() in users:
|
|
status = users[msg.getFrom()]
|
|
|
|
for listener in status_listeners:
|
|
listener(msg.getFrom(),msg.getStatus(),status)
|
|
|
|
def __ignore_first(conn,msg):
|
|
global join_time
|
|
dt = time.time()-join_time
|
|
if dt>IGNORE_FIRST:
|
|
cl.RegisterHandler('message',__messageCB)
|
|
cl.UnregisterHandler('message',__ignore_first)
|
|
__messageCB(conn,msg)
|
|
|
|
|
|
|
|
def connect_and_join():
|
|
"""Connect to the server and join the group chat room from the config."""
|
|
global room_nick
|
|
global room_to
|
|
global last_connect
|
|
global cl
|
|
global join_time
|
|
last_connect=time.time()
|
|
|
|
jid=xmpp.protocol.JID(node=NAME,domain=SRV,resource=RESOURCE)
|
|
cl=xmpp.Client(jid.getDomain(),debug=[])
|
|
#connect to server
|
|
con=cl.connect()
|
|
if not con:
|
|
print 'could not connect!'
|
|
raise Exception('could not connect!')
|
|
print 'connected'
|
|
|
|
cl.RegisterHandler('message',__ignore_first)
|
|
cl.RegisterHandler('presence',__presenceCB)
|
|
cl.RegisterHandler('iq',stream_ft.ft_listener)
|
|
|
|
#authenticate
|
|
auth=cl.auth(NAME,PWD,resource=jid.getResource())
|
|
if not auth:
|
|
print 'could not authenticate!'
|
|
raise Exception('could not authenticate!')
|
|
print 'auth'
|
|
|
|
|
|
room_to = "%s/%s" % (ROOM, NAME)
|
|
room_nick = ROOM+"/"+NAME
|
|
msg = xmpp.Presence(to=room_to)
|
|
if PWD_ROOM != "":
|
|
msg.addChild('x', namespace='http://jabber.org/protocol/muc').addChild('password',payload=PWD_ROOM)
|
|
|
|
#add the help command
|
|
addCommand("help","Prints this help message",__help)
|
|
addPrivateCommand("help","Prints this help message (in private chat)",__help2)
|
|
|
|
#join the room
|
|
cl.send(msg)
|
|
join_time = time.time()
|
|
#set status
|
|
cl.send(xmpp.Presence(to=room_to,status=NAME+" --(brathuhn bot framework (v"+VERSION+"), type !help)"))
|
|
|
|
def loop():
|
|
"""Loop forever and handle the event-queue."""
|
|
global last_connect
|
|
global call_ats
|
|
global reconnect
|
|
while True:
|
|
try:
|
|
|
|
if reconnect or cl.connected: #TODO check if it works
|
|
print "--reconnect--"
|
|
s=RECONNECT_INTERVAL-(time.time()-last_connect)
|
|
if s>0:
|
|
print "--reconnect interval not met (%dsek), sleeping[sek] %d" % (RECONNECT_INTERVAL,s)
|
|
time.sleep(s)
|
|
reconnect = False
|
|
print "--wake--"
|
|
connect_and_join()
|
|
cl.Process(1)
|
|
to_remove = []
|
|
for cid in call_ats:
|
|
call=call_ats[cid]
|
|
if time.time()>=call[1]:
|
|
call[0](*call[2])
|
|
to_remove.append(cid)
|
|
for cid in to_remove:
|
|
del call_ats[cid]
|
|
|
|
except xmpp.protocol.Conflict:
|
|
print "detected another connection, quitting..."
|
|
sys.exit(0)
|
|
except KeyboardInterrupt:
|
|
print "keyboard interrupt, quitting..."
|
|
sys.exit(0)
|
|
except select.error as e:
|
|
if not (e.args and e.args[0] == errno.EINTR):
|
|
print_error()
|
|
except:
|
|
print_error()
|
|
|
|
def print_error():
|
|
print "-------ERROR OCCURED-------", datetime.datetime.now.strftime("%Y-%h-%d, %H:%M:%S")
|
|
print"--time: ",
|
|
traceback.print_exc(file=sys.stdout)
|
|
print "---------------------------"
|
|
|
|
def callAt(func,_time,*args):
|
|
"""Call a function at given time; The call is not very exact but it is
|
|
guaranteed that it will happen after the specified time. The call happens
|
|
in the same thread, so no synchronization is needed. You can append any
|
|
number of arguments you need. Returns the call id, needed to cancel a call
|
|
|
|
E.g this will call foo(arg1,arg2) in ~5 seconds
|
|
callAt(foo,time.time()+5,arg1,arg2)
|
|
|
|
@param func -- Function to be called
|
|
@param time -- The time when the message will be called"""
|
|
global call_ats
|
|
global call_ats_id
|
|
call_ats[call_ats_id]=[func,_time,args]
|
|
call_ats_id+=1
|
|
return call_ats_id-1
|
|
|
|
def cancelCall(cid):
|
|
"""Remove the call from the list of pending calls
|
|
|
|
@param cid -- The id of the call to remove
|
|
"""
|
|
if cid in call_ats:
|
|
del call_ats[cid]
|
|
|
|
def sendFile(to,path,method=SI):
|
|
"""Send a file using method specified
|
|
|
|
@param to -- Reciever of the file
|
|
@param path -- Path to the file
|
|
@param method -- Initiation method, currently only stream initiation is supported
|
|
"""
|
|
|
|
size = os.path.getsize(path)
|
|
name = os.path.basename(path)
|
|
fd = open(path,'rb')
|
|
if method==SI:
|
|
cim = stream_ft.create_init_message(room_nick,to,name,size,fd)
|
|
cl.send(cim)
|
|
else:
|
|
fd.close()
|
|
|
|
def sendFileStream(to,size,name,fd,method=SI):
|
|
"""Send a file using method specified
|
|
|
|
@param to -- Reciever of the file
|
|
@param size -- Number of bytes to send
|
|
@param name -- The name of the file the user will see
|
|
@param fd -- A file like object
|
|
@param method -- Initiation method, currently only stream initiation is supported
|
|
"""
|
|
|
|
if method==SI:
|
|
cim = stream_ft.create_init_message(room_nick,to,name,size,fd)
|
|
cl.send(cim)
|
|
else:
|
|
fd.close()
|
|
|
|
def statusString(status):
|
|
if status==STATUS_JOIN:
|
|
return "join"
|
|
if status==STATUS_ONLINE:
|
|
return "online"
|
|
if status==STATUS_OFFLINE:
|
|
return "leave"
|
|
if status==STATUS_RENAME:
|
|
return "rename"
|
|
if status==STATUS_XAWAY:
|
|
return "extended away"
|
|
if status==STATUS_AWAY:
|
|
return "away"
|
|
if status==STATUS_UNKNOWN:
|
|
return "???"
|
|
|
|
|