Files reorganisation

This commit is contained in:
Goffi 2010-12-29 01:06:29 +01:00
commit e4452c4b03
9 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