Files reorganisation

This commit is contained in:
Goffi 2010-12-29 01:06:29 +01:00
commit e4452c4b03
20 changed files with 10 additions and 8 deletions

0
src/plugins/__init__.py Normal file
View file

266
src/plugins/plugin_misc_cs.py Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
#!/bin/sh
twistd -noy sat.tac

796
src/sat.tac Normal file
View 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
View file

401
src/sat/bridge/DBus.py Executable file
View 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))

View file

68
src/sat/bridge/bridge.py Executable file
View 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

View file

80
src/sat/tools/games.py Normal file
View 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
View 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
View 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
View 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()