- core: 2 new convenient methods: getJidNStream and getClient - new param in plugin info: "handler" to know if there is a handler to plug on profiles clients - plugins with handler now use an other class which is returned to profile client with the new method "getHandler" and pluged when connecting
575 lines
23 KiB
Python
575 lines
23 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
SAT: a jabber client
|
|
Copyright (C) 2009, 2010 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
This program 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 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
from __future__ import with_statement
|
|
|
|
import os.path
|
|
import time
|
|
import pickle
|
|
from xml.dom import minidom
|
|
from logging import debug, info, error
|
|
import pdb
|
|
from twisted.internet import defer
|
|
from twisted.words.protocols.jabber import jid
|
|
|
|
SAVEFILE_PARAM_XML="/param" #xml parameters template
|
|
SAVEFILE_PARAM_DATA="/param" #individual & general parameters; _ind and _gen suffixes will be added
|
|
SAVEFILE_HISTORY="/history"
|
|
SAVEFILE_PRIVATE="/private" #file used to store misc values (mainly for plugins)
|
|
|
|
class Param():
|
|
"""This class manage parameter with xml"""
|
|
### TODO: add desciption in params
|
|
|
|
#TODO: mettre Watched dans un plugin
|
|
default_xml = u"""
|
|
<params>
|
|
<general>
|
|
</general>
|
|
<individual>
|
|
<category name="Connection">
|
|
<param name="JabberID" value="goffi@necton2.int/TestScript" type="string" />
|
|
<param name="Password" value="toto" type="password" />
|
|
<param name="Server" value="necton2.int" type="string" />
|
|
<param name="NewAccount" value="Register new account" type="button" callback="registerNewAccount"/>
|
|
</category>
|
|
<category name="Misc">
|
|
<param name="Watched" value="test@Jabber.goffi.int" type="string" />
|
|
</category>
|
|
</individual>
|
|
</params>
|
|
"""
|
|
|
|
def load_default_params(self):
|
|
self.dom = minidom.parseString(Param.default_xml.encode('utf-8'))
|
|
|
|
def load_xml(self, file):
|
|
"""Load parameters template from file"""
|
|
self.dom = minidom.parse(file)
|
|
|
|
def load_data(self, file):
|
|
"""Load parameters data from file"""
|
|
file_ind = file + '_ind'
|
|
file_gen = file + '_gen'
|
|
|
|
if os.path.exists(file_gen):
|
|
try:
|
|
with open(file_gen, 'r') as file_gen_pickle:
|
|
self.params_gen=pickle.load(file_gen_pickle)
|
|
debug("general params data loaded")
|
|
except:
|
|
error ("Can't load general params data !")
|
|
|
|
if os.path.exists(file_ind):
|
|
try:
|
|
with open(file_ind, 'r') as file_ind_pickle:
|
|
self.params=pickle.load(file_ind_pickle)
|
|
debug("individual params data loaded")
|
|
except:
|
|
error ("Can't load individual params data !")
|
|
|
|
def save_xml(self, file):
|
|
"""Save parameters template to xml file"""
|
|
with open(file, 'wb') as xml_file:
|
|
self.dom.writexml(xml_file)
|
|
|
|
def save_data(self, file):
|
|
"""Save parameters data to file"""
|
|
#TODO: save properly in a separate file/database,
|
|
# use different behaviour depending of the data type (e.g. password encrypted)
|
|
|
|
#general params
|
|
with open(file+'_gen', 'w') as param_gen_pickle:
|
|
pickle.dump(self.params_gen, param_gen_pickle)
|
|
|
|
#then individual params
|
|
with open(file+'_ind', 'w') as param_ind_pickle:
|
|
pickle.dump(self.params, param_ind_pickle)
|
|
|
|
def __init__(self, host):
|
|
debug("Parameters init")
|
|
self.host = host
|
|
self.default_profile = None
|
|
self.params = {'goffi':{}} #gof:
|
|
self.params_gen = {}
|
|
host.set_const('savefile_param_xml', SAVEFILE_PARAM_XML)
|
|
host.set_const('savefile_param_data', SAVEFILE_PARAM_DATA)
|
|
host.registerGeneralCB("registerNewAccount", host.registerNewAccountCB)
|
|
|
|
def getProfilesList(self):
|
|
return self.params.keys()
|
|
|
|
def createProfile(self, name, default=False):
|
|
"""Create a new profile
|
|
@param name: Name of the profile
|
|
@param default: True if default value"""
|
|
if self.params.has_key(name):
|
|
info ('The profile name already exists')
|
|
return 1
|
|
self.params[name]={}
|
|
return 0
|
|
|
|
def getProfileName(self, profile_key):
|
|
"""return profile according to profile_key
|
|
@param profile_key: profile name or key which can be
|
|
@ALL@ for all profiles
|
|
@DEFAULT@ for default profile
|
|
@return: requested profile name or None if it doesn't exist"""
|
|
if profile_key=='@DEFAULT@':
|
|
if not self.params:
|
|
return None
|
|
info('No default profile, returning first one') #TODO: manage real default profile
|
|
return self.params.keys()[0] #FIXME: gof: temporary, must use real default value, and fallback to first one if it doesn't exists
|
|
if not self.params.has_key(profile_key):
|
|
error ('Trying to access an unknown profile')
|
|
return None
|
|
return profile_key
|
|
|
|
def __get_unique_node(self, parent, tag, name):
|
|
"""return node with given tag
|
|
@param parent: parent of nodes to check (e.g. documentElement)
|
|
@param tag: tag to check (e.g. "category")
|
|
@param name: name to check (e.g. "JID")
|
|
@return: node if it exist or None
|
|
"""
|
|
for node in parent.childNodes:
|
|
if node.nodeName == tag and node.getAttribute("name") == name:
|
|
#the node already exists
|
|
return node
|
|
#the node is new
|
|
return None
|
|
|
|
def importParams(self, xml):
|
|
"""import xml in parameters, do nothing if the param already exist
|
|
@param xml: parameters in xml form"""
|
|
src_dom = minidom.parseString(xml.encode('utf-8'))
|
|
|
|
def import_node(tgt_parent, src_parent):
|
|
for child in src_parent.childNodes:
|
|
if child.nodeName == '#text':
|
|
continue
|
|
node = self.__get_unique_node(tgt_parent, child.nodeName, child.getAttribute("name"))
|
|
if not node: #The node is new
|
|
tgt_parent.appendChild(child)
|
|
else:
|
|
import_node(node, child)
|
|
|
|
import_node(self.dom.documentElement, src_dom.documentElement)
|
|
|
|
def __default_ok(self, value, name, category):
|
|
#FIXME: gof: will not work with individual parameters
|
|
self.setParam(name, value, category) #FIXME: better to set param xml value ???
|
|
|
|
def __default_ko(self, failure, name, category):
|
|
error ("Can't determine default value for [%s/%s]: %s" % (category, name, str(failure.value)))
|
|
|
|
def setDefault(self, name, category, callback, errback=None):
|
|
"""Set default value of parameter
|
|
'default_cb' attibute of parameter must be set to 'yes'
|
|
@param name: name of the parameter
|
|
@param category: category of the parameter
|
|
@param callback: must return a string with the value (use deferred if needed)
|
|
@param errback: must manage the error with args failure, name, category
|
|
"""
|
|
#TODO: send signal param update if value changed
|
|
node = self.__getParamNode(name, category, '@ALL@')
|
|
if not node:
|
|
error("Requested param [%s] in category [%s] doesn't exist !" , name, category)
|
|
return
|
|
if node[1].getAttribute('default_cb') == 'yes':
|
|
del node[1].attributes['default_cb']
|
|
d = defer.maybeDeferred(callback)
|
|
d.addCallback(self.__default_ok, name, category)
|
|
d.addErrback(errback or self.__default_ko, name, category)
|
|
|
|
def getParamA(self, name, category, attr="value", profile_key="@DEFAULT@"):
|
|
"""Helper method to get a specific attribute
|
|
@param name: name of the parameter
|
|
@param category: category of the parameter
|
|
@param attr: name of the attribute (default: "value")
|
|
@param profile: owner of the param (@ALL@ for everyone)
|
|
|
|
@return: attribute"""
|
|
node = self.__getParamNode(name, category)
|
|
if not node:
|
|
error("Requested param [%s] in category [%s] doesn't exist !" , name, category)
|
|
return None
|
|
|
|
if node[0] == 'general':
|
|
value = self.__getParam(None, category, name, 'general')
|
|
return value or node[1].getAttribute(attr)
|
|
|
|
assert(node[0] == 'individual')
|
|
|
|
profile = self.getProfileName(profile_key)
|
|
if not profile:
|
|
error('Requesting a param for an non-existant profile')
|
|
return None
|
|
|
|
if attr == "value":
|
|
return self.__getParam(profile, category, name) or node[1].getAttribute(attr)
|
|
else:
|
|
return node[1].getAttribute(attr)
|
|
|
|
|
|
def __getParam(self, profile, category, name, type='individual'):
|
|
"""Return the param, or None if it doesn't exist
|
|
@param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@)
|
|
@param category: param category
|
|
@param name: param name
|
|
"""
|
|
if type == 'general':
|
|
if self.params_gen.has_key((category, name)):
|
|
return self.params_gen[(category, name)]
|
|
return None #This general param has the default value
|
|
assert (type == 'individual')
|
|
if not self.params.has_key(profile) or not self.params[profile].has_key((category, name)):
|
|
return None
|
|
return self.params[profile][(category, name)]
|
|
|
|
def __constructProfileXml(self, profile):
|
|
"""Construct xml for asked profile, filling values when needed
|
|
/!\ as noticed in doc, don't forget to unlink the minidom.Document
|
|
@param profile: profile name (not key !)
|
|
@return: minidom.Document of the profile xml (cf warning above)
|
|
"""
|
|
prof_xml = minidom.parseString('<params/>')
|
|
|
|
for type_node in self.dom.documentElement.childNodes:
|
|
if type_node.nodeName == 'general' or type_node.nodeName == 'individual': #we use all params, general and individual
|
|
for cat_node in type_node.childNodes:
|
|
if cat_node.nodeName == 'category':
|
|
category = cat_node.getAttribute('name')
|
|
cat_copy = cat_node.cloneNode(True) #we make a copy for the new xml
|
|
params = cat_copy.getElementsByTagName("param")
|
|
for param_node in params:
|
|
name = param_node.getAttribute('name')
|
|
profile_value = self.__getParam(profile, category, name, type_node.nodeName)
|
|
if profile_value: #there is a value for this profile, we must change the default
|
|
param_node.setAttribute('value', profile_value)
|
|
prof_xml.documentElement.appendChild(cat_copy)
|
|
return prof_xml
|
|
|
|
|
|
|
|
def getParams(self, profile_key='@DEFAULT@'):
|
|
"""Construct xml for asked profile
|
|
Take params xml as skeleton"""
|
|
profile = self.getProfileName(profile_key)
|
|
if not profile:
|
|
error("Asking params for inexistant profile")
|
|
return ""
|
|
prof_xml = self.__constructProfileXml(profile)
|
|
return_xml = prof_xml.toxml()
|
|
prof_xml.unlink()
|
|
|
|
return return_xml
|
|
|
|
def getParamsForCategory(self, category, profile_key='@DEFAULT@'):
|
|
"""Return node's xml for selected category"""
|
|
#TODO: manage category of general type (without existant profile)
|
|
profile = self.getProfileName(profile_key)
|
|
if not profile:
|
|
error("Asking params for inexistant profile")
|
|
return ""
|
|
prof_xml = self.__constructProfileXml(profile)
|
|
|
|
for node in prof_xml.getElementsByTagName("category"):
|
|
if node.nodeName == "category" and node.getAttribute("name") == category:
|
|
result = node.toxml()
|
|
prof_xml.unlink()
|
|
return result
|
|
|
|
prof_xml.unlink()
|
|
return "<category />"
|
|
|
|
def __getParamNode(self, name, category, type="@ALL@"): #FIXME: is type useful ?
|
|
"""Return a node from the param_xml
|
|
@param name: name of the node
|
|
@param category: category of the node
|
|
@type: keyword for search:
|
|
@ALL@ search everywhere
|
|
@GENERAL@ only search in general type
|
|
@INDIVIDUAL@ only search in individual type
|
|
@return: a tuple with the node type and the the node, or None if not found"""
|
|
|
|
for type_node in self.dom.documentElement.childNodes:
|
|
if ( ((type == "@ALL@" or type == "@GENERAL@") and type_node.nodeName == 'general')
|
|
or ( (type == "@ALL@" or type == "@INDIVIDUAL@") and type_node.nodeName == 'individual') ):
|
|
for node in type_node.getElementsByTagName('category'):
|
|
if node.getAttribute("name") == category:
|
|
params = node.getElementsByTagName("param")
|
|
for param in params:
|
|
if param.getAttribute("name") == name:
|
|
return (type_node.nodeName, param)
|
|
return None
|
|
|
|
def getParamsCategories(self):
|
|
"""return the categories availables"""
|
|
categories=[]
|
|
for cat in self.dom.getElementsByTagName("category"):
|
|
categories.append(cat.getAttribute("name"))
|
|
return categories
|
|
|
|
def setParam(self, name, value, category, profile_key='@DEFAULT@'):
|
|
"""Set a parameter, return None if the parameter is not in param xml"""
|
|
|
|
node = self.__getParamNode(name, category, '@ALL@')
|
|
if not node:
|
|
error('Requesting an unknown parameter (%s/%s)' % (category, name))
|
|
return
|
|
|
|
if node[0] == 'general':
|
|
self.params_gen[(category, name)] = value
|
|
self.host.bridge.paramUpdate(name, value, category) #TODO: add profile in signal
|
|
return
|
|
|
|
assert (node[0] == 'individual')
|
|
|
|
profile = self.getProfileName(profile_key)
|
|
if not profile:
|
|
error('Trying to set parameter for an unknown profile')
|
|
return #TODO: throw an error
|
|
|
|
type = node[1].getAttribute("type")
|
|
if type=="button":
|
|
print "clique",node.toxml()
|
|
else:
|
|
self.params[profile][(category, name)] = value
|
|
self.host.bridge.paramUpdate(name, value, category) #TODO: add profile in signal
|
|
|
|
class Memory:
|
|
"""This class manage all persistent informations"""
|
|
|
|
def __init__(self, host):
|
|
info ("Memory manager init")
|
|
self.host = host
|
|
self.contacts={}
|
|
self.presenceStatus={}
|
|
self.subscriptions={}
|
|
self.params=Param(host)
|
|
self.history={} #used to store chat history (key: short jid)
|
|
self.private={} #used to store private value
|
|
host.set_const('savefile_history', SAVEFILE_HISTORY)
|
|
host.set_const('savefile_private', SAVEFILE_PRIVATE)
|
|
self.load()
|
|
|
|
def load(self):
|
|
"""Load parameters and all memory things from file/db"""
|
|
param_file_xml = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_param_xml'))
|
|
param_file_data = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_param_data'))
|
|
history_file = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_history'))
|
|
private_file = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_private'))
|
|
|
|
#parameters
|
|
if os.path.exists(param_file_xml):
|
|
try:
|
|
self.params.load_xml(param_file_xml)
|
|
debug("params template loaded")
|
|
except:
|
|
error ("Can't load params template !")
|
|
self.params.load_default_params()
|
|
else:
|
|
info ("No params template, using default template")
|
|
self.params.load_default_params()
|
|
|
|
try:
|
|
self.params.load_data(param_file_data)
|
|
debug("params loaded")
|
|
except:
|
|
error ("Can't load params !")
|
|
|
|
#history
|
|
if os.path.exists(history_file):
|
|
try:
|
|
with open(history_file, 'r') as history_pickle:
|
|
self.history=pickle.load(history_pickle)
|
|
debug("history loaded")
|
|
except:
|
|
error ("Can't load history !")
|
|
|
|
#private
|
|
if os.path.exists(private_file):
|
|
try:
|
|
with open(private_file, 'r') as private_pickle:
|
|
self.private=pickle.load(private_pickle)
|
|
debug("private values loaded")
|
|
except:
|
|
error ("Can't load private values !")
|
|
|
|
def save(self):
|
|
"""Save parameters and all memory things to file/db"""
|
|
#TODO: need to encrypt files (at least passwords !) and set permissions
|
|
param_file_xml = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_param_xml'))
|
|
param_file_data = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_param_data'))
|
|
history_file = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_history'))
|
|
private_file = os.path.expanduser(self.host.get_const('local_dir')+
|
|
self.host.get_const('savefile_private'))
|
|
|
|
self.params.save_xml(param_file_xml)
|
|
self.params.save_data(param_file_data)
|
|
debug("params saved")
|
|
with open(history_file, 'w') as history_pickle:
|
|
pickle.dump(self.history, history_pickle)
|
|
debug("history saved")
|
|
with open(private_file, 'w') as private_pickle:
|
|
pickle.dump(self.private, private_pickle)
|
|
debug("private values saved")
|
|
|
|
def getProfilesList(self):
|
|
return self.params.getProfilesList()
|
|
|
|
|
|
def getProfileName(self, profile_key):
|
|
"""Return name of profile from keyword
|
|
@param profile_key: can be the profile name or a keywork (like @DEFAULT@)
|
|
@return: profile name or None if it doesn't exist"""
|
|
return self.params.getProfileName(profile_key)
|
|
|
|
def createProfile(self, name, default=False):
|
|
"""Create a new profile
|
|
@param name: Profile name
|
|
@param default: True if default profile (replace previous default)
|
|
"""
|
|
return self.params.createProfile(name, default)
|
|
|
|
def addToHistory(self, me_jid, from_jid, to_jid, type, message):
|
|
me_short=me_jid.userhost()
|
|
from_short=from_jid.userhost()
|
|
to_short=to_jid.userhost()
|
|
|
|
if from_jid==me_jid:
|
|
key=to_short
|
|
else:
|
|
key=from_short
|
|
|
|
if not self.history.has_key(me_short):
|
|
self.history[me_short]={}
|
|
if not self.history[me_short].has_key(key):
|
|
self.history[me_short][key]={}
|
|
|
|
self.history[me_short][key][int(time.time())] = (from_short, message)
|
|
|
|
def getHistory(self, from_jid, to_jid, size):
|
|
ret={}
|
|
if not self.history.has_key(from_jid):
|
|
error("source JID not found !")
|
|
#TODO: throw an error here
|
|
return {}
|
|
if not self.history[from_jid].has_key(to_jid):
|
|
error("dest JID not found !")
|
|
#TODO: throw an error here
|
|
return {}
|
|
stamps=self.history[from_jid][to_jid].keys()
|
|
stamps.sort()
|
|
for stamp in stamps[-size:]:
|
|
ret[stamp]=self.history[from_jid][to_jid][stamp]
|
|
|
|
return ret
|
|
|
|
def setPrivate(self, key, value):
|
|
"""Save a misc private value (mainly useful for plugins)"""
|
|
self.private[key] = value
|
|
|
|
def getPrivate(self, key):
|
|
"""return a private value
|
|
@param key: name of wanted value
|
|
@return: value or None if value don't exist"""
|
|
if self.private.has_key(key):
|
|
return self.private[key]
|
|
return None
|
|
|
|
|
|
def addContact(self, contact_jid, attributes, groups):
|
|
debug("Memory addContact: %s",contact_jid.userhost())
|
|
assert(isinstance(attributes,dict))
|
|
assert(isinstance(groups,set))
|
|
self.contacts[contact_jid.userhost()]=[attributes, groups]
|
|
|
|
def delContact(self, contact_jid):
|
|
debug("Memory delContact: %s",contact_jid.userhost())
|
|
if self.contacts.has_key(contact_jid.userhost()):
|
|
del self.contacts[contact_jid.userhost()]
|
|
|
|
def getContact(self, contact_jid):
|
|
if self.contacts.has_key(contact_jid.userhost()):
|
|
self.contacts[contact_jid.userhost()]
|
|
else:
|
|
return None
|
|
|
|
def getContacts(self):
|
|
debug ("Memory getContact OK (%s)", self.contacts)
|
|
ret=[]
|
|
for contact in self.contacts:
|
|
attr, groups = self.contacts[contact]
|
|
ret.append([contact, attr, groups ])
|
|
return ret
|
|
|
|
def addPresenceStatus(self, contact_jid, show, priority, statuses):
|
|
if not self.presenceStatus.has_key(contact_jid.userhost()):
|
|
self.presenceStatus[contact_jid.userhost()] = {}
|
|
resource = jid.parse(contact_jid.full())[2] or ''
|
|
self.presenceStatus[contact_jid.userhost()][resource] = (show, priority, statuses)
|
|
|
|
def addWaitingSub(self, type, contact_jid):
|
|
"""Called when a subcription request is received"""
|
|
self.subscriptions[contact_jid] = type
|
|
|
|
def delWaitingSub(self, contact_jid):
|
|
"""Called when a subcription request is finished"""
|
|
if self.subscriptions.has_key(contact_jid):
|
|
del self.subscriptions[contact_jid]
|
|
|
|
def getWaitingSub(self):
|
|
"""Called to get a list of currently waiting subscription requests"""
|
|
return self.subscriptions
|
|
|
|
def getPresenceStatus(self):
|
|
debug ("Memory getPresenceStatus (%s)", self.presenceStatus)
|
|
return self.presenceStatus
|
|
|
|
def getParamA(self, name, category, attr="value", profile_key="@DEFAULT@"):
|
|
return self.params.getParamA(name, category, attr, profile_key)
|
|
|
|
def getParams(self):
|
|
return self.params.getParams()
|
|
|
|
def getParamsForCategory(self, category):
|
|
return self.params.getParamsForCategory(category)
|
|
|
|
def getParamsCategories(self):
|
|
return self.params.getParamsCategories()
|
|
|
|
def setParam(self, name, value, category, profile_key='@DEFAULT@'):
|
|
return self.params.setParam(name, value, category, profile_key)
|
|
|
|
def importParams(self, xml):
|
|
return self.params.importParams(xml)
|
|
|
|
def setDefault(self, name, category, callback, errback=None):
|
|
return self.params.setDefault(name, category, callback, errback)
|