comparison src/sat/tools/memory.py @ 223:86d249b6d9b7

Files reorganisation
author Goffi <goffi@goffi.org>
date Wed, 29 Dec 2010 01:06:29 +0100
parents tools/memory.py@e178e8f6d13a
children
comparison
equal deleted inserted replaced
222:3198bfd66daa 223:86d249b6d9b7
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 """
5 SAT: a jabber client
6 Copyright (C) 2009, 2010 Jérôme Poisson (goffi@goffi.org)
7
8 This program is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
20 """
21
22 from __future__ import with_statement
23
24 import os.path
25 import time
26 import cPickle as pickle
27 from xml.dom import minidom
28 from logging import debug, info, error
29 import pdb
30 from twisted.internet import defer
31 from twisted.words.protocols.jabber import jid
32 from sat.tools.xml_tools import paramsXml2xmlUI
33
34 SAVEFILE_PARAM_XML="/param" #xml parameters template
35 SAVEFILE_PARAM_DATA="/param" #individual & general parameters; _ind and _gen suffixes will be added
36 SAVEFILE_HISTORY="/history"
37 SAVEFILE_PRIVATE="/private" #file used to store misc values (mainly for plugins)
38
39 class Param():
40 """This class manage parameters with xml"""
41 ### TODO: add desciption in params
42
43 #TODO: move Watched in a plugin
44 default_xml = u"""
45 <params>
46 <general>
47 </general>
48 <individual>
49 <category name="Connection" label="%(category_connection)s">
50 <param name="JabberID" value="goffi@necton2.int/TestScript" type="string" />
51 <param name="Password" value="toto" type="password" />
52 <param name="Server" value="necton2.int" type="string" />
53 <param name="NewAccount" value="%(label_NewAccount)s" type="button" callback_id="registerNewAccount"/>
54 <param name="autoconnect" label="%(label_autoconnect)s" value="true" type="bool" />
55 <param name="autodisconnect" label="%(label_autodisconnect)s" value="false" type="bool" />
56 </category>
57 <category name="Misc" label="%(category_misc)s">
58 <param name="Watched" value="test@Jabber.goffi.int" type="string" />
59 </category>
60 </individual>
61 </params>
62 """ % {'category_connection': _("Connection"),
63 'label_NewAccount': _("Register new account"),
64 'label_autoconnect': _('Connect on frontend startup'),
65 'label_autodisconnect': _('Disconnect on frontend closure'),
66 'category_misc': _("Misc")
67 }
68
69 def load_default_params(self):
70 self.dom = minidom.parseString(Param.default_xml.encode('utf-8'))
71
72 def load_xml(self, file):
73 """Load parameters template from file"""
74 self.dom = minidom.parse(file)
75
76 def load_data(self, file):
77 """Load parameters data from file"""
78 file_ind = file + '_ind'
79 file_gen = file + '_gen'
80
81 if os.path.exists(file_gen):
82 try:
83 with open(file_gen, 'r') as file_gen_pickle:
84 self.params_gen=pickle.load(file_gen_pickle)
85 debug(_("general params data loaded"))
86 except:
87 error (_("Can't load general params data !"))
88
89 if os.path.exists(file_ind):
90 try:
91 with open(file_ind, 'r') as file_ind_pickle:
92 self.params=pickle.load(file_ind_pickle)
93 debug(_("individual params data loaded"))
94 except:
95 error (_("Can't load individual params data !"))
96
97 def save_xml(self, file):
98 """Save parameters template to xml file"""
99 with open(file, 'wb') as xml_file:
100 xml_file.write(self.dom.toxml('utf-8'))
101
102 def save_data(self, file):
103 """Save parameters data to file"""
104 #TODO: save properly in a separate file/database,
105 # use different behaviour depending of the data type (e.g. password encrypted)
106
107 #general params
108 with open(file+'_gen', 'w') as param_gen_pickle:
109 pickle.dump(self.params_gen, param_gen_pickle)
110
111 #then individual params
112 with open(file+'_ind', 'w') as param_ind_pickle:
113 pickle.dump(self.params, param_ind_pickle)
114
115 def __init__(self, host):
116 debug("Parameters init")
117 self.host = host
118 self.default_profile = None
119 self.params = {}
120 self.params_gen = {}
121 host.set_const('savefile_param_xml', SAVEFILE_PARAM_XML)
122 host.set_const('savefile_param_data', SAVEFILE_PARAM_DATA)
123 host.registerGeneralCB("registerNewAccount", host.registerNewAccountCB)
124
125 def getProfilesList(self):
126 return self.params.keys()
127
128 def createProfile(self, name):
129 """Create a new profile
130 @param name: Name of the profile"""
131 if self.params.has_key(name):
132 info (_('The profile name already exists'))
133 return 1
134 self.params[name]={}
135 return 0
136
137 def deleteProfile(self, name):
138 """Delete an existing profile
139 @param name: Name of the profile"""
140 if not self.params.has_key(name):
141 error (_('Trying to delete an unknown profile'))
142 return 1
143 del self.params[name]
144 return 0
145
146 def getProfileName(self, profile_key):
147 """return profile according to profile_key
148 @param profile_key: profile name or key which can be
149 @ALL@ for all profiles
150 @DEFAULT@ for default profile
151 @return: requested profile name or None if it doesn't exist"""
152 if profile_key=='@DEFAULT@':
153 if not self.params:
154 return ""
155 default = self.host.memory.getPrivate('Profile_default')
156 if not default or not default in self.params:
157 info(_('No default profile, returning first one')) #TODO: manage real default profile
158 default = self.params.keys()[0]
159 self.host.memory.setPrivate('Profile_default', default)
160 return default #FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists
161 if not self.params.has_key(profile_key):
162 info (_('Trying to access an unknown profile'))
163 return ""
164 return profile_key
165
166 def __get_unique_node(self, parent, tag, name):
167 """return node with given tag
168 @param parent: parent of nodes to check (e.g. documentElement)
169 @param tag: tag to check (e.g. "category")
170 @param name: name to check (e.g. "JID")
171 @return: node if it exist or None
172 """
173 for node in parent.childNodes:
174 if node.nodeName == tag and node.getAttribute("name") == name:
175 #the node already exists
176 return node
177 #the node is new
178 return None
179
180 def importParams(self, xml):
181 """import xml in parameters, do nothing if the param already exist
182 @param xml: parameters in xml form"""
183 src_dom = minidom.parseString(xml.encode('utf-8'))
184
185 def import_node(tgt_parent, src_parent):
186 for child in src_parent.childNodes:
187 if child.nodeName == '#text':
188 continue
189 node = self.__get_unique_node(tgt_parent, child.nodeName, child.getAttribute("name"))
190 if not node: #The node is new
191 tgt_parent.appendChild(child)
192 else:
193 import_node(node, child)
194
195 import_node(self.dom.documentElement, src_dom.documentElement)
196
197 def __default_ok(self, value, name, category):
198 #FIXME: gof: will not work with individual parameters
199 self.setParam(name, value, category) #FIXME: better to set param xml value ???
200
201 def __default_ko(self, failure, name, category):
202 error (_("Can't determine default value for [%(category)s/%(name)s]: %(reason)s") % {'category':category, 'name':name, 'reason':str(failure.value)})
203
204 def setDefault(self, name, category, callback, errback=None):
205 """Set default value of parameter
206 'default_cb' attibute of parameter must be set to 'yes'
207 @param name: name of the parameter
208 @param category: category of the parameter
209 @param callback: must return a string with the value (use deferred if needed)
210 @param errback: must manage the error with args failure, name, category
211 """
212 #TODO: send signal param update if value changed
213 node = self.__getParamNode(name, category, '@ALL@')
214 if not node:
215 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name':name, 'category':category})
216 return
217 if node[1].getAttribute('default_cb') == 'yes':
218 del node[1].attributes['default_cb']
219 d = defer.maybeDeferred(callback)
220 d.addCallback(self.__default_ok, name, category)
221 d.addErrback(errback or self.__default_ko, name, category)
222
223 def getParamA(self, name, category, attr="value", profile_key="@DEFAULT@"):
224 """Helper method to get a specific attribute
225 @param name: name of the parameter
226 @param category: category of the parameter
227 @param attr: name of the attribute (default: "value")
228 @param profile: owner of the param (@ALL@ for everyone)
229
230 @return: attribute"""
231 node = self.__getParamNode(name, category)
232 if not node:
233 error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name':name, 'category':category})
234 return None
235
236 if node[0] == 'general':
237 value = self.__getParam(None, category, name, 'general')
238 return value or node[1].getAttribute(attr)
239
240 assert(node[0] == 'individual')
241
242 profile = self.getProfileName(profile_key)
243 if not profile:
244 error(_('Requesting a param for an non-existant profile'))
245 return None
246
247 if attr == "value":
248 return self.__getParam(profile, category, name) or node[1].getAttribute(attr)
249 else:
250 return node[1].getAttribute(attr)
251
252
253 def __getParam(self, profile, category, name, type='individual'):
254 """Return the param, or None if it doesn't exist
255 @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@)
256 @param category: param category
257 @param name: param name
258 """
259 if type == 'general':
260 if self.params_gen.has_key((category, name)):
261 return self.params_gen[(category, name)]
262 return None #This general param has the default value
263 assert (type == 'individual')
264 if not self.params.has_key(profile) or not self.params[profile].has_key((category, name)):
265 return None
266 return self.params[profile][(category, name)]
267
268 def __constructProfileXml(self, profile):
269 """Construct xml for asked profile, filling values when needed
270 /!\ as noticed in doc, don't forget to unlink the minidom.Document
271 @param profile: profile name (not key !)
272 @return: minidom.Document of the profile xml (cf warning above)
273 """
274 prof_xml = minidom.parseString('<params/>')
275
276 for type_node in self.dom.documentElement.childNodes:
277 if type_node.nodeName == 'general' or type_node.nodeName == 'individual': #we use all params, general and individual
278 for cat_node in type_node.childNodes:
279 if cat_node.nodeName == 'category':
280 category = cat_node.getAttribute('name')
281 cat_copy = cat_node.cloneNode(True) #we make a copy for the new xml
282 params = cat_copy.getElementsByTagName("param")
283 for param_node in params:
284 name = param_node.getAttribute('name')
285 profile_value = self.__getParam(profile, category, name, type_node.nodeName)
286 if profile_value: #there is a value for this profile, we must change the default
287 param_node.setAttribute('value', profile_value)
288 prof_xml.documentElement.appendChild(cat_copy)
289 return prof_xml
290
291
292 def getParamsUI(self, profile_key='@DEFAULT@'):
293 """Return a SàT XMLUI for parameters, with given profile"""
294 profile = self.getProfileName(profile_key)
295 if not profile:
296 error(_("Asking params for inexistant profile"))
297 return ""
298 param_xml = self.getParams(profile)
299 return paramsXml2xmlUI(param_xml)
300
301 def getParams(self, profile_key='@DEFAULT@'):
302 """Construct xml for asked profile
303 Take params xml as skeleton"""
304 profile = self.getProfileName(profile_key)
305 if not profile:
306 error(_("Asking params for inexistant profile"))
307 return ""
308 prof_xml = self.__constructProfileXml(profile)
309 return_xml = prof_xml.toxml()
310 prof_xml.unlink()
311
312 return return_xml
313
314 def getParamsForCategory(self, category, profile_key='@DEFAULT@'):
315 """Return node's xml for selected category"""
316 #TODO: manage category of general type (without existant profile)
317 profile = self.getProfileName(profile_key)
318 if not profile:
319 error(_("Asking params for inexistant profile"))
320 return ""
321 prof_xml = self.__constructProfileXml(profile)
322
323 for node in prof_xml.getElementsByTagName("category"):
324 if node.nodeName == "category" and node.getAttribute("name") == category:
325 result = node.toxml()
326 prof_xml.unlink()
327 return result
328
329 prof_xml.unlink()
330 return "<category />"
331
332 def __getParamNode(self, name, category, type="@ALL@"): #FIXME: is type useful ?
333 """Return a node from the param_xml
334 @param name: name of the node
335 @param category: category of the node
336 @type: keyword for search:
337 @ALL@ search everywhere
338 @GENERAL@ only search in general type
339 @INDIVIDUAL@ only search in individual type
340 @return: a tuple with the node type and the the node, or None if not found"""
341
342 for type_node in self.dom.documentElement.childNodes:
343 if ( ((type == "@ALL@" or type == "@GENERAL@") and type_node.nodeName == 'general')
344 or ( (type == "@ALL@" or type == "@INDIVIDUAL@") and type_node.nodeName == 'individual') ):
345 for node in type_node.getElementsByTagName('category'):
346 if node.getAttribute("name") == category:
347 params = node.getElementsByTagName("param")
348 for param in params:
349 if param.getAttribute("name") == name:
350 return (type_node.nodeName, param)
351 return None
352
353 def getParamsCategories(self):
354 """return the categories availables"""
355 categories=[]
356 for cat in self.dom.getElementsByTagName("category"):
357 categories.append(cat.getAttribute("name"))
358 return categories
359
360 def setParam(self, name, value, category, profile_key='@DEFAULT@'):
361 """Set a parameter, return None if the parameter is not in param xml"""
362 profile = self.getProfileName(profile_key)
363 if not profile:
364 error(_('Trying to set parameter for an unknown profile'))
365 return #TODO: throw an error
366
367 node = self.__getParamNode(name, category, '@ALL@')
368 if not node:
369 error(_('Requesting an unknown parameter (%(category)s/%(name)s)') % {'category':category, 'name':name})
370 return
371
372 if node[0] == 'general':
373 self.params_gen[(category, name)] = value
374 self.host.bridge.paramUpdate(name, value, category, profile) #TODO: add profile in signal
375 return
376
377 assert (node[0] == 'individual')
378
379 type = node[1].getAttribute("type")
380 if type=="button":
381 print "clique",node.toxml()
382 else:
383 self.params[profile][(category, name)] = value
384 self.host.bridge.paramUpdate(name, value, category, profile) #TODO: add profile in signal
385
386 class Memory:
387 """This class manage all persistent informations"""
388
389 def __init__(self, host):
390 info (_("Memory manager init"))
391 self.host = host
392 self.contacts={}
393 self.presenceStatus={}
394 self.subscriptions={}
395 self.params=Param(host)
396 self.history={} #used to store chat history (key: short jid)
397 self.private={} #used to store private value
398 host.set_const('savefile_history', SAVEFILE_HISTORY)
399 host.set_const('savefile_private', SAVEFILE_PRIVATE)
400 self.load()
401
402 def load(self):
403 """Load parameters and all memory things from file/db"""
404 param_file_xml = os.path.expanduser(self.host.get_const('local_dir')+
405 self.host.get_const('savefile_param_xml'))
406 param_file_data = os.path.expanduser(self.host.get_const('local_dir')+
407 self.host.get_const('savefile_param_data'))
408 history_file = os.path.expanduser(self.host.get_const('local_dir')+
409 self.host.get_const('savefile_history'))
410 private_file = os.path.expanduser(self.host.get_const('local_dir')+
411 self.host.get_const('savefile_private'))
412
413 #parameters
414 if os.path.exists(param_file_xml):
415 try:
416 self.params.load_xml(param_file_xml)
417 debug(_("params template loaded"))
418 except:
419 error (_("Can't load params template !"))
420 self.params.load_default_params()
421 else:
422 info (_("No params template, using default template"))
423 self.params.load_default_params()
424
425 try:
426 self.params.load_data(param_file_data)
427 debug(_("params loaded"))
428 except:
429 error (_("Can't load params !"))
430
431 #history
432 if os.path.exists(history_file):
433 try:
434 with open(history_file, 'r') as history_pickle:
435 self.history=pickle.load(history_pickle)
436 debug(_("history loaded"))
437 except:
438 error (_("Can't load history !"))
439
440 #private
441 if os.path.exists(private_file):
442 try:
443 with open(private_file, 'r') as private_pickle:
444 self.private=pickle.load(private_pickle)
445 debug(_("private values loaded"))
446 except:
447 error (_("Can't load private values !"))
448
449 def save(self):
450 """Save parameters and all memory things to file/db"""
451 #TODO: need to encrypt files (at least passwords !) and set permissions
452 param_file_xml = os.path.expanduser(self.host.get_const('local_dir')+
453 self.host.get_const('savefile_param_xml'))
454 param_file_data = os.path.expanduser(self.host.get_const('local_dir')+
455 self.host.get_const('savefile_param_data'))
456 history_file = os.path.expanduser(self.host.get_const('local_dir')+
457 self.host.get_const('savefile_history'))
458 private_file = os.path.expanduser(self.host.get_const('local_dir')+
459 self.host.get_const('savefile_private'))
460
461 self.params.save_xml(param_file_xml)
462 self.params.save_data(param_file_data)
463 debug(_("params saved"))
464 with open(history_file, 'w') as history_pickle:
465 pickle.dump(self.history, history_pickle)
466 debug(_("history saved"))
467 with open(private_file, 'w') as private_pickle:
468 pickle.dump(self.private, private_pickle)
469 debug(_("private values saved"))
470
471 def getProfilesList(self):
472 return self.params.getProfilesList()
473
474
475 def getProfileName(self, profile_key):
476 """Return name of profile from keyword
477 @param profile_key: can be the profile name or a keywork (like @DEFAULT@)
478 @return: profile name or None if it doesn't exist"""
479 return self.params.getProfileName(profile_key)
480
481 def createProfile(self, name):
482 """Create a new profile
483 @param name: Profile name
484 """
485 return self.params.createProfile(name)
486
487 def deleteProfile(self, name):
488 """Delete an existing profile
489 @param name: Name of the profile"""
490 return self.params.deleteProfile(name)
491
492 def addToHistory(self, me_jid, from_jid, to_jid, type, message):
493 me_short=me_jid.userhost()
494 from_short=from_jid.userhost()
495 to_short=to_jid.userhost()
496
497 if from_jid==me_jid:
498 key=to_short
499 else:
500 key=from_short
501
502 if not self.history.has_key(me_short):
503 self.history[me_short]={}
504 if not self.history[me_short].has_key(key):
505 self.history[me_short][key]={}
506
507 self.history[me_short][key][int(time.time())] = (from_jid.full(), message)
508
509 def getHistory(self, from_jid, to_jid, size):
510 ret={}
511 if not self.history.has_key(from_jid):
512 error(_("source JID not found !"))
513 #TODO: throw an error here
514 return {}
515 if not self.history[from_jid].has_key(to_jid):
516 error(_("dest JID not found !"))
517 #TODO: throw an error here
518 return {}
519 stamps=self.history[from_jid][to_jid].keys()
520 stamps.sort()
521 for stamp in stamps[-size:]:
522 ret[stamp]=self.history[from_jid][to_jid][stamp]
523
524 return ret
525
526 def setPrivate(self, key, value):
527 """Save a misc private value (mainly useful for plugins)"""
528 self.private[key] = value
529
530 def getPrivate(self, key):
531 """return a private value
532 @param key: name of wanted value
533 @return: value or None if value don't exist"""
534 if self.private.has_key(key):
535 return self.private[key]
536 return None
537
538
539 def addContact(self, contact_jid, attributes, groups, profile_key='@DEFAULT@'):
540 debug("Memory addContact: %s",contact_jid.userhost())
541 profile = self.getProfileName(profile_key)
542 if not profile:
543 error (_('Trying to add a contact to a non-existant profile'))
544 return
545 assert(isinstance(attributes,dict))
546 assert(isinstance(groups,set))
547 if not self.contacts.has_key(profile):
548 self.contacts[profile] = {}
549 self.contacts[profile][contact_jid.userhost()]=[attributes, groups]
550
551 def delContact(self, contact_jid, profile_key='@DEFAULT@'):
552 debug("Memory delContact: %s",contact_jid.userhost())
553 profile = self.getProfileName(profile_key)
554 if not profile:
555 error (_('Trying to delete a contact for a non-existant profile'))
556 return
557 if self.contacts.has_key(profile) and self.contacts[profile].has_key(contact_jid.userhost()):
558 del self.contacts[profile][contact_jid.userhost()]
559
560 def getContact(self, contact_jid, profile_key='@DEFAULT@'):
561 profile = self.getProfileName(profile_key)
562 if not profile:
563 error(_('Asking a contact for a non-existant profile'))
564 return None
565 if self.contacts.has_key(profile) and self.contacts[profile].has_key(contact_jid.userhost()):
566 self.contacts[profile][contact_jid.userhost()]
567 else:
568 return None
569
570 def getContacts(self, profile_key='@DEFAULT@'):
571 """Return list of contacts for given profile
572 @param profile_key: profile key
573 @return list of [contact, attr, groups]"""
574 debug ("Memory getContact OK (%s)", self.contacts)
575 profile = self.getProfileName(profile_key)
576 if not profile:
577 error(_('Asking contacts for a non-existant profile'))
578 return []
579 ret=[]
580 for contact in self.contacts[profile]:
581 attr, groups = self.contacts[profile][contact]
582 ret.append([contact, attr, groups ])
583 return ret
584
585 def addPresenceStatus(self, contact_jid, show, priority, statuses, profile_key='@DEFAULT@'):
586 profile = self.getProfileName(profile_key)
587 if not profile:
588 error(_('Trying to add presence status to a non-existant profile'))
589 return
590 if not self.presenceStatus.has_key(profile):
591 self.presenceStatus[profile] = {}
592 if not self.presenceStatus[profile].has_key(contact_jid.userhost()):
593 self.presenceStatus[profile][contact_jid.userhost()] = {}
594 resource = jid.parse(contact_jid.full())[2] or ''
595 self.presenceStatus[profile][contact_jid.userhost()][resource] = (show, priority, statuses)
596
597 def addWaitingSub(self, type, contact_jid, profile_key):
598 """Called when a subcription request is received"""
599 profile = self.getProfileName(profile_key)
600 assert(profile)
601 if not self.subscriptions.has_key(profile):
602 self.subscriptions[profile] = {}
603 self.subscriptions[profile][contact_jid] = type
604
605 def delWaitingSub(self, contact_jid, profile_key):
606 """Called when a subcription request is finished"""
607 profile = self.getProfileName(profile_key)
608 assert(profile)
609 if self.subscriptions.has_key(profile) and self.subscriptions[profile].has_key(contact_jid):
610 del self.subscriptions[profile][contact_jid]
611
612 def getWaitingSub(self, profile_key='@DEFAULT@'):
613 """Called to get a list of currently waiting subscription requests"""
614 profile = self.getProfileName(profile_key)
615 if not profile:
616 error(_('Asking waiting subscriptions for a non-existant profile'))
617 return {}
618 if not self.subscriptions.has_key(profile):
619 return {}
620
621 return self.subscriptions[profile]
622
623 def getPresenceStatus(self, profile_key='@DEFAULT@'):
624 profile = self.getProfileName(profile_key)
625 if not profile:
626 error(_('Asking contacts for a non-existant profile'))
627 return {}
628 if not self.presenceStatus.has_key(profile):
629 self.presenceStatus[profile] = {}
630 debug ("Memory getPresenceStatus (%s)", self.presenceStatus[profile])
631 return self.presenceStatus[profile]
632
633 def getParamA(self, name, category, attr="value", profile_key="@DEFAULT@"):
634 return self.params.getParamA(name, category, attr, profile_key)
635
636 def getParamsUI(self, profile_key='@DEFAULT@'):
637 return self.params.getParamsUI(profile_key)
638
639 def getParams(self, profile_key='@DEFAULT@'):
640 return self.params.getParams(profile_key)
641
642 def getParamsForCategory(self, category, profile_key='@DEFAULT@'):
643 return self.params.getParamsForCategory(category, profile_key)
644
645 def getParamsCategories(self):
646 return self.params.getParamsCategories()
647
648 def setParam(self, name, value, category, profile_key='@DEFAULT@'):
649 return self.params.setParam(name, value, category, profile_key)
650
651 def importParams(self, xml):
652 return self.params.importParams(xml)
653
654 def setDefault(self, name, category, callback, errback=None):
655 return self.params.setDefault(name, category, callback, errback)