Files reorganisation
This commit is contained in:
parent
c8b5f1db90
commit
e4452c4b03
20 changed files with 10 additions and 8 deletions
0
src/plugins/__init__.py
Normal file
0
src/plugins/__init__.py
Normal file
266
src/plugins/plugin_misc_cs.py
Executable file
266
src/plugins/plugin_misc_cs.py
Executable file
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing xep-0045
|
||||
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 logging import debug, info, warning, error
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import protocol, defer, threads, reactor
|
||||
from twisted.words.protocols.jabber import client, jid, xmlstream
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
from twisted.words.protocols.jabber.xmlstream import IQ
|
||||
from twisted.web.client import getPage
|
||||
import os.path
|
||||
import pdb
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
from wokkel import disco, iwokkel, data_form
|
||||
from sat.tools.xml_tools import XMLUI
|
||||
import urllib
|
||||
import webbrowser
|
||||
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
import re
|
||||
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "CouchSurfing plugin",
|
||||
"import_name": "CS",
|
||||
"type": "Misc",
|
||||
"protocols": [],
|
||||
"dependencies": [],
|
||||
"main": "CS_Plugin",
|
||||
"handler": "no",
|
||||
"description": _(u"""This plugin allow to manage your CouchSurfing account throught your SàT frontend""")
|
||||
}
|
||||
|
||||
AGENT = 'Salut à Toi XMPP/CS Plugin'
|
||||
|
||||
class CS_Plugin():
|
||||
|
||||
params = """
|
||||
<params>
|
||||
<individual>
|
||||
<category name="CouchSurfing">
|
||||
<param name="Login" type="string" />
|
||||
<param name="Password" type="password" />
|
||||
</category>
|
||||
</individual>
|
||||
</params>
|
||||
"""
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Plugin CS initialization"))
|
||||
self.host = host
|
||||
#parameters
|
||||
host.memory.importParams(CS_Plugin.params)
|
||||
#menu
|
||||
host.importMenu(_("Plugin"), "CouchSurfing", self.menuSelected, help_string = _("Launch CoushSurfing management interface"))
|
||||
self.data=self.host.memory.getPrivate('plugin_cs_data') or {} #TODO: delete cookies/data after a while
|
||||
self.host.registerGeneralCB("plugin_CS_sendMessage", self.sendMessage)
|
||||
self.host.registerGeneralCB("plugin_CS_showUnreadMessages", self.showUnreadMessages)
|
||||
|
||||
def erroCB(self, e, id):
|
||||
"""Called when something is going wrong when contacting CS website"""
|
||||
#pdb.set_trace()
|
||||
message_data={"reason": "connection error", "message":_(u"Impossible to contact CS website, please check your login/password, connection or try again later")}
|
||||
self.host.bridge.actionResult("ERROR", id, message_data)
|
||||
|
||||
def menuSelected(self, id, profile):
|
||||
"""Called when the couchsurfing menu item is selected"""
|
||||
login = self.host.memory.getParamA("Login", "CouchSurfing", profile_key=profile)
|
||||
password = self.host.memory.getParamA("Password", "CouchSurfing", profile_key=profile)
|
||||
if not login or not password:
|
||||
message_data={"reason": "uncomplete", "message":_(u"You have to fill your CouchSurfing login & password in parameters before using this interface")}
|
||||
self.host.bridge.actionResult("ERROR", id, message_data)
|
||||
return
|
||||
|
||||
post_data = urllib.urlencode({'auth_login[un]':login,'auth_login[pw]':password,'auth_login[action]':'Login...'})
|
||||
|
||||
if not self.data.has_key(profile):
|
||||
self.data[profile] = {'cookies':{}}
|
||||
else:
|
||||
self.data[profile]['cookies'] = {}
|
||||
|
||||
d = getPage('http://www.couchsurfing.org/login.html', method='POST', postdata=post_data, headers={'Content-Type':'application/x-www-form-urlencoded'} , agent=AGENT, cookies=self.data[profile]['cookies'])
|
||||
d.addCallback(self.__connectionCB, id, profile)
|
||||
d.addErrback(self.erroCB, id)
|
||||
|
||||
#self.host.bridge.actionResult("SUPPRESS", id, {})
|
||||
|
||||
|
||||
#pages parsing callbacks
|
||||
def savePage(self, name, html):
|
||||
f = open ('/tmp/CS_'+name+'.html','w')
|
||||
f.write(html)
|
||||
f.close()
|
||||
print "page [%s] sauvee" % name
|
||||
#pdb.set_trace()
|
||||
|
||||
def __connectionCB(self, html, id, profile):
|
||||
print 'Response received'
|
||||
#self.savePage('principale',html)
|
||||
soup = BeautifulSoup(html)
|
||||
self.data[profile]['user_nick'] = soup.find('a','item_link',href='/home.html').contents[0]
|
||||
self.data[profile]['user_name'] = soup.html.head.title.string.split(' - ')[1]
|
||||
#unread messages
|
||||
try:
|
||||
self.data[profile]['unread_messages'] = int(soup.find(lambda tag: tag.name=='div' and ('class','item_bubble') in tag.attrs and tag.find('a', href="/messages.html?message_status=inbox")).find(text=True))
|
||||
except:
|
||||
self.data[profile]['unread_messages'] = 0
|
||||
#unread couchrequest messages
|
||||
try:
|
||||
self.data[profile]['unread_CR_messages'] = int(soup.find(lambda tag: tag.name=='div' and ('class','item_bubble') in tag.attrs and tag.find('a', href="/couchmanager")).find(text=True))
|
||||
except:
|
||||
self.data[profile]['unread_CR_messages'] = 0
|
||||
|
||||
#if we have already the list of friend, no need to make new requests
|
||||
if not self.data[profile].has_key('friends'):
|
||||
self.data[profile]['friends'] = {}
|
||||
d = getPage('http://www.couchsurfing.org/connections.html?type=myfriends&show=10000', agent=AGENT, cookies=self.data[profile]['cookies'])
|
||||
d.addCallback(self.__friendsPageCB, id=id, profile=profile)
|
||||
d.addErrback(self.erroCB, id)
|
||||
else:
|
||||
self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":self.__buildUI(self.data[profile])})
|
||||
|
||||
def __buildUI(self, data):
|
||||
"""Build the XML UI of the plugin
|
||||
@param data: data store for the profile"""
|
||||
user_nick = data['user_nick']
|
||||
user_name = data['user_name']
|
||||
unread_mess = data['unread_messages']
|
||||
unread_CR_mess = data['unread_CR_messages']
|
||||
friends_list = data['friends'].keys()
|
||||
friends_list.sort()
|
||||
interface = XMLUI('window','tabs', title='CouchSurfing management')
|
||||
interface.addCategory(_("Messages"), "vertical")
|
||||
interface.addText(_("G'day %(name)s, you have %(nb_message)i unread message%(plural_mess)s and %(unread_CR_mess)s unread couch request message%(plural_CR)s\nIf you want to send a message, select the recipient(s) in the list below") % {'name':user_name, 'nb_message':unread_mess, 'plural_mess':'s' if unread_mess>1 else '', 'unread_CR_mess': unread_CR_mess, 'plural_CR':'s' if unread_CR_mess>1 else ''})
|
||||
if unread_mess:
|
||||
interface.addButton('plugin_CS_showUnreadMessages', 'showUnreadMessages', _('Show unread message%(plural)s in external web browser') % {'plural':'s' if unread_mess>1 else ''})
|
||||
interface.addList(friends_list, 'friends', style=['multi'])
|
||||
interface.changeLayout('pairs')
|
||||
interface.addLabel(_("Subject"))
|
||||
interface.addString('subject')
|
||||
interface.changeLayout('vertical')
|
||||
interface.addLabel(_("Message"))
|
||||
interface.addText("(use %name% for contact name and %firstname% for guessed first name)")
|
||||
interface.addTextBox('message')
|
||||
interface.addButton('plugin_CS_sendMessage', 'sendMessage', _('send'), fields_back=['friends','subject','message'])
|
||||
#interface.addCategory(_("Events"), "vertical") #TODO: coming soon, hopefuly :)
|
||||
#interface.addCategory(_("Couch search"), "vertical")
|
||||
return interface.toXml()
|
||||
|
||||
def __meetingPageCB(self, html):
|
||||
"""Called when the meeting page has been received"""
|
||||
|
||||
def __friendsPageCB(self, html, id, profile):
|
||||
"""Called when the friends list page has been received"""
|
||||
self.savePage('friends',html)
|
||||
soup = BeautifulSoup(html.replace('"formtable width="400','"formtable" width="400"')) #CS html fix #TODO: report the bug to CS dev team
|
||||
friends = self.data[profile]['friends']
|
||||
for _tr in soup.findAll('tr', {'class':re.compile("^msgRow*")}): #we parse the row with friends infos
|
||||
_nobr = _tr.find('nobr') #contain the friend name
|
||||
friend_name = unicode(_nobr.string)
|
||||
friend_link = u'http://www.couchsurfing.org'+_nobr.parent['href']
|
||||
regex_href = re.compile(r'/connections\.html\?id=([^&]+)')
|
||||
a_tag = _tr.find('a',href=regex_href)
|
||||
friend_id = regex_href.search(unicode(a_tag)).groups()[0]
|
||||
|
||||
debug(_("CS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)") % {'friend_name':friend_name, 'friend_id':friend_id, 'friend_link':friend_link})
|
||||
friends[friend_name] = {'link':friend_link,'id':friend_id}
|
||||
a = soup.find('td','barmiddle next').a #is there several pages ?
|
||||
if a:
|
||||
#yes, we parse the next page
|
||||
d = getPage('http://www.couchsurfing.org/'+str(a['href']), agent=AGENT, cookies=self.data[profile]['cookies'])
|
||||
d.addCallback(self.__friendsPageCB, id=id, profile=profile)
|
||||
d.addErrback(self.erroCB, id)
|
||||
else:
|
||||
#no, we show the result
|
||||
self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":self.__buildUI(self.data[profile])})
|
||||
#and save the data
|
||||
self.host.memory.setPrivate('plugin_cs_data', self.data)
|
||||
|
||||
def __sendMessage(self, answer, subject, message, data, recipient_list, id, profile):
|
||||
"""Send actually the message
|
||||
@param subject: subject of the message
|
||||
@param message: body of the message
|
||||
@param data: data of the profile
|
||||
@param recipient_list: list of friends names, names are removed once message is sent
|
||||
@param id: id of the action
|
||||
@param profile: profile who launched the action
|
||||
"""
|
||||
if answer:
|
||||
if not 'Here is a copy of the email that was sent' in answer:
|
||||
error(_("INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has been modified ?"))
|
||||
#TODO: throw a warning to the frontend, saying that maybe the message has not been sent and to contact dev of this plugin
|
||||
#debug(_('HTML answer: %s') % answer)
|
||||
if recipient_list:
|
||||
recipient = recipient_list.pop()
|
||||
try:
|
||||
friend_id = data['friends'][recipient]['id']
|
||||
except KeyError:
|
||||
error('INTERNAL ERROR: unknown friend')
|
||||
return #send an error to the frontend
|
||||
mess = message.replace('%name%',recipient).replace('%firstname%',recipient.split(' ')[0])
|
||||
info(_('Sending message to %s') % recipient)
|
||||
debug(_("\nsubject: %(subject)s\nmessage: \n---\n%(message)s\n---\n\n") % {'subject':subject,'message':mess})
|
||||
post_data = urllib.urlencode({'email[subject]':subject.encode('utf-8'),'email[body]':mess.encode('utf-8'),'email[id]':friend_id,'email[action]':'Send Message','email[replied_id]':'','email[couchsurf]':'','email[directions_to_add]':''})
|
||||
d = getPage("http://www.couchsurfing.org/send_message.html", method='POST', postdata=post_data, headers={'Content-Type':'application/x-www-form-urlencoded'} , agent=AGENT, cookies=data['cookies'])
|
||||
d.addCallback(self.__sendMessage, subject, message, data, recipient_list, id, profile)
|
||||
d.addErrback(self.erroCB, id)
|
||||
else:
|
||||
interface = XMLUI('window', title=_('Message sent')) #TODO: create particular actionResult for alerts ?
|
||||
interface.addText(_('The message has been sent to every recipients'))
|
||||
self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":interface.toXml()})
|
||||
|
||||
def sendMessage(self, id, data, profile):
|
||||
"""Called to send a message to a friend
|
||||
@param data: dict with the following keys:
|
||||
friend: name of the recipient
|
||||
subject: subject of the message
|
||||
message: body of the message, with the following special keywords:
|
||||
- %name%: name of the friend
|
||||
- %firstname%: guessed first name of the friend (currently the first part of the name)
|
||||
"""
|
||||
if not data['friends']:
|
||||
message_data={"reason": "bad data", "message":_(u"There is not recipient selected for this message !")}
|
||||
self.host.bridge.actionResult("ERROR", id, message_data)
|
||||
return
|
||||
friends = data['friends'].split('\t')
|
||||
subject = data['subject']
|
||||
message = data['message']
|
||||
print "send message \o/ :) :) :)"
|
||||
info(_("sending message to %(friends)s with subject [%(subject)s]" % {'friends':friends, 'subject':subject}))
|
||||
self.__sendMessage(None, subject, message, self.data[profile], friends, id, profile)
|
||||
|
||||
def __showUnreadMessages2(self, html, id, profile):
|
||||
"""Called when the inbox page has been received"""
|
||||
#FIXME: that's really too fragile, only works if the unread messages are in the first page, and it would be too resources consuming for the website to DL each time all pages. In addition, the show attribute doesn't work as expected.
|
||||
soup = BeautifulSoup(html)
|
||||
for tag in soup.findAll(lambda tag: tag.name=='strong' and tag.a and tag.a['href'].startswith('messages.html?message_status=inbox')):
|
||||
link = "http://www.couchsurfing.org/"+str(tag.a['href'])
|
||||
webbrowser.open_new_tab(link) #TODO: the web browser need to already have CS cookies (i.e. already be opened & logged on CS, or be permanently loggued), a warning to the user should be sent/or a balloon-tip
|
||||
|
||||
def showUnreadMessages(self, id, data, profile):
|
||||
"""Called when user want to see all unread messages in the external browser"""
|
||||
d = getPage("http://www.couchsurfing.org/messages.html?message_status=inbox&show=10000", agent=AGENT, cookies=self.data[profile]['cookies'])
|
||||
d.addCallback(self.__showUnreadMessages2, id, profile)
|
||||
d.addErrback(self.erroCB, id)
|
||||
|
||||
713
src/plugins/plugin_misc_tarot.py
Executable file
713
src/plugins/plugin_misc_tarot.py
Executable file
|
|
@ -0,0 +1,713 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing xep-0045
|
||||
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 logging import debug, info, warning, error
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import protocol, defer, threads, reactor
|
||||
from twisted.words.protocols.jabber import client, jid, xmlstream
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
from twisted.words.protocols.jabber.xmlstream import IQ
|
||||
import os.path
|
||||
import pdb
|
||||
import random
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
from wokkel import disco, iwokkel, data_form
|
||||
from sat.tools.xml_tools import dataForm2xml
|
||||
from sat.tools.games import TarotCard
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
except ImportError:
|
||||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
MESSAGE = '/message'
|
||||
NS_CG = 'http://www.goffi.org/protocol/card_game'
|
||||
CG_TAG = 'card_game'
|
||||
CG_REQUEST = MESSAGE + '/' + CG_TAG + '[@xmlns="' + NS_CG + '"]'
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "Tarot cards plugin",
|
||||
"import_name": "Tarot",
|
||||
"type": "Misc",
|
||||
"protocols": [],
|
||||
"dependencies": ["XEP_0045"],
|
||||
"main": "Tarot",
|
||||
"handler": "yes",
|
||||
"description": _("""Implementation of Tarot card game""")
|
||||
}
|
||||
|
||||
|
||||
class Tarot():
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Plugin Tarot initialization"))
|
||||
self.host = host
|
||||
self.games={}
|
||||
self.contrats = [_('Passe'), _('Petite'), _('Garde'), _('Garde Sans'), _('Garde Contre')]
|
||||
host.bridge.addMethod("tarotGameCreate", ".communication", in_sign='sass', out_sign='', method=self.createGame) #args: room_jid, players, profile
|
||||
host.bridge.addMethod("tarotGameReady", ".communication", in_sign='sss', out_sign='', method=self.newPlayerReady) #args: player, referee, profile
|
||||
host.bridge.addMethod("tarotGameContratChoosed", ".communication", in_sign='ssss', out_sign='', method=self.contratChoosed) #args: player, referee, contrat, profile
|
||||
host.bridge.addMethod("tarotGamePlayCards", ".communication", in_sign='ssa(ss)s', out_sign='', method=self.play_cards) #args: player, referee, cards, profile
|
||||
host.bridge.addSignal("tarotGameStarted", ".communication", signature='ssass') #args: room_jid, referee, players, profile
|
||||
host.bridge.addSignal("tarotGameNew", ".communication", signature='sa(ss)s') #args: room_jid, hand, profile
|
||||
host.bridge.addSignal("tarotGameChooseContrat", ".communication", signature='sss') #args: room_jid, xml_data, profile
|
||||
host.bridge.addSignal("tarotGameShowCards", ".communication", signature='ssa(ss)a{ss}s') #args: room_jid, type ["chien", "poignée",...], cards, data[dict], profile
|
||||
host.bridge.addSignal("tarotGameCardsPlayed", ".communication", signature='ssa(ss)s') #args: room_jid, player, type ["chien", "poignée",...], cards, data[dict], profile
|
||||
host.bridge.addSignal("tarotGameYourTurn", ".communication", signature='ss') #args: room_jid, profile
|
||||
host.bridge.addSignal("tarotGameScore", ".communication", signature='ssasass') #args: room_jid, xml_data, winners (list of nicks), loosers (list of nicks), profile
|
||||
host.bridge.addSignal("tarotGameInvalidCards", ".communication", signature='ssa(ss)a(ss)s') #args: room_jid, game phase, played_cards, invalid_cards, profile
|
||||
self.deck_ordered = []
|
||||
for value in ['excuse']+map(str,range(1,22)):
|
||||
self.deck_ordered.append(TarotCard(("atout",value)))
|
||||
for suit in ["pique", "coeur", "carreau", "trefle"]:
|
||||
for value in map(str,range(1,11))+["valet","cavalier","dame","roi"]:
|
||||
self.deck_ordered.append(TarotCard((suit, value)))
|
||||
|
||||
def createGameElt(self, to_jid, type="normal"):
|
||||
type = "normal" if to_jid.resource else "groupchat"
|
||||
elt = domish.Element(('jabber:client','message'))
|
||||
elt["to"] = to_jid.full()
|
||||
elt["type"] = type
|
||||
elt.addElement((NS_CG, CG_TAG))
|
||||
return elt
|
||||
|
||||
def __card_list_to_xml(self, cards_list, elt_name):
|
||||
"""Convert a card list to domish element"""
|
||||
cards_list_elt = domish.Element(('',elt_name))
|
||||
for card in cards_list:
|
||||
card_elt = domish.Element(('','card'))
|
||||
card_elt['suit'] = card.suit
|
||||
card_elt['value'] = card.value
|
||||
cards_list_elt.addChild(card_elt)
|
||||
return cards_list_elt
|
||||
|
||||
def __xml_to_list(self, cards_list_elt):
|
||||
"""Convert a domish element with cards to a list of tuples"""
|
||||
cards_list = []
|
||||
for card in cards_list_elt.elements():
|
||||
cards_list.append((card['suit'], card['value']))
|
||||
return cards_list
|
||||
|
||||
def __create_started_elt(self, players):
|
||||
"""Create a game_started domish element"""
|
||||
started_elt = domish.Element(('','started'))
|
||||
idx = 0
|
||||
for player in players:
|
||||
player_elt = domish.Element(('','player'))
|
||||
player_elt.addContent(player)
|
||||
player_elt['index'] = str(idx)
|
||||
idx+=1
|
||||
started_elt.addChild(player_elt)
|
||||
return started_elt
|
||||
|
||||
def __ask_contrat(self):
|
||||
"""Create a element for asking contrat"""
|
||||
contrat_elt = domish.Element(('','contrat'))
|
||||
form = data_form.Form('form', title=_('contrat selection'))
|
||||
field = data_form.Field('list-single', 'contrat', options=map(data_form.Option, self.contrats), required=True)
|
||||
form.addField(field)
|
||||
contrat_elt.addChild(form.toElement())
|
||||
return contrat_elt
|
||||
|
||||
def __give_scores(self, scores, winners, loosers):
|
||||
"""Create an element to give scores
|
||||
@param scores: unicode (can contain line feed)
|
||||
@param winners: list of unicode nicks of winners
|
||||
@param loosers: list of unicode nicks of loosers"""
|
||||
|
||||
score_elt = domish.Element(('','score'))
|
||||
form = data_form.Form('form', title=_('scores'))
|
||||
for line in scores.split('\n'):
|
||||
field = data_form.Field('fixed', value = line)
|
||||
form.addField(field)
|
||||
score_elt.addChild(form.toElement())
|
||||
for winner in winners:
|
||||
winner_elt = domish.Element(('','winner'))
|
||||
winner_elt.addContent(winner)
|
||||
score_elt.addChild(winner_elt)
|
||||
for looser in loosers:
|
||||
looser_elt = domish.Element(('','looser'))
|
||||
looser_elt.addContent(looser)
|
||||
score_elt.addChild(looser_elt)
|
||||
return score_elt
|
||||
|
||||
def __invalid_cards_elt(self, played_cards, invalid_cards, game_phase):
|
||||
"""Create a element for invalid_cards error
|
||||
@param list_cards: list of Card
|
||||
@param game_phase: phase of the game ['ecart', 'play']"""
|
||||
error_elt = domish.Element(('','error'))
|
||||
played_elt = self.__card_list_to_xml(played_cards, 'played')
|
||||
invalid_elt = self.__card_list_to_xml(invalid_cards, 'invalid')
|
||||
error_elt['type'] = 'invalid_cards'
|
||||
error_elt['phase'] = game_phase
|
||||
error_elt.addChild(played_elt)
|
||||
error_elt.addChild(invalid_elt)
|
||||
return error_elt
|
||||
|
||||
def __next_player(self, game_data, next_pl = None):
|
||||
"""Increment player number & return player name
|
||||
@param next_pl: if given, then next_player is forced to this one
|
||||
"""
|
||||
if next_pl:
|
||||
game_data['current_player'] = game_data['players'].index(next_pl)
|
||||
return next_pl
|
||||
else:
|
||||
pl_idx = game_data['current_player'] = (game_data['current_player'] + 1) % len(game_data['players'])
|
||||
return game_data['players'][pl_idx]
|
||||
|
||||
def __winner(self, game_data):
|
||||
"""give the nick of the player who win this trick"""
|
||||
players_data = game_data['players_data']
|
||||
first = game_data['first_player']
|
||||
first_idx = game_data['players'].index(first)
|
||||
suit_asked = None
|
||||
strongest = None
|
||||
winner = None
|
||||
for idx in [(first_idx + i) % 4 for i in range(4)]:
|
||||
player = game_data['players'][idx]
|
||||
card = players_data[player]['played']
|
||||
if card.value == "excuse":
|
||||
continue
|
||||
if suit_asked == None:
|
||||
suit_asked = card.suit
|
||||
if (card.suit == suit_asked or card.suit == "atout") and card > strongest:
|
||||
strongest = card
|
||||
winner = player
|
||||
assert winner
|
||||
return winner
|
||||
|
||||
def __excuse_hack(self, game_data, played, winner):
|
||||
"""give a low card to other team and keep excuse if trick is lost
|
||||
@param game_data: data of the game
|
||||
@param played: cards currently on the table
|
||||
@param winner: nick of the trick winner"""
|
||||
#TODO: manage the case where excuse is played on the last trick (and lost)
|
||||
#TODO: gof: manage excuse (fool)
|
||||
players_data = game_data['players_data']
|
||||
excuse = TarotCard(("atout","excuse"))
|
||||
|
||||
#we first check if the Excuse was already player
|
||||
#and if somebody is waiting for a card
|
||||
for player in game_data['players']:
|
||||
if players_data[player]['wait_for_low']:
|
||||
#the excuse owner has to give a card to somebody
|
||||
if winner == player:
|
||||
#the excuse owner win the trick, we check if we have something to give
|
||||
for card in played:
|
||||
if card.points == 0.5:
|
||||
pl_waiting = players_data[player]['wait_for_low']
|
||||
played.remove(card)
|
||||
players_data[pl_waiting]['levees'].append(card)
|
||||
debug (_('Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for Excuse compensation') % {"excuse_owner":player, "card_waited": card, "player_waiting":pl_waiting})
|
||||
return
|
||||
return
|
||||
|
||||
if not excuse in played:
|
||||
#the Excuse is not on the table, nothing to do
|
||||
return
|
||||
|
||||
excuse_player = None #Who has played the Excuse ?
|
||||
for player in game_data['players']:
|
||||
if players_data[player]['played'] == excuse:
|
||||
excuse_player = player
|
||||
break
|
||||
|
||||
if excuse_player == winner:
|
||||
return #the excuse player win the trick, nothing to do
|
||||
|
||||
#first we remove the excuse from played cards
|
||||
played.remove(excuse)
|
||||
#then we give it back to the original owner
|
||||
owner_levees = players_data[excuse_player]['levees']
|
||||
owner_levees.append(excuse)
|
||||
#finally we give a low card to the trick winner
|
||||
low_card = None
|
||||
#We look backward in cards won by the Excuse owner to
|
||||
#find a low value card
|
||||
for card_idx in range(len(owner_levees)-1, -1, -1):
|
||||
if owner_levees[card_idx].points == 0.5:
|
||||
low_card = owner_levees[card_idx]
|
||||
del owner_levees[card_idx]
|
||||
players_data[winner]['levees'].append(low_card)
|
||||
debug (_('Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for Excuse compensation') % {"excuse_owner":excuse_player, "card_waited": low_card, "player_waiting":winner})
|
||||
break
|
||||
if not low_card: #The player has no low card yet
|
||||
#TODO: manage case when player never win a trick with low card
|
||||
players_data[excuse_player]['wait_for_low'] = winner
|
||||
debug(_("%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is waiting for one") % {'excuse_owner':excuse_player, 'winner':winner})
|
||||
|
||||
|
||||
def __calculate_scores(self, game_data):
|
||||
"""The game is finished, time to know who won :)
|
||||
@param game_data: data of the game
|
||||
@return: tuple with (string victory message, list of winners, list of loosers)"""
|
||||
players_data = game_data['players_data']
|
||||
levees = players_data[game_data['attaquant']]['levees']
|
||||
score = 0
|
||||
nb_bouts = 0
|
||||
bouts = []
|
||||
for card in levees:
|
||||
if card.bout:
|
||||
nb_bouts +=1
|
||||
bouts.append(card.value)
|
||||
score += card.points
|
||||
|
||||
#We we do a basic check on score calculation
|
||||
check_score = 0
|
||||
defenseurs = game_data['players'][:]
|
||||
defenseurs.remove(game_data['attaquant'])
|
||||
for defenseur in defenseurs:
|
||||
for card in players_data[defenseur]['levees']:
|
||||
check_score+=card.points
|
||||
if game_data['contrat'] == "Garde Contre":
|
||||
for card in game_data['chien']:
|
||||
check_score+=card.points
|
||||
assert (score + check_score == 91)
|
||||
|
||||
point_limit = None
|
||||
if nb_bouts == 3:
|
||||
point_limit = 36
|
||||
elif nb_bouts == 2:
|
||||
point_limit = 41
|
||||
elif nb_bouts == 1:
|
||||
point_limit = 51
|
||||
else:
|
||||
point_limit = 56
|
||||
if game_data['contrat'] == 'Petite':
|
||||
contrat_mult = 1
|
||||
elif game_data['contrat'] == 'Garde':
|
||||
contrat_mult = 2
|
||||
elif game_data['contrat'] == 'Garde Sans':
|
||||
contrat_mult = 4
|
||||
elif game_data['contrat'] == 'Garde Contre':
|
||||
contrat_mult = 6
|
||||
else:
|
||||
error(_('INTERNAL ERROR: contrat not managed (mispelled ?)'))
|
||||
assert(False)
|
||||
|
||||
victory = (score >= point_limit)
|
||||
margin = abs(score - point_limit)
|
||||
points_defenseur = (margin + 25) * contrat_mult * (-1 if victory else 1)
|
||||
winners = []
|
||||
loosers = []
|
||||
player_score = {}
|
||||
for player in game_data['players']:
|
||||
#TODO: adjust this for 3 and 5 players variants
|
||||
#TODO: manage bonuses (petit au bout, poignée, chelem)
|
||||
player_score[player] = points_defenseur if player != game_data['attaquant'] else points_defenseur * -3
|
||||
players_data[player]['score'] += player_score[player] #we add score of this game to the global score
|
||||
if player_score[player] > 0:
|
||||
winners.append(player)
|
||||
else:
|
||||
loosers.append(player)
|
||||
|
||||
scores_str = _('The attacker (%(attaquant)s) makes %(points)i and needs to make %(point_limit)i (%(nb_bouts)s oulder%(plural)s%(separator)s%(bouts)s): he %(victory)s') % {'attaquant':game_data['attaquant'], 'points':score, 'point_limit':point_limit, 'nb_bouts': nb_bouts, 'plural': 's' if nb_bouts>1 else '', 'separator':': ' if nb_bouts != 0 else '', 'bouts':','.join(map(str,bouts)), 'victory': 'win' if victory else 'loose'}
|
||||
scores_str+='\n'
|
||||
for player in game_data['players']:
|
||||
scores_str+=_("\n--\n%(player)s:\nscore for this game ==> %(score_game)i\ntotal score ==> %(total_score)i") % {'player':player, 'score_game':player_score[player], 'total_score': players_data[player]['score']}
|
||||
debug(scores_str)
|
||||
|
||||
return (scores_str, winners, loosers)
|
||||
|
||||
def __invalid_cards(self, game_data, cards):
|
||||
"""Checks that the player has the right to play what he wants to
|
||||
@param game_data: Game data
|
||||
@param cards: cards the player want to play
|
||||
@return forbidden_cards cards or empty list if cards are ok"""
|
||||
forbidden_cards = []
|
||||
if game_data['stage'] == 'ecart':
|
||||
for card in cards:
|
||||
if card.bout or card.value=="roi":
|
||||
forbidden_cards.append(card)
|
||||
#TODO: manage case where atouts (trumps) are in the dog
|
||||
elif game_data['stage'] == 'play':
|
||||
biggest_atout = None
|
||||
suit_asked = None
|
||||
players = game_data['players']
|
||||
players_data = game_data['players_data']
|
||||
idx = players.index(game_data['first_player'])
|
||||
current_idx = game_data['current_player']
|
||||
current_player = players[current_idx]
|
||||
if idx == current_idx:
|
||||
#the player is the first to play, he can play what he wants
|
||||
return forbidden_cards
|
||||
while (idx != current_idx):
|
||||
player = players[idx]
|
||||
played_card = players_data[player]['played']
|
||||
if not suit_asked and played_card.value != "excuse":
|
||||
suit_asked = played_card.suit
|
||||
if played_card.suit == "atout" and played_card > biggest_atout:
|
||||
biggest_atout = played_card
|
||||
idx = (idx + 1) % len(players)
|
||||
has_suit = False #True if there is one card of the asked suit in the hand of the player
|
||||
has_atout = False
|
||||
biggest_hand_atout = None
|
||||
|
||||
for hand_card in game_data['hand'][current_player]:
|
||||
if hand_card.suit == suit_asked:
|
||||
has_suit = True
|
||||
if hand_card.suit == "atout":
|
||||
has_atout = True
|
||||
if hand_card.suit == "atout" and hand_card > biggest_hand_atout:
|
||||
biggest_hand_atout = hand_card
|
||||
|
||||
assert len(cards) == 1
|
||||
card = cards[0]
|
||||
if card.suit != suit_asked and has_suit and card.value != "excuse":
|
||||
forbidden_cards.append(card)
|
||||
return forbidden_cards
|
||||
if card.suit != suit_asked and card.suit != "atout" and has_atout:
|
||||
forbidden_cards.append(card)
|
||||
return forbidden_cards
|
||||
if card.suit == "atout" and card < biggest_atout and biggest_hand_atout > biggest_atout and card.value != "excuse":
|
||||
forbidden_cards.append(card)
|
||||
else:
|
||||
error(_('Internal error: unmanaged game stage'))
|
||||
return forbidden_cards
|
||||
|
||||
|
||||
def __start_play(self, room_jid, game_data, profile):
|
||||
"""Start the game (tell to the first player after dealer to play"""
|
||||
game_data['stage'] = "play"
|
||||
next_player_idx = game_data['current_player'] = (game_data['init_player'] + 1) % len(game_data['players']) #the player after the dealer start
|
||||
game_data['first_player'] = next_player = game_data['players'][next_player_idx]
|
||||
to_jid = jid.JID(room_jid.userhost()+"/"+next_player) #FIXME: gof:
|
||||
mess = self.createGameElt(to_jid)
|
||||
yourturn_elt = mess.firstChildElement().addElement('your_turn')
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
|
||||
def createGame(self, room_jid_param, players, profile_key='@DEFAULT@'):
|
||||
"""Create a new game"""
|
||||
debug (_("Creating Tarot game"))
|
||||
room_jid = jid.JID(room_jid_param)
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_("profile %s is unknown") % profile_key)
|
||||
return
|
||||
if self.games.has_key(room_jid):
|
||||
warning (_("Tarot game already started in room %s") % room_jid.userhost())
|
||||
else:
|
||||
room_nick = self.host.plugins["XEP_0045"].getRoomNick(room_jid.userhost(), profile)
|
||||
if not room_nick:
|
||||
error ('Internal error')
|
||||
return
|
||||
referee = room_jid.userhost() + '/' + room_nick
|
||||
status = {}
|
||||
players_data = {}
|
||||
for player in players:
|
||||
players_data[player] = {'score':0}
|
||||
status[player] = "init"
|
||||
self.games[room_jid.userhost()] = {'referee':referee, 'players':players, 'status':status, 'players_data':players_data, 'hand_size':18, 'init_player':0, 'current_player': None, 'contrat': None, 'stage': None}
|
||||
for player in players:
|
||||
mess = self.createGameElt(jid.JID(room_jid.userhost()+'/'+player))
|
||||
mess.firstChildElement().addChild(self.__create_started_elt(players))
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
def newPlayerReady(self, player, referee, profile_key='@DEFAULT@'):
|
||||
"""Must be called when player is ready to start a new game"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_("profile %s is unknown") % profile_key)
|
||||
return
|
||||
debug ('new player ready: %s' % profile)
|
||||
mess = self.createGameElt(jid.JID(referee))
|
||||
ready_elt = mess.firstChildElement().addElement('player_ready')
|
||||
ready_elt['player'] = player
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
def contratChoosed(self, player, referee, contrat, profile_key='@DEFAULT@'):
|
||||
"""Must be call by player when the contrat is selected
|
||||
@param player: player's name
|
||||
@param referee: arbiter jid
|
||||
@contrat: contrat choosed (must be the exact same string than in the give list options)
|
||||
@profile_key: profile
|
||||
"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_("profile %s is unknown") % profile_key)
|
||||
return
|
||||
debug (_('contrat [%(contrat)s] choosed by %(profile)s') % {'contrat':contrat, 'profile':profile})
|
||||
mess = self.createGameElt(jid.JID(referee))
|
||||
contrat_elt = mess.firstChildElement().addElement(('','contrat_choosed'), content=contrat)
|
||||
contrat_elt['player'] = player
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
def play_cards(self, player, referee, cards, profile_key='@DEFAULT@'):
|
||||
"""Must be call by player when the contrat is selected
|
||||
@param player: player's name
|
||||
@param referee: arbiter jid
|
||||
@cards: cards played (list of tuples)
|
||||
@profile_key: profile
|
||||
"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_("profile %s is unknown") % profile_key)
|
||||
return
|
||||
debug (_('Cards played by %(profile)s: [%(cards)s]') % {'profile':profile,'cards':cards})
|
||||
mess = self.createGameElt(jid.JID(referee))
|
||||
playcard_elt = mess.firstChildElement().addChild(self.__card_list_to_xml(TarotCard.from_tuples(cards), 'cards_played'))
|
||||
playcard_elt['player'] = player
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
def newGame(self, room_jid, profile):
|
||||
"""Launch a new round"""
|
||||
debug (_('new Tarot game'))
|
||||
deck = self.deck_ordered[:]
|
||||
random.shuffle(deck)
|
||||
game_data = self.games[room_jid.userhost()]
|
||||
players = game_data['players']
|
||||
players_data = game_data['players_data']
|
||||
current_player = game_data['current_player']
|
||||
game_data['stage'] = "init"
|
||||
game_data['first_player'] = None #first player for the current trick
|
||||
game_data['contrat'] = None
|
||||
hand = game_data['hand'] = {}
|
||||
hand_size = game_data['hand_size']
|
||||
chien = game_data['chien'] = []
|
||||
for i in range(4): #TODO: distribute according to real Tarot rules (3 by 3 counter-clockwise, 1 card at once to chien)
|
||||
hand[players[i]] = deck[0:hand_size]
|
||||
del deck[0:hand_size]
|
||||
chien.extend(deck)
|
||||
del(deck[:])
|
||||
|
||||
for player in players:
|
||||
to_jid = jid.JID(room_jid.userhost()+"/"+player) #FIXME: gof:
|
||||
mess = self.createGameElt(to_jid)
|
||||
mess.firstChildElement().addChild(self.__card_list_to_xml(hand[player], 'hand'))
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
players_data[player]['contrat'] = None
|
||||
players_data[player]['levees'] = [] #cards won
|
||||
players_data[player]['played'] = None #card on the table
|
||||
players_data[player]['wait_for_low'] = None #Used when a player wait for a low card because of excuse
|
||||
|
||||
pl_idx = game_data['current_player'] = (game_data['init_player'] + 1) % len(players) #the player after the dealer start
|
||||
player = players[pl_idx]
|
||||
to_jid = jid.JID(room_jid.userhost()+"/"+player) #FIXME: gof:
|
||||
mess = self.createGameElt(to_jid)
|
||||
mess.firstChildElement().addChild(self.__ask_contrat())
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
|
||||
def card_game_cmd(self, mess_elt, profile):
|
||||
from_jid = jid.JID(mess_elt['from'])
|
||||
room_jid = jid.JID(from_jid.userhost())
|
||||
game_elt = mess_elt.firstChildElement()
|
||||
game_data = self.games[room_jid.userhost()]
|
||||
players_data = game_data['players_data']
|
||||
|
||||
for elt in game_elt.elements():
|
||||
|
||||
if elt.name == 'started': #new game created
|
||||
players = []
|
||||
for player in elt.elements():
|
||||
players.append(unicode(player))
|
||||
self.host.bridge.tarotGameStarted(room_jid.userhost(), from_jid.full(), players, profile)
|
||||
|
||||
elif elt.name == 'player_ready': #ready to play
|
||||
player = elt['player']
|
||||
status = self.games[room_jid.userhost()]['status']
|
||||
nb_players = len(self.games[room_jid.userhost()]['players'])
|
||||
status[player] = 'ready'
|
||||
debug (_('Player %(player)s is ready to start [status: %(status)s]') % {'player':player, 'status':status})
|
||||
if status.values().count('ready') == nb_players: #everybody is ready, we can start the game
|
||||
self.newGame(room_jid, profile)
|
||||
|
||||
elif elt.name == 'hand': #a new hand has been received
|
||||
self.host.bridge.tarotGameNew(room_jid.userhost(), self.__xml_to_list(elt), profile)
|
||||
|
||||
elif elt.name == 'contrat': #it's time to choose contrat
|
||||
form = data_form.Form.fromElement(elt.firstChildElement())
|
||||
xml_data = dataForm2xml(form)
|
||||
self.host.bridge.tarotGameChooseContrat(room_jid.userhost(), xml_data, profile)
|
||||
|
||||
elif elt.name == 'contrat_choosed':
|
||||
#TODO: check we receive the contrat from the right person
|
||||
#TODO: use proper XEP-0004 way for answering form
|
||||
player = elt['player']
|
||||
players_data[player]['contrat'] = unicode(elt)
|
||||
contrats = [players_data[player]['contrat'] for player in game_data['players']]
|
||||
if contrats.count(None):
|
||||
#not everybody has choosed his contrat, it's next one turn
|
||||
player = self.__next_player(game_data)
|
||||
to_jid = jid.JID(room_jid.userhost()+"/"+player) #FIXME: gof:
|
||||
mess = self.createGameElt(to_jid)
|
||||
mess.firstChildElement().addChild(self.__ask_contrat())
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
else:
|
||||
#TODO: gof: manage "everybody pass" case
|
||||
best_contrat = [None, "Passe"]
|
||||
for player in game_data['players']:
|
||||
contrat = players_data[player]['contrat']
|
||||
idx_best = self.contrats.index(best_contrat[1])
|
||||
idx_pl = self.contrats.index(contrat)
|
||||
if idx_pl > idx_best:
|
||||
best_contrat[0] = player
|
||||
best_contrat[1] = contrat
|
||||
debug (_("%(player)s win the bid with %(contrat)s") % {'player':best_contrat[0],'contrat':best_contrat[1]})
|
||||
game_data['contrat'] = best_contrat[1]
|
||||
|
||||
if game_data['contrat'] == "Garde Sans" or game_data['contrat'] == "Garde Contre":
|
||||
self.__start_play(room_jid, game_data, profile)
|
||||
game_data['attaquant'] = best_contrat[0]
|
||||
else:
|
||||
#Time to show the chien to everybody
|
||||
to_jid = jid.JID(room_jid.userhost()) #FIXME: gof:
|
||||
mess = self.createGameElt(to_jid)
|
||||
chien_elt = mess.firstChildElement().addChild(self.__card_list_to_xml(game_data['chien'], 'chien'))
|
||||
chien_elt['attaquant'] = best_contrat[0]
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
#the attacker (attaquant) get the chien
|
||||
game_data['hand'][best_contrat[0]].extend(game_data['chien'])
|
||||
del game_data['chien'][:]
|
||||
|
||||
if game_data['contrat'] == "Garde Sans":
|
||||
#The chien go into attaquant's (attacker) levees
|
||||
players_data[best_contrat[0]]['levees'].extend(game_data['chien'])
|
||||
del game_data['chien'][:]
|
||||
|
||||
|
||||
elif elt.name == 'chien': #we have received the chien
|
||||
debug (_("tarot: chien received"))
|
||||
data = {"attaquant":elt['attaquant']}
|
||||
game_data['stage'] = "ecart"
|
||||
game_data['attaquant'] = elt['attaquant']
|
||||
self.host.bridge.tarotGameShowCards(room_jid.userhost(), "chien", self.__xml_to_list(elt), data, profile)
|
||||
|
||||
elif elt.name == 'cards_played':
|
||||
if game_data['stage'] == "ecart":
|
||||
#TODO: show atouts (trumps) if player put some in écart
|
||||
assert (game_data['attaquant'] == elt['player']) #TODO: throw an xml error here
|
||||
list_cards = TarotCard.from_tuples(self.__xml_to_list(elt))
|
||||
#we now check validity of card
|
||||
invalid_cards = self.__invalid_cards(game_data, list_cards)
|
||||
if invalid_cards:
|
||||
mess = self.createGameElt(jid.JID(room_jid.userhost()+'/'+elt['player']))
|
||||
mess.firstChildElement().addChild(self.__invalid_cards_elt(list_cards, invalid_cards, game_data['stage']))
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
return
|
||||
|
||||
#FIXME: gof: manage Garde Sans & Garde Contre cases
|
||||
players_data[elt['player']]['levees'].extend(list_cards) #we add the chien to attaquant's levées
|
||||
for card in list_cards:
|
||||
game_data['hand'][elt['player']].remove(card)
|
||||
|
||||
self.__start_play(room_jid, game_data, profile)
|
||||
|
||||
elif game_data['stage'] == "play":
|
||||
current_player = game_data['players'][game_data['current_player']]
|
||||
cards = TarotCard.from_tuples(self.__xml_to_list(elt))
|
||||
|
||||
if mess_elt['type'] == 'groupchat':
|
||||
self.host.bridge.tarotGameCardsPlayed(room_jid.userhost(), elt['player'], self.__xml_to_list(elt), profile)
|
||||
else:
|
||||
#we first check validity of card
|
||||
invalid_cards = self.__invalid_cards(game_data, cards)
|
||||
if invalid_cards:
|
||||
mess = self.createGameElt(jid.JID(room_jid.userhost()+'/'+current_player))
|
||||
mess.firstChildElement().addChild(self.__invalid_cards_elt(cards, invalid_cards, game_data['stage']))
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
return
|
||||
#the card played is ok, we forward it to everybody
|
||||
#first we remove it from the hand and put in on the table
|
||||
game_data['hand'][current_player].remove(cards[0])
|
||||
players_data[current_player]['played'] = cards[0]
|
||||
|
||||
#then we forward the message
|
||||
mess = self.createGameElt(room_jid)
|
||||
playcard_elt = mess.firstChildElement().addChild(elt)
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
#Did everybody played ?
|
||||
played = [players_data[player]['played'] for player in game_data['players']]
|
||||
if all(played):
|
||||
#everybody has played
|
||||
winner = self.__winner(game_data)
|
||||
debug (_('The winner of this trick is %s') % winner)
|
||||
#the winner win the trick
|
||||
self.__excuse_hack(game_data, played, winner)
|
||||
players_data[elt['player']]['levees'].extend(played)
|
||||
#nothing left on the table
|
||||
for player in game_data['players']:
|
||||
players_data[player]['played'] = None
|
||||
if len(game_data['hand'][current_player]) == 0:
|
||||
#no card lef: the game is finished
|
||||
to_jid = jid.JID(room_jid.userhost()) #FIXME: gof:
|
||||
mess = self.createGameElt(to_jid)
|
||||
chien_elt = mess.firstChildElement().addChild(self.__give_scores(*self.__calculate_scores(game_data)))
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
return
|
||||
#next player is the winner
|
||||
next_player = game_data['first_player'] = self.__next_player(game_data, winner)
|
||||
else:
|
||||
next_player = self.__next_player(game_data)
|
||||
|
||||
#finally, we tell to the next player to play
|
||||
to_jid = jid.JID(room_jid.userhost()+"/"+next_player) #FIXME: gof:
|
||||
mess = self.createGameElt(to_jid)
|
||||
yourturn_elt = mess.firstChildElement().addElement('your_turn')
|
||||
self.host.profiles[profile].xmlstream.send(mess)
|
||||
|
||||
elif elt.name == 'your_turn':
|
||||
self.host.bridge.tarotGameYourTurn(room_jid.userhost(), profile)
|
||||
|
||||
elif elt.name == 'score':
|
||||
form_elt = elt.elements(name='x',uri='jabber:x:data').next()
|
||||
winners = []
|
||||
loosers = []
|
||||
for winner in elt.elements(name='winner', uri=''):
|
||||
winners.append(unicode(winner))
|
||||
for looser in elt.elements(name='looser', uri=''):
|
||||
loosers.append(unicode(looser))
|
||||
form = data_form.Form.fromElement(form_elt)
|
||||
xml_data = dataForm2xml(form)
|
||||
self.host.bridge.tarotGameScore(room_jid.userhost(), xml_data, winners, loosers, profile)
|
||||
elif elt.name == 'error':
|
||||
if elt['type'] == 'invalid_cards':
|
||||
played_cards = self.__xml_to_list(elt.elements(name='played',uri='').next())
|
||||
invalid_cards = self.__xml_to_list(elt.elements(name='invalid',uri='').next())
|
||||
self.host.bridge.tarotGameInvalidCards(room_jid.userhost(), elt['phase'], played_cards, invalid_cards, profile)
|
||||
else:
|
||||
error (_('Unmanaged error type: %s') % elt['type'])
|
||||
else:
|
||||
error (_('Unmanaged card game element: %s') % elt.name)
|
||||
|
||||
def getHandler(self, profile):
|
||||
return CardGameHandler(self)
|
||||
|
||||
class CardGameHandler (XMPPHandler):
|
||||
implements(iwokkel.IDisco)
|
||||
|
||||
def __init__(self, plugin_parent):
|
||||
self.plugin_parent = plugin_parent
|
||||
self.host = plugin_parent.host
|
||||
|
||||
def connectionInitialized(self):
|
||||
self.xmlstream.addObserver(CG_REQUEST, self.plugin_parent.card_game_cmd, profile = self.parent.profile)
|
||||
|
||||
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
|
||||
return [disco.DiscoFeature(NS_CG)]
|
||||
|
||||
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
||||
return []
|
||||
|
||||
197
src/plugins/plugin_xep_0045.py
Executable file
197
src/plugins/plugin_xep_0045.py
Executable file
|
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing xep-0045
|
||||
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 logging import debug, info, warning, error
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import protocol, defer, threads, reactor
|
||||
from twisted.words.protocols.jabber import client, jid, xmlstream
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
from twisted.words.protocols.jabber.xmlstream import IQ
|
||||
import os.path
|
||||
import pdb
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
from wokkel import disco, iwokkel, muc
|
||||
|
||||
from base64 import b64decode
|
||||
from hashlib import sha1
|
||||
from time import sleep
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
except ImportError:
|
||||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
AVATAR_PATH = "/avatars"
|
||||
|
||||
IQ_GET = '/iq[@type="get"]'
|
||||
NS_VCARD = 'vcard-temp'
|
||||
VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' #TODO: manage requests
|
||||
|
||||
PRESENCE = '/presence'
|
||||
NS_VCARD_UPDATE = 'vcard-temp:x:update'
|
||||
VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]'
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0045 Plugin",
|
||||
"import_name": "XEP_0045",
|
||||
"type": "XEP",
|
||||
"protocols": ["XEP-0045"],
|
||||
"dependencies": [],
|
||||
"main": "XEP_0045",
|
||||
"handler": "yes",
|
||||
"description": _("""Implementation of Multi-User Chat""")
|
||||
}
|
||||
|
||||
class XEP_0045():
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Plugin XEP_0045 initialization"))
|
||||
self.host = host
|
||||
self.clients={}
|
||||
host.bridge.addMethod("joinMUC", ".communication", in_sign='ssss', out_sign='', method=self.join)
|
||||
host.bridge.addMethod("getRoomJoined", ".communication", in_sign='s', out_sign='a(ssass)', method=self.getRoomJoined)
|
||||
host.bridge.addMethod("getRoomSubjects", ".communication", in_sign='s', out_sign='a(sss)', method=self.getRoomSubjects)
|
||||
host.bridge.addSignal("roomJoined", ".communication", signature='ssasss') #args: room_id, room_service, room_nicks, user_nick, profile
|
||||
host.bridge.addSignal("roomUserJoined", ".communication", signature='sssa{ss}s') #args: room_id, room_service, user_nick, user_data, profile
|
||||
host.bridge.addSignal("roomUserLeft", ".communication", signature='sssa{ss}s') #args: room_id, room_service, user_nick, user_data, profile
|
||||
host.bridge.addSignal("roomNewSubject", ".communication", signature='ssss') #args: room_id, room_service, subject, profile
|
||||
|
||||
def __check_profile(self, profile):
|
||||
"""check if profile is used and connected
|
||||
if profile known but disconnected, remove it from known profiles
|
||||
@param profile: profile to check
|
||||
@return: True if the profile is known and connected, else False"""
|
||||
if not profile or not self.clients.has_key(profile) or not self.host.isConnected(profile):
|
||||
error (_('Unknown or disconnected profile (%s)') % profile)
|
||||
if self.clients.has_key(profile):
|
||||
del self.clients[profile]
|
||||
return False
|
||||
return True
|
||||
|
||||
def __room_joined(self, room, profile):
|
||||
"""Called when the user is in the requested room"""
|
||||
room_jid = room.roomIdentifier+'@'+room.service
|
||||
self.clients[profile].joined_rooms[room_jid] = room
|
||||
self.host.bridge.roomJoined(room.roomIdentifier, room.service, [user.nick for user in room.roster.values()], room.nick, profile)
|
||||
|
||||
def __err_joining_room(self, failure, profile):
|
||||
"""Called when something is going wrong when joining the room"""
|
||||
mess = _("Error when joining the room")
|
||||
error (mess)
|
||||
self.host.bridge.newAlert(mess, _("Group chat error"), "ERROR", profile)
|
||||
|
||||
def getRoomJoined(self, profile_key='@DEFAULT@'):
|
||||
"""Return room where user is"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
result = []
|
||||
if not self.__check_profile(profile):
|
||||
return result
|
||||
for room in self.clients[profile].joined_rooms.values():
|
||||
result.append((room.roomIdentifier, room.service, [user.nick for user in room.roster.values()], room.nick))
|
||||
return result
|
||||
|
||||
def getRoomNick(self, room_jid, profile_key='@DEFAULT@'):
|
||||
"""return nick used in room by user
|
||||
@param room_jid: unicode room id
|
||||
@profile_key: profile
|
||||
@return: nick or empty string in case of error"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
if not self.__check_profile(profile) or not self.clients[profile].joined_rooms.has_key(room_jid):
|
||||
return ''
|
||||
return self.clients[profile].joined_rooms[room_jid].nick
|
||||
|
||||
|
||||
def getRoomSubjects(self, profile_key='@DEFAULT@'):
|
||||
"""Return received subjects of rooms"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
if not self.__check_profile(profile):
|
||||
return []
|
||||
return self.clients[profile].rec_subjects.values()
|
||||
|
||||
def join(self, service, roomId, nick, profile_key='@DEFAULT@'):
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
if not self.__check_profile(profile):
|
||||
return
|
||||
room_jid = roomId+'@'+service
|
||||
if self.clients[profile].joined_rooms.has_key(room_jid):
|
||||
warning(_('%(profile)s is already in room %(room_jid)s') % {'profile':profile, 'room_jid':room_jid})
|
||||
return
|
||||
info (_("[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile':profile,'room':roomId+'@'+service, 'nick':nick})
|
||||
try:
|
||||
self.clients[profile].join(service, roomId, nick).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile':profile}, errbackKeywords={'profile':profile})
|
||||
except:
|
||||
#XXX: this is a ugly workaround as MUCClient thrown an error if there is invalid chars in the room jid (like with the default string)
|
||||
#FIXME: must be removed when MUCClient manage this better
|
||||
self.__err_joining_room(None, profile)
|
||||
|
||||
def getHandler(self, profile):
|
||||
#reactor.callLater(15,self.join,"conference.necton2.int", "test", "Goffi \o/", profile)
|
||||
self.clients[profile] = SatMUCClient(self)
|
||||
return self.clients[profile]
|
||||
|
||||
|
||||
|
||||
class SatMUCClient (muc.MUCClient):
|
||||
#implements(iwokkel.IDisco)
|
||||
|
||||
def __init__(self, plugin_parent):
|
||||
self.plugin_parent = plugin_parent
|
||||
self.host = plugin_parent.host
|
||||
muc.MUCClient.__init__(self)
|
||||
self.joined_rooms = {}
|
||||
self.rec_subjects = {}
|
||||
print "init SatMUCClient OK"
|
||||
|
||||
def receivedGroupChat(self, room, user, body):
|
||||
debug('receivedGroupChat: room=%s user=%s body=%s', room, user, body)
|
||||
|
||||
def userJoinedRoom(self, room, user):
|
||||
debug (_("user %(nick)s has joined room (%(room_id)s)") % {'nick':user.nick, 'room_id':room.occupantJID.userhost()})
|
||||
user_data={'entity':user.entity or '', 'affiliation':user.affiliation, 'role':user.role}
|
||||
self.host.bridge.roomUserJoined(room.roomIdentifier, room.service, user.nick, user_data, self.parent.profile)
|
||||
|
||||
def userLeftRoom(self, room, user):
|
||||
debug (_("user %(nick)s left room (%(room_id)s)") % {'nick':user.nick, 'room_id':room.occupantJID.userhost()})
|
||||
user_data={'entity':user.entity or '', 'affiliation':user.affiliation, 'role':user.role}
|
||||
self.host.bridge.roomUserLeft(room.roomIdentifier, room.service, user.nick, user_data, self.parent.profile)
|
||||
|
||||
def userUpdatedStatus(self, room, user, show, status):
|
||||
print("FIXME: MUC status not managed yet")
|
||||
#FIXME: gof
|
||||
|
||||
def receivedSubject(self, occupantJID, subject):
|
||||
room = self._getRoom(occupantJID)
|
||||
debug (_("New subject for room (%(room_id)s): %(subject)s") % {'room_id':room.occupantJID.userhost(),'subject':subject})
|
||||
room_jid = room.roomIdentifier+'@'+room.service
|
||||
self.rec_subjects[room_jid] = (room.roomIdentifier, room.service, subject)
|
||||
self.host.bridge.roomNewSubject(room.roomIdentifier, room.service, subject, self.parent.profile)
|
||||
|
||||
#def connectionInitialized(self):
|
||||
#pass
|
||||
|
||||
#def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
|
||||
#return [disco.DiscoFeature(NS_VCARD)]
|
||||
|
||||
#def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
||||
#return []
|
||||
|
||||
246
src/plugins/plugin_xep_0054.py
Executable file
246
src/plugins/plugin_xep_0054.py
Executable file
|
|
@ -0,0 +1,246 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing xep-0054
|
||||
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 logging import debug, info, error
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import protocol, defer, threads, reactor
|
||||
from twisted.words.protocols.jabber import client, jid, xmlstream
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
from twisted.words.protocols.jabber.xmlstream import IQ
|
||||
import os.path
|
||||
import pdb
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
from wokkel import disco, iwokkel
|
||||
|
||||
from base64 import b64decode
|
||||
from hashlib import sha1
|
||||
from time import sleep
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
except ImportError:
|
||||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
AVATAR_PATH = "/avatars"
|
||||
|
||||
IQ_GET = '/iq[@type="get"]'
|
||||
NS_VCARD = 'vcard-temp'
|
||||
VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]' #TODO: manage requests
|
||||
|
||||
PRESENCE = '/presence'
|
||||
NS_VCARD_UPDATE = 'vcard-temp:x:update'
|
||||
VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]'
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0054 Plugin",
|
||||
"import_name": "XEP_0054",
|
||||
"type": "XEP",
|
||||
"protocols": ["XEP-0054", "XEP-0153"],
|
||||
"dependencies": [],
|
||||
"main": "XEP_0054",
|
||||
"handler": "yes",
|
||||
"description": _("""Implementation of vcard-temp""")
|
||||
}
|
||||
|
||||
class XEP_0054():
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Plugin XEP_0054 initialization"))
|
||||
self.host = host
|
||||
self.avatar_path = os.path.expanduser(self.host.get_const('local_dir') + AVATAR_PATH)
|
||||
self.vcard_cache = host.memory.getPrivate("vcard_cache") or {} #used to store nicknames and avatar, key = jid
|
||||
if not os.path.exists(self.avatar_path):
|
||||
os.makedirs(self.avatar_path)
|
||||
host.bridge.addMethod("getCard", ".communication", in_sign='ss', out_sign='s', method=self.getCard)
|
||||
host.bridge.addMethod("getAvatarFile", ".communication", in_sign='s', out_sign='s', method=self.getAvatarFile)
|
||||
host.bridge.addMethod("getCardCache", ".communication", in_sign='s', out_sign='a{ss}', method=self.getCardCache)
|
||||
|
||||
def getHandler(self, profile):
|
||||
return XEP_0054_handler(self)
|
||||
|
||||
def update_cache(self, jid, name, value):
|
||||
"""update cache value
|
||||
- save value in memory in case of change
|
||||
- send updatedValue signal if the value is new or updated
|
||||
"""
|
||||
if not self.vcard_cache.has_key(jid.userhost()):
|
||||
self.vcard_cache[jid.userhost()] = {}
|
||||
|
||||
cache = self.vcard_cache[jid.userhost()]
|
||||
old_value = cache[name] if cache.has_key(name) else None
|
||||
if not old_value or value != old_value:
|
||||
cache[name] = value
|
||||
self.host.memory.setPrivate("vcard_cache", self.vcard_cache)
|
||||
self.host.bridge.updatedValue('card_'+name, {'jid':jid.userhost(), name:value})
|
||||
|
||||
def get_cache(self, jid, name):
|
||||
"""return cached value for jid
|
||||
@param jid: target contact
|
||||
@param name: name of the value ('nick' or 'avatar')
|
||||
@return: wanted value or None"""
|
||||
try:
|
||||
return self.vcard_cache[jid.userhost()][name]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def save_photo(self, photo_xml):
|
||||
"""Parse a <PHOTO> elem and save the picture"""
|
||||
for elem in photo_xml.elements():
|
||||
if elem.name == 'TYPE':
|
||||
info(_('Photo of type [%s] found') % str(elem))
|
||||
if elem.name == 'BINVAL':
|
||||
debug(_('Decoding binary'))
|
||||
decoded = b64decode(str(elem))
|
||||
hash = sha1(decoded).hexdigest()
|
||||
filename = self.avatar_path+'/'+hash
|
||||
if not os.path.exists(filename):
|
||||
with open(filename,'wb') as file:
|
||||
file.write(decoded)
|
||||
debug(_("file saved to %s") % hash)
|
||||
else:
|
||||
debug(_("file [%s] already in cache") % hash)
|
||||
return hash
|
||||
|
||||
@defer.deferredGenerator
|
||||
def vCard2Dict(self, vcard, target):
|
||||
"""Convert a VCard to a dict, and save binaries"""
|
||||
debug (_("parsing vcard"))
|
||||
dictionary = {}
|
||||
d = defer.Deferred()
|
||||
|
||||
for elem in vcard.elements():
|
||||
if elem.name == 'FN':
|
||||
dictionary['fullname'] = unicode(elem)
|
||||
elif elem.name == 'NICKNAME':
|
||||
dictionary['nick'] = unicode(elem)
|
||||
self.update_cache(target, 'nick', dictionary['nick'])
|
||||
elif elem.name == 'URL':
|
||||
dictionary['website'] = unicode(elem)
|
||||
elif elem.name == 'EMAIL':
|
||||
dictionary['email'] = unicode(elem)
|
||||
elif elem.name == 'BDAY':
|
||||
dictionary['birthday'] = unicode(elem)
|
||||
elif elem.name == 'PHOTO':
|
||||
d2 = defer.waitForDeferred(
|
||||
threads.deferToThread(self.save_photo, elem))
|
||||
yield d2
|
||||
dictionary["avatar"] = d2.getResult()
|
||||
if not dictionary["avatar"]: #can happen in case of e.g. empty photo elem
|
||||
del dictionary['avatar']
|
||||
else:
|
||||
self.update_cache(target, 'avatar', dictionary['avatar'])
|
||||
else:
|
||||
info (_('FIXME: [%s] VCard tag is not managed yet') % elem.name)
|
||||
|
||||
yield dictionary
|
||||
|
||||
def vcard_ok(self, answer):
|
||||
"""Called after the first get IQ"""
|
||||
debug (_("VCard found"))
|
||||
|
||||
if answer.firstChildElement().name == "vCard":
|
||||
d = self.vCard2Dict(answer.firstChildElement(), jid.JID(answer["from"]))
|
||||
d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data))
|
||||
else:
|
||||
error (_("FIXME: vCard not found as first child element"))
|
||||
self.host.bridge.actionResult("SUPPRESS", answer['id'], {}) #FIXME: maybe an error message would be best
|
||||
|
||||
def vcard_err(self, failure):
|
||||
"""Called when something is wrong with registration"""
|
||||
error (_("Can't find VCard of %s") % failure.value.stanza['from'])
|
||||
self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}) #FIXME: maybe an error message would be best
|
||||
|
||||
def getCard(self, target, profile_key='@DEFAULT@'):
|
||||
"""Ask server for VCard
|
||||
@param target: jid from which we want the VCard
|
||||
@result: id to retrieve the profile"""
|
||||
current_jid, xmlstream = self.host.getJidNStream(profile_key)
|
||||
if not xmlstream:
|
||||
error (_('Asking vcard for an non-existant or not connected profile'))
|
||||
return ""
|
||||
to_jid = jid.JID(target)
|
||||
debug(_("Asking for %s's VCard") % to_jid.userhost())
|
||||
reg_request=IQ(xmlstream,'get')
|
||||
reg_request["from"]=current_jid.full()
|
||||
reg_request["to"] = to_jid.userhost()
|
||||
query=reg_request.addElement('vCard', NS_VCARD)
|
||||
reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err)
|
||||
return reg_request["id"]
|
||||
|
||||
def getAvatarFile(self, hash):
|
||||
"""Give the full path of avatar from hash
|
||||
@param hash: SHA1 hash
|
||||
@return full_path
|
||||
"""
|
||||
filename = self.avatar_path+'/'+hash
|
||||
if not os.path.exists(filename):
|
||||
error (_("Asking for an uncached avatar [%s]") % hash)
|
||||
return ""
|
||||
return filename
|
||||
|
||||
def getCardCache(self, target):
|
||||
"""Request for cached values of profile
|
||||
return the cached nickname and avatar if exists, else get VCard
|
||||
"""
|
||||
to_jid = jid.JID(target)
|
||||
result = {}
|
||||
nick = self.get_cache(to_jid, 'nick')
|
||||
if nick:
|
||||
result['nick'] = nick
|
||||
avatar = self.get_cache(to_jid, 'avatar')
|
||||
if avatar:
|
||||
result['avatar'] = avatar
|
||||
return result
|
||||
|
||||
|
||||
|
||||
class XEP_0054_handler(XMPPHandler):
|
||||
implements(iwokkel.IDisco)
|
||||
|
||||
def __init__(self, plugin_parent):
|
||||
self.plugin_parent = plugin_parent
|
||||
self.host = plugin_parent.host
|
||||
|
||||
def connectionInitialized(self):
|
||||
self.xmlstream.addObserver(VCARD_UPDATE, self.update)
|
||||
|
||||
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
|
||||
return [disco.DiscoFeature(NS_VCARD)]
|
||||
|
||||
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
||||
return []
|
||||
|
||||
def update(self, presence):
|
||||
"""Request for VCard's nickname
|
||||
return the cached nickname if exists, else get VCard
|
||||
"""
|
||||
to_jid = jid.JID(presence['from'])
|
||||
x_elem = filter (lambda x:x.name == "x", presence.elements())[0] #We only want the "x" element
|
||||
for elem in x_elem.elements():
|
||||
if elem.name == 'photo':
|
||||
hash = str(elem)
|
||||
old_avatar = self.plugin_parent.get_cache(to_jid, 'avatar')
|
||||
if not old_avatar or old_avatar != hash:
|
||||
debug(_('New avatar found, requesting vcard'))
|
||||
self.plugin_parent.getCard(to_jid.userhost(), self.parent.profile)
|
||||
558
src/plugins/plugin_xep_0065.py
Executable file
558
src/plugins/plugin_xep_0065.py
Executable file
|
|
@ -0,0 +1,558 @@
|
|||
#!/usr/bin/python
|
||||
#-*- coding: utf-8 -*-
|
||||
"""
|
||||
SAT plugin for managing xep-0065
|
||||
|
||||
Copyright (C)
|
||||
2002-2004 Dave Smith (dizzyd@jabber.org)
|
||||
2007-2008 Fabio Forno (xmpp:ff@jabber.bluendo.com)
|
||||
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/>.
|
||||
|
||||
--
|
||||
|
||||
This program is based on proxy65 (http://code.google.com/p/proxy65),
|
||||
originaly written by David Smith and modified by Fabio Forno.
|
||||
It is sublicensed under GPL v3 (or any later version) as allowed by the original
|
||||
license.
|
||||
|
||||
--
|
||||
|
||||
Here is a copy of the original license:
|
||||
|
||||
Copyright (C)
|
||||
2002-2004 Dave Smith (dizzyd@jabber.org)
|
||||
2007-2008 Fabio Forno (xmpp:ff@jabber.bluendo.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from logging import debug, info, error
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.protocols.basic import FileSender
|
||||
from twisted.words.xish import domish
|
||||
from twisted.web.client import getPage
|
||||
import struct
|
||||
import urllib
|
||||
import hashlib, pdb
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
except ImportError:
|
||||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
from wokkel import disco, iwokkel
|
||||
|
||||
IQ_SET = '/iq[@type="set"]'
|
||||
NS_BS = 'http://jabber.org/protocol/bytestreams'
|
||||
BS_REQUEST = IQ_SET + '/query[@xmlns="' + NS_BS + '"]'
|
||||
|
||||
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0065 Plugin",
|
||||
"import_name": "XEP_0065",
|
||||
"type": "XEP",
|
||||
"protocols": ["XEP-0065"],
|
||||
"main": "XEP_0065",
|
||||
"handler": "yes",
|
||||
"description": _("""Implementation of SOCKS5 Bytestreams""")
|
||||
}
|
||||
|
||||
STATE_INITIAL = 0
|
||||
STATE_AUTH = 1
|
||||
STATE_REQUEST = 2
|
||||
STATE_READY = 3
|
||||
STATE_AUTH_USERPASS = 4
|
||||
STATE_TARGET_INITIAL = 5
|
||||
STATE_TARGET_AUTH = 6
|
||||
STATE_TARGET_REQUEST = 7
|
||||
STATE_TARGET_READY = 8
|
||||
STATE_LAST = 9
|
||||
|
||||
STATE_CONNECT_PENDING = STATE_LAST + 1
|
||||
|
||||
SOCKS5_VER = 0x05
|
||||
|
||||
ADDR_IPV4 = 0x01
|
||||
ADDR_DOMAINNAME = 0x03
|
||||
ADDR_IPV6 = 0x04
|
||||
|
||||
CMD_CONNECT = 0x01
|
||||
CMD_BIND = 0x02
|
||||
CMD_UDPASSOC = 0x03
|
||||
|
||||
AUTHMECH_ANON = 0x00
|
||||
AUTHMECH_USERPASS = 0x02
|
||||
AUTHMECH_INVALID = 0xFF
|
||||
|
||||
REPLY_SUCCESS = 0x00
|
||||
REPLY_GENERAL_FAILUR = 0x01
|
||||
REPLY_CONN_NOT_ALLOWED = 0x02
|
||||
REPLY_NETWORK_UNREACHABLE = 0x03
|
||||
REPLY_HOST_UNREACHABLE = 0x04
|
||||
REPLY_CONN_REFUSED = 0x05
|
||||
REPLY_TTL_EXPIRED = 0x06
|
||||
REPLY_CMD_NOT_SUPPORTED = 0x07
|
||||
REPLY_ADDR_NOT_SUPPORTED = 0x08
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SOCKSv5(protocol.Protocol, FileSender):
|
||||
def __init__(self):
|
||||
debug(_("Protocol init"))
|
||||
self.state = STATE_INITIAL
|
||||
self.buf = ""
|
||||
self.supportedAuthMechs = [ AUTHMECH_ANON ]
|
||||
self.supportedAddrs = [ ADDR_DOMAINNAME ]
|
||||
self.enabledCommands = [ CMD_CONNECT ]
|
||||
self.peersock = None
|
||||
self.addressType = 0
|
||||
self.requestType = 0
|
||||
self.activeConns = {}
|
||||
self.pendingConns = {}
|
||||
self.transfered = 0 #nb of bytes already copied
|
||||
|
||||
def _startNegotiation(self):
|
||||
debug("_startNegotiation")
|
||||
self.state = STATE_TARGET_AUTH
|
||||
self.transport.write(struct.pack('!3B', SOCKS5_VER, 1, AUTHMECH_ANON))
|
||||
|
||||
def _parseNegotiation(self):
|
||||
debug("_parseNegotiation")
|
||||
try:
|
||||
# Parse out data
|
||||
ver, nmethod = struct.unpack('!BB', self.buf[:2])
|
||||
methods = struct.unpack('%dB' % nmethod, self.buf[2:nmethod+2])
|
||||
|
||||
# Ensure version is correct
|
||||
if ver != 5:
|
||||
self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID))
|
||||
self.transport.loseConnection()
|
||||
return
|
||||
|
||||
# Trim off front of the buffer
|
||||
self.buf = self.buf[nmethod+2:]
|
||||
|
||||
# Check for supported auth mechs
|
||||
for m in self.supportedAuthMechs:
|
||||
if m in methods:
|
||||
# Update internal state, according to selected method
|
||||
if m == AUTHMECH_ANON:
|
||||
self.state = STATE_REQUEST
|
||||
elif m == AUTHMECH_USERPASS:
|
||||
self.state = STATE_AUTH_USERPASS
|
||||
# Complete negotiation w/ this method
|
||||
self.transport.write(struct.pack('!BB', SOCKS5_VER, m))
|
||||
return
|
||||
|
||||
# No supported mechs found, notify client and close the connection
|
||||
self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID))
|
||||
self.transport.loseConnection()
|
||||
except struct.error:
|
||||
pass
|
||||
|
||||
def _parseUserPass(self):
|
||||
debug("_parseUserPass")
|
||||
try:
|
||||
# Parse out data
|
||||
ver, ulen = struct.unpack('BB', self.buf[:2])
|
||||
uname, = struct.unpack('%ds' % ulen, self.buf[2:ulen + 2])
|
||||
plen, = struct.unpack('B', self.buf[ulen + 2])
|
||||
password, = struct.unpack('%ds' % plen, self.buf[ulen + 3:ulen + 3 + plen])
|
||||
# Trim off fron of the buffer
|
||||
self.buf = self.buf[3 + ulen + plen:]
|
||||
# Fire event to authenticate user
|
||||
if self.authenticateUserPass(uname, password):
|
||||
# Signal success
|
||||
self.state = STATE_REQUEST
|
||||
self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x00))
|
||||
else:
|
||||
# Signal failure
|
||||
self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x01))
|
||||
self.transport.loseConnection()
|
||||
except struct.error:
|
||||
pass
|
||||
|
||||
def sendErrorReply(self, errorcode):
|
||||
debug("sendErrorReply")
|
||||
# Any other address types are not supported
|
||||
result = struct.pack('!BBBBIH', SOCKS5_VER, errorcode, 0, 1, 0, 0)
|
||||
self.transport.write(result)
|
||||
self.transport.loseConnection()
|
||||
|
||||
def addConnection(self, address, connection):
|
||||
info(_("Adding connection: %(address)s, %(connection)s") % {'address':address, 'connection':connection})
|
||||
olist = self.pendingConns.get(address, [])
|
||||
if len(olist) <= 1:
|
||||
olist.append(connection)
|
||||
self.pendingConns[address] = olist
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def removePendingConnection(self, address, connection):
|
||||
olist = self.pendingConns[address]
|
||||
if len(olist) == 1:
|
||||
del self.pendingConns[address]
|
||||
else:
|
||||
olist.remove(connection)
|
||||
self.pendingConns[address] = olist
|
||||
|
||||
def removeActiveConnection(self, address):
|
||||
del self.activeConns[address]
|
||||
|
||||
def _parseRequest(self):
|
||||
debug("_parseRequest")
|
||||
try:
|
||||
# Parse out data and trim buffer accordingly
|
||||
ver, cmd, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4])
|
||||
|
||||
# Ensure we actually support the requested address type
|
||||
if self.addressType not in self.supportedAddrs:
|
||||
self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
|
||||
return
|
||||
|
||||
# Deal with addresses
|
||||
if self.addressType == ADDR_IPV4:
|
||||
addr, port = struct.unpack('!IH', self.buf[4:10])
|
||||
self.buf = self.buf[10:]
|
||||
elif self.addressType == ADDR_DOMAINNAME:
|
||||
nlen = ord(self.buf[4])
|
||||
addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:])
|
||||
self.buf = self.buf[7 + len(addr):]
|
||||
else:
|
||||
# Any other address types are not supported
|
||||
self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
|
||||
return
|
||||
|
||||
# Ensure command is supported
|
||||
if cmd not in self.enabledCommands:
|
||||
# Send a not supported error
|
||||
self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED)
|
||||
return
|
||||
|
||||
# Process the command
|
||||
if cmd == CMD_CONNECT:
|
||||
self.connectRequested(addr, port)
|
||||
elif cmd == CMD_BIND:
|
||||
self.bindRequested(addr, port)
|
||||
else:
|
||||
# Any other command is not supported
|
||||
self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED)
|
||||
|
||||
except struct.error, why:
|
||||
return None
|
||||
|
||||
def _makeRequest(self):
|
||||
debug("_makeRequest")
|
||||
self.state = STATE_TARGET_REQUEST
|
||||
sha1 = hashlib.sha1(self.sid + self.initiator_jid + self.target_jid).hexdigest()
|
||||
request = struct.pack('!5B%dsH' % len(sha1), SOCKS5_VER, CMD_CONNECT, 0, ADDR_DOMAINNAME, len(sha1), sha1, 0)
|
||||
self.transport.write(request)
|
||||
|
||||
def _parseRequestReply(self):
|
||||
debug("_parseRequestReply")
|
||||
try:
|
||||
ver, rep, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4])
|
||||
# Ensure we actually support the requested address type
|
||||
if self.addressType not in self.supportedAddrs:
|
||||
self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
|
||||
return
|
||||
|
||||
# Deal with addresses
|
||||
if self.addressType == ADDR_IPV4:
|
||||
addr, port = struct.unpack('!IH', self.buf[4:10])
|
||||
self.buf = self.buf[10:]
|
||||
elif self.addressType == ADDR_DOMAINNAME:
|
||||
nlen = ord(self.buf[4])
|
||||
addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:])
|
||||
self.buf = self.buf[7 + len(addr):]
|
||||
else:
|
||||
# Any other address types are not supported
|
||||
self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
|
||||
return
|
||||
|
||||
# Ensure reply is OK
|
||||
if rep != REPLY_SUCCESS:
|
||||
self.loseConnection()
|
||||
return
|
||||
|
||||
debug(_("Saving file in %s."), self.data["dest_path"])
|
||||
self.dest_file = open(self.data["dest_path"], 'w')
|
||||
self.state = STATE_TARGET_READY
|
||||
self.activateCB(self.target_jid, self.initiator_jid, self.sid, self.IQ_id, self.xmlstream)
|
||||
|
||||
|
||||
except struct.error, why:
|
||||
return None
|
||||
|
||||
def connectionMade(self):
|
||||
debug("connectionMade (mode = %s)" % self.mode)
|
||||
self.host.registerProgressCB(self.transfert_id, self.getProgress)
|
||||
|
||||
if self.mode == "target":
|
||||
self.state = STATE_TARGET_INITIAL
|
||||
self._startNegotiation()
|
||||
|
||||
def connectRequested(self, addr, port):
|
||||
debug("connectRequested")
|
||||
# Check for special connect to the namespace -- this signifies that the client
|
||||
# is just checking to ensure it can connect to the streamhost
|
||||
if addr == "http://jabber.org/protocol/bytestreams":
|
||||
self.connectCompleted(addr, 0)
|
||||
self.transport.loseConnection()
|
||||
return
|
||||
|
||||
# Save addr, for cleanup
|
||||
self.addr = addr
|
||||
|
||||
# Check to see if the requested address is already
|
||||
# activated -- send an error if so
|
||||
if addr in self.activeConns:
|
||||
self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED)
|
||||
return
|
||||
|
||||
# Add this address to the pending connections
|
||||
if self.addConnection(addr, self):
|
||||
self.connectCompleted(addr, 0)
|
||||
self.transport.stopReading()
|
||||
else:
|
||||
self.sendErrorReply(socks5.REPLY_CONN_REFUSED)
|
||||
|
||||
def getProgress(self, data):
|
||||
"""Fill data with position of current transfert"""
|
||||
try:
|
||||
data["position"] = str(self.dest_file.tell())
|
||||
data["size"] = self.filesize
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
def fileTransfered(self, d):
|
||||
info(_("File transfer completed, closing connection"))
|
||||
self.transport.loseConnection()
|
||||
try:
|
||||
self.dest_file.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def updateTransfered(self, data):
|
||||
self.transfered+=len(data)
|
||||
return data
|
||||
|
||||
def connectCompleted(self, remotehost, remoteport):
|
||||
debug("connectCompleted")
|
||||
if self.addressType == ADDR_IPV4:
|
||||
result = struct.pack('!BBBBIH', SOCKS5_VER, REPLY_SUCCESS, 0, 1, remotehost, remoteport)
|
||||
elif self.addressType == ADDR_DOMAINNAME:
|
||||
result = struct.pack('!BBBBB%dsH' % len(remotehost), SOCKS5_VER, REPLY_SUCCESS, 0,
|
||||
ADDR_DOMAINNAME, len(remotehost), remotehost, remoteport)
|
||||
self.transport.write(result)
|
||||
self.state = STATE_READY
|
||||
self.dest_file=open(self.filepath)
|
||||
d=self.beginFileTransfer(self.dest_file, self.transport, self.updateTransfered)
|
||||
d.addCallback(self.fileTransfered)
|
||||
|
||||
def bindRequested(self, addr, port):
|
||||
pass
|
||||
|
||||
def authenticateUserPass(self, user, passwd):
|
||||
debug("User/pass: %s/%s", user, passwd)
|
||||
return True
|
||||
|
||||
def dataReceived(self, buf):
|
||||
if self.state == STATE_TARGET_READY:
|
||||
self.dest_file.write(buf)
|
||||
self.transfered+=len(buf)
|
||||
return
|
||||
|
||||
self.buf = self.buf + buf
|
||||
if self.state == STATE_INITIAL:
|
||||
self._parseNegotiation()
|
||||
if self.state == STATE_AUTH_USERPASS:
|
||||
self._parseUserPass()
|
||||
if self.state == STATE_REQUEST:
|
||||
self._parseRequest()
|
||||
if self.state == STATE_TARGET_AUTH:
|
||||
ver, method = struct.unpack('!BB', buf)
|
||||
self.buf = self.buf[2:]
|
||||
if ver!=SOCKS5_VER or method!=AUTHMECH_ANON:
|
||||
self.transport.loseConnection()
|
||||
else:
|
||||
self._makeRequest()
|
||||
if self.state == STATE_TARGET_REQUEST:
|
||||
self._parseRequestReply()
|
||||
|
||||
|
||||
def clientConnectionLost(self, reason):
|
||||
debug("clientConnectionLost")
|
||||
self.transport.loseConnection()
|
||||
|
||||
def connectionLost(self, reason):
|
||||
debug("connectionLost")
|
||||
self.host.removeProgressCB(self.transfert_id)
|
||||
if self.state == STATE_CONNECT_PENDING:
|
||||
self.removePendingConnection(self.addr, self)
|
||||
else:
|
||||
self.transport.unregisterProducer()
|
||||
if self.peersock != None:
|
||||
self.peersock.peersock = None
|
||||
self.peersock.transport.unregisterProducer()
|
||||
self.peersock = None
|
||||
self.removeActiveConnection(self.addr)
|
||||
|
||||
class Socks5ServerFactory(protocol.ServerFactory):
|
||||
protocol = SOCKSv5
|
||||
protocol.mode = "initiator" #FIXME: Q&D way, fix it
|
||||
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
debug (_("Socks 5 server connection started"))
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
debug (_("Socks 5 server connection lost (reason: %s)"), reason)
|
||||
|
||||
class Socks5ClientFactory(protocol.ClientFactory):
|
||||
protocol = SOCKSv5
|
||||
protocol.mode = "target" #FIXME: Q&D way, fix it
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
debug (_("Socks 5 client connection started"))
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
debug (_("Socks 5 client connection lost (reason: %s)"), reason)
|
||||
|
||||
|
||||
class XEP_0065():
|
||||
|
||||
params = """
|
||||
<params>
|
||||
<general>
|
||||
<category name="File Transfert">
|
||||
<param name="IP" value='0.0.0.0' default_cb='yes' type="string" />
|
||||
<param name="Port" value="28915" type="string" />
|
||||
</category>
|
||||
</general>
|
||||
</params>
|
||||
"""
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Plugin XEP_0065 initialization"))
|
||||
self.host = host
|
||||
debug(_("registering"))
|
||||
self.server_factory = Socks5ServerFactory()
|
||||
self.server_factory.protocol.host = self.host #needed for progress CB
|
||||
self.client_factory = Socks5ClientFactory()
|
||||
|
||||
#parameters
|
||||
host.memory.importParams(XEP_0065.params)
|
||||
host.memory.setDefault("IP", "File Transfert", self.getExternalIP)
|
||||
|
||||
port = int(self.host.memory.getParamA("Port", "File Transfert"))
|
||||
info(_("Launching Socks5 Stream server on port %d"), port)
|
||||
reactor.listenTCP(port, self.server_factory)
|
||||
|
||||
def getHandler(self, profile):
|
||||
return XEP_0065_handler(self)
|
||||
|
||||
def getExternalIP(self):
|
||||
"""Return IP visible from outside, by asking to a website"""
|
||||
return getPage("http://www.goffi.org/sat_tools/get_ip.php")
|
||||
|
||||
def setData(self, data, id):
|
||||
self.data = data
|
||||
self.transfert_id = id
|
||||
|
||||
def sendFile(self, id, filepath, size):
|
||||
#lauching socks5 initiator
|
||||
debug(_("Launching socks5 initiator"))
|
||||
self.server_factory.protocol.mode = "initiator"
|
||||
self.server_factory.protocol.filepath = filepath
|
||||
self.server_factory.protocol.filesize = size
|
||||
self.server_factory.protocol.transfert_id = id
|
||||
|
||||
def getFile(self, iq, profile_key='@DEFAULT@'):
|
||||
"""Get file using byte stream"""
|
||||
client = self.host.getClient(profile_key)
|
||||
assert(client)
|
||||
iq.handled = True
|
||||
SI_elem = iq.firstChildElement()
|
||||
IQ_id = iq['id']
|
||||
for element in SI_elem.elements():
|
||||
if element.name == "streamhost":
|
||||
info (_("Stream proposed: host=[%(host)s] port=[%(port)s]") % {'host':element['host'], 'port':element['port']})
|
||||
factory = self.client_factory
|
||||
self.server_factory.protocol.mode = "target"
|
||||
factory.protocol.host = self.host #needed for progress CB
|
||||
factory.protocol.xmlstream = client.xmlstream
|
||||
factory.protocol.data = self.data
|
||||
factory.protocol.transfert_id = self.transfert_id
|
||||
factory.protocol.filesize = self.data["size"]
|
||||
factory.protocol.sid = SI_elem['sid']
|
||||
factory.protocol.initiator_jid = element['jid']
|
||||
factory.protocol.target_jid = client.jid.full()
|
||||
factory.protocol.IQ_id = IQ_id
|
||||
factory.protocol.activateCB = self.activateStream
|
||||
reactor.connectTCP(element['host'], int(element['port']), factory)
|
||||
|
||||
def activateStream(self, from_jid, to_jid, sid, IQ_id, xmlstream):
|
||||
debug(_("activating stream"))
|
||||
result = domish.Element(('', 'iq'))
|
||||
result['type'] = 'result'
|
||||
result['id'] = IQ_id
|
||||
result['from'] = from_jid
|
||||
result['to'] = to_jid
|
||||
query = result.addElement('query', 'http://jabber.org/protocol/bytestreams')
|
||||
query['sid'] = sid
|
||||
streamhost = query.addElement('streamhost-used')
|
||||
streamhost['jid'] = to_jid #FIXME: use real streamhost
|
||||
xmlstream.send(result)
|
||||
|
||||
class XEP_0065_handler(XMPPHandler):
|
||||
implements(iwokkel.IDisco)
|
||||
|
||||
def __init__(self, plugin_parent):
|
||||
self.plugin_parent = plugin_parent
|
||||
self.host = plugin_parent.host
|
||||
|
||||
def connectionInitialized(self):
|
||||
self.xmlstream.addObserver(BS_REQUEST, self.plugin_parent.getFile)
|
||||
|
||||
|
||||
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
|
||||
return [disco.DiscoFeature(NS_BS)]
|
||||
|
||||
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
||||
return []
|
||||
143
src/plugins/plugin_xep_0077.py
Executable file
143
src/plugins/plugin_xep_0077.py
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing xep-0077
|
||||
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 logging import debug, info, error
|
||||
from twisted.words.protocols.jabber import client, jid
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
from twisted.words.protocols.jabber.xmlstream import IQ
|
||||
from twisted.internet import reactor
|
||||
from sat.tools.xml_tools import dataForm2xml
|
||||
import pdb
|
||||
|
||||
from wokkel import data_form
|
||||
|
||||
NS_REG = 'jabber:iq:register'
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0077 Plugin",
|
||||
"import_name": "XEP_0077",
|
||||
"type": "XEP",
|
||||
"protocols": ["XEP-0077"],
|
||||
"dependencies": [],
|
||||
"main": "XEP_0077",
|
||||
"description": _("""Implementation of in-band registration""")
|
||||
}
|
||||
|
||||
class XEP_0077():
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Plugin XEP_0077 initialization"))
|
||||
self.host = host
|
||||
self.triggers = {} #used by other protocol (e.g. XEP-0100) to finish registration. key = target_jid
|
||||
host.bridge.addMethod("in_band_register", ".communication", in_sign='ss', out_sign='s', method=self.in_band_register)
|
||||
host.bridge.addMethod("in_band_submit", ".request", in_sign='sa(ss)', out_sign='s', method=self.in_band_submit)
|
||||
|
||||
def addTrigger(self, target, cb, profile):
|
||||
"""Add a callback which is called when registration to target is successful"""
|
||||
self.triggers[target] = (cb, profile)
|
||||
|
||||
def reg_ok(self, answer):
|
||||
"""Called after the first get IQ"""
|
||||
try:
|
||||
x_elem = filter (lambda x:x.name == "x", answer.firstChildElement().elements())[0] #We only want the "x" element (data form)
|
||||
except IndexError:
|
||||
info(_("No data form found"))
|
||||
#TODO: manage registration without data form
|
||||
answer_data={"reason": "unmanaged", "message":_("This gateway can't be managed by SàT, sorry :(")}
|
||||
answer_type = "ERROR"
|
||||
self.host.bridge.actionResult(answer_type, answer['id'], answer_data)
|
||||
return
|
||||
|
||||
form = data_form.Form.fromElement(x_elem)
|
||||
xml_data = dataForm2xml(form)
|
||||
self.host.bridge.actionResult("XMLUI", answer['id'], {"target":answer["from"], "type":"registration", "xml":xml_data})
|
||||
|
||||
def reg_err(self, failure):
|
||||
"""Called when something is wrong with registration"""
|
||||
info (_("Registration failure: %s") % str(failure.value))
|
||||
answer_data = {}
|
||||
answer_data['reason'] = 'unknown'
|
||||
answer_data={"message":"%s [code: %s]" % (failure.value.condition, unicode(failure.value))}
|
||||
answer_type = "ERROR"
|
||||
self.host.bridge.actionResult(answer_type, failure.value.stanza['id'], answer_data)
|
||||
|
||||
def unregistrationAnswer(self, answer):
|
||||
debug (_("registration answer: %s") % answer.toXml())
|
||||
answer_type = "SUCCESS"
|
||||
answer_data={"message":_("Your are now unregistred")}
|
||||
self.host.bridge.actionResult(answer_type, answer['id'], answer_data)
|
||||
|
||||
def unregistrationFailure(self, failure):
|
||||
info (_("Unregistration failure: %s") % str(failure.value))
|
||||
answer_type = "ERROR"
|
||||
answer_data = {}
|
||||
answer_data['reason'] = 'unknown'
|
||||
answer_data={"message":_("Unregistration failed: %s") % failure.value.condition}
|
||||
self.host.bridge.actionResult(answer_type, failure.value.stanza['id'], answer_data)
|
||||
|
||||
def registrationAnswer(self, answer):
|
||||
debug (_("registration answer: %s") % answer.toXml())
|
||||
answer_type = "SUCCESS"
|
||||
answer_data={"message":_("Registration successfull")}
|
||||
self.host.bridge.actionResult(answer_type, answer['id'], answer_data)
|
||||
if self.triggers.has_key(answer["from"]):
|
||||
callback,profile = self.triggers[answer["from"]]
|
||||
callback(answer["from"], profile)
|
||||
del self.triggers[answer["from"]]
|
||||
|
||||
def registrationFailure(self, failure):
|
||||
info (_("Registration failure: %s") % str(failure.value))
|
||||
print failure.value.stanza.toXml()
|
||||
answer_type = "ERROR"
|
||||
answer_data = {}
|
||||
if failure.value.condition == 'conflict':
|
||||
answer_data['reason'] = 'conflict'
|
||||
answer_data={"message":_("Username already exists, please choose an other one")}
|
||||
else:
|
||||
answer_data['reason'] = 'unknown'
|
||||
answer_data={"message":_("Registration failed")}
|
||||
self.host.bridge.actionResult(answer_type, failure.value.stanza['id'], answer_data)
|
||||
if self.triggers.has_key(answer["from"]):
|
||||
del self.triggers[answer["from"]]
|
||||
|
||||
def in_band_submit(self, action, target, fields, profile):
|
||||
"""Submit a form for registration, using data_form"""
|
||||
id, deferred = self.host.submitForm(action, target, fields, profile)
|
||||
if action == 'CANCEL':
|
||||
deferred.addCallbacks(self.unregistrationAnswer, self.unregistrationFailure)
|
||||
else:
|
||||
deferred.addCallbacks(self.registrationAnswer, self.registrationFailure)
|
||||
return id
|
||||
|
||||
def in_band_register(self, target, profile_key='@DEFAULT@'):
|
||||
"""register to a target JID"""
|
||||
current_jid, xmlstream = self.host.getJidNStream(profile_key)
|
||||
if not xmlstream:
|
||||
error (_('Asking for an non-existant or not connected profile'))
|
||||
return ""
|
||||
to_jid = jid.JID(target)
|
||||
debug(_("Asking registration for [%s]") % to_jid.full())
|
||||
reg_request=IQ(xmlstream,'get')
|
||||
reg_request["from"]=current_jid.full()
|
||||
reg_request["to"] = to_jid.full()
|
||||
query=reg_request.addElement('query', NS_REG)
|
||||
reg_request.send(to_jid.full()).addCallbacks(self.reg_ok, self.reg_err)
|
||||
return reg_request["id"]
|
||||
208
src/plugins/plugin_xep_0096.py
Executable file
208
src/plugins/plugin_xep_0096.py
Executable file
|
|
@ -0,0 +1,208 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing xep-0096
|
||||
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 logging import debug, info, error
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import protocol
|
||||
from twisted.words.protocols.jabber import client, jid
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
import os.path
|
||||
from twisted.internet import reactor #FIXME best way ???
|
||||
import pdb
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
except ImportError:
|
||||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
from wokkel import disco, iwokkel
|
||||
|
||||
IQ_SET = '/iq[@type="set"]'
|
||||
NS_SI = 'http://jabber.org/protocol/si'
|
||||
SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0096 Plugin",
|
||||
"import_name": "XEP_0096",
|
||||
"type": "XEP",
|
||||
"protocols": ["XEP-0096"],
|
||||
"dependencies": ["XEP_0065"],
|
||||
"main": "XEP_0096",
|
||||
"handler": "yes",
|
||||
"description": _("""Implementation of SI File Transfert""")
|
||||
}
|
||||
|
||||
class XEP_0096():
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Plugin XEP_0096 initialization"))
|
||||
self.host = host
|
||||
self._waiting_for_approval = {}
|
||||
host.bridge.addMethod("sendFile", ".communication", in_sign='sss', out_sign='s', method=self.sendFile)
|
||||
|
||||
def getHandler(self, profile):
|
||||
return XEP_0096_handler(self)
|
||||
|
||||
def xep_96(self, IQ, profile):
|
||||
info (_("XEP-0096 management"))
|
||||
IQ.handled=True
|
||||
SI_elem = IQ.firstChildElement()
|
||||
debug(SI_elem.toXml())
|
||||
filename = ""
|
||||
file_size = ""
|
||||
for element in SI_elem.elements():
|
||||
if element.name == "file":
|
||||
info (_("File proposed: name=[%(name)s] size=%(size)s") % {'name':element['name'], 'size':element['size']})
|
||||
filename = element["name"]
|
||||
file_size = element["size"]
|
||||
elif element.name == "feature":
|
||||
from_jid = IQ["from"]
|
||||
self._waiting_for_approval[IQ["id"]] = (element, from_jid, file_size, profile)
|
||||
data={ "filename":filename, "from":from_jid, "size":file_size }
|
||||
self.host.askConfirmation(IQ["id"], "FILE_TRANSFERT", data, self.confirmationCB)
|
||||
|
||||
def confirmationCB(self, id, accepted, data):
|
||||
"""Called on confirmation answer"""
|
||||
if accepted:
|
||||
data['size'] = self._waiting_for_approval[id][2]
|
||||
self.host.plugins["XEP_0065"].setData(data, id)
|
||||
self.approved(id)
|
||||
else:
|
||||
debug (_("Transfert [%s] refused"), id)
|
||||
del(self._waiting_for_approval[id])
|
||||
|
||||
def approved(self, id):
|
||||
"""must be called when a file transfert has be accepted by client"""
|
||||
debug (_("Transfert [%s] accepted"), id)
|
||||
|
||||
if ( not self._waiting_for_approval.has_key(id) ):
|
||||
error (_("Approved unknow id !"))
|
||||
#TODO: manage this (maybe approved by several frontends)
|
||||
else:
|
||||
element, from_id, size, profile = self._waiting_for_approval[id]
|
||||
del(self._waiting_for_approval[id])
|
||||
self.negociate(element, id, from_id, profile)
|
||||
|
||||
def negociate(self, feat_elem, id, to_jid, profile):
|
||||
#TODO: put this in a plugin
|
||||
#FIXME: over ultra mega ugly, need to be generic
|
||||
client = self.host.getClient(profile)
|
||||
assert(client)
|
||||
info (_("Feature negociation"))
|
||||
data = feat_elem.firstChildElement()
|
||||
field = data.firstChildElement()
|
||||
#FIXME: several options ! Q&D code for test only
|
||||
option = field.firstChildElement()
|
||||
value = option.firstChildElement()
|
||||
if unicode(value) == "http://jabber.org/protocol/bytestreams":
|
||||
#ugly, as usual, need to be entirely rewritten (just for test !)
|
||||
result = domish.Element(('', 'iq'))
|
||||
result['type'] = 'result'
|
||||
result['id'] = id
|
||||
result['to'] = to_jid
|
||||
si = result.addElement('si', 'http://jabber.org/protocol/si')
|
||||
file = si.addElement('file', 'http://jabber.org/protocol/si/profile/file-transfer')
|
||||
feature = si.addElement('feature', 'http://jabber.org/protocol/feature-neg')
|
||||
x = feature.addElement('x', 'jabber:x:data')
|
||||
x['type'] = 'submit'
|
||||
field = x.addElement('field')
|
||||
field['var'] = 'stream-method'
|
||||
value = field.addElement('value')
|
||||
value.addContent('http://jabber.org/protocol/bytestreams')
|
||||
client.xmlstream.send(result)
|
||||
|
||||
def fileCB(self, answer, xmlstream, current_jid):
|
||||
if answer['type']=="result": #FIXME FIXME FIXME ugly ugly ugly ! and temp FIXME FIXME FIXME
|
||||
info("SENDING UGLY ANSWER")
|
||||
offer=client.IQ(xmlstream,'set')
|
||||
offer["from"]=current_jid.full()
|
||||
offer["to"]=answer['from']
|
||||
query=offer.addElement('query', 'http://jabber.org/protocol/bytestreams')
|
||||
query['mode']='tcp'
|
||||
streamhost=query.addElement('streamhost')
|
||||
streamhost['host']=self.host.memory.getParamA("IP", "File Transfert")
|
||||
streamhost['port']=self.host.memory.getParamA("Port", "File Transfert")
|
||||
streamhost['jid']=current_jid.full()
|
||||
offer.send()
|
||||
|
||||
def sendFile(self, to, filepath, profile_key='@DEFAULT@'):
|
||||
"""send a file using XEP-0096
|
||||
Return an unique id to identify the transfert
|
||||
"""
|
||||
current_jid, xmlstream = self.host.getJidNStream(profile_key)
|
||||
if not xmlstream:
|
||||
error (_('Asking for an non-existant or not connected profile'))
|
||||
return ""
|
||||
debug ("sendfile (%s) to %s", filepath, to )
|
||||
print type(filepath), type(to)
|
||||
|
||||
statinfo = os.stat(filepath)
|
||||
|
||||
offer=client.IQ(xmlstream,'set')
|
||||
debug ("Transfert ID: %s", offer["id"])
|
||||
|
||||
self.host.plugins["XEP_0065"].sendFile(offer["id"], filepath, str(statinfo.st_size))
|
||||
|
||||
offer["from"]=current_jid.full()
|
||||
offer["to"]=jid.JID(to).full()
|
||||
si=offer.addElement('si','http://jabber.org/protocol/si')
|
||||
si["mime-type"]='text/plain'
|
||||
si["profile"]='http://jabber.org/protocol/si/profile/file-transfer'
|
||||
file = si.addElement('file', 'http://jabber.org/protocol/si/profile/file-transfer')
|
||||
file['name']=os.path.basename(filepath)
|
||||
file['size']=str(statinfo.st_size)
|
||||
|
||||
###
|
||||
# FIXME: Ugly temporary hard coded implementation of XEP-0020 & XEP-0004,
|
||||
# Need to be recoded elsewhere in a more generic way
|
||||
###
|
||||
|
||||
feature=si.addElement('feature', "http://jabber.org/protocol/feature-neg")
|
||||
x=feature.addElement('x', "jabber:x:data")
|
||||
x['type']='form'
|
||||
field=x.addElement('field')
|
||||
field['type']='list-single'
|
||||
field['var']='stream-method'
|
||||
option = field.addElement('option')
|
||||
value = option.addElement('value', content='http://jabber.org/protocol/bytestreams')
|
||||
|
||||
offer.addCallback(self.fileCB, current_jid = current_jid, xmlstream = xmlstream)
|
||||
offer.send()
|
||||
return offer["id"] #XXX: using IQ id as file transfert id seems OK as IQ id are required
|
||||
|
||||
class XEP_0096_handler(XMPPHandler):
|
||||
implements(iwokkel.IDisco)
|
||||
|
||||
def __init__(self, plugin_parent):
|
||||
self.plugin_parent = plugin_parent
|
||||
self.host = plugin_parent.host
|
||||
|
||||
def connectionInitialized(self):
|
||||
self.xmlstream.addObserver(SI_REQUEST, self.plugin_parent.xep_96, profile = self.parent.profile)
|
||||
|
||||
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
|
||||
return [disco.DiscoFeature(NS_SI)]
|
||||
|
||||
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
||||
return []
|
||||
|
||||
127
src/plugins/plugin_xep_0100.py
Executable file
127
src/plugins/plugin_xep_0100.py
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing gateways (xep-0100)
|
||||
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 logging import debug, info, error
|
||||
from twisted.internet import protocol
|
||||
from twisted.words.protocols.jabber import client, jid
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
import twisted.internet.error
|
||||
import pdb
|
||||
|
||||
from wokkel import disco, iwokkel
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "Gateways Plugin",
|
||||
"import_name": "XEP_0100",
|
||||
"type": "XEP",
|
||||
"protocols": ["XEP-0100"],
|
||||
"dependencies": ["XEP_0077"],
|
||||
"main": "XEP_0100",
|
||||
"description": _("""Implementation of Gateways protocol""")
|
||||
}
|
||||
|
||||
class XEP_0100():
|
||||
|
||||
def __init__(self, host):
|
||||
info(_("Gateways plugin initialization"))
|
||||
self.host = host
|
||||
self.__gateways = {} #dict used to construct the answer to findGateways. Key = target jid
|
||||
host.bridge.addMethod("findGateways", ".communication", in_sign='ss', out_sign='s', method=self.findGateways)
|
||||
host.bridge.addMethod("gatewayRegister", ".request", in_sign='ssa(ss)s', out_sign='s', method=self.gatewayRegister)
|
||||
|
||||
def __inc_handled_items(self, request_id):
|
||||
self.__gateways[request_id]['__handled_items']+=1
|
||||
|
||||
if self.__gateways[request_id]['__total_items'] == self.__gateways[request_id]['__handled_items']:
|
||||
debug (_("All items checked for id [%s]") % str(request_id))
|
||||
|
||||
del self.__gateways[request_id]['__total_items']
|
||||
del self.__gateways[request_id]['__handled_items']
|
||||
self.host.actionResultExt(request_id,"DICT_DICT",self.__gateways[request_id])
|
||||
|
||||
def discoInfo(self, disco, entity, request_id):
|
||||
"""Find disco infos about entity, to check if it is a gateway"""
|
||||
|
||||
for identity in disco.identities:
|
||||
if identity[0] == 'gateway':
|
||||
print (_("Found gateway (%(jid)s): %(identity)s") % {'jid':entity.full(), 'identity':disco.identities[identity]})
|
||||
self.__gateways[request_id][entity.full()] = {
|
||||
'name':disco.identities[identity],
|
||||
'type':identity[1]
|
||||
}
|
||||
|
||||
self.__inc_handled_items(request_id)
|
||||
|
||||
def discoInfoErr(self, failure, entity, request_id):
|
||||
"""Something is going wrong with disco"""
|
||||
failure.trap(jab_error.StanzaError,twisted.internet.error.ConnectionLost)
|
||||
error(_("Error when discovering [%(jid)s]: %(error)s") % {'jid':entity.full(), 'error':failure.getErrorMessage()})
|
||||
self.__inc_handled_items(request_id)
|
||||
|
||||
|
||||
def discoItems(self, disco, request_id, target, client):
|
||||
"""Look for items with disco protocol, and ask infos for each one"""
|
||||
#FIXME: target is used as we can't find the original iq node (parent is None)
|
||||
# an other way would avoid this useless parameter (is there a way with wokkel ?)
|
||||
if len(disco._items) == 0:
|
||||
debug (_("No gateway found"))
|
||||
self.host.actionResultExt(request_id,"DICT_DICT",{})
|
||||
return
|
||||
|
||||
self.__gateways[request_id] = {'__total_items':len(disco._items), '__handled_items':0, '__private__':{'target':target.full()}}
|
||||
for item in disco._items:
|
||||
#TODO: need to set a timeout for theses requests
|
||||
debug (_("item found: %s"), item.name)
|
||||
client.disco.requestInfo(item.entity).addCallback(self.discoInfo, entity=item.entity, request_id=request_id).addErrback(self.discoInfoErr, entity=item.entity, request_id=request_id)
|
||||
|
||||
def discoItemsErr(self, failure, request_id, target, client):
|
||||
"""Something is going wrong with disco"""
|
||||
error(_("Error when discovering [%(target)s]: %(condition)s") % {'target':target.full(), 'condition':unicode(failure.value)})
|
||||
message_data={"reason": "connection error", "message":_(u"Error while trying to discover %(target)s gateways: %(error_mess)s") % {'target':target.full(), 'error_mess':unicode(failure.value)}}
|
||||
self.host.bridge.actionResult("ERROR", request_id, message_data)
|
||||
|
||||
|
||||
def registrationSuccessful(self, target, profile):
|
||||
"""Called when in_band registration is ok, we must now follow the rest of procedure"""
|
||||
debug (_("Registration successful, doing the rest"))
|
||||
self.host.addContact(target, profile)
|
||||
self.host.setPresence(target, profile)
|
||||
|
||||
def gatewayRegister(self, action, target, fields, profile_key='@DEFAULT@'):
|
||||
"""Register gateway using in-band registration, then log-in to gateway"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
assert(profile) #FIXME: return an error here
|
||||
if action == 'SUBMIT':
|
||||
self.host.plugins["XEP_0077"].addTrigger(target, self.registrationSuccessful, profile)
|
||||
return self.host.plugins["XEP_0077"].in_band_submit(action, target, fields, profile)
|
||||
|
||||
def findGateways(self, target, profile_key='@DEFAULT@'):
|
||||
"""Find gateways in the target JID, using discovery protocol
|
||||
Return an id used for retrieving the list of gateways
|
||||
"""
|
||||
profile = self.host.memory.getProfileName(profile_key)
|
||||
client = self.host.getClient(profile_key)
|
||||
assert(client)
|
||||
to_jid = jid.JID(target)
|
||||
debug (_("find gateways (target = %(target)s, profile = %(profile)s)") % {'target':to_jid.full(), 'profile':profile})
|
||||
request_id = self.host.get_next_id()
|
||||
client.disco.requestItems(to_jid).addCallback(self.discoItems, request_id=request_id, target = to_jid, client = client).addErrback(self.discoItemsErr, request_id=request_id, target = to_jid, client = client)
|
||||
return request_id
|
||||
3
src/sat.sh
Executable file
3
src/sat.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
twistd -noy sat.tac
|
||||
|
||||
796
src/sat.tac
Normal file
796
src/sat.tac
Normal file
|
|
@ -0,0 +1,796 @@
|
|||
#!/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/>.
|
||||
"""
|
||||
|
||||
CONST = {
|
||||
'client_name' : u'SàT (Salut à toi)',
|
||||
'client_version' : u'0.0.3D', #Please add 'D' at the end for dev versions
|
||||
'local_dir' : '~/.sat'
|
||||
}
|
||||
|
||||
import gettext
|
||||
gettext.install('sat', "i18n", unicode=True)
|
||||
|
||||
from twisted.application import internet, service
|
||||
from twisted.internet import glib2reactor, protocol, task
|
||||
glib2reactor.install()
|
||||
|
||||
from twisted.words.protocols.jabber import jid, xmlstream
|
||||
from twisted.words.protocols.jabber import error as jab_error
|
||||
from twisted.words.xish import domish
|
||||
|
||||
from twisted.internet import reactor
|
||||
import pdb
|
||||
|
||||
from wokkel import client, disco, xmppim, generic, compat
|
||||
|
||||
from sat.bridge.DBus import DBusBridge
|
||||
import logging
|
||||
from logging import debug, info, error
|
||||
|
||||
import signal, sys
|
||||
import os.path
|
||||
|
||||
from sat.tools.memory import Memory
|
||||
from sat.tools.xml_tools import tupleList2dataForm
|
||||
from glob import glob
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
except ImportError:
|
||||
from wokkel.subprotocols import XMPPHandler
|
||||
|
||||
|
||||
### logging configuration FIXME: put this elsewhere ###
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(message)s')
|
||||
###
|
||||
|
||||
|
||||
sat_id = 0
|
||||
|
||||
def sat_next_id():
|
||||
global sat_id
|
||||
sat_id+=1
|
||||
return "sat_id_"+str(sat_id)
|
||||
|
||||
class SatXMPPClient(client.XMPPClient):
|
||||
|
||||
def __init__(self, host_app, profile, user_jid, password, host=None, port=5222):
|
||||
client.XMPPClient.__init__(self, user_jid, password, host, port)
|
||||
self.factory.clientConnectionLost = self.connectionLost
|
||||
self.__connected=False
|
||||
self.profile = profile
|
||||
self.host_app = host_app
|
||||
|
||||
def _authd(self, xmlstream):
|
||||
print "SatXMPPClient"
|
||||
client.XMPPClient._authd(self, xmlstream)
|
||||
self.__connected=True
|
||||
info (_("********** [%s] CONNECTED **********") % self.profile)
|
||||
self.streamInitialized()
|
||||
self.host_app.bridge.connected(self.profile) #we send the signal to the clients
|
||||
|
||||
def streamInitialized(self):
|
||||
"""Called after _authd"""
|
||||
debug (_("XML stream is initialized"))
|
||||
self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") #Needed to avoid disconnection (specially with openfire)
|
||||
self.keep_alife.start(180)
|
||||
|
||||
self.disco = SatDiscoProtocol(self)
|
||||
self.disco.setHandlerParent(self)
|
||||
self.discoHandler = disco.DiscoHandler()
|
||||
self.discoHandler.setHandlerParent(self)
|
||||
|
||||
self.roster.requestRoster()
|
||||
|
||||
self.presence.available()
|
||||
|
||||
self.disco.requestInfo(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDisco) #FIXME: use these informations
|
||||
|
||||
def isConnected(self):
|
||||
return self.__connected
|
||||
|
||||
def connectionLost(self, connector, unused_reason):
|
||||
self.__connected=False
|
||||
info (_("********** [%s] DISCONNECTED **********") % self.profile)
|
||||
try:
|
||||
self.keep_alife.stop()
|
||||
except AttributeError:
|
||||
debug (_("No keep_alife"))
|
||||
self.host_app.bridge.disconnected(self.profile) #we send the signal to the clients
|
||||
|
||||
|
||||
class SatMessageProtocol(xmppim.MessageProtocol):
|
||||
|
||||
def __init__(self, host):
|
||||
xmppim.MessageProtocol.__init__(self)
|
||||
self.host = host
|
||||
|
||||
def onMessage(self, message):
|
||||
debug (_(u"got message from: %s"), message["from"])
|
||||
for e in message.elements():
|
||||
if e.name == "body":
|
||||
type = message['type'] if message.hasAttribute('type') else 'chat' #FIXME: check specs
|
||||
self.host.bridge.newMessage(message["from"], e.children[0], type, profile=self.parent.profile)
|
||||
self.host.memory.addToHistory(self.parent.jid, jid.JID(message["from"]), self.parent.jid, "chat", e.children[0])
|
||||
break
|
||||
|
||||
class SatRosterProtocol(xmppim.RosterClientProtocol):
|
||||
|
||||
def __init__(self, host):
|
||||
xmppim.RosterClientProtocol.__init__(self)
|
||||
self.host = host
|
||||
|
||||
def rosterCb(self, roster):
|
||||
for raw_jid, item in roster.iteritems():
|
||||
self.onRosterSet(item)
|
||||
|
||||
def requestRoster(self):
|
||||
""" ask the server for Roster list """
|
||||
debug("requestRoster")
|
||||
self.getRoster().addCallback(self.rosterCb)
|
||||
|
||||
def removeItem(self, to):
|
||||
"""Remove a contact from roster list"""
|
||||
xmppim.RosterClientProtocol.removeItem(self, to)
|
||||
#TODO: check IQ result
|
||||
|
||||
#XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
|
||||
#def addItem(self, to):
|
||||
#"""Add a contact to roster list"""
|
||||
#xmppim.RosterClientProtocol.addItem(self, to)
|
||||
#TODO: check IQ result"""
|
||||
|
||||
def onRosterSet(self, item):
|
||||
"""Called when a new/update roster item is received"""
|
||||
#TODO: send a signal to frontends
|
||||
item_attr = {'to': str(item.subscriptionTo),
|
||||
'from': str(item.subscriptionFrom),
|
||||
'ask': str(item.ask)
|
||||
}
|
||||
if item.name:
|
||||
item_attr['name'] = item.name
|
||||
info (_("new contact in roster list: %s"), item.jid.full())
|
||||
self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile)
|
||||
self.host.bridge.newContact(item.jid.full(), item_attr, item.groups, self.parent.profile)
|
||||
|
||||
def onRosterRemove(self, entity):
|
||||
"""Called when a roster removal event is received"""
|
||||
#TODO: send a signal to frontends
|
||||
print _("removing %s from roster list") % entity.full()
|
||||
self.host.memory.delContact(entity, self.parent.profile)
|
||||
|
||||
class SatPresenceProtocol(xmppim.PresenceClientProtocol):
|
||||
|
||||
def __init__(self, host):
|
||||
xmppim.PresenceClientProtocol.__init__(self)
|
||||
self.host = host
|
||||
|
||||
def availableReceived(self, entity, show=None, statuses=None, priority=0):
|
||||
debug (_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity':entity, 'show':show, 'statuses':statuses, 'priority':priority})
|
||||
|
||||
if statuses.has_key(None): #we only want string keys
|
||||
statuses["default"] = statuses[None]
|
||||
del statuses[None]
|
||||
|
||||
self.host.memory.addPresenceStatus(entity, show or "",
|
||||
int(priority), statuses, self.parent.profile)
|
||||
|
||||
#now it's time to notify frontends
|
||||
self.host.bridge.presenceUpdate(entity.full(), show or "",
|
||||
int(priority), statuses, self.parent.profile)
|
||||
|
||||
def unavailableReceived(self, entity, statuses=None):
|
||||
debug (_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity':entity, 'statuses':statuses})
|
||||
if statuses and statuses.has_key(None): #we only want string keys
|
||||
statuses["default"] = statuses[None]
|
||||
del statuses[None]
|
||||
self.host.memory.addPresenceStatus(entity, "unavailable", 0, statuses, self.parent.profile)
|
||||
|
||||
#now it's time to notify frontends
|
||||
self.host.bridge.presenceUpdate(entity.full(), "unavailable", 0, statuses, self.parent.profile)
|
||||
|
||||
|
||||
def available(self, entity=None, show=None, statuses=None, priority=0):
|
||||
if statuses and statuses.has_key('default'):
|
||||
statuses[None] = statuses['default']
|
||||
del statuses['default']
|
||||
xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority)
|
||||
|
||||
def subscribedReceived(self, entity):
|
||||
debug (_("subscription approved for [%s]") % entity.userhost())
|
||||
self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
|
||||
self.host.bridge.subscribe('subscribed', entity.userhost(), self.parent.profile)
|
||||
|
||||
def unsubscribedReceived(self, entity):
|
||||
debug (_("unsubscription confirmed for [%s]") % entity.userhost())
|
||||
self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
|
||||
self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile)
|
||||
|
||||
def subscribeReceived(self, entity):
|
||||
debug (_("subscription request for [%s]") % entity.userhost())
|
||||
self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile)
|
||||
self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile)
|
||||
|
||||
def unsubscribeReceived(self, entity):
|
||||
debug (_("unsubscription asked for [%s]") % entity.userhost())
|
||||
self.host.memory.addWaitingSub('unsubscribe', entity.userhost(), self.parent.profile)
|
||||
self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile)
|
||||
|
||||
class SatDiscoProtocol(disco.DiscoClientProtocol):
|
||||
def __init__(self, host):
|
||||
disco.DiscoClientProtocol.__init__(self)
|
||||
|
||||
class SatFallbackHandler(generic.FallbackHandler):
|
||||
def __init__(self, host):
|
||||
generic.FallbackHandler.__init__(self)
|
||||
|
||||
def iqFallback(self, iq):
|
||||
debug (u"iqFallback: xml = [%s], handled=%s" % (iq.toXml(), "True" if iq.handled else "False"))
|
||||
generic.FallbackHandler.iqFallback(self, iq)
|
||||
|
||||
class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
|
||||
|
||||
def __init__(self, host, jabber_host, user_login, user_pass, answer_id):
|
||||
xmlstream.ConnectAuthenticator.__init__(self, jabber_host)
|
||||
self.host = host
|
||||
self.jabber_host = jabber_host
|
||||
self.user_login = user_login
|
||||
self.user_pass = user_pass
|
||||
self.answer_id = answer_id
|
||||
print _("Registration asked for"),user_login, user_pass, jabber_host
|
||||
|
||||
def connectionMade(self):
|
||||
print "connectionMade"
|
||||
|
||||
self.xmlstream.namespace = "jabber:client"
|
||||
self.xmlstream.sendHeader()
|
||||
|
||||
iq = compat.IQ(self.xmlstream, 'set')
|
||||
iq["to"] = self.jabber_host
|
||||
query = iq.addElement(('jabber:iq:register', 'query'))
|
||||
_user = query.addElement('username')
|
||||
_user.addContent(self.user_login)
|
||||
_pass = query.addElement('password')
|
||||
_pass.addContent(self.user_pass)
|
||||
reg = iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure)
|
||||
|
||||
def registrationAnswer(self, answer):
|
||||
debug (_("registration answer: %s") % answer.toXml())
|
||||
answer_type = "SUCCESS"
|
||||
answer_data={"message":_("Registration successfull")}
|
||||
self.host.bridge.actionResult(answer_type, self.answer_id, answer_data)
|
||||
self.xmlstream.sendFooter()
|
||||
|
||||
def registrationFailure(self, failure):
|
||||
info (_("Registration failure: %s") % str(failure.value))
|
||||
answer_type = "ERROR"
|
||||
answer_data = {}
|
||||
if failure.value.condition == 'conflict':
|
||||
answer_data['reason'] = 'conflict'
|
||||
answer_data={"message":_("Username already exists, please choose an other one")}
|
||||
else:
|
||||
answer_data['reason'] = 'unknown'
|
||||
answer_data={"message":_("Registration failed (%s)") % str(failure.value.condition)}
|
||||
self.host.bridge.actionResult(answer_type, self.answer_id, answer_data)
|
||||
self.xmlstream.sendFooter()
|
||||
|
||||
|
||||
class SAT(service.Service):
|
||||
|
||||
def get_next_id(self):
|
||||
return sat_next_id()
|
||||
|
||||
def get_const(self, name):
|
||||
"""Return a constant"""
|
||||
if not CONST.has_key(name):
|
||||
error(_('Trying to access an undefined constant'))
|
||||
raise Exception
|
||||
return CONST[name]
|
||||
|
||||
def set_const(self, name, value):
|
||||
"""Save a constant"""
|
||||
if CONST.has_key(name):
|
||||
error(_('Trying to redefine a constant'))
|
||||
raise Exception
|
||||
CONST[name] = value
|
||||
|
||||
def __init__(self):
|
||||
#TODO: standardize callback system
|
||||
|
||||
local_dir = os.path.expanduser(self.get_const('local_dir'))
|
||||
if not os.path.exists(local_dir):
|
||||
os.makedirs(local_dir)
|
||||
|
||||
self.__waiting_conf = {} #callback called when a confirmation is received
|
||||
self.__progress_cb_map = {} #callback called when a progress is requested (key = progress id)
|
||||
self.__general_cb_map = {} #callback called for general reasons (key = name)
|
||||
self.__private_data = {} #used for internal callbacks (key = id)
|
||||
self.profiles = {}
|
||||
self.plugins = {}
|
||||
self.menus = {} #used to know which new menus are wanted by plugins
|
||||
|
||||
self.memory=Memory(self)
|
||||
self.server_features=[] #XXX: temp dic, need to be transfered into self.memory in the future
|
||||
|
||||
self.bridge=DBusBridge()
|
||||
self.bridge.register("getVersion", lambda: self.get_const('client_version'))
|
||||
self.bridge.register("getProfileName", self.memory.getProfileName)
|
||||
self.bridge.register("getProfilesList", self.memory.getProfilesList)
|
||||
self.bridge.register("createProfile", self.memory.createProfile)
|
||||
self.bridge.register("deleteProfile", self.memory.deleteProfile)
|
||||
self.bridge.register("registerNewAccount", self.registerNewAccount)
|
||||
self.bridge.register("connect", self.connect)
|
||||
self.bridge.register("disconnect", self.disconnect)
|
||||
self.bridge.register("getContacts", self.memory.getContacts)
|
||||
self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus)
|
||||
self.bridge.register("getWaitingSub", self.memory.getWaitingSub)
|
||||
self.bridge.register("sendMessage", self.sendMessage)
|
||||
self.bridge.register("setParam", self.setParam)
|
||||
self.bridge.register("getParamA", self.memory.getParamA)
|
||||
self.bridge.register("getParamsUI", self.memory.getParamsUI)
|
||||
self.bridge.register("getParams", self.memory.getParams)
|
||||
self.bridge.register("getParamsForCategory", self.memory.getParamsForCategory)
|
||||
self.bridge.register("getParamsCategories", self.memory.getParamsCategories)
|
||||
self.bridge.register("getHistory", self.memory.getHistory)
|
||||
self.bridge.register("setPresence", self.setPresence)
|
||||
self.bridge.register("subscription", self.subscription)
|
||||
self.bridge.register("addContact", self.addContact)
|
||||
self.bridge.register("delContact", self.delContact)
|
||||
self.bridge.register("isConnected", self.isConnected)
|
||||
self.bridge.register("launchAction", self.launchAction)
|
||||
self.bridge.register("confirmationAnswer", self.confirmationAnswer)
|
||||
self.bridge.register("getProgress", self.getProgress)
|
||||
self.bridge.register("getMenus", self.getMenus)
|
||||
self.bridge.register("getMenuHelp", self.getMenuHelp)
|
||||
self.bridge.register("callMenu", self.callMenu)
|
||||
|
||||
self._import_plugins()
|
||||
|
||||
|
||||
def _import_plugins(self):
|
||||
"""Import all plugins found in plugins directory"""
|
||||
#TODO: manage dependencies
|
||||
plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob ("plugins/plugin*.py"))]
|
||||
|
||||
for plug in plug_lst:
|
||||
plug_path = 'plugins.'+plug
|
||||
__import__(plug_path)
|
||||
mod = sys.modules[plug_path]
|
||||
plug_info = mod.PLUGIN_INFO
|
||||
info (_("importing plugin: %s"), plug_info['name'])
|
||||
self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self)
|
||||
if plug_info.has_key('handler') and plug_info['handler'] == 'yes':
|
||||
self.plugins[plug_info['import_name']].is_handler = True
|
||||
else:
|
||||
self.plugins[plug_info['import_name']].is_handler = False
|
||||
#TODO: test xmppclient presence and register handler parent
|
||||
|
||||
def connect(self, profile_key = '@DEFAULT@'):
|
||||
"""Connect to jabber server"""
|
||||
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
if not profile_key:
|
||||
error (_('Trying to connect a non-exsitant profile'))
|
||||
return
|
||||
|
||||
if (self.isConnected(profile)):
|
||||
info(_("already connected !"))
|
||||
return
|
||||
current = self.profiles[profile] = SatXMPPClient(self, profile,
|
||||
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile_key), profile),
|
||||
self.memory.getParamA("Password", "Connection", profile_key = profile_key),
|
||||
self.memory.getParamA("Server", "Connection", profile_key = profile_key), 5222)
|
||||
|
||||
current.messageProt = SatMessageProtocol(self)
|
||||
current.messageProt.setHandlerParent(current)
|
||||
|
||||
current.roster = SatRosterProtocol(self)
|
||||
current.roster.setHandlerParent(current)
|
||||
|
||||
current.presence = SatPresenceProtocol(self)
|
||||
current.presence.setHandlerParent(current)
|
||||
|
||||
current.fallBack = SatFallbackHandler(self)
|
||||
current.fallBack.setHandlerParent(current)
|
||||
|
||||
current.versionHandler = generic.VersionHandler(self.get_const('client_name'),
|
||||
self.get_const('client_version'))
|
||||
current.versionHandler.setHandlerParent(current)
|
||||
|
||||
debug (_("setting plugins parents"))
|
||||
|
||||
for plugin in self.plugins.iteritems():
|
||||
if plugin[1].is_handler:
|
||||
plugin[1].getHandler(profile).setHandlerParent(current)
|
||||
|
||||
current.startService()
|
||||
|
||||
def disconnect(self, profile_key='@DEFAULT@'):
|
||||
"""disconnect from jabber server"""
|
||||
if (not self.isConnected(profile_key)):
|
||||
info(_("not connected !"))
|
||||
return
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
info(_("Disconnecting..."))
|
||||
self.profiles[profile].stopService()
|
||||
|
||||
def startService(self):
|
||||
info("Salut à toi ô mon frère !")
|
||||
#TODO: manage autoconnect
|
||||
#self.connect()
|
||||
|
||||
def stopService(self):
|
||||
self.memory.save()
|
||||
info("Salut aussi à Rantanplan")
|
||||
|
||||
def run(self):
|
||||
debug(_("running app"))
|
||||
reactor.run()
|
||||
|
||||
def stop(self):
|
||||
debug(_("stopping app"))
|
||||
reactor.stop()
|
||||
|
||||
## Misc methods ##
|
||||
|
||||
def getJidNStream(self, profile_key):
|
||||
"""Convenient method to get jid and stream from profile key
|
||||
@return: tuple (jid, xmlstream) from profile, can be None"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
if not profile or not self.profiles[profile].isConnected():
|
||||
return (None, None)
|
||||
return (self.profiles[profile].jid, self.profiles[profile].xmlstream)
|
||||
|
||||
def getClient(self, profile_key):
|
||||
"""Convenient method to get client from profile key
|
||||
@return: client or None if it doesn't exist"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
return None
|
||||
return self.profiles[profile]
|
||||
|
||||
def registerNewAccount(self, login, password, server, port = 5222, id = None):
|
||||
"""Connect to a server and create a new account using in-band registration"""
|
||||
|
||||
next_id = id or sat_next_id() #the id is used to send server's answer
|
||||
serverRegistrer = xmlstream.XmlStreamFactory(RegisteringAuthenticator(self, server, login, password, next_id))
|
||||
connector = reactor.connectTCP(server, port, serverRegistrer)
|
||||
serverRegistrer.clientConnectionLost = lambda conn, reason: connector.disconnect()
|
||||
|
||||
return next_id
|
||||
|
||||
def registerNewAccountCB(self, id, data, profile):
|
||||
user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0]
|
||||
password = self.memory.getParamA("Password", "Connection", profile_key=profile)
|
||||
server = self.memory.getParamA("Server", "Connection", profile_key=profile)
|
||||
|
||||
if not user or not password or not server:
|
||||
info (_('No user or server given'))
|
||||
#TODO: a proper error message must be sent to frontend
|
||||
self.actionResult(id, "ERROR", {'message':_("No user, password or server given, can't register new account.")})
|
||||
return
|
||||
|
||||
confirm_id = sat_next_id()
|
||||
self.__private_data[confirm_id]=(id,profile)
|
||||
|
||||
self.askConfirmation(confirm_id, "YES/NO",
|
||||
{"message":_("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user':user, 'server':server, 'profile':profile}},
|
||||
self.regisConfirmCB)
|
||||
print ("===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============")
|
||||
print "id=",id
|
||||
print "data=",data
|
||||
|
||||
def regisConfirmCB(self, id, accepted, data):
|
||||
print _("register Confirmation CB ! (%s)") % str(accepted)
|
||||
action_id,profile = self.__private_data[id]
|
||||
del self.__private_data[id]
|
||||
if accepted:
|
||||
user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0]
|
||||
password = self.memory.getParamA("Password", "Connection", profile_key=profile)
|
||||
server = self.memory.getParamA("Server", "Connection", profile_key=profile)
|
||||
self.registerNewAccount(user, password, server, id=action_id)
|
||||
else:
|
||||
self.actionResult(action_id, "SUPPRESS", {})
|
||||
|
||||
def submitForm(self, action, target, fields, profile_key='@DEFAULT@'):
|
||||
"""submit a form
|
||||
@param target: target jid where we are submitting
|
||||
@param fields: list of tuples (name, value)
|
||||
@return: tuple: (id, deferred)
|
||||
"""
|
||||
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
to_jid = jid.JID(target)
|
||||
|
||||
iq = compat.IQ(self.profiles[profile].xmlstream, 'set')
|
||||
iq["to"] = target
|
||||
iq["from"] = self.profiles[profile].jid.full()
|
||||
query = iq.addElement(('jabber:iq:register', 'query'))
|
||||
if action=='SUBMIT':
|
||||
form = tupleList2dataForm(fields)
|
||||
query.addChild(form.toElement())
|
||||
elif action=='CANCEL':
|
||||
query.addElement('remove')
|
||||
else:
|
||||
error (_("FIXME FIXME FIXME: Unmanaged action (%s) in submitForm") % action)
|
||||
raise NotImplementedError
|
||||
|
||||
deferred = iq.send(target)
|
||||
return (iq['id'], deferred)
|
||||
|
||||
## Client management ##
|
||||
|
||||
def setParam(self, name, value, category, profile_key='@DEFAULT@'):
|
||||
"""set wanted paramater and notice observers"""
|
||||
info (_("setting param: %(name)s=%(value)s in category %(category)s") % {'name':name, 'value':value, 'category':category})
|
||||
self.memory.setParam(name, value, category, profile_key)
|
||||
|
||||
def isConnected(self, profile_key='@DEFAULT@'):
|
||||
"""Return connection status of profile
|
||||
@param profile_key: key_word or profile name to determine profile name
|
||||
@return True if connected
|
||||
"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_('asking connection status for a non-existant profile'))
|
||||
return
|
||||
if not self.profiles.has_key(profile):
|
||||
return False
|
||||
return self.profiles[profile].isConnected()
|
||||
|
||||
def launchAction(self, type, data, profile_key='@DEFAULT@'):
|
||||
"""Launch a specific action asked by client
|
||||
@param type: action type (button)
|
||||
@param data: needed data to launch the action
|
||||
|
||||
@return: action id for result, or empty string in case or error
|
||||
"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_('trying to launch action with a non-existant profile'))
|
||||
raise Exception #TODO: raise a proper exception
|
||||
if type=="button":
|
||||
try:
|
||||
cb_name = data['callback_id']
|
||||
except KeyError:
|
||||
error (_("Incomplete data"))
|
||||
return ""
|
||||
id = sat_next_id()
|
||||
self.callGeneralCB(cb_name, id, data, profile = profile)
|
||||
return id
|
||||
else:
|
||||
error (_("Unknown action type"))
|
||||
return ""
|
||||
|
||||
|
||||
## jabber methods ##
|
||||
|
||||
def sendMessage(self,to,msg,type='chat', profile_key='@DEFAULT@'):
|
||||
#FIXME: check validity of recipient
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
current_jid = self.profiles[profile].jid
|
||||
debug(_("Sending jabber message to %s..."), to)
|
||||
message = domish.Element(('jabber:client','message'))
|
||||
message["to"] = jid.JID(to).full()
|
||||
message["from"] = current_jid.full()
|
||||
message["type"] = type
|
||||
message.addElement("body", "jabber:client", msg)
|
||||
self.profiles[profile].xmlstream.send(message)
|
||||
self.memory.addToHistory(current_jid, current_jid, jid.JID(to), message["type"], unicode(msg))
|
||||
if type!="groupchat":
|
||||
self.bridge.newMessage(message['from'], unicode(msg), to=message['to'], type=type, profile=profile) #We send back the message, so all clients are aware of it
|
||||
|
||||
|
||||
def setPresence(self, to="", show="", priority = 0, statuses={}, profile_key='@DEFAULT@'):
|
||||
"""Send our presence information"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
to_jid = jid.JID(to) if to else None
|
||||
self.profiles[profile].presence.available(to_jid, show, statuses, priority)
|
||||
|
||||
def subscription(self, subs_type, raw_jid, profile_key='@DEFAULT@'):
|
||||
"""Called to manage subscription
|
||||
@param subs_type: subsciption type (cf RFC 3921)
|
||||
@param raw_jid: unicode entity's jid
|
||||
@param profile_key: profile"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
to_jid = jid.JID(raw_jid)
|
||||
debug (_('subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type':subs_type, 'jid':to_jid.full()})
|
||||
if subs_type=="subscribe":
|
||||
self.profiles[profile].presence.subscribe(to_jid)
|
||||
elif subs_type=="subscribed":
|
||||
self.profiles[profile].presence.subscribed(to_jid)
|
||||
contact = self.memory.getContact(to_jid)
|
||||
if not contact or not bool(contact['to']): #we automatically subscribe to 'to' presence
|
||||
debug(_('sending automatic "to" subscription request'))
|
||||
self.subscription('subscribe', to_jid.userhost())
|
||||
elif subs_type=="unsubscribe":
|
||||
self.profiles[profile].presence.unsubscribe(to_jid)
|
||||
elif subs_type=="unsubscribed":
|
||||
self.profiles[profile].presence.unsubscribed(to_jid)
|
||||
|
||||
|
||||
def addContact(self, to, profile_key='@DEFAULT@'):
|
||||
"""Add a contact in roster list"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
to_jid=jid.JID(to)
|
||||
#self.profiles[profile].roster.addItem(to_jid) XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
|
||||
self.profiles[profile].presence.subscribe(to_jid)
|
||||
|
||||
def delContact(self, to, profile_key='@DEFAULT@'):
|
||||
"""Remove contact from roster list"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
to_jid=jid.JID(to)
|
||||
self.profiles[profile].roster.removeItem(to_jid)
|
||||
self.profiles[profile].presence.unsubscribe(to_jid)
|
||||
self.bridge.contactDeleted(to, profile)
|
||||
|
||||
|
||||
## callbacks ##
|
||||
|
||||
def serverDisco(self, disco):
|
||||
"""xep-0030 Discovery Protocol."""
|
||||
for feature in disco.features:
|
||||
debug (_("Feature found: %s"),feature)
|
||||
self.server_features.append(feature)
|
||||
for cat, type in disco.identities:
|
||||
debug (_("Identity found: [%(category)s/%(type)s] %(identity)s") % {'category':cat, 'type':type, 'identity':disco.identities[(cat,type)]})
|
||||
|
||||
|
||||
## Generic HMI ##
|
||||
|
||||
def actionResult(self, id, type, data):
|
||||
"""Send the result of an action
|
||||
@param id: same id used with action
|
||||
@param type: result type ("PARAM", "SUCCESS", "ERROR", "XMLUI")
|
||||
@param data: dictionary
|
||||
"""
|
||||
self.bridge.actionResult(type, id, data)
|
||||
|
||||
def actionResultExt(self, id, type, data):
|
||||
"""Send the result of an action, extended version
|
||||
@param id: same id used with action
|
||||
@param type: result type /!\ only "DICT_DICT" for this method
|
||||
@param data: dictionary of dictionaries
|
||||
"""
|
||||
if type != "DICT_DICT":
|
||||
error(_("type for actionResultExt must be DICT_DICT, fixing it"))
|
||||
type = "DICT_DICT"
|
||||
self.bridge.actionResultExt(type, id, data)
|
||||
|
||||
|
||||
|
||||
def askConfirmation(self, id, type, data, cb):
|
||||
"""Add a confirmation callback
|
||||
@param id: id used to get answer
|
||||
@param type: confirmation type ("YES/NO", "FILE_TRANSFERT")
|
||||
@param data: data (depend of confirmation type)
|
||||
@param cb: callback called with the answer
|
||||
"""
|
||||
if self.__waiting_conf.has_key(id):
|
||||
error (_("Attempt to register two callbacks for the same confirmation"))
|
||||
else:
|
||||
self.__waiting_conf[id] = cb
|
||||
self.bridge.askConfirmation(type, id, data)
|
||||
|
||||
|
||||
def confirmationAnswer(self, id, accepted, data):
|
||||
"""Called by frontends to answer confirmation requests"""
|
||||
debug (_("Received confirmation answer for id [%(id)s]: %(success)s") % {'id': id, 'success':_("accepted") if accepted else _("refused")})
|
||||
if not self.__waiting_conf.has_key(id):
|
||||
error (_("Received an unknown confirmation"))
|
||||
else:
|
||||
cb = self.__waiting_conf[id]
|
||||
del self.__waiting_conf[id]
|
||||
cb(id, accepted, data)
|
||||
|
||||
def registerProgressCB(self, id, CB):
|
||||
"""Register a callback called when progress is requested for id"""
|
||||
self.__progress_cb_map[id] = CB
|
||||
|
||||
def removeProgressCB(self, id):
|
||||
"""Remove a progress callback"""
|
||||
if not self.__progress_cb_map.has_key(id):
|
||||
error (_("Trying to remove an unknow progress callback"))
|
||||
else:
|
||||
del self.__progress_cb_map[id]
|
||||
|
||||
def getProgress(self, id):
|
||||
"""Return a dict with progress information
|
||||
data['position'] : current possition
|
||||
data['size'] : end_position
|
||||
"""
|
||||
data = {}
|
||||
try:
|
||||
self.__progress_cb_map[id](data)
|
||||
except KeyError:
|
||||
pass
|
||||
#debug("Requested progress for unknown id")
|
||||
return data
|
||||
|
||||
def registerGeneralCB(self, name, CB):
|
||||
"""Register a callback called for general reason"""
|
||||
self.__general_cb_map[name] = CB
|
||||
|
||||
def removeGeneralCB(self, name):
|
||||
"""Remove a general callback"""
|
||||
if not self.__general_cb_map.has_key(name):
|
||||
error (_("Trying to remove an unknow general callback"))
|
||||
else:
|
||||
del self.__general_cb_map[name]
|
||||
|
||||
def callGeneralCB(self, name, *args, **kwargs):
|
||||
"""Call general function back"""
|
||||
try:
|
||||
return self.__general_cb_map[name](*args, **kwargs)
|
||||
except KeyError:
|
||||
error(_("Trying to call unknown function (%s)") % name)
|
||||
return None
|
||||
|
||||
#Menus management
|
||||
|
||||
def importMenu(self, category, name, callback, help_string = "", type = "NORMAL"):
|
||||
"""register a new menu for frontends
|
||||
@param category: category of the menu
|
||||
@param name: menu item entry
|
||||
@param callback: method to be called when menuitem is selected"""
|
||||
if self.menus.has_key((category,name)):
|
||||
error ("Want to register a menu which already existe")
|
||||
return
|
||||
self.menus[(category,name,type)] = {'callback':callback, 'help_string':help_string, 'type':type}
|
||||
|
||||
def getMenus(self):
|
||||
"""Return all menus registered"""
|
||||
return self.menus.keys()
|
||||
|
||||
def getMenuHelp(self, category, name, type="NORMAL"):
|
||||
"""return the help string of the menu"""
|
||||
try:
|
||||
return self.menus[(category,name,type)]['help_string']
|
||||
except KeyError:
|
||||
error (_("Trying to access an unknown menu"))
|
||||
return ""
|
||||
|
||||
def callMenu(self, category, name, type="NORMAL", profile_key='@DEFAULT@'):
|
||||
"""return the help string of the menu"""
|
||||
profile = self.memory.getProfileName(profile_key)
|
||||
if not profile_key:
|
||||
error (_('Non-exsitant profile'))
|
||||
return ""
|
||||
if self.menus.has_key((category,name,type)):
|
||||
id = self.get_next_id()
|
||||
self.menus[(category,name,type)]['callback'](id, profile)
|
||||
return id
|
||||
else:
|
||||
error (_("Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)")%{'category':category, 'name':name,'type':type})
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
application = service.Application('SàT')
|
||||
service = SAT()
|
||||
service.setServiceParent(application)
|
||||
0
src/sat/__init__.py
Normal file
0
src/sat/__init__.py
Normal file
401
src/sat/bridge/DBus.py
Executable file
401
src/sat/bridge/DBus.py
Executable file
|
|
@ -0,0 +1,401 @@
|
|||
#!/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 bridge import Bridge
|
||||
import dbus
|
||||
import dbus.service
|
||||
import dbus.mainloop.glib
|
||||
import pdb
|
||||
from logging import debug, info, error
|
||||
|
||||
const_INT_PREFIX = "org.goffi.SAT" #Interface prefix
|
||||
const_COMM_SUFFIX = ".communication"
|
||||
const_REQ_SUFFIX = ".request"
|
||||
|
||||
class DbusObject(dbus.service.Object):
|
||||
|
||||
def __init__(self, bus, path):
|
||||
dbus.service.Object.__init__(self, bus, path)
|
||||
debug("Init DbusObject...")
|
||||
self.cb={}
|
||||
|
||||
def register(self, name, cb):
|
||||
self.cb[name]=cb
|
||||
|
||||
### signals ###
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='s')
|
||||
def connected(self, profile):
|
||||
debug("Connected signal")
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='s')
|
||||
def disconnected(self, profile):
|
||||
debug("Disconnected signal")
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='sa{ss}ass')
|
||||
def newContact(self, contact, attributes, groups, profile):
|
||||
debug("new contact signal (%s) sended (profile: %s)", contact, profile)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='sssss')
|
||||
def newMessage(self, from_jid, msg, type, to, profile):
|
||||
debug("new message signal (from:%s msg:%s type:%s to:%s) sended", from_jid, msg, type, to)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='ssss')
|
||||
def newAlert(self, msg, title, type, profile):
|
||||
debug("new alert signal (title:%s type:%s msg:%s profile:%s) sended", type, title, msg, profile)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='ssia{ss}s')
|
||||
def presenceUpdate(self, entity, show, priority, statuses, profile):
|
||||
debug("presence update signal (from:%s show:%s priority:%d statuses:%s profile:%s) sended" , entity, show, priority, statuses, profile)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='sss')
|
||||
def subscribe(self, type, entity, profile):
|
||||
debug("subscribe (type: [%s] from:[%s] profile:[%s])" , type, entity, profile)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='ssss')
|
||||
def paramUpdate(self, name, value, category, profile):
|
||||
debug("param update signal: %s=%s in category %s (profile: %s)", name, value, category, profile)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
signature='ss')
|
||||
def contactDeleted(self, entity, profile):
|
||||
debug("contact deleted signal: %s (profile: %s)", entity, profile)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
signature='ssa{ss}')
|
||||
def askConfirmation(self, type, id, data):
|
||||
debug("asking for confirmation: id = [%s] type = %s data = %s", id, type, data)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
signature='ssa{ss}')
|
||||
def actionResult(self, type, id, data):
|
||||
debug("result of action: id = [%s] type = %s data = %s", id, type, data)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
signature='ssa{sa{ss}}')
|
||||
def actionResultExt(self, type, id, data):
|
||||
debug("extended result of action: id = [%s] type = %s data = %s", id, type, data)
|
||||
|
||||
@dbus.service.signal(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
signature='sa{ss}')
|
||||
def updatedValue(self, name, value):
|
||||
debug("updated value: %s = %s", name, value)
|
||||
|
||||
### methods ###
|
||||
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='', out_signature='s')
|
||||
def getVersion(self):
|
||||
return self.cb["getVersion"]()
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='s', out_signature='s')
|
||||
def getProfileName(self, profile_key):
|
||||
return self.cb["getProfileName"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='', out_signature='as')
|
||||
def getProfilesList(self):
|
||||
info ('Profile list asked')
|
||||
return self.cb["getProfilesList"]()
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='s', out_signature='i')
|
||||
def createProfile(self, name):
|
||||
info ('Profile creation asked')
|
||||
return self.cb["createProfile"](unicode(name))
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='s', out_signature='i')
|
||||
def deleteProfile(self, name):
|
||||
info ('Profile deletion asked')
|
||||
return self.cb["deleteProfile"](str(name))
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='sssi', out_signature='s')
|
||||
def registerNewAccount(self, login, password, host, port=5222):
|
||||
info ("New account registration asked")
|
||||
return self.cb["registerNewAccount"](login, password, host, port)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='s', out_signature='')
|
||||
def connect(self, profile_key='@DEFAULT@'):
|
||||
info ("Connection asked")
|
||||
return self.cb["connect"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='s', out_signature='')
|
||||
def disconnect(self, profile_key='@DEFAULT@'):
|
||||
info ("Disconnection asked")
|
||||
return self.cb["disconnect"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='', out_signature='b')
|
||||
def isConnected(self, profile_key='@DEFAULT@'):
|
||||
info ("Connection status asked")
|
||||
return self.cb["isConnected"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='s', out_signature='a(sa{ss}as)')
|
||||
def getContacts(self, profile_key='@DEFAULT@'):
|
||||
debug("getContacts...")
|
||||
return self.cb["getContacts"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='s', out_signature='a{sa{s(sia{ss})}}')
|
||||
def getPresenceStatus(self, profile_key='@DEFAULT@'):
|
||||
debug("getPresenceStatus...")
|
||||
return self.cb["getPresenceStatus"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='s', out_signature='a{ss}')
|
||||
def getWaitingSub(self, profile_key='@DEFAULT@'):
|
||||
debug("getWaitingSub...")
|
||||
return self.cb["getWaitingSub"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='ssss', out_signature='')
|
||||
def sendMessage(self, to, message, type='chat', profile_key='@DEFAULT@'):
|
||||
debug("sendMessage...")
|
||||
print "sendtype=", type #gof
|
||||
self.cb["sendMessage"](to, message, type, profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='ssia{ss}s', out_signature='')
|
||||
def setPresence(self, to="", show="", priority=0, statuses={}, profile_key='@DEFAULT@'):
|
||||
self.cb["setPresence"](to, show, priority, statuses, profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='sss', out_signature='')
|
||||
def subscription(self, type, entity, profile_key='@DEFAULT@'):
|
||||
self.cb["subscription"](type, entity, profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='ssss', out_signature='')
|
||||
def setParam(self, name, value, category, profile_key='@DEFAULT@'):
|
||||
self.cb["setParam"](unicode(name), unicode(value), unicode(category), profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='sss', out_signature='s')
|
||||
def getParamA(self, name, category="default", profile_key='@DEFAULT@'):
|
||||
return self.cb["getParamA"](name, category, profile_key = profile_key)
|
||||
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='s', out_signature='s')
|
||||
def getParamsUI(self, profile_key='@DEFAULT@'):
|
||||
return self.cb["getParamsUI"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='s', out_signature='s')
|
||||
def getParams(self, profile_key='@DEFAULT@'):
|
||||
return self.cb["getParams"](profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='ss', out_signature='s')
|
||||
def getParamsForCategory(self, category, profile_key='@DEFAULT@'):
|
||||
return self.cb["getParamsForCategory"](category, profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='', out_signature='as')
|
||||
def getParamsCategories(self):
|
||||
return self.cb["getParamsCategories"]()
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='ssi', out_signature='a{i(ss)}')
|
||||
def getHistory(self, from_jid, to_jid, size):
|
||||
debug("History asked for %s", to_jid)
|
||||
return self.cb["getHistory"](from_jid, to_jid, size)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='ss', out_signature='')
|
||||
def addContact(self, entity, profile_key='@DEFAULT@'):
|
||||
debug("Subscription asked for %s (profile %s)", entity, profile_key)
|
||||
return self.cb["addContact"](entity, profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_COMM_SUFFIX,
|
||||
in_signature='ss', out_signature='')
|
||||
def delContact(self, entity, profile_key='@DEFAULT@'):
|
||||
debug("Unsubscription asked for %s (profile %s)", entity, profile_key)
|
||||
return self.cb["delContact"](entity, profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='sa{ss}s', out_signature='s')
|
||||
def launchAction(self, type, data, profile_key='@DEFAULT@'):
|
||||
return self.cb["launchAction"](type, data, profile_key)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='sba{ss}', out_signature='')
|
||||
def confirmationAnswer(self, id, accepted, data):
|
||||
debug("Answer for confirmation [%s]: %s", id, "Accepted" if accepted else "Refused")
|
||||
return self.cb["confirmationAnswer"](id, accepted, data)
|
||||
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='s', out_signature='a{ss}')
|
||||
def getProgress(self, id):
|
||||
#debug("Progress asked for %s", id)
|
||||
return self.cb["getProgress"](id)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='', out_signature='a(sss)')
|
||||
def getMenus(self):
|
||||
return self.cb["getMenus"]()
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='sss', out_signature='s')
|
||||
def getMenuHelp(self, category, name, type="NORMAL"):
|
||||
return self.cb["getMenuHelp"](category, name, type)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
|
||||
in_signature='ssss', out_signature='s')
|
||||
def callMenu(self, category, name, type, profile_key):
|
||||
return self.cb["callMenu"](category, name, type, profile_key)
|
||||
|
||||
def __attribute_string(self, in_sign):
|
||||
i=0
|
||||
idx=0
|
||||
attr_string=""
|
||||
while i<len(in_sign):
|
||||
if in_sign[i] not in ['b','y','n','i','x','q','u','t','d','s','a']:
|
||||
raise Exception #FIXME: create an exception here (unmanaged attribute type)
|
||||
|
||||
attr_string += ("" if idx==0 else ",") + ("arg_%i" % idx)
|
||||
idx+=1
|
||||
|
||||
if in_sign[i] == 'a':
|
||||
i+=1
|
||||
if in_sign[i]!='{' and in_sign[i]!='(': #FIXME: must manage tuples out of arrays
|
||||
i+=1
|
||||
continue #we have a simple type for the array
|
||||
while (True): #we have a dict or a list of tuples
|
||||
i+=1
|
||||
if i>=len(in_sign):
|
||||
raise Exception #FIXME: create an exception here (the '}' is not presend)
|
||||
if in_sign[i] == '}' or in_sign[i] == ')':
|
||||
break
|
||||
i+=1
|
||||
return attr_string
|
||||
|
||||
|
||||
|
||||
def addMethod(self, name, int_suffix, in_sign, out_sign):
|
||||
"""Dynamically add a method to Dbus Bridge"""
|
||||
#FIXME: Better way ???
|
||||
attributes = self.__attribute_string(in_sign)
|
||||
|
||||
code = compile ('def '+name+' (self,'+attributes+'): return self.cb["'+name+'"]('+attributes+')', '<DBus bridge>','exec')
|
||||
exec (code)
|
||||
method = locals()[name]
|
||||
setattr(DbusObject, name, dbus.service.method(
|
||||
const_INT_PREFIX+int_suffix, in_signature=in_sign, out_signature=out_sign)(method))
|
||||
|
||||
def addSignal(self, name, int_suffix, signature):
|
||||
"""Dynamically add a signal to Dbus Bridge"""
|
||||
#FIXME: Better way ???
|
||||
attributes = self.__attribute_string(signature)
|
||||
|
||||
code = compile ('def '+name+' (self,'+attributes+'): debug ("'+name+' signal")', '<DBus bridge>','exec')
|
||||
exec (code)
|
||||
signal = locals()[name]
|
||||
setattr(DbusObject, name, dbus.service.signal(
|
||||
const_INT_PREFIX+int_suffix, signature=signature)(signal))
|
||||
|
||||
class DBusBridge(Bridge):
|
||||
def __init__(self):
|
||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
Bridge.__init__(self)
|
||||
info ("Init DBus...")
|
||||
self.session_bus = dbus.SessionBus()
|
||||
self.dbus_name = dbus.service.BusName(const_INT_PREFIX, self.session_bus)
|
||||
self.dbus_bridge = DbusObject(self.session_bus, '/org/goffi/SAT/bridge')
|
||||
|
||||
def connected(self, profile):
|
||||
self.dbus_bridge.connected(profile)
|
||||
|
||||
def disconnected(self, profile):
|
||||
self.dbus_bridge.disconnected(profile)
|
||||
|
||||
def newContact(self, contact, attributes, groups, profile):
|
||||
self.dbus_bridge.newContact(contact, attributes, groups, profile)
|
||||
|
||||
def newMessage(self, from_jid, msg, type='chat', to='', profile='@NONE@'):
|
||||
debug("sending message...")
|
||||
self.dbus_bridge.newMessage(from_jid, msg, type, to, profile)
|
||||
|
||||
def newAlert(self, msg, title="", alert_type="INFO", profile='@NONE@'):
|
||||
self.dbus_bridge.newAlert(msg, title, alert_type, profile)
|
||||
|
||||
def presenceUpdate(self, entity, show, priority, statuses, profile):
|
||||
debug("updating presence for %s",entity)
|
||||
self.dbus_bridge.presenceUpdate(entity, show, priority, statuses, profile)
|
||||
|
||||
def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile):
|
||||
self.dbus_bridge.roomJoined(room_id, room_service, room_nicks, user_nick, profile)
|
||||
|
||||
def subscribe(self, sub_type, entity, profile):
|
||||
debug("subscribe request for %s",entity)
|
||||
self.dbus_bridge.subscribe(sub_type, entity, profile)
|
||||
|
||||
def paramUpdate(self, name, value, category, profile):
|
||||
debug("updating param [%s] %s ", category, name)
|
||||
self.dbus_bridge.paramUpdate(name, value, category, profile)
|
||||
|
||||
def contactDeleted(self, entity, profile):
|
||||
debug("sending contact deleted signal %s ", entity)
|
||||
self.dbus_bridge.contactDeleted(entity, profile)
|
||||
|
||||
def askConfirmation(self, type, id, data):
|
||||
self.dbus_bridge.askConfirmation(type, id, data)
|
||||
|
||||
def actionResult(self, type, id, data):
|
||||
self.dbus_bridge.actionResult(type, id, data)
|
||||
|
||||
def actionResultExt(self, type, id, data):
|
||||
self.dbus_bridge.actionResultExt(type, id, data)
|
||||
|
||||
def updatedValue(self, name, value):
|
||||
self.dbus_bridge.updatedValue(name, value)
|
||||
|
||||
def register(self, name, callback):
|
||||
debug("registering DBus bridge method [%s]", name)
|
||||
self.dbus_bridge.register(name, callback)
|
||||
|
||||
def addMethod(self, name, int_suffix, in_sign, out_sign, method):
|
||||
"""Dynamically add a method to Dbus Bridge"""
|
||||
print ("Adding method [%s] to DBus bridge" % name)
|
||||
self.dbus_bridge.addMethod(name, int_suffix, in_sign, out_sign)
|
||||
self.register(name, method)
|
||||
|
||||
def addSignal(self, name, int_suffix, signature):
|
||||
self.dbus_bridge.addSignal(name, int_suffix, signature)
|
||||
setattr(DBusBridge, name, getattr(self.dbus_bridge, name))
|
||||
|
||||
0
src/sat/bridge/__init__.py
Normal file
0
src/sat/bridge/__init__.py
Normal file
68
src/sat/bridge/bridge.py
Executable file
68
src/sat/bridge/bridge.py
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
#!/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 logging import debug, info, error
|
||||
|
||||
class Bridge:
|
||||
def __init__(self):
|
||||
info ("Bridge initialization")
|
||||
|
||||
##signals
|
||||
def newContact(self, contact):
|
||||
raise NotImplementedError
|
||||
|
||||
def newMessage(self, from_jid, msg, type='chat'):
|
||||
raise NotImplementedError
|
||||
|
||||
def presenceUpdate(self, type, jid, show, status, priority):
|
||||
raise NotImplementedError
|
||||
|
||||
def paramUpdate(self, name, value):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
##methods
|
||||
def connect(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def getContacts(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def getPresenceStatus(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def sendMessage(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def setPresence(self, to="", type="", show="", status="", priority=0):
|
||||
raise NotImplementedError
|
||||
|
||||
def setParam(self, name, value, namespace):
|
||||
raise NotImplementedError
|
||||
|
||||
def getParam(self, name, namespace):
|
||||
raise NotImplementedError
|
||||
|
||||
def getParams(self, namespace):
|
||||
raise NotImplementedError
|
||||
|
||||
def getHistory(self, from_jid, to_jid, size):
|
||||
raise NotImplementedError
|
||||
0
src/sat/tools/__init__.py
Normal file
0
src/sat/tools/__init__.py
Normal file
80
src/sat/tools/games.py
Normal file
80
src/sat/tools/games.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#!/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 logging import debug, info, error
|
||||
|
||||
"""This library help manage general games (e.g. card games)"""
|
||||
|
||||
|
||||
suits_order = ['pique', 'coeur', 'trefle', 'carreau', 'atout'] #I have switched the usual order 'trefle' and 'carreau' because card are more easy to see if suit colour change (black, red, black, red)
|
||||
values_order = map(str,range(1,11))+["valet","cavalier","dame","roi"]
|
||||
|
||||
class TarotCard():
|
||||
"""This class is used to represent a car logically"""
|
||||
#TODO: move this in a library in tools, and share this with frontends (e.g. card_game in wix use the same class)
|
||||
|
||||
def __init__(self, tuple_card):
|
||||
"""@param tuple_card: tuple (suit, value)"""
|
||||
self.suit, self.value = tuple_card
|
||||
self.bout = True if self.suit=="atout" and self.value in ["1","21","excuse"] else False
|
||||
if self.bout or self.value == "roi":
|
||||
self.points = 4.5
|
||||
elif self.value == "dame":
|
||||
self.points = 3.5
|
||||
elif self.value == "cavalier":
|
||||
self.points = 2.5
|
||||
elif self.value == "valet":
|
||||
self.points = 1.5
|
||||
else:
|
||||
self.points = 0.5
|
||||
|
||||
def get_tuple(self):
|
||||
return (self.suit,self.value)
|
||||
|
||||
@staticmethod
|
||||
def from_tuples(tuple_list):
|
||||
result = []
|
||||
for card_tuple in tuple_list:
|
||||
result.append(TarotCard(card_tuple))
|
||||
return result
|
||||
|
||||
def __cmp__(self, other):
|
||||
if other == None:
|
||||
return 1
|
||||
if self.suit != other.suit:
|
||||
idx1 = suits_order.index(self.suit)
|
||||
idx2 = suits_order.index(other.suit)
|
||||
return idx1.__cmp__(idx2)
|
||||
if self.suit == 'atout':
|
||||
if self.value == other.value == 'excuse':
|
||||
return 0
|
||||
if self.value == 'excuse':
|
||||
return -1
|
||||
if other.value == 'excuse':
|
||||
return 1
|
||||
return int(self.value).__cmp__(int(other.value))
|
||||
#at this point we have the same suit which is not 'atout'
|
||||
idx1 = values_order.index(self.value)
|
||||
idx2 = values_order.index(other.value)
|
||||
return idx1.__cmp__(idx2)
|
||||
|
||||
def __str__(self):
|
||||
return "[%s,%s]" % (self.suit, self.value)
|
||||
51
src/sat/tools/jid.py
Normal file
51
src/sat/tools/jid.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#!/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/>.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class JID(unicode):
|
||||
"""This class help manage JID (Node@Domaine/Resource)"""
|
||||
|
||||
def __new__(cls, jid):
|
||||
self = unicode.__new__(cls, jid)
|
||||
self.__parse()
|
||||
return self
|
||||
|
||||
def __parse(self):
|
||||
"""find node domaine and resource"""
|
||||
node_end=self.find('@')
|
||||
if node_end<0:
|
||||
node_end=0
|
||||
domain_end=self.find('/')
|
||||
if domain_end<1:
|
||||
domain_end=len(self)
|
||||
self.node=self[:node_end]
|
||||
self.domain=self[(node_end+1) if node_end else 0:domain_end]
|
||||
self.resource=self[domain_end+1:]
|
||||
if not node_end:
|
||||
self.short=self
|
||||
else:
|
||||
self.short=self.node+'@'+self.domain
|
||||
|
||||
def is_valid(self):
|
||||
"""return True if the jid is xmpp compliant"""
|
||||
#FIXME: always return True for the moment
|
||||
return True
|
||||
655
src/sat/tools/memory.py
Normal file
655
src/sat/tools/memory.py
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
#!/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 cPickle as 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
|
||||
from sat.tools.xml_tools import paramsXml2xmlUI
|
||||
|
||||
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 parameters with xml"""
|
||||
### TODO: add desciption in params
|
||||
|
||||
#TODO: move Watched in a plugin
|
||||
default_xml = u"""
|
||||
<params>
|
||||
<general>
|
||||
</general>
|
||||
<individual>
|
||||
<category name="Connection" label="%(category_connection)s">
|
||||
<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="%(label_NewAccount)s" type="button" callback_id="registerNewAccount"/>
|
||||
<param name="autoconnect" label="%(label_autoconnect)s" value="true" type="bool" />
|
||||
<param name="autodisconnect" label="%(label_autodisconnect)s" value="false" type="bool" />
|
||||
</category>
|
||||
<category name="Misc" label="%(category_misc)s">
|
||||
<param name="Watched" value="test@Jabber.goffi.int" type="string" />
|
||||
</category>
|
||||
</individual>
|
||||
</params>
|
||||
""" % {'category_connection': _("Connection"),
|
||||
'label_NewAccount': _("Register new account"),
|
||||
'label_autoconnect': _('Connect on frontend startup'),
|
||||
'label_autodisconnect': _('Disconnect on frontend closure'),
|
||||
'category_misc': _("Misc")
|
||||
}
|
||||
|
||||
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:
|
||||
xml_file.write(self.dom.toxml('utf-8'))
|
||||
|
||||
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 = {}
|
||||
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):
|
||||
"""Create a new profile
|
||||
@param name: Name of the profile"""
|
||||
if self.params.has_key(name):
|
||||
info (_('The profile name already exists'))
|
||||
return 1
|
||||
self.params[name]={}
|
||||
return 0
|
||||
|
||||
def deleteProfile(self, name):
|
||||
"""Delete an existing profile
|
||||
@param name: Name of the profile"""
|
||||
if not self.params.has_key(name):
|
||||
error (_('Trying to delete an unknown profile'))
|
||||
return 1
|
||||
del 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 ""
|
||||
default = self.host.memory.getPrivate('Profile_default')
|
||||
if not default or not default in self.params:
|
||||
info(_('No default profile, returning first one')) #TODO: manage real default profile
|
||||
default = self.params.keys()[0]
|
||||
self.host.memory.setPrivate('Profile_default', default)
|
||||
return default #FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists
|
||||
if not self.params.has_key(profile_key):
|
||||
info (_('Trying to access an unknown profile'))
|
||||
return ""
|
||||
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 [%(category)s/%(name)s]: %(reason)s") % {'category':category, 'name':name, 'reason':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 [%(name)s] in category [%(category)s] doesn't exist !") % {'name':name, 'category':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 [%(name)s] in category [%(category)s] doesn't exist !") % {'name':name, 'category':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 getParamsUI(self, profile_key='@DEFAULT@'):
|
||||
"""Return a SàT XMLUI for parameters, with given profile"""
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error(_("Asking params for inexistant profile"))
|
||||
return ""
|
||||
param_xml = self.getParams(profile)
|
||||
return paramsXml2xmlUI(param_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"""
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error(_('Trying to set parameter for an unknown profile'))
|
||||
return #TODO: throw an error
|
||||
|
||||
node = self.__getParamNode(name, category, '@ALL@')
|
||||
if not node:
|
||||
error(_('Requesting an unknown parameter (%(category)s/%(name)s)') % {'category':category, 'name':name})
|
||||
return
|
||||
|
||||
if node[0] == 'general':
|
||||
self.params_gen[(category, name)] = value
|
||||
self.host.bridge.paramUpdate(name, value, category, profile) #TODO: add profile in signal
|
||||
return
|
||||
|
||||
assert (node[0] == 'individual')
|
||||
|
||||
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, profile) #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):
|
||||
"""Create a new profile
|
||||
@param name: Profile name
|
||||
"""
|
||||
return self.params.createProfile(name)
|
||||
|
||||
def deleteProfile(self, name):
|
||||
"""Delete an existing profile
|
||||
@param name: Name of the profile"""
|
||||
return self.params.deleteProfile(name)
|
||||
|
||||
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_jid.full(), 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, profile_key='@DEFAULT@'):
|
||||
debug("Memory addContact: %s",contact_jid.userhost())
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_('Trying to add a contact to a non-existant profile'))
|
||||
return
|
||||
assert(isinstance(attributes,dict))
|
||||
assert(isinstance(groups,set))
|
||||
if not self.contacts.has_key(profile):
|
||||
self.contacts[profile] = {}
|
||||
self.contacts[profile][contact_jid.userhost()]=[attributes, groups]
|
||||
|
||||
def delContact(self, contact_jid, profile_key='@DEFAULT@'):
|
||||
debug("Memory delContact: %s",contact_jid.userhost())
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error (_('Trying to delete a contact for a non-existant profile'))
|
||||
return
|
||||
if self.contacts.has_key(profile) and self.contacts[profile].has_key(contact_jid.userhost()):
|
||||
del self.contacts[profile][contact_jid.userhost()]
|
||||
|
||||
def getContact(self, contact_jid, profile_key='@DEFAULT@'):
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error(_('Asking a contact for a non-existant profile'))
|
||||
return None
|
||||
if self.contacts.has_key(profile) and self.contacts[profile].has_key(contact_jid.userhost()):
|
||||
self.contacts[profile][contact_jid.userhost()]
|
||||
else:
|
||||
return None
|
||||
|
||||
def getContacts(self, profile_key='@DEFAULT@'):
|
||||
"""Return list of contacts for given profile
|
||||
@param profile_key: profile key
|
||||
@return list of [contact, attr, groups]"""
|
||||
debug ("Memory getContact OK (%s)", self.contacts)
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error(_('Asking contacts for a non-existant profile'))
|
||||
return []
|
||||
ret=[]
|
||||
for contact in self.contacts[profile]:
|
||||
attr, groups = self.contacts[profile][contact]
|
||||
ret.append([contact, attr, groups ])
|
||||
return ret
|
||||
|
||||
def addPresenceStatus(self, contact_jid, show, priority, statuses, profile_key='@DEFAULT@'):
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error(_('Trying to add presence status to a non-existant profile'))
|
||||
return
|
||||
if not self.presenceStatus.has_key(profile):
|
||||
self.presenceStatus[profile] = {}
|
||||
if not self.presenceStatus[profile].has_key(contact_jid.userhost()):
|
||||
self.presenceStatus[profile][contact_jid.userhost()] = {}
|
||||
resource = jid.parse(contact_jid.full())[2] or ''
|
||||
self.presenceStatus[profile][contact_jid.userhost()][resource] = (show, priority, statuses)
|
||||
|
||||
def addWaitingSub(self, type, contact_jid, profile_key):
|
||||
"""Called when a subcription request is received"""
|
||||
profile = self.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
if not self.subscriptions.has_key(profile):
|
||||
self.subscriptions[profile] = {}
|
||||
self.subscriptions[profile][contact_jid] = type
|
||||
|
||||
def delWaitingSub(self, contact_jid, profile_key):
|
||||
"""Called when a subcription request is finished"""
|
||||
profile = self.getProfileName(profile_key)
|
||||
assert(profile)
|
||||
if self.subscriptions.has_key(profile) and self.subscriptions[profile].has_key(contact_jid):
|
||||
del self.subscriptions[profile][contact_jid]
|
||||
|
||||
def getWaitingSub(self, profile_key='@DEFAULT@'):
|
||||
"""Called to get a list of currently waiting subscription requests"""
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error(_('Asking waiting subscriptions for a non-existant profile'))
|
||||
return {}
|
||||
if not self.subscriptions.has_key(profile):
|
||||
return {}
|
||||
|
||||
return self.subscriptions[profile]
|
||||
|
||||
def getPresenceStatus(self, profile_key='@DEFAULT@'):
|
||||
profile = self.getProfileName(profile_key)
|
||||
if not profile:
|
||||
error(_('Asking contacts for a non-existant profile'))
|
||||
return {}
|
||||
if not self.presenceStatus.has_key(profile):
|
||||
self.presenceStatus[profile] = {}
|
||||
debug ("Memory getPresenceStatus (%s)", self.presenceStatus[profile])
|
||||
return self.presenceStatus[profile]
|
||||
|
||||
def getParamA(self, name, category, attr="value", profile_key="@DEFAULT@"):
|
||||
return self.params.getParamA(name, category, attr, profile_key)
|
||||
|
||||
def getParamsUI(self, profile_key='@DEFAULT@'):
|
||||
return self.params.getParamsUI(profile_key)
|
||||
|
||||
def getParams(self, profile_key='@DEFAULT@'):
|
||||
return self.params.getParams(profile_key)
|
||||
|
||||
def getParamsForCategory(self, category, profile_key='@DEFAULT@'):
|
||||
return self.params.getParamsForCategory(category, profile_key)
|
||||
|
||||
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)
|
||||
302
src/sat/tools/xml_tools.py
Normal file
302
src/sat/tools/xml_tools.py
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
#!/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 logging import debug, info, error
|
||||
from xml.dom import minidom
|
||||
from wokkel import data_form
|
||||
import pdb
|
||||
|
||||
"""This library help manage XML used in SàT (parameters, registration, etc) """
|
||||
|
||||
|
||||
def dataForm2xml(form):
|
||||
"""Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT xml"""
|
||||
|
||||
form_ui = XMLUI("form", "vertical")
|
||||
|
||||
if form.instructions:
|
||||
form_ui.addText('\n'.join(form.instructions), 'instructions')
|
||||
|
||||
labels = filter(lambda field:field.label,form.fieldList)
|
||||
if labels:
|
||||
#if there is no label, we don't need to use pairs
|
||||
form_ui.changeLayout("pairs")
|
||||
|
||||
for field in form.fieldList:
|
||||
if field.fieldType == 'fixed':
|
||||
__field_type = 'text'
|
||||
elif field.fieldType == 'text-single':
|
||||
__field_type = "string"
|
||||
elif field.fieldType == 'text-private':
|
||||
__field_type = "password"
|
||||
elif field.fieldType == 'list-single':
|
||||
__field_type = "list"
|
||||
else:
|
||||
error (u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType)
|
||||
__field_type = "string"
|
||||
|
||||
if labels:
|
||||
if field.label:
|
||||
form_ui.addLabel(field.label)
|
||||
else:
|
||||
form_ui.addEmpty()
|
||||
|
||||
elem = form_ui.addElement(__field_type, field.var, field.value, [option.value for option in field.options])
|
||||
return form_ui.toXml()
|
||||
|
||||
def tupleList2dataForm(values):
|
||||
"""convert a list of tuples (name,value) to a wokkel submit data form"""
|
||||
form = data_form.Form('submit')
|
||||
for value in values:
|
||||
field = data_form.Field(var=value[0], value=value[1])
|
||||
form.addField(field)
|
||||
|
||||
return form
|
||||
|
||||
def paramsXml2xmlUI(xml):
|
||||
"""Convert the xml for parameter to a SàT XML User Interface"""
|
||||
params_doc = minidom.parseString(xml.encode('utf-8'))
|
||||
top = params_doc.documentElement
|
||||
if top.nodeName != 'params':
|
||||
error(_('INTERNAL ERROR: parameters xml not valid'))
|
||||
assert(False)
|
||||
param_ui = XMLUI("param", "tabs")
|
||||
for category in top.getElementsByTagName("category"):
|
||||
name = category.getAttribute('name')
|
||||
label = category.getAttribute('label')
|
||||
if not name:
|
||||
error(_('INTERNAL ERROR: params categories must have a name'))
|
||||
assert(False)
|
||||
param_ui.addCategory(name, 'pairs', label=label)
|
||||
for param in category.getElementsByTagName("param"):
|
||||
name = param.getAttribute('name')
|
||||
label = param.getAttribute('label')
|
||||
if not name:
|
||||
error(_('INTERNAL ERROR: params must have a name'))
|
||||
assert(False)
|
||||
type = param.getAttribute('type')
|
||||
value = param.getAttribute('value') or None
|
||||
callback_id = param.getAttribute('callback_id') or None
|
||||
if type == "button":
|
||||
param_ui.addEmpty()
|
||||
else:
|
||||
param_ui.addLabel(label or name)
|
||||
param_ui.addElement(name=name, type=type, value=value, callback_id=callback_id)
|
||||
|
||||
return param_ui.toXml()
|
||||
|
||||
|
||||
|
||||
|
||||
class XMLUI:
|
||||
"""This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
|
||||
|
||||
def __init__(self, panel_type, layout="vertical", title=None):
|
||||
"""Init SàT XML Panel
|
||||
@param panel_type: one of
|
||||
- window (new window)
|
||||
- form (formulaire, depend of the frontend, usually a panel with cancel/submit buttons)
|
||||
- param (parameters, presentatio depend of the frontend)
|
||||
@param layout: disposition of elements, one of:
|
||||
- vertical: elements are disposed up to bottom
|
||||
- horizontal: elements are disposed left to right
|
||||
- pairs: elements come on two aligned columns
|
||||
(usually one for a label, the next for the element)
|
||||
- tabs: elemens are in categories with tabs (notebook)
|
||||
@param title: title or default if None
|
||||
"""
|
||||
if not panel_type in ['window', 'form', 'param']:
|
||||
error(_("Unknown panel type [%s]") % panel_type)
|
||||
assert(False)
|
||||
self.type = panel_type
|
||||
impl = minidom.getDOMImplementation()
|
||||
|
||||
self.doc = impl.createDocument(None, "sat_xmlui", None)
|
||||
top_element = self.doc.documentElement
|
||||
top_element.setAttribute("type", panel_type)
|
||||
if title:
|
||||
top_element.setAttribute("title", title)
|
||||
self.parentTabsLayout = None #used only we have 'tabs' layout
|
||||
self.currentCategory = None #used only we have 'tabs' layout
|
||||
self.changeLayout(layout)
|
||||
|
||||
def __del__(self):
|
||||
self.doc.unlink()
|
||||
|
||||
def __createLayout(self, layout, parent=None):
|
||||
"""Create a layout element
|
||||
@param type: layout type (cf init doc)
|
||||
@parent: parent element or None
|
||||
"""
|
||||
if not layout in ['vertical', 'horizontal', 'pairs', 'tabs']:
|
||||
error (_("Unknown layout type [%s]") % layout)
|
||||
assert (False)
|
||||
layout_elt = self.doc.createElement('layout')
|
||||
layout_elt.setAttribute('type',layout)
|
||||
if parent != None:
|
||||
parent.appendChild(layout_elt)
|
||||
return layout_elt
|
||||
|
||||
def __createElem(self, type, name=None, parent = None):
|
||||
"""Create an element
|
||||
@param type: one of
|
||||
- empty: empty element (usefull to skip something in a layout, e.g. skip first element in a PAIRS layout)
|
||||
- text: text to be displayed in an multi-line area, e.g. instructions
|
||||
@param name: name of the element or None
|
||||
@param parent: parent element or None
|
||||
"""
|
||||
elem = self.doc.createElement('elem')
|
||||
if name:
|
||||
elem.setAttribute('name', name)
|
||||
elem.setAttribute('type', type)
|
||||
if parent != None:
|
||||
parent.appendChild(elem)
|
||||
return elem
|
||||
|
||||
def changeLayout(self, layout):
|
||||
"""Change the current layout"""
|
||||
self.currentLayout = self.__createLayout(layout, self.currentCategory if self.currentCategory else self.doc.documentElement)
|
||||
if layout == "tabs":
|
||||
self.parentTabsLayout = self.currentLayout
|
||||
|
||||
|
||||
def addEmpty(self, name=None):
|
||||
"""Add a multi-lines text"""
|
||||
elem = self.__createElem('empty', name, self.currentLayout)
|
||||
|
||||
def addText(self, text, name=None):
|
||||
"""Add a multi-lines text"""
|
||||
elem = self.__createElem('text', name, self.currentLayout)
|
||||
text = self.doc.createTextNode(text)
|
||||
elem.appendChild(text)
|
||||
|
||||
def addLabel(self, text, name=None):
|
||||
"""Add a single line text, mainly useful as label before element"""
|
||||
elem = self.__createElem('label', name, self.currentLayout)
|
||||
elem.setAttribute('value', text)
|
||||
|
||||
def addString(self, name=None, value=None):
|
||||
"""Add a string box"""
|
||||
elem = self.__createElem('string', name, self.currentLayout)
|
||||
if value:
|
||||
elem.setAttribute('value', value)
|
||||
|
||||
def addPassword(self, name=None, value=None):
|
||||
"""Add a password box"""
|
||||
elem = self.__createElem('password', name, self.currentLayout)
|
||||
if value:
|
||||
elem.setAttribute('value', value)
|
||||
|
||||
def addTextBox(self, name=None, value=None):
|
||||
"""Add a string box"""
|
||||
elem = self.__createElem('textbox', name, self.currentLayout)
|
||||
if value:
|
||||
elem.setAttribute('value', value)
|
||||
|
||||
def addBool(self, name=None, value="true"):
|
||||
"""Add a string box"""
|
||||
assert value in ["true","false"]
|
||||
elem = self.__createElem('bool', name, self.currentLayout)
|
||||
elem.setAttribute('value', value)
|
||||
|
||||
def addList(self, options, name=None, value=None, style=set()):
|
||||
"""Add a list of choices"""
|
||||
styles = set(style)
|
||||
assert (options)
|
||||
assert (styles.issubset(['multi']))
|
||||
elem = self.__createElem('list', name, self.currentLayout)
|
||||
self.addOptions(options, elem)
|
||||
if value:
|
||||
elem.setAttribute('value', value)
|
||||
for style in styles:
|
||||
elem.setAttribute(style, 'yes')
|
||||
|
||||
def addButton(self, callback_id, name, value, fields_back=[]):
|
||||
"""Add a button
|
||||
@param callback: callback which will be called if button is pressed
|
||||
@param name: name
|
||||
@param value: label of the button
|
||||
@fields_back: list of names of field to give back when pushing the button"""
|
||||
elem = self.__createElem('button', name, self.currentLayout)
|
||||
elem.setAttribute('callback_id', callback_id)
|
||||
elem.setAttribute('value', value)
|
||||
for field in fields_back:
|
||||
fback_el = self.doc.createElement('field_back')
|
||||
fback_el.setAttribute('name', field)
|
||||
elem.appendChild(fback_el)
|
||||
|
||||
|
||||
|
||||
def addElement(self, type, name = None, value = None, options = None, callback_id = None):
|
||||
"""Convenience method to add element, the params correspond to the ones in addSomething methods"""
|
||||
if type == 'empty':
|
||||
self.addEmpty(name)
|
||||
elif type == 'text':
|
||||
assert(value!=None)
|
||||
self.addText(value, name)
|
||||
elif type == 'label':
|
||||
assert(value)
|
||||
self.addLabel(value)
|
||||
elif type == 'string':
|
||||
self.addString(name, value)
|
||||
elif type == 'password':
|
||||
self.addPassword(name, value)
|
||||
elif type == 'textbox':
|
||||
self.addTextBox(name, value)
|
||||
elif type == 'bool':
|
||||
if not value:
|
||||
value = "true"
|
||||
self.addBool(name, value)
|
||||
elif type == 'list':
|
||||
self.addList(options, name, value)
|
||||
elif type == 'button':
|
||||
assert(callback_id and value)
|
||||
self.addButton(callback_id, name, value)
|
||||
|
||||
def addOptions(self, options, parent):
|
||||
"""Add options to a multi-values element (e.g. list)
|
||||
@param parent: multi-values element"""
|
||||
for option in options:
|
||||
opt = self.doc.createElement('option')
|
||||
opt.setAttribute('value', option)
|
||||
parent.appendChild(opt)
|
||||
|
||||
def addCategory(self, name, layout, label=None):
|
||||
"""Add a category to current layout (must be a tabs layout)"""
|
||||
assert(layout != 'tabs')
|
||||
if not self.parentTabsLayout:
|
||||
error(_("Trying to add a category without parent tabs layout"))
|
||||
assert(False)
|
||||
if self.parentTabsLayout.getAttribute('type') != 'tabs':
|
||||
error(_("parent layout of a category is not tabs"))
|
||||
assert(False)
|
||||
|
||||
if not label:
|
||||
label = name
|
||||
self.currentCategory = cat = self.doc.createElement('category')
|
||||
cat.setAttribute('name', name)
|
||||
cat.setAttribute('label', label)
|
||||
self.changeLayout(layout)
|
||||
self.parentTabsLayout.appendChild(cat)
|
||||
|
||||
def toXml(self):
|
||||
"""return the XML representation of the panel"""
|
||||
return self.doc.toxml()
|
||||
Loading…
Reference in a new issue