Mercurial > libervia-backend
changeset 223:86d249b6d9b7
Files reorganisation
line wrap: on
line diff
--- a/README4TRANSLATORS Tue Dec 28 23:10:13 2010 +0100 +++ b/README4TRANSLATORS Wed Dec 29 01:06:29 2010 +0100 @@ -1,5 +1,7 @@ First of all, thank you for helping translating SàT :) +NOTE: *.po files are in i18n directory + To translate a file, you can use a dedicated tool as the excellent gtranslator: - use the template .po file (e.g. sat.po) and name it to your translated language (e.g. fr.po for french); you can preferably generate a new template directly from the source with the following command (eventually adapted): > xgettext -L python -d sat sat.tac tools/*py plugins/*py
--- a/fr.po Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1070 +0,0 @@ -# SàT french translation. -# Copyright (C) 2009, 2010 Jérôme Poisson -# This file is distributed under the same license as the SàT package. -# Jérôme Poisson <goffi@goffi.org>, 2009, 2010. -# Goffi <goffi@goffi.org>, 2010. -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 0.0.2D\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-08-19 21:54+0800\n" -"PO-Revision-Date: 2010-08-19 22:14+0800\n" -"Last-Translator: Goffi <goffi@goffi.org>\n" -"Language-Team: French <goffi@goffi.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: sat.tac:87 -#, python-format -msgid "********** [%s] CONNECTED **********" -msgstr "********** [%s] CONNECTÉ **********" - -#: sat.tac:93 -msgid "XML stream is initialized" -msgstr "Le flux XML est initialisé" - -#: sat.tac:113 -#, python-format -msgid "********** [%s] DISCONNECTED **********" -msgstr "********** [%s] DÉCONNECTÉ **********" - -#: sat.tac:117 -msgid "No keep_alife" -msgstr "Pas de \"keep_alife\"" - -#: sat.tac:128 -#, python-format -msgid "got message from: %s" -msgstr "message reçu de: %s" - -#: sat.tac:171 -#, python-format -msgid "new contact in roster list: %s" -msgstr "nouveau contact: %s" - -#: sat.tac:178 -#, python-format -msgid "removing %s from roster list" -msgstr "supppression du contact %s" - -#: sat.tac:188 -#, python-format -msgid "" -"presence update for [%(entity)s] (available, show=%(show)s statuses=%" -"(statuses)s priority=%(priority)d)" -msgstr "" -"Mise à jour des information de présence pour [%(entity)s] (available, show=%" -"(show)s statuses=%(statuses)s priority=%(priority)d)" - -#: sat.tac:202 -#, python-format -msgid "presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)" -msgstr "" -"Mise à jour de l'information de présence pour [%(entity)s] (unavailable, " -"statuses=%(statuses)s)" - -#: sat.tac:219 -#, python-format -msgid "subscription approved for [%s]" -msgstr "inscription approuvée pour [%s]" - -#: sat.tac:224 -#, python-format -msgid "unsubscription confirmed for [%s]" -msgstr "désinscription confirmée pour [%s]" - -#: sat.tac:229 -#, python-format -msgid "subscription request for [%s]" -msgstr "demande d'inscription pour [%s]" - -#: sat.tac:234 -#, python-format -msgid "unsubscription asked for [%s]" -msgstr "demande de désinscription pour [%s]" - -#: sat.tac:259 -msgid "Registration asked for" -msgstr "inscription demandée pour" - -#: sat.tac:277 plugins/plugin_xep_0077.py:83 plugins/plugin_xep_0077.py:97 -#, python-format -msgid "registration answer: %s" -msgstr "réponse à la demande d'inscription: %s" - -#: sat.tac:279 plugins/plugin_xep_0077.py:99 -msgid "Registration successfull" -msgstr "Inscription réussie" - -#: sat.tac:284 plugins/plugin_xep_0077.py:75 plugins/plugin_xep_0077.py:107 -#, python-format -msgid "Registration failure: %s" -msgstr "Échec de l'inscription: %s" - -#: sat.tac:289 plugins/plugin_xep_0077.py:113 -msgid "Username already exists, please choose an other one" -msgstr "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre" - -#: sat.tac:292 -#, python-format -msgid "Registration failed (%s)" -msgstr "Éched de l'insciption (%s)" - -#: sat.tac:305 -msgid "Trying to access an undefined constant" -msgstr "Vous essayer d'utiliser une constante indéfinie" - -#: sat.tac:312 -msgid "Trying to redefine a constant" -msgstr "Vous essayez de ré-attribuer une constante" - -#: sat.tac:379 -#, python-format -msgid "importing plugin: %s" -msgstr "Importation du plugin: %s" - -#: sat.tac:392 -msgid "Trying to connect a non-exsitant profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat.tac:396 -msgid "already connected !" -msgstr "Vous êtes déjà connecté !" - -#: sat.tac:419 -msgid "setting plugins parents" -msgstr "Configuration des parents des extensions" - -#: sat.tac:430 -msgid "not connected !" -msgstr "Vous n'êtes pas connecté !" - -#: sat.tac:433 -msgid "Disconnecting..." -msgstr "Déconnexion..." - -#: sat.tac:445 -msgid "running app" -msgstr "Lancement de l'application" - -#: sat.tac:449 -msgid "stopping app" -msgstr "Arrêt de l'application" - -#: sat.tac:486 -msgid "No user or server given" -msgstr "L'utilisateur ou le serveur n'ont pas été spécifié" - -#: sat.tac:488 -msgid "No user, password or server given, can't register new account." -msgstr "" -"L'utilisateur, le mot de passe ou le serveur n'ont pas été spécifiés, " -"impossible d'inscrire un nouveau compte." - -#: sat.tac:495 -#, python-format -msgid "Are you sure to register new account [%(user)s] to server %(server)s ?" -msgstr "" -"Êtes vous sûr de vouloir inscrire le nouveau compte [%(user)s] au serveur %" -"(server)s ?" - -#: sat.tac:502 -#, python-format -msgid "register Confirmation CB ! (%s)" -msgstr "Callback de confirmation d'inscription !" - -#: sat.tac:534 -#, python-format -msgid "FIXME FIXME FIXME: Unmanaged action (%s) in submitForm" -msgstr "" -"CORRIGEZ-MOI CORRIGEZ-MOI CORRIGEZ-MOI: Action non gérée (%s) dans " -"\"submitForm\"" - -#: sat.tac:544 -#, python-format -msgid "setting param: %(name)s=%(value)s in category %(category)s" -msgstr "" -"Le paramètre %(name)s vaut désormais %(value)s dans la catégorie %(category)s" - -#: sat.tac:554 -msgid "asking connection status for a non-existant profile" -msgstr "demande de l'état de connexion pour un profile qui n'existe pas" - -#: sat.tac:569 -#, fuzzy -msgid "trying to launch action with a non-existant profile" -msgstr "Tentative d'ajout d'un contact à un profile inexistant" - -#: sat.tac:575 -msgid "Incomplete data" -msgstr "Données incomplétes" - -#: sat.tac:581 -msgid "Unknown action type" -msgstr "Type d'action inconnu" - -#: sat.tac:592 -#, python-format -msgid "Sending jabber message to %s..." -msgstr "Envoi du message jabber à %s" - -#: sat.tac:619 -#, fuzzy, python-format -msgid "subsciption request [%(subs_type)s] for %(jid)s" -msgstr "demande d'inscription [%(type)s] pour %(jid)s" - -#: sat.tac:626 -msgid "sending automatic \"to\" subscription request" -msgstr "envoi automatique de la demande d'inscription \"to\"" - -#: sat.tac:657 -#, python-format -msgid "Feature found: %s" -msgstr "Fonctionnalité trouvée: %s" - -#: sat.tac:660 -#, python-format -msgid "Identity found: [%(category)s/%(type)s] %(identity)s" -msgstr "Identité trouvée: [%(category)s/%(type)s] %(identity)s" - -#: sat.tac:680 -msgid "type for actionResultExt must be DICT_DICT, fixing it" -msgstr "Le type pour actionResultExt doit être DICT_DICT, correction" - -#: sat.tac:694 -msgid "Attempt to register two callbacks for the same confirmation" -msgstr "Tentative de déclaration de 2 callbacks pour la même configuration" - -#: sat.tac:702 -#, python-format -msgid "Received confirmation answer for id [%(id)s]: %(success)s" -msgstr "Réponse pour confirmation reçu (id [%(id)s]): %(success)s" - -#: sat.tac:702 -msgid "accepted" -msgstr "accepté" - -#: sat.tac:702 -msgid "refused" -msgstr "refusé" - -#: sat.tac:704 -msgid "Received an unknown confirmation" -msgstr "Confirmation inconnue reçue" - -#: sat.tac:717 -msgid "Trying to remove an unknow progress callback" -msgstr "Tentative d'effacement d'une callback de progression inconnue." - -#: sat.tac:741 -msgid "Trying to remove an unknow general callback" -msgstr "Tentative d'effacement d'une callback générale inconnue." - -#: sat.tac:750 -#, fuzzy, python-format -msgid "Trying to call unknown function (%s)" -msgstr "Tentative d'appel d'une fonction inconnue" - -#: sat.tac:774 -#, fuzzy -msgid "Trying to access an unknown menu" -msgstr "Tentative d'accès à un profile inconnu" - -#: sat.tac:781 -#, fuzzy -msgid "Non-exsitant profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat.tac:788 -#, fuzzy, python-format -msgid "Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)" -msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" - -#: tools/memory.py:62 -#, fuzzy -msgid "Connection" -msgstr "Déconnexion..." - -#: tools/memory.py:63 -msgid "Register new account" -msgstr "Enregistrement d'un nouveau compte" - -#: tools/memory.py:64 -msgid "Connect on frontend startup" -msgstr "Connexion au démarrage des frontends" - -#: tools/memory.py:65 -msgid "Disconnect on frontend closure" -msgstr "Déconnexion à la fermeture des frontends" - -#: tools/memory.py:66 -msgid "Misc" -msgstr "Divers" - -#: tools/memory.py:85 -msgid "general params data loaded" -msgstr "Paramètres généraux chargés" - -#: tools/memory.py:87 -msgid "Can't load general params data !" -msgstr "Impossible de charger les paramètres généraux !" - -#: tools/memory.py:93 -msgid "individual params data loaded" -msgstr "Paramètres individuels chargés" - -#: tools/memory.py:95 -msgid "Can't load individual params data !" -msgstr "Impossible de charger les paramètres individuels !" - -#: tools/memory.py:132 -msgid "The profile name already exists" -msgstr "Ce nom de profile existe déjà" - -#: tools/memory.py:141 -msgid "Trying to delete an unknown profile" -msgstr "Tentative d'appel d'un profile inconnue" - -#: tools/memory.py:157 -msgid "No default profile, returning first one" -msgstr "Pas de profile par défaut, envoi du premier" - -#: tools/memory.py:162 -msgid "Trying to access an unknown profile" -msgstr "Tentative d'accès à un profile inconnu" - -#: tools/memory.py:202 -#, python-format -msgid "Can't determine default value for [%(category)s/%(name)s]: %(reason)s" -msgstr "" -"Impossible de déterminer la valeur par défaut pour [%(category)s/%(name)s]: %" -"(reason)s" - -#: tools/memory.py:215 tools/memory.py:233 -#, python-format -msgid "Requested param [%(name)s] in category [%(category)s] doesn't exist !" -msgstr "" -"Le paramètre demandé [%(name)s] dans la catégorie [%(category)s] n'existe " -"pas !" - -#: tools/memory.py:244 -msgid "Requesting a param for an non-existant profile" -msgstr "Demande d'un paramètre pour un profile inconnu" - -#: tools/memory.py:296 tools/memory.py:306 tools/memory.py:319 -msgid "Asking params for inexistant profile" -msgstr "Demande de paramètres pour un profile inconnu" - -#: tools/memory.py:365 -#, python-format -msgid "Requesting an unknown parameter (%(category)s/%(name)s)" -msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" - -#: tools/memory.py:377 -msgid "Trying to set parameter for an unknown profile" -msgstr "Tentative d'assigner un paramètre à un profile inconnu" - -#: tools/memory.py:391 -msgid "Memory manager init" -msgstr "Initialisation du gestionnaire de mémoire" - -#: tools/memory.py:418 -msgid "params template loaded" -msgstr "Modèle des paramètres chargé" - -#: tools/memory.py:420 -msgid "Can't load params template !" -msgstr "Impossible de charger le modèle des paramètres !" - -#: tools/memory.py:423 -msgid "No params template, using default template" -msgstr "Pas de modèle de paramètres, utilisation du modèle par défaut" - -#: tools/memory.py:428 -msgid "params loaded" -msgstr "paramètres chargés" - -#: tools/memory.py:430 -msgid "Can't load params !" -msgstr "Impossible de charger les paramètres !" - -#: tools/memory.py:437 -msgid "history loaded" -msgstr "Historique chargée" - -#: tools/memory.py:439 -msgid "Can't load history !" -msgstr "Impossible de charger l'historique !" - -#: tools/memory.py:446 -msgid "private values loaded" -msgstr "Données privées chargées" - -#: tools/memory.py:448 -msgid "Can't load private values !" -msgstr "Impossible de charger les données privées !" - -#: tools/memory.py:464 -msgid "params saved" -msgstr "Paramètres sauvés" - -#: tools/memory.py:467 -msgid "history saved" -msgstr "Historique sauvée" - -#: tools/memory.py:470 -msgid "private values saved" -msgstr "Données privées sauvées" - -#: tools/memory.py:513 -msgid "source JID not found !" -msgstr "JID source introuvable !" - -#: tools/memory.py:517 -msgid "dest JID not found !" -msgstr "JID destination introuvable !" - -#: tools/memory.py:544 -msgid "Trying to add a contact to a non-existant profile" -msgstr "Tentative d'ajout d'un contact à un profile inexistant" - -#: tools/memory.py:556 -msgid "Trying to delete a contact for a non-existant profile" -msgstr "Tentative de suppression d'un contact pour un profile inexistant" - -#: tools/memory.py:564 -msgid "Asking a contact for a non-existant profile" -msgstr "Demande d'un contact pour un profile inexistant" - -#: tools/memory.py:578 tools/memory.py:627 -msgid "Asking contacts for a non-existant profile" -msgstr "Demande de contacts pour un profile inexistant" - -#: tools/memory.py:589 -msgid "Trying to add presence status to a non-existant profile" -msgstr "Tentative d'ajout d'informations de présence à un profile inexistant" - -#: tools/memory.py:617 -msgid "Asking waiting subscriptions for a non-existant profile" -msgstr "Demande des inscriptions en attente pour un profile inexistant" - -#: tools/xml_tools.py:79 -msgid "INTERNAL ERROR: parameters xml not valid" -msgstr "ERREUR INTERNE: paramètres xml non valides" - -#: tools/xml_tools.py:86 -msgid "INTERNAL ERROR: params categories must have a name" -msgstr "ERREUR INTERNE: les catégories des paramètres doivent avoir un nom" - -#: tools/xml_tools.py:93 -msgid "INTERNAL ERROR: params must have a name" -msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" - -#: tools/xml_tools.py:127 -#, fuzzy, python-format -msgid "Unknown panel type [%s]" -msgstr "Type d'action inconnu" - -#: tools/xml_tools.py:150 -#, fuzzy, python-format -msgid "Unknown layout type [%s]" -msgstr "Type d'action inconnu" - -#: tools/xml_tools.py:286 -msgid "Trying to add a category without parent tabs layout" -msgstr "" -"Tentative d'ajout d'une catégorie sans disposition dans l'onglet parent" - -#: tools/xml_tools.py:289 -msgid "parent layout of a category is not tabs" -msgstr "la disposition parente d'une catégorie n'est pas \"tabs\" (onglets)" - -#: plugins/plugin_misc_cs.py:52 -msgid "" -"This plugin allow to manage your CouchSurfing account throught your SàT " -"frontend" -msgstr "" -"Cette extension vous permet de gérer votre compte CouchSurfing à travers " -"votre frontend SàT" - -#: plugins/plugin_misc_cs.py:71 -#, fuzzy -msgid "Plugin CS initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: plugins/plugin_misc_cs.py:76 -msgid "Plugin" -msgstr "Extension" - -#: plugins/plugin_misc_cs.py:76 -msgid "Launch CoushSurfing mangement interface" -msgstr "Lancement de l'interface de gestion de CouchSurfing" - -#: plugins/plugin_misc_cs.py:84 -msgid "" -"Impossible to contact CS website, please check your login/password, " -"connection or try again later" -msgstr "" -"Impossible de contacter le site CouchSurfing, veuillez vérifier vos " -"identifiant/mot de passe, votre connexion, ou essayez un peu plus tard" - -#: plugins/plugin_misc_cs.py:92 -msgid "" -"You have to fill your CouchSurfing login & password in parameters before " -"using this interface" -msgstr "" -"Vous devez remplir vos identifiant & mot de passe CouchSurfing dans les " -"paramètres avant d'utiliser cette interface" - -#: plugins/plugin_misc_cs.py:167 -msgid "Messages" -msgstr "Messages" - -#: plugins/plugin_misc_cs.py:168 -#, python-format -msgid "" -"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\n" -"If you want to send a message, select the recipient(s) in the list below" -msgstr "" -"Bonjour %(name)s, vous avez %(nb_message)i message%(plural_mess)s non lus et " -"%(unread_CR_mess)s requête%(plural_CR)s d'hébergement en attente\n" -"Si vous voulez envoyer un message, sélectionnez le(s) destinataire(s) dans " -"la liste ci-dessous." - -#: plugins/plugin_misc_cs.py:170 -#, python-format -msgid "Show unread message%(plural)s in external web browser" -msgstr "Afficher le%(plural)s message%(plural)s non lu dans un navigateur web" - -#: plugins/plugin_misc_cs.py:173 -msgid "Subject" -msgstr "Sujet" - -#: plugins/plugin_misc_cs.py:176 -msgid "Message" -msgstr "Message" - -#: plugins/plugin_misc_cs.py:179 -msgid "send" -msgstr "envoyer" - -#: plugins/plugin_misc_cs.py:200 -#, python-format -msgid "" -"CS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" -msgstr "" -"Amis CS trouvé: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" - -#: plugins/plugin_misc_cs.py:225 -msgid "" -"INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has " -"been modified ?" -msgstr "" -"ERREUR INTERNE: aucune confirmation du message envoyée par CS, peut être que " -"le site a été modifié ?" - -#: plugins/plugin_misc_cs.py:236 -#, fuzzy, python-format -msgid "Sending message to %s" -msgstr "Envoi du message jabber à %s" - -#: plugins/plugin_misc_cs.py:237 -#, python-format -msgid "" -"\n" -"subject: %(subject)s\n" -"message: \n" -"---\n" -"%(message)s\n" -"---\n" -"\n" -msgstr "" -"\n" -"sujet: %(subject)s\n" -"message: \n" -"---\n" -"%(message)s\n" -"---\n" -"\n" - -#: plugins/plugin_misc_cs.py:243 -msgid "Message sent" -msgstr "Message envoyé" - -#: plugins/plugin_misc_cs.py:244 -msgid "The message has been sent to every recipients" -msgstr "Le message a été envoyé à tous les destinataires" - -#: plugins/plugin_misc_cs.py:257 -msgid "There is not recipient selected for this message !" -msgstr "Il n'y a aucun destinataire pour ce message !" - -#: plugins/plugin_misc_cs.py:264 -#, python-format -msgid "sending message to %(friends)s with subject [%(subject)s]" -msgstr "Envoi du message à %(friends)s avec le sujet [%(subject)s]" - -#: plugins/plugin_misc_tarot.py:56 -#, fuzzy -msgid "Implementation of Tarot card game" -msgstr "Implementation de vcard-temp" - -#: plugins/plugin_misc_tarot.py:63 -#, fuzzy -msgid "Plugin Tarot initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Passe" -msgstr "Passe" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Petite" -msgstr "Petite" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Garde" -msgstr "Garde" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Garde Sans" -msgstr "Garde Sans" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Garde Contre" -msgstr "Garde Contre" - -#: plugins/plugin_misc_tarot.py:126 -msgid "contrat selection" -msgstr "Sélection du contrat" - -#: plugins/plugin_misc_tarot.py:139 -msgid "scores" -msgstr "points" - -#: plugins/plugin_misc_tarot.py:221 plugins/plugin_misc_tarot.py:252 -#, python-format -msgid "" -"Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for " -"Excuse compensation" -msgstr "" -"Le joueur %(excuse_owner)s donne %(card_waited)s à %(player_waiting)s en " -"compensation pour l'Excuse" - -#: plugins/plugin_misc_tarot.py:257 -#, python-format -msgid "" -"%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is " -"waiting for one" -msgstr "" -"%(excuse_owner)s garde l'Excuse mais n'a aucune carte à donner, %(winner)s " -"en attend une" - -#: plugins/plugin_misc_tarot.py:305 -msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" -msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" - -#: plugins/plugin_misc_tarot.py:324 -#, python-format -msgid "" -"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" -msgstr "" -"L'attaquant (%(attaquant)s) fait %(points)i et joue pour %(point_limit)i (%" -"(nb_bouts)s bout%(plural)s%(separator)s%(bouts)s): il %(victory)s" - -#: plugins/plugin_misc_tarot.py:327 -#, python-format -msgid "" -"\n" -"--\n" -"%(player)s:\n" -"score for this game ==> %(score_game)i\n" -"total score ==> %(total_score)i" -msgstr "" -"\n" -"--\n" -"%(player)s:\n" -"points pour cette partie ==> %(score_game)i\n" -"point au total ==> %(total_score)i" - -#: plugins/plugin_misc_tarot.py:385 -msgid "Internal error: unmanaged game stage" -msgstr "ERREUR INTERNE: état de jeu inconnu" - -#: plugins/plugin_misc_tarot.py:402 -msgid "Creating Tarot game" -msgstr "Construction du jeu de Tarot" - -#: plugins/plugin_misc_tarot.py:406 plugins/plugin_misc_tarot.py:431 -#: plugins/plugin_misc_tarot.py:448 plugins/plugin_misc_tarot.py:465 -#, python-format -msgid "profile %s is unknown" -msgstr "le profil %s est inconnu" - -#: plugins/plugin_misc_tarot.py:409 -#, python-format -msgid "Tarot game already started in room %s" -msgstr "Un jeu de Tarot est déjà lancé dans le salon %s" - -#: plugins/plugin_misc_tarot.py:450 -#, python-format -msgid "contrat [%(contrat)s] choosed by %(profile)s" -msgstr "contrat [%(contrat)s] choisi par %(profile)s" - -#: plugins/plugin_misc_tarot.py:467 -#, python-format -msgid "Cards played by %(profile)s: [%(cards)s]" -msgstr "Cartes jouées par %(profile)s: [%(cards)s]" - -#: plugins/plugin_misc_tarot.py:475 -msgid "new Tarot game" -msgstr "nouveau jeu de Tarot" - -#: plugins/plugin_misc_tarot.py:532 -#, python-format -msgid "Player %(player)s is ready to start [status: %(status)s]" -msgstr "Le joueur %(player)s est prêt à commencer [statut: %(status)s]" - -#: plugins/plugin_misc_tarot.py:567 -#, python-format -msgid "%(player)s win the bid with %(contrat)s" -msgstr "%(player)s remporte l'enchère avec %(contrat)s" - -#: plugins/plugin_misc_tarot.py:591 -msgid "tarot: chien received" -msgstr "tarot: chien reçu" - -#: plugins/plugin_misc_tarot.py:646 -#, python-format -msgid "The winner of this trick is %s" -msgstr "le vainqueur de cette main est %s" - -#: plugins/plugin_misc_tarot.py:691 -#, python-format -msgid "Unmanaged error type: %s" -msgstr "type d'erreur inconnu: %s" - -#: plugins/plugin_misc_tarot.py:693 -#, python-format -msgid "Unmanaged card game element: %s" -msgstr "élément de jeu de carte inconnu: %s" - -#: plugins/plugin_xep_0045.py:62 -#, fuzzy -msgid "Implementation of Multi-User Chat" -msgstr "" -"Implémentation de l'initialisation de flux pour le transfert de fichier " - -#: plugins/plugin_xep_0045.py:68 -#, fuzzy -msgid "Plugin XEP_0045 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: plugins/plugin_xep_0045.py:85 -#, python-format -msgid "Unknown or disconnected profile (%s)" -msgstr "Profil inconnu ou déconnecté (%s)" - -#: plugins/plugin_xep_0045.py:99 -msgid "Error when joining the room" -msgstr "Erreur en tentant de rejoindre le salon" - -#: plugins/plugin_xep_0045.py:101 -msgid "Group chat error" -msgstr "Erreur de salon de discussion" - -#: plugins/plugin_xep_0045.py:137 -#, python-format -msgid "%(profile)s is already in room %(room_jid)s" -msgstr "%(profile)s est déjà dans le salon %(room_jid)s" - -#: plugins/plugin_xep_0045.py:139 -#, python-format -msgid "[%(profile)s] is joining room %(room)s with nick %(nick)s" -msgstr "[%(profile)s] rejoint %(room)s avec %(nick)s" - -#: plugins/plugin_xep_0045.py:164 -#, python-format -msgid "user %(nick)s has joined room (%(room_id)s)" -msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" - -#: plugins/plugin_xep_0045.py:169 -#, python-format -msgid "user %(nick)s left room (%(room_id)s)" -msgstr "L'utilisateur %(nick)s a quitté le salon (%(room_id)s)" - -#: plugins/plugin_xep_0045.py:178 -#, python-format -msgid "New subject for room (%(room_id)s): %(subject)s" -msgstr "Nouveau sujet pour le salon (%(room_id)s): %(subject)s" - -#: plugins/plugin_xep_0054.py:62 -msgid "Implementation of vcard-temp" -msgstr "Implementation de vcard-temp" - -#: plugins/plugin_xep_0054.py:68 -msgid "Plugin XEP_0054 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: plugins/plugin_xep_0054.py:111 -#, python-format -msgid "Photo of type [%s] found" -msgstr "Photo du type [%s] trouvée" - -#: plugins/plugin_xep_0054.py:113 -msgid "Decoding binary" -msgstr "Décodage des données" - -#: plugins/plugin_xep_0054.py:120 -#, python-format -msgid "file saved to %s" -msgstr "fichier enregistré dans %s" - -#: plugins/plugin_xep_0054.py:122 -#, python-format -msgid "file [%s] already in cache" -msgstr "fichier [%s] déjà en cache" - -#: plugins/plugin_xep_0054.py:128 -msgid "parsing vcard" -msgstr "Analyse de la vcard" - -#: plugins/plugin_xep_0054.py:154 -#, python-format -msgid "FIXME: [%s] VCard tag is not managed yet" -msgstr "CORRIGEZ-MOI: la balise VCard [%s] VCard n'est pas encore gérée" - -#: plugins/plugin_xep_0054.py:160 -msgid "VCard found" -msgstr "VCard trouvée" - -#: plugins/plugin_xep_0054.py:166 -msgid "FIXME: vCard not found as first child element" -msgstr "CORRIGEZ-MOI: la vCard n'est pas le premier élément enfant" - -#: plugins/plugin_xep_0054.py:171 -#, python-format -msgid "Can't find VCard of %s" -msgstr "Impossible de trouver la VCard de %s" - -#: plugins/plugin_xep_0054.py:180 -msgid "Asking vcard for an non-existant or not connected profile" -msgstr "Demande de vcard pour un profile inexistant ou non connecté" - -#: plugins/plugin_xep_0054.py:183 -#, python-format -msgid "Asking for %s's VCard" -msgstr "Demande de la VCard de %s" - -#: plugins/plugin_xep_0054.py:198 -#, python-format -msgid "Asking for an uncached avatar [%s]" -msgstr "Demande d'un avatar qui n'est pas en cache [%s]" - -#: plugins/plugin_xep_0054.py:245 -msgid "New avatar found, requesting vcard" -msgstr "Nouvel avatar trouvé, demande de vcard" - -#: plugins/plugin_xep_0065.py:89 -msgid "Implementation of SOCKS5 Bytestreams" -msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" - -#: plugins/plugin_xep_0065.py:135 -msgid "Protocol init" -msgstr "Initialisation du protocole" - -#: plugins/plugin_xep_0065.py:217 -#, python-format -msgid "Adding connection: %(address)s, %(connection)s" -msgstr "Ajout d'une connexion: %(address)s, %(connection)s" - -#: plugins/plugin_xep_0065.py:313 -#, python-format -msgid "Saving file in %s." -msgstr "Sauvegarde du fichier dans %s." - -#: plugins/plugin_xep_0065.py:364 -msgid "File transfer completed, closing connection" -msgstr "Transfert de fichier terminé, fermeture de la connexion" - -#: plugins/plugin_xep_0065.py:442 -msgid "Socks 5 server connection started" -msgstr "Connexion du serveur SOCKS 5 démarrée" - -#: plugins/plugin_xep_0065.py:445 -#, python-format -msgid "Socks 5 server connection lost (reason: %s)" -msgstr "Connexion du serveur SOCKS5 perdue (raison: %s)" - -#: plugins/plugin_xep_0065.py:452 -msgid "Socks 5 client connection started" -msgstr "Connexion du client SOCKS 5 démarrée" - -#: plugins/plugin_xep_0065.py:455 -#, python-format -msgid "Socks 5 client connection lost (reason: %s)" -msgstr "Connexion du client SOCKS5 perdue (raison: %s)" - -#: plugins/plugin_xep_0065.py:472 -msgid "Plugin XEP_0065 initialization" -msgstr "Initialisation du plugin XEP_0065" - -#: plugins/plugin_xep_0065.py:474 -msgid "registering" -msgstr "enregistrement" - -#: plugins/plugin_xep_0065.py:484 -#, python-format -msgid "Launching Socks5 Stream server on port %d" -msgstr "Lancement du serveur de flux Socks5 sur le port %d" - -#: plugins/plugin_xep_0065.py:500 -msgid "Launching socks5 initiator" -msgstr "Lancement de socks5 en mode initiateur" - -#: plugins/plugin_xep_0065.py:515 -#, fuzzy, python-format -msgid "Stream proposed: host=[%(host)s] port=[%(port)s]" -msgstr "Flux proposé: serveur=[%(host)s] port=[%(post)s]" - -#: plugins/plugin_xep_0065.py:531 -msgid "activating stream" -msgstr "Lancement du flux" - -#: plugins/plugin_xep_0077.py:41 -msgid "Implementation of in-band registration" -msgstr "Implémentation de l'enregistrement en ligne" - -#: plugins/plugin_xep_0077.py:47 -msgid "Plugin XEP_0077 initialization" -msgstr "Initialisation du plugin XEP_0077" - -#: plugins/plugin_xep_0077.py:62 -msgid "No data form found" -msgstr "Aucune donnée trouvée" - -#: plugins/plugin_xep_0077.py:64 -msgid "This gateway can't be managed by SàT, sorry :(" -msgstr "Ce transport ne peut être gérée par SàT, désolé :(" - -#: plugins/plugin_xep_0077.py:85 -msgid "Your are now unregistred" -msgstr "Vous êtes maintenant désinscrit" - -#: plugins/plugin_xep_0077.py:89 -#, python-format -msgid "Unregistration failure: %s" -msgstr "Échec de la désinscription: %s" - -#: plugins/plugin_xep_0077.py:93 -#, python-format -msgid "Unregistration failed: %s" -msgstr "Échec de la désinscription: %s" - -#: plugins/plugin_xep_0077.py:116 -msgid "Registration failed" -msgstr "Échec de l'inscription" - -#: plugins/plugin_xep_0077.py:134 plugins/plugin_xep_0096.py:154 -msgid "Asking for an non-existant or not connected profile" -msgstr "Demande d'un profile inexistant ou non connecté" - -#: plugins/plugin_xep_0077.py:137 -#, python-format -msgid "Asking registration for [%s]" -msgstr "Demande d'enregistrement pour [%s]" - -#: plugins/plugin_xep_0096.py:52 -msgid "Implementation of SI File Transfert" -msgstr "" -"Implémentation de l'initialisation de flux pour le transfert de fichier " - -#: plugins/plugin_xep_0096.py:58 -msgid "Plugin XEP_0096 initialization" -msgstr "Initialisation du plugin XEP_0096" - -#: plugins/plugin_xep_0096.py:67 -msgid "XEP-0096 management" -msgstr "Gestion de XEP-0096" - -#: plugins/plugin_xep_0096.py:75 -#, python-format -msgid "File proposed: name=[%(name)s] size=%(size)s" -msgstr "Fichier proposé: nom=[%(name)s] taille=%(size)s" - -#: plugins/plugin_xep_0096.py:91 -#, python-format -msgid "Transfert [%s] refused" -msgstr "Transfert [%s] refusé" - -#: plugins/plugin_xep_0096.py:96 -#, python-format -msgid "Transfert [%s] accepted" -msgstr "Transfert [%s] accepté" - -#: plugins/plugin_xep_0096.py:99 -msgid "Approved unknow id !" -msgstr "id inconnue approuvée !" - -#: plugins/plugin_xep_0096.py:111 -msgid "Feature negociation" -msgstr "Négociation de fonctionnalités" - -#: plugins/plugin_xep_0100.py:38 -msgid "Implementation of Gateways protocol" -msgstr "Implémentation du protocole de transports" - -#: plugins/plugin_xep_0100.py:44 -msgid "Gateways plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: plugins/plugin_xep_0100.py:54 -#, python-format -msgid "All items checked for id [%s]" -msgstr "Tous les points ont été vérifiés pour l'id [%s]" - -#: plugins/plugin_xep_0100.py:65 -#, python-format -msgid "Found gateway (%(jid)s): %(identity)s" -msgstr "Transport trouvé (%(jid)s): %(identity)s" - -#: plugins/plugin_xep_0100.py:76 -#, fuzzy, python-format -msgid "Error when discovering [%(jid)s]: %(error)s" -msgstr "Erreur en analysant [%(jid)s]: %(condition)s" - -#: plugins/plugin_xep_0100.py:85 -msgid "No gateway found" -msgstr "Aucun transport trouvé" - -#: plugins/plugin_xep_0100.py:92 -#, python-format -msgid "item found: %s" -msgstr "object trouvé: %s" - -#: plugins/plugin_xep_0100.py:97 -#, fuzzy, python-format -msgid "Error when discovering [%(target)s]: %(condition)s" -msgstr "Erreur en analysant [%(jid)s]: %(condition)s" - -#: plugins/plugin_xep_0100.py:98 -#, python-format -msgid "Error while trying to discover %(target)s gateways: %(error_mess)s" -msgstr "Erreur en essayant d'analyser %(target)s portails: %(error_mess)s" - -#: plugins/plugin_xep_0100.py:104 -msgid "Registration successful, doing the rest" -msgstr "Inscription réussie, lancement du reste de la procédure" - -#: plugins/plugin_xep_0100.py:124 -#, fuzzy, python-format -msgid "find gateways (target = %(target)s, profile = %(profile)s)" -msgstr "transports trouvée (cible = %s)" - -#~ msgid "presence update for [%s]" -#~ msgstr "mise à jour de l'information de présence pour [%s]"
--- a/frontends/fr.po Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1095 +0,0 @@ -# SàT frontends french translation file. -# Copyright (C) 2009, 2010 Jérôme Poisson -# This file is distributed under the same license as the SàT frontends packages. -# Jérôme Poisson <goffi@goffi.org>, 2009, 2010. -# Goffi <goffi@goffi.org>, 2010. -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 0.0.2D\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-08-19 21:17+0800\n" -"PO-Revision-Date: 2010-08-19 21:48+0800\n" -"Last-Translator: Goffi <goffi@goffi.org>\n" -"Language-Team: French <goffi@goffi.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: primitivus/primitivus:188 -msgid "Pleeeeasse, I can't even breathe !" -msgstr "Pitiééééééééé, je ne peux même pas respirer !" - -#: primitivus/primitivus:200 wix/profile.py:84 -msgid "General" -msgstr "Général" - -#: primitivus/primitivus:201 primitivus/profile_manager.py:50 -#: wix/profile_manager.py:70 -msgid "Connect" -msgstr "Connexion" - -#: primitivus/primitivus:202 -#, fuzzy -msgid "Disconnect" -msgstr "Déconnecté" - -#: primitivus/primitivus:203 -#, fuzzy -msgid "Parameters" -msgstr "&Paramètres" - -#: primitivus/primitivus:204 primitivus/primitivus:495 -msgid "About" -msgstr "À propos" - -#: primitivus/primitivus:205 -#, fuzzy -msgid "Exit" -msgstr "Quitter" - -#: primitivus/primitivus:206 -#, fuzzy -msgid "Contact" -msgstr "&Contacts" - -#: primitivus/primitivus:207 -#, fuzzy -msgid "Add contact" -msgstr "&Ajouter un contact" - -#: primitivus/primitivus:208 -#, fuzzy -msgid "Remove contact" -msgstr "Supp&rimer un contact" - -#: primitivus/primitivus:209 -#, fuzzy -msgid "Communication" -msgstr "&Communication" - -#: primitivus/primitivus:210 -msgid "Join room" -msgstr "Rejoindre un salon" - -#: primitivus/primitivus:211 -msgid "Find Gateways" -msgstr "Chercher les transports" - -#: primitivus/primitivus:224 -#, fuzzy -msgid "Main menu" -msgstr "Construction des menus" - -#: primitivus/primitivus:295 primitivus/primitivus:323 -#, fuzzy -msgid "Chat menu" -msgstr "Construction des menus" - -#: primitivus/primitivus:369 wix/main_window.py:218 -#, python-format -msgid "unmanaged dialog type: %s" -msgstr "type de discussion non géré: %s" - -#: primitivus/primitivus:384 -msgid "INTERNAL ERROR: Unexpected class for main widget's footer" -msgstr "" -"ERREUR INTERNE: Classe inattendue pour le pied de page du widget principal" - -#: primitivus/primitivus:392 wix/main_window.py:277 -msgid "unknown id, ignoring" -msgstr "id inconnue, on l'ignore" - -#: primitivus/primitivus:398 wix/main_window.py:299 -msgid "XML user interface received" -msgstr "Interface utilisateur XML reçue" - -#: primitivus/primitivus:401 wix/main_window.py:302 -msgid "Form" -msgstr "Formulaire" - -#: primitivus/primitivus:403 wix/main_window.py:304 -msgid "Registration" -msgstr "Inscription" - -#: primitivus/primitivus:413 primitivus/primitivus:439 -#: primitivus/primitivus:449 primitivus/primitivus:489 -#: primitivus/gateways.py:52 wix/card_game.py:125 wix/main_window.py:292 -#: wix/main_window.py:419 wix/main_window.py:442 -msgid "Error" -msgstr "Erreur" - -#: primitivus/primitivus:427 wix/main_window.py:321 -#, python-format -msgid "FIXME FIXME FIXME: type [%s] not implemented" -msgstr "CORRIGER-MOI: le type [%s] n'est pas implémenté" - -#: primitivus/primitivus:437 primitivus/primitivus:447 wix/main_window.py:409 -#: wix/main_window.py:469 -#, python-format -msgid "'%s' is an invalid JID !" -msgstr "'%s' n'est pas un JID valide !" - -#: primitivus/primitivus:453 wix/main_window.py:432 -#, python-format -msgid "Unsubscribing %s presence" -msgstr "Désinscription à la présence de %s" - -#: primitivus/primitivus:473 wix/main_window.py:462 -msgid "Entering a MUC room" -msgstr "Entrée dans le salon MUC" - -#: primitivus/primitivus:473 wix/main_window.py:461 -#, fuzzy -msgid "Please enter MUC's JID" -msgstr "Veuillez entrer le JID de votre nouveau contact" - -#: primitivus/primitivus:477 wix/main_window.py:472 -msgid "Find Gateways request" -msgstr "Demande de recherche de transports" - -#: primitivus/primitivus:483 wix/main_window.py:402 -msgid "Adding a contact" -msgstr "Ajout d'un contact" - -#: primitivus/primitivus:483 wix/main_window.py:401 -msgid "Please enter new contact JID" -msgstr "Veuillez entrer le JID de votre nouveau contact" - -#: primitivus/primitivus:489 -#, fuzzy -msgid "You have not selected any contact to delete !" -msgstr "Vous n'avez sélectionné aucun contact !" - -#: primitivus/primitivus:491 -#, fuzzy, python-format -msgid "Are you sure you want to delete the contact [%s] ?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: primitivus/card_game.py:262 wix/card_game.py:103 -msgid "Please choose your contrat" -msgstr "Veuillez choisir votre contrat" - -#: primitivus/card_game.py:277 wix/card_game.py:108 -msgid "You win \\o/" -msgstr "Victoire \\o/" - -#: primitivus/card_game.py:277 wix/card_game.py:108 -msgid "You loose :(" -msgstr "Vous perdez :(" - -#: primitivus/card_game.py:288 wix/card_game.py:125 -msgid "Cards played are invalid !" -msgstr "Les cartes jouées sont invalides !" - -#: primitivus/card_game.py:317 wix/card_game.py:233 -msgid "Do you put these cards in chien ?" -msgstr "Voulez-vous placer ces cartes au chien ?" - -#: primitivus/chat.py:131 -msgid "Game" -msgstr "Jeu" - -#: primitivus/chat.py:134 -#, fuzzy -msgid "Action" -msgstr "&Action" - -#: primitivus/chat.py:134 -#, fuzzy -msgid "Send file" -msgstr "Envoi un fichier" - -#: primitivus/chat.py:266 wix/chat.py:260 -msgid "Can't start game" -msgstr "Impossible de démarrer le jeu" - -#: primitivus/chat.py:266 wix/chat.py:260 -msgid "You need to be exactly 4 peoples in the room to start a Tarot game" -msgstr "" -"Vous devez être exactement 4 personnes dans le salon pour commencer un jeu " -"de Tarot" - -#: primitivus/contact_list.py:39 -#, fuzzy -msgid "Contacts" -msgstr "&Contacts" - -#: primitivus/custom_widgets.py:164 -msgid "WARNING: unknown text type" -msgstr "ATTENTION: type de texte inconnu" - -#: primitivus/custom_widgets.py:739 primitivus/files_management.py:156 -#: primitivus/xmlui.py:178 primitivus/xmlui.py:186 -msgid "Cancel" -msgstr "Annuler" - -#: primitivus/custom_widgets.py:740 primitivus/custom_widgets.py:748 -msgid "Ok" -msgstr "Ok" - -#: primitivus/custom_widgets.py:744 -msgid "Yes" -msgstr "Oui" - -#: primitivus/custom_widgets.py:745 -msgid "No" -msgstr "Non" - -#: primitivus/custom_widgets.py:961 -msgid "INTERNAL ERROR: Tab not found" -msgstr "ERREUR INTERNE: Onglet non trouvé" - -#: primitivus/files_management.py:105 -msgid "Impossible to list directory" -msgstr "Impossible de lister les répertoires" - -#: primitivus/files_management.py:130 -#, fuzzy -msgid "Please select a file" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: primitivus/files_management.py:137 -msgid "Path: " -msgstr "Chemin:" - -#: primitivus/files_management.py:150 -msgid "Bookmarks" -msgstr "Favoris" - -#: primitivus/files_management.py:199 -msgid "No GTK bookmarks file found" -msgstr "Aucun fichier favori pour GTK trouvé" - -#: primitivus/files_management.py:209 -msgid "No KDE bookmarks file found" -msgstr "Aucun fichier favori pour KDE trouvé" - -#: primitivus/gateways.py:30 quick_frontend/quick_gateways.py:28 -#: wix/gateways.py:33 -msgid "Gateways manager" -msgstr "Gestionnaire de transport" - -#: primitivus/gateways.py:40 wix/gateways.py:88 -msgid "Use external XMPP server: " -msgstr "Utiliser un autre serveur XMPP:" - -#: primitivus/gateways.py:41 wix/gateways.py:91 -msgid "GO !" -msgstr "C'est parti !" - -#: primitivus/gateways.py:52 -#, fuzzy -msgid "You must enter an external server JID" -msgstr "Utiliser un autre serveur XMPP:" - -#: primitivus/gateways.py:70 wix/gateways.py:148 -msgid "Register" -msgstr "Inscription" - -#: primitivus/gateways.py:73 wix/gateways.py:152 -msgid "Unregister" -msgstr "Désinscription" - -#: primitivus/profile_manager.py:36 -#, fuzzy -msgid "Login:" -msgstr "Identifiant" - -#: primitivus/profile_manager.py:37 wix/profile_manager.py:67 -msgid "Password:" -msgstr "Mot de passe:" - -#: primitivus/profile_manager.py:42 wix/profile_manager.py:52 -msgid "New" -msgstr "Nouveau" - -#: primitivus/profile_manager.py:43 wix/profile_manager.py:53 -msgid "Delete" -msgstr "Suppression" - -#: primitivus/profile_manager.py:55 -#, fuzzy -msgid "Profile Manager" -msgstr "Mauvais nom de profile" - -#: primitivus/profile_manager.py:84 wix/profile_manager.py:96 -msgid "New profile" -msgstr "Nouveau profile" - -#: primitivus/profile_manager.py:84 -#, fuzzy -msgid "Please enter a new profile name" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: primitivus/profile_manager.py:88 -#, fuzzy, python-format -msgid "Are you sure you want to delete the profile %s ?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: primitivus/profile_manager.py:102 wix/profile_manager.py:130 -msgid "No profile selected" -msgstr "Aucun profile sélectionné" - -#: primitivus/profile_manager.py:102 -#, fuzzy -msgid "You need to create and select a profile before connecting" -msgstr "" -"Vous devez sélectionner un profile ou en créer un nouveau avant de vous " -"connecter." - -#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 -#: wix/profile_manager.py:133 -msgid "Bad profile name" -msgstr "Mauvais nom de profile" - -#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 -#: wix/profile_manager.py:133 -msgid "A profile name can't start with a @" -msgstr "Un nom de profile ne peut pas commencer avec un @" - -#: primitivus/progress.py:35 -msgid "Clear progress list" -msgstr "Effacer la liste" - -#: primitivus/xmlui.py:67 wix/xmlui.py:61 -msgid "Unmanaged tag" -msgstr "Tab inconnu" - -#: primitivus/xmlui.py:80 wix/xmlui.py:75 -msgid "text node has no child !" -msgstr "le nœud text n'a pas d'enfant !" - -#: primitivus/xmlui.py:105 wix/xmlui.py:107 -#, python-format -msgid "FIXME FIXME FIXME: type [%s] is not implemented" -msgstr "" -"CORRIGEZ-MOI CORRIGEZ-MOI CORRIGEZ-MOI: le type [%s] n'est pas implémenté" - -#: primitivus/xmlui.py:135 wix/xmlui.py:138 -msgid "Unknown layout, using default one" -msgstr "Disposition inconnue, utilisation de celle par defaut" - -#: primitivus/xmlui.py:148 wix/xmlui.py:157 -#, fuzzy -msgid "Unknown tag" -msgstr "Messagerie inconnue" - -#: primitivus/xmlui.py:176 wix/xmlui.py:179 -msgid "Submit" -msgstr "Envoyer" - -#: primitivus/xmlui.py:185 -msgid "Save" -msgstr "Sauvegarder" - -#: primitivus/xmlui.py:204 -#, python-format -msgid "INTERNAL ERROR: Unmanaged show_type (%s)" -msgstr "ERREUR INTERNE: show_type inconnu (%s)" - -#: primitivus/xmlui.py:245 wix/xmlui.py:230 -msgid "The form data is not sent back, the type is not managed properly" -msgstr "" -"Les données du formulaire ne sont pas envoyées, il y a une erreur dans la " -"gestion du type" - -#: quick_frontend/quick_app.py:44 -msgid "Can't connect to SàT backend, are you sure it's launched ?" -msgstr "" -"Impossible de se connecter au démon SàT, êtes vous sûr qu'il est lancé ?" - -#: quick_frontend/quick_app.py:84 -#, fuzzy, python-format -msgid "Trying to plug an unknown profile (%s)" -msgstr "Tentative de mise à jour d'un contact inconnu: %s" - -#: quick_frontend/quick_app.py:90 -msgid "" -"\n" -" %prog [options]\n" -"\n" -" %prog --help for options list\n" -" " -msgstr "" -"\n" -" %prog [options]\n" -"\n" -" %prog --help pour la liste des options\n" -" " - -#: quick_frontend/quick_app.py:97 -msgid "Select the profile to use" -msgstr "Veuillez sélectionner le profile à utiliser" - -#: quick_frontend/quick_app.py:107 -msgid "There is already one profile plugged (we are in single profile mode) !" -msgstr "Il y a déjà un profile utilisé (nous comme en mode profile unique) !" - -#: quick_frontend/quick_app.py:111 -msgid "The profile asked doesn't exist" -msgstr "Le profile demandé n'existe pas" - -#: quick_frontend/quick_app.py:114 -msgid "The profile is already plugged" -msgstr "Le profile est déjà utilisé" - -#: quick_frontend/quick_app.py:167 -msgid "This profile is not plugged" -msgstr "Ce profile n'est pas utilisé" - -#: quick_frontend/quick_app.py:178 -msgid "Connected" -msgstr "Connecté" - -#: quick_frontend/quick_app.py:187 -msgid "Disconnected" -msgstr "Déconnecté" - -#: quick_frontend/quick_app.py:221 -#, fuzzy, python-format -msgid "" -"presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%" -"(statuses)s) [profile:%(profile)s]" -msgstr "" -"Mise à jour de l'information de présence pour %(jid)s (show=%(show)s, " -"statuses=%(statuses)s)" - -#: quick_frontend/quick_app.py:242 -#, python-format -msgid "Watched jid [%s] is connected !" -msgstr "Le jid surveillé [%s] est connecté !" - -#: quick_frontend/quick_app.py:267 -#, python-format -msgid "Room [%(room_name)s] joined by %(profile)s, users presents:%(users)s" -msgstr "" -"%(profile)s a rejoint le salon [%(room_name)s], utilisateurs présents:%" -"(users)s" - -#: quick_frontend/quick_app.py:282 -#, python-format -msgid "user [%(user_nick)s] joined room [%(room_jid)s]" -msgstr "l'utilisateur [%(user_nick)s] a rejoint le salon [%(room_jid)s]" - -#: quick_frontend/quick_app.py:291 -#, python-format -msgid "user [%(user_nick)s] left room [%(room_jid)s]" -msgstr "l'utilisateur [%(user_nick)s] a quitté le salon [%(room_jid)s]" - -#: quick_frontend/quick_app.py:300 -#, python-format -msgid "new subject for room [%(room_jid)s]: %(subject)s" -msgstr "nouveau sujet pour le salon [%(room_jid)s]: %(subject)s" - -#: quick_frontend/quick_app.py:305 -msgid "Tarot Game Started \\o/" -msgstr "Jeu de Tarot commencé \\o/" - -#: quick_frontend/quick_app.py:308 -#, python-format -msgid "" -"new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %" -"(players)s" -msgstr "" -"nouveau jeu de Tarot lancé par [%(referee)s] dans le salon [%(room_jid)s] " -"avec %(players)s" - -#: quick_frontend/quick_app.py:313 -msgid "New Tarot Game" -msgstr "Nouveau jeu de Tarot" - -#: quick_frontend/quick_app.py:321 -#, fuzzy -msgid "Tarot: need to select a contrat" -msgstr "Vous n'avez sélectionné aucun contact !" - -#: quick_frontend/quick_app.py:328 -#, fuzzy -msgid "Show cards" -msgstr "Affichage du chat" - -#: quick_frontend/quick_app.py:335 -msgid "My turn to play" -msgstr "C'est à moi de jouer" - -#: quick_frontend/quick_app.py:343 -#, fuzzy -msgid "Tarot: score received" -msgstr "Formulaire reçu" - -#: quick_frontend/quick_app.py:350 -#, python-format -msgid "Card(s) played (%(player)s): %(cards)s" -msgstr "Carte(s) jouée(s) (%(player)s): %(cards)s" - -#: quick_frontend/quick_app.py:357 -#, python-format -msgid "Cards played are not valid: %s" -msgstr "Les cartes jouées sont invalides: %s" - -#: quick_frontend/quick_app.py:375 -#, python-format -msgid "The contact %s has accepted your subscription" -msgstr "Le contact %s a accepté votre inscription" - -#: quick_frontend/quick_app.py:375 quick_frontend/quick_app.py:381 -msgid "Subscription confirmation" -msgstr "Confirmation d'inscription" - -#: quick_frontend/quick_app.py:378 -#, python-format -msgid "The contact %s has refused your subscription" -msgstr "Le contact %s a refusé votre inscription" - -#: quick_frontend/quick_app.py:378 -msgid "Subscription refusal" -msgstr "Refus d'inscription" - -#: quick_frontend/quick_app.py:381 -#, python-format -msgid "" -"The contact %s wants to subscribe to your presence.\n" -"Do you accept ?" -msgstr "" -"Le contact %s veut s'inscrire à vos informations de présence\n" -"Acceptez vous ?" - -#: quick_frontend/quick_app.py:392 -#, python-format -msgid "param update: [%(namespace)s] %(name)s = %(value)s" -msgstr "Le paramètre [%(namespace)s] %(name)s vaut désormais %(value)s" - -#: quick_frontend/quick_app.py:394 -#, python-format -msgid "Changing JID to %s" -msgstr "Changement du JID pour %s" - -#: quick_frontend/quick_chat.py:47 -#, python-format -msgid "Adding users %s to room" -msgstr "Ajout de l'utilisateur %s dans le salon" - -#: quick_frontend/quick_chat.py:49 -msgid "[INTERNAL] trying to set presents nicks for a non group chat window" -msgstr "" -"[INTERNAL] tentative d'indiquer les utilisateurs présents pour une fenêtre " -"de chat qui n'est pas un chat de groupe" - -#: quick_frontend/quick_chat.py:55 wix/chat.py:138 -#, python-format -msgid "Replacing user %s" -msgstr "Remplacement de l'utilisateur %s" - -#: quick_frontend/quick_chat.py:57 wix/chat.py:140 -msgid "[INTERNAL] trying to replace user for a non group chat window" -msgstr "" -"[INTERNAL] tentative de remplacer un utilisateur pour une fenêtre de chat " -"qui n'est pas un chat de groupe" - -#: quick_frontend/quick_chat.py:70 -#, fuzzy, python-format -msgid "Removing user %s" -msgstr "suppression de %s" - -#: quick_frontend/quick_chat.py:72 -msgid "[INTERNAL] trying to remove user for a non group chat window" -msgstr "" -"[INTERNAL] tentative de supprimer un utilisateur pour une fenêtre de chat " -"qui n'est pas un chat de groupe" - -#: quick_frontend/quick_chat.py:79 -#, python-format -msgid "Setting subject to %s" -msgstr "Changement du sujet pour %s" - -#: quick_frontend/quick_chat.py:81 -msgid "[INTERNAL] trying to set subject for a non group chat window" -msgstr "" -"[INTERNAL] tentative de changer le sujet pour une fenêtre de chat qui n'est " -"pas un chat de groupe" - -#: quick_frontend/quick_chat.py:86 -msgid "now we print history" -msgstr "Maintenant on affiche l'historique" - -#: quick_frontend/quick_chat.py:122 -msgid "startGame is not implemented in this frontend" -msgstr "startGame n'est pas implémenté dans ce frontend" - -#: quick_frontend/quick_chat.py:127 -msgid "getGame is not implemented in this frontend" -msgstr "getGame n'est pas implémenté dans ce frontend" - -#: quick_frontend/quick_contact_list.py:33 -msgid "Contact List init" -msgstr "Initialisation de la liste de contacts" - -#: quick_frontend/quick_contact_management.py:67 -msgid "Trying to get attribute for an unknown contact" -msgstr "Tentative d'accès aux attributs d'un contact inconnu" - -#: quick_frontend/quick_contact_management.py:83 -msgid "INTERNAL ERROR: Key error" -msgstr "ERREUR INTERNE: erreur de clé" - -#: quick_frontend/quick_contact_management.py:95 -#, python-format -msgid "Trying to update an unknown contact: %s" -msgstr "Tentative de mise à jour d'un contact inconnu: %s" - -#: quick_frontend/quick_gateways.py:29 -msgid "" -"Be careful ! Gateways allow you to use an external IM (legacy IM), so you " -"can see your contact as jabber contacts.\n" -"But when you do this, all your messages go throught the external legacy IM " -"server, it is a huge privacy issue (i.e.: all your messages throught the " -"gateway can be monitored, recorded, analyzed by the external server, most of " -"time a private company)." -msgstr "" -"Soyez prudent ! Les transports vous permettent d'utiliser une messagerie " -"externe, de façon à pouvoir afficher vos contacts comme des contacts " -"jabber.\n" -"Mais si vous faites cela, tous vos messages passeront par les serveurs de la " -"messagerie externe, c'est un gros problème pour votre vie privée (comprenez: " -"tous vos messages à travers le transport pourront être affichés, " -"enregistrés, analysés par ces serveurs externes, la plupart du temps une " -"entreprise privée)." - -#: quick_frontend/quick_gateways.py:36 -msgid "Unknown IM" -msgstr "Messagerie inconnue" - -#: wix/card_game.py:95 -#, fuzzy -msgid "Contrat choosed" -msgstr "Contact choisi: %s" - -#: wix/card_game.py:233 -msgid "Écart" -msgstr "Écart" - -#: wix/chat.py:115 -msgid "configure chat window for Tarot game" -msgstr "Configuration de la fenêtre de chat pour un jeu de Tarot" - -#: wix/chat.py:167 -msgid "&SendFile\tCTRL-s" -msgstr "Envoi de fichier\tCTRL-s" - -#: wix/chat.py:167 -msgid " Send a file to contact" -msgstr "Envoi un fichier à un contact" - -#: wix/chat.py:168 -msgid "&Action" -msgstr "&Action" - -#: wix/chat.py:179 -msgid "Start &Tarot game\tCTRL-t" -msgstr "Lancer un jeu de &Tarot\tCTRL-t" - -#: wix/chat.py:179 -msgid " Start a Tarot card game" -msgstr " Commence un jeu de Tarot" - -#: wix/chat.py:180 -msgid "&Games" -msgstr "Jeux" - -#: wix/chat.py:248 -msgid "Send File" -msgstr "Envoi un fichier" - -#: wix/chat.py:249 -msgid "Choose a file to send" -msgstr "Veuillez choisir le fichier à envoyer" - -#: wix/chat.py:251 -#, python-format -msgid "filename: %s" -msgstr "nom du fichier: %s" - -#: wix/chat.py:254 wix/main_window.py:252 -msgid "File Transfer" -msgstr "Transfert de fichier" - -#: wix/chat.py:254 wix/main_window.py:252 -#, python-format -msgid "Copying %s" -msgstr "Copie de %s" - -#: wix/chat.py:257 -msgid "Starting Tarot game" -msgstr "Lancement d'un jeu de Tarot" - -#: wix/chat.py:258 -msgid "FIXME: temporary menu, must be changed" -msgstr "CORRIGEZ-MOI: menu temporaire, doit être remplacé" - -#: wix/constants.py:8 -#, fuzzy -msgid "offline" -msgstr "En ligne" - -#: wix/constants.py:9 -#, fuzzy -msgid "online" -msgstr "En ligne" - -#: wix/constants.py:11 -msgid "Online" -msgstr "En ligne" - -#: wix/constants.py:12 -msgid "Free for chat" -msgstr "Libre pour discuter" - -#: wix/constants.py:13 -msgid "AFK" -msgstr "Loin du clavier" - -#: wix/constants.py:14 -msgid "DND" -msgstr "Ne pas déranger" - -#: wix/constants.py:15 -msgid "Away" -msgstr "Absent" - -#: wix/contact_list.py:53 -#, python-format -msgid "update %s" -msgstr "mise à jour de %s" - -#: wix/contact_list.py:121 -#, python-format -msgid "adding %s" -msgstr "ajout de %s" - -#: wix/contact_list.py:139 -#, python-format -msgid "removing %s" -msgstr "suppression de %s" - -#: wix/gateways.py:106 -#, python-format -msgid "Opening gateways manager on [%s]" -msgstr "Ouverture du gestionnaire de transports pour [%s]" - -#: wix/gateways.py:166 wix/param.py:137 wix/profile.py:89 wix/xmlui.py:242 -msgid "close" -msgstr "fermeture" - -#: wix/main_window.py:96 -msgid "Wix jabber client" -msgstr "client jabber Wix" - -#: wix/main_window.py:121 -#, python-format -msgid "plugin profile %s" -msgstr "branchement du profil %s" - -#: wix/main_window.py:130 -msgid "Creating menus" -msgstr "Construction des menus" - -#: wix/main_window.py:132 -msgid "&Connect\tCTRL-c" -msgstr "&Connexion\tCTRL-c" - -#: wix/main_window.py:132 -msgid " Connect to the server" -msgstr " Connexion au serveur" - -#: wix/main_window.py:133 -msgid "&Disconnect\tCTRL-d" -msgstr "&Déconnexion\tCTRL-d" - -#: wix/main_window.py:133 -msgid " Disconnect from the server" -msgstr " Déconnexion du serveur" - -#: wix/main_window.py:134 -msgid "&Parameters" -msgstr "&Paramètres" - -#: wix/main_window.py:134 -msgid " Configure the program" -msgstr " Configurer l'application" - -#: wix/main_window.py:136 -msgid "A&bout" -msgstr "À propos" - -#: wix/main_window.py:136 -#, python-format -msgid " About %s" -msgstr " À propos %s" - -#: wix/main_window.py:137 -msgid "E&xit" -msgstr "Quitter" - -#: wix/main_window.py:137 -msgid " Terminate the program" -msgstr " Ferme l'application" - -#: wix/main_window.py:139 -msgid "&Add contact" -msgstr "&Ajouter un contact" - -#: wix/main_window.py:139 -msgid " Add a contact to your list" -msgstr " Ajouter un contact à votre liste" - -#: wix/main_window.py:140 -msgid "&Remove contact" -msgstr "Supp&rimer un contact" - -#: wix/main_window.py:140 -msgid " Remove the selected contact from your list" -msgstr " Supprime le contact sélectionné de votre liste" - -#: wix/main_window.py:142 -msgid "&Show profile" -msgstr "Afficher profile" - -#: wix/main_window.py:142 -msgid " Show contact's profile" -msgstr " Affiche le profile du contact" - -#: wix/main_window.py:144 -msgid "&Join Room" -msgstr "Re&joindre un salon" - -#: wix/main_window.py:144 -msgid " Join a Multi-User Chat room" -msgstr " Rejointre un salon de discussion" - -#: wix/main_window.py:145 -msgid "&Find Gateways" -msgstr "Trouver transports" - -#: wix/main_window.py:145 -msgid " Find gateways to legacy IM" -msgstr " Trouve les transports vers les messageries externes" - -#: wix/main_window.py:147 -msgid "&General" -msgstr "&Général" - -#: wix/main_window.py:148 -msgid "&Contacts" -msgstr "&Contacts" - -#: wix/main_window.py:149 -msgid "&Communication" -msgstr "&Communication" - -#: wix/main_window.py:238 -msgid "Confirmation asked" -msgstr "Confirmation demandée" - -#: wix/main_window.py:241 -msgid "File transfert confirmation asked" -msgstr "Demande de confirmation pour un transfert de fichier demandée" - -#: wix/main_window.py:242 -#, python-format -msgid "" -"The contact %(jid)s wants to send you the file %(filename)s\n" -"Do you accept ?" -msgstr "" -"Le contact %(jid)s veut vous envoyer le fichier %(filename)s\n" -"Êtes vous d'accord ?" - -#: wix/main_window.py:243 -msgid "File Request" -msgstr "Gestion de fichiers" - -#: wix/main_window.py:248 -msgid "Where do you want to save the file ?" -msgstr "Où voulez-vous sauvegarder le fichier ?" - -#: wix/main_window.py:261 -msgid "Yes/No confirmation asked" -msgstr "confirmation de type Oui/Non demandée" - -#: wix/main_window.py:263 wix/profile_manager.py:112 -msgid "Confirmation" -msgstr "Confirmation" - -#: wix/main_window.py:275 -#, python-format -msgid "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" -msgstr "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" - -#: wix/main_window.py:284 -msgid "Success" -msgstr "Succès" - -#: wix/main_window.py:303 -#, fuzzy -msgid "registration" -msgstr "Inscription" - -#: wix/main_window.py:351 -#, python-format -msgid "onContactActivated: %s" -msgstr "onContactActivated: %s" - -#: wix/main_window.py:369 -msgid "Status change request" -msgstr "Demande de changement de statut" - -#: wix/main_window.py:373 -msgid "Param request" -msgstr "Gestion des paramètres" - -#: wix/main_window.py:383 -#, python-format -msgid "%(name)s is a SàT (Salut à Toi) frontend\n" -msgstr "%(name)s est un frontend pour SàT (Salut à Toi)\n" - -#: wix/main_window.py:399 -msgid "Add contact request" -msgstr "Demande d'ajout de contact" - -#: wix/main_window.py:402 -msgid "name@server.tld" -msgstr "nom@serveur.ext" - -#: wix/main_window.py:415 -msgid "Remove contact request" -msgstr "Demande de suppression de contact" - -#: wix/main_window.py:418 wix/main_window.py:441 -msgid "You haven't selected any contact !" -msgstr "Vous n'avez sélectionné aucun contact !" - -#: wix/main_window.py:426 -#, python-format -msgid "Are you sure you want to delete %s from your roster list ?" -msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" - -#: wix/main_window.py:427 -msgid "Contact suppression" -msgstr "Suppression de contact" - -#: wix/main_window.py:438 -msgid "Show contact's profile request" -msgstr "Demande d'affichage du profile d'un contact" - -#: wix/main_window.py:454 -#, python-format -msgid "Profile received: [%s]" -msgstr "Profile reçu: [%s]" - -#: wix/main_window.py:485 -msgid "Exiting..." -msgstr "Sortie..." - -#: wix/main_window.py:491 -msgid "Tray Click" -msgstr "Clic sur l'icône de la barre de tâches" - -#: wix/param.py:32 -msgid "Configuration" -msgstr "Configuration" - -#: wix/param.py:83 -msgid "FIXME FIXME FIXME" -msgstr "CORRIGER-MOI" - -#: wix/profile_manager.py:47 -msgid "Profile:" -msgstr "Profile:" - -#: wix/profile_manager.py:60 -msgid "Login" -msgstr "Identifiant" - -#: wix/profile_manager.py:96 -msgid "Please enter the new profile name" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: wix/profile_manager.py:112 -#, python-format -msgid "Are you sure to delete the profile [%s]" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: wix/profile_manager.py:130 -msgid "You must select a profile or create a new one before connecting" -msgstr "" -"Vous devez sélectionner un profile ou en créer un nouveau avant de vous " -"connecter." - -#: wix/profile_manager.py:142 -#, fuzzy -msgid "Saving new JID and server" -msgstr "Sauvegarde du nouveau JID" - -#: wix/profile_manager.py:146 -msgid "Saving new password" -msgstr "Sauvegarde du nouveau mot de passe" - -#: wix/profile.py:35 -msgid "Full Name" -msgstr "Nom complet" - -#: wix/profile.py:36 -msgid "Nickname" -msgstr "Surnon" - -#: wix/profile.py:37 -msgid "Birthday" -msgstr "Date de naissance" - -#: wix/profile.py:38 -msgid "Phone #" -msgstr "N° de Tél:" - -#: wix/profile.py:39 -msgid "Website" -msgstr "Site Web" - -#: wix/profile.py:40 -msgid "E-mail" -msgstr "Courriel" - -#: wix/profile.py:41 -msgid "Avatar" -msgstr "Avatar" - -#: wix/xmlui.py:214 -msgid "Submitting form" -msgstr "Envoi du formulaire" - -#: wix/xmlui.py:236 -msgid "Cancelling form" -msgstr "Annulation du formulaire" - -#~ msgid "Contact List" -#~ msgstr "Liste de contacts" - -#~ msgid "Activating colors" -#~ msgstr "Activation des couleurs" - -#~ msgid "Deactivating colors" -#~ msgstr "Désactivation des couleurs" - -#~ msgid "hiding %s" -#~ msgstr "On cache %s" - -#~ msgid "showing %s" -#~ msgstr "On afficher %s" - -#~ msgid "FIXME: askConfirmation not implemented" -#~ msgstr "CORRIGEZ-MOI: askConfirmation n'est pas implémenté" - -#~ msgid "FIXME: actionResult not implemented" -#~ msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" - -#~ msgid "The profile is new, we create it" -#~ msgstr "Le profile est nouveau, on le créé"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/i18n/fr.po Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,1095 @@ +# SàT frontends french translation file. +# Copyright (C) 2009, 2010 Jérôme Poisson +# This file is distributed under the same license as the SàT frontends packages. +# Jérôme Poisson <goffi@goffi.org>, 2009, 2010. +# Goffi <goffi@goffi.org>, 2010. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.0.2D\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-08-19 21:17+0800\n" +"PO-Revision-Date: 2010-08-19 21:48+0800\n" +"Last-Translator: Goffi <goffi@goffi.org>\n" +"Language-Team: French <goffi@goffi.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: primitivus/primitivus:188 +msgid "Pleeeeasse, I can't even breathe !" +msgstr "Pitiééééééééé, je ne peux même pas respirer !" + +#: primitivus/primitivus:200 wix/profile.py:84 +msgid "General" +msgstr "Général" + +#: primitivus/primitivus:201 primitivus/profile_manager.py:50 +#: wix/profile_manager.py:70 +msgid "Connect" +msgstr "Connexion" + +#: primitivus/primitivus:202 +#, fuzzy +msgid "Disconnect" +msgstr "Déconnecté" + +#: primitivus/primitivus:203 +#, fuzzy +msgid "Parameters" +msgstr "&Paramètres" + +#: primitivus/primitivus:204 primitivus/primitivus:495 +msgid "About" +msgstr "À propos" + +#: primitivus/primitivus:205 +#, fuzzy +msgid "Exit" +msgstr "Quitter" + +#: primitivus/primitivus:206 +#, fuzzy +msgid "Contact" +msgstr "&Contacts" + +#: primitivus/primitivus:207 +#, fuzzy +msgid "Add contact" +msgstr "&Ajouter un contact" + +#: primitivus/primitivus:208 +#, fuzzy +msgid "Remove contact" +msgstr "Supp&rimer un contact" + +#: primitivus/primitivus:209 +#, fuzzy +msgid "Communication" +msgstr "&Communication" + +#: primitivus/primitivus:210 +msgid "Join room" +msgstr "Rejoindre un salon" + +#: primitivus/primitivus:211 +msgid "Find Gateways" +msgstr "Chercher les transports" + +#: primitivus/primitivus:224 +#, fuzzy +msgid "Main menu" +msgstr "Construction des menus" + +#: primitivus/primitivus:295 primitivus/primitivus:323 +#, fuzzy +msgid "Chat menu" +msgstr "Construction des menus" + +#: primitivus/primitivus:369 wix/main_window.py:218 +#, python-format +msgid "unmanaged dialog type: %s" +msgstr "type de discussion non géré: %s" + +#: primitivus/primitivus:384 +msgid "INTERNAL ERROR: Unexpected class for main widget's footer" +msgstr "" +"ERREUR INTERNE: Classe inattendue pour le pied de page du widget principal" + +#: primitivus/primitivus:392 wix/main_window.py:277 +msgid "unknown id, ignoring" +msgstr "id inconnue, on l'ignore" + +#: primitivus/primitivus:398 wix/main_window.py:299 +msgid "XML user interface received" +msgstr "Interface utilisateur XML reçue" + +#: primitivus/primitivus:401 wix/main_window.py:302 +msgid "Form" +msgstr "Formulaire" + +#: primitivus/primitivus:403 wix/main_window.py:304 +msgid "Registration" +msgstr "Inscription" + +#: primitivus/primitivus:413 primitivus/primitivus:439 +#: primitivus/primitivus:449 primitivus/primitivus:489 +#: primitivus/gateways.py:52 wix/card_game.py:125 wix/main_window.py:292 +#: wix/main_window.py:419 wix/main_window.py:442 +msgid "Error" +msgstr "Erreur" + +#: primitivus/primitivus:427 wix/main_window.py:321 +#, python-format +msgid "FIXME FIXME FIXME: type [%s] not implemented" +msgstr "CORRIGER-MOI: le type [%s] n'est pas implémenté" + +#: primitivus/primitivus:437 primitivus/primitivus:447 wix/main_window.py:409 +#: wix/main_window.py:469 +#, python-format +msgid "'%s' is an invalid JID !" +msgstr "'%s' n'est pas un JID valide !" + +#: primitivus/primitivus:453 wix/main_window.py:432 +#, python-format +msgid "Unsubscribing %s presence" +msgstr "Désinscription à la présence de %s" + +#: primitivus/primitivus:473 wix/main_window.py:462 +msgid "Entering a MUC room" +msgstr "Entrée dans le salon MUC" + +#: primitivus/primitivus:473 wix/main_window.py:461 +#, fuzzy +msgid "Please enter MUC's JID" +msgstr "Veuillez entrer le JID de votre nouveau contact" + +#: primitivus/primitivus:477 wix/main_window.py:472 +msgid "Find Gateways request" +msgstr "Demande de recherche de transports" + +#: primitivus/primitivus:483 wix/main_window.py:402 +msgid "Adding a contact" +msgstr "Ajout d'un contact" + +#: primitivus/primitivus:483 wix/main_window.py:401 +msgid "Please enter new contact JID" +msgstr "Veuillez entrer le JID de votre nouveau contact" + +#: primitivus/primitivus:489 +#, fuzzy +msgid "You have not selected any contact to delete !" +msgstr "Vous n'avez sélectionné aucun contact !" + +#: primitivus/primitivus:491 +#, fuzzy, python-format +msgid "Are you sure you want to delete the contact [%s] ?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: primitivus/card_game.py:262 wix/card_game.py:103 +msgid "Please choose your contrat" +msgstr "Veuillez choisir votre contrat" + +#: primitivus/card_game.py:277 wix/card_game.py:108 +msgid "You win \\o/" +msgstr "Victoire \\o/" + +#: primitivus/card_game.py:277 wix/card_game.py:108 +msgid "You loose :(" +msgstr "Vous perdez :(" + +#: primitivus/card_game.py:288 wix/card_game.py:125 +msgid "Cards played are invalid !" +msgstr "Les cartes jouées sont invalides !" + +#: primitivus/card_game.py:317 wix/card_game.py:233 +msgid "Do you put these cards in chien ?" +msgstr "Voulez-vous placer ces cartes au chien ?" + +#: primitivus/chat.py:131 +msgid "Game" +msgstr "Jeu" + +#: primitivus/chat.py:134 +#, fuzzy +msgid "Action" +msgstr "&Action" + +#: primitivus/chat.py:134 +#, fuzzy +msgid "Send file" +msgstr "Envoi un fichier" + +#: primitivus/chat.py:266 wix/chat.py:260 +msgid "Can't start game" +msgstr "Impossible de démarrer le jeu" + +#: primitivus/chat.py:266 wix/chat.py:260 +msgid "You need to be exactly 4 peoples in the room to start a Tarot game" +msgstr "" +"Vous devez être exactement 4 personnes dans le salon pour commencer un jeu " +"de Tarot" + +#: primitivus/contact_list.py:39 +#, fuzzy +msgid "Contacts" +msgstr "&Contacts" + +#: primitivus/custom_widgets.py:164 +msgid "WARNING: unknown text type" +msgstr "ATTENTION: type de texte inconnu" + +#: primitivus/custom_widgets.py:739 primitivus/files_management.py:156 +#: primitivus/xmlui.py:178 primitivus/xmlui.py:186 +msgid "Cancel" +msgstr "Annuler" + +#: primitivus/custom_widgets.py:740 primitivus/custom_widgets.py:748 +msgid "Ok" +msgstr "Ok" + +#: primitivus/custom_widgets.py:744 +msgid "Yes" +msgstr "Oui" + +#: primitivus/custom_widgets.py:745 +msgid "No" +msgstr "Non" + +#: primitivus/custom_widgets.py:961 +msgid "INTERNAL ERROR: Tab not found" +msgstr "ERREUR INTERNE: Onglet non trouvé" + +#: primitivus/files_management.py:105 +msgid "Impossible to list directory" +msgstr "Impossible de lister les répertoires" + +#: primitivus/files_management.py:130 +#, fuzzy +msgid "Please select a file" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: primitivus/files_management.py:137 +msgid "Path: " +msgstr "Chemin:" + +#: primitivus/files_management.py:150 +msgid "Bookmarks" +msgstr "Favoris" + +#: primitivus/files_management.py:199 +msgid "No GTK bookmarks file found" +msgstr "Aucun fichier favori pour GTK trouvé" + +#: primitivus/files_management.py:209 +msgid "No KDE bookmarks file found" +msgstr "Aucun fichier favori pour KDE trouvé" + +#: primitivus/gateways.py:30 quick_frontend/quick_gateways.py:28 +#: wix/gateways.py:33 +msgid "Gateways manager" +msgstr "Gestionnaire de transport" + +#: primitivus/gateways.py:40 wix/gateways.py:88 +msgid "Use external XMPP server: " +msgstr "Utiliser un autre serveur XMPP:" + +#: primitivus/gateways.py:41 wix/gateways.py:91 +msgid "GO !" +msgstr "C'est parti !" + +#: primitivus/gateways.py:52 +#, fuzzy +msgid "You must enter an external server JID" +msgstr "Utiliser un autre serveur XMPP:" + +#: primitivus/gateways.py:70 wix/gateways.py:148 +msgid "Register" +msgstr "Inscription" + +#: primitivus/gateways.py:73 wix/gateways.py:152 +msgid "Unregister" +msgstr "Désinscription" + +#: primitivus/profile_manager.py:36 +#, fuzzy +msgid "Login:" +msgstr "Identifiant" + +#: primitivus/profile_manager.py:37 wix/profile_manager.py:67 +msgid "Password:" +msgstr "Mot de passe:" + +#: primitivus/profile_manager.py:42 wix/profile_manager.py:52 +msgid "New" +msgstr "Nouveau" + +#: primitivus/profile_manager.py:43 wix/profile_manager.py:53 +msgid "Delete" +msgstr "Suppression" + +#: primitivus/profile_manager.py:55 +#, fuzzy +msgid "Profile Manager" +msgstr "Mauvais nom de profile" + +#: primitivus/profile_manager.py:84 wix/profile_manager.py:96 +msgid "New profile" +msgstr "Nouveau profile" + +#: primitivus/profile_manager.py:84 +#, fuzzy +msgid "Please enter a new profile name" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: primitivus/profile_manager.py:88 +#, fuzzy, python-format +msgid "Are you sure you want to delete the profile %s ?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: primitivus/profile_manager.py:102 wix/profile_manager.py:130 +msgid "No profile selected" +msgstr "Aucun profile sélectionné" + +#: primitivus/profile_manager.py:102 +#, fuzzy +msgid "You need to create and select a profile before connecting" +msgstr "" +"Vous devez sélectionner un profile ou en créer un nouveau avant de vous " +"connecter." + +#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 +#: wix/profile_manager.py:133 +msgid "Bad profile name" +msgstr "Mauvais nom de profile" + +#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 +#: wix/profile_manager.py:133 +msgid "A profile name can't start with a @" +msgstr "Un nom de profile ne peut pas commencer avec un @" + +#: primitivus/progress.py:35 +msgid "Clear progress list" +msgstr "Effacer la liste" + +#: primitivus/xmlui.py:67 wix/xmlui.py:61 +msgid "Unmanaged tag" +msgstr "Tab inconnu" + +#: primitivus/xmlui.py:80 wix/xmlui.py:75 +msgid "text node has no child !" +msgstr "le nœud text n'a pas d'enfant !" + +#: primitivus/xmlui.py:105 wix/xmlui.py:107 +#, python-format +msgid "FIXME FIXME FIXME: type [%s] is not implemented" +msgstr "" +"CORRIGEZ-MOI CORRIGEZ-MOI CORRIGEZ-MOI: le type [%s] n'est pas implémenté" + +#: primitivus/xmlui.py:135 wix/xmlui.py:138 +msgid "Unknown layout, using default one" +msgstr "Disposition inconnue, utilisation de celle par defaut" + +#: primitivus/xmlui.py:148 wix/xmlui.py:157 +#, fuzzy +msgid "Unknown tag" +msgstr "Messagerie inconnue" + +#: primitivus/xmlui.py:176 wix/xmlui.py:179 +msgid "Submit" +msgstr "Envoyer" + +#: primitivus/xmlui.py:185 +msgid "Save" +msgstr "Sauvegarder" + +#: primitivus/xmlui.py:204 +#, python-format +msgid "INTERNAL ERROR: Unmanaged show_type (%s)" +msgstr "ERREUR INTERNE: show_type inconnu (%s)" + +#: primitivus/xmlui.py:245 wix/xmlui.py:230 +msgid "The form data is not sent back, the type is not managed properly" +msgstr "" +"Les données du formulaire ne sont pas envoyées, il y a une erreur dans la " +"gestion du type" + +#: quick_frontend/quick_app.py:44 +msgid "Can't connect to SàT backend, are you sure it's launched ?" +msgstr "" +"Impossible de se connecter au démon SàT, êtes vous sûr qu'il est lancé ?" + +#: quick_frontend/quick_app.py:84 +#, fuzzy, python-format +msgid "Trying to plug an unknown profile (%s)" +msgstr "Tentative de mise à jour d'un contact inconnu: %s" + +#: quick_frontend/quick_app.py:90 +msgid "" +"\n" +" %prog [options]\n" +"\n" +" %prog --help for options list\n" +" " +msgstr "" +"\n" +" %prog [options]\n" +"\n" +" %prog --help pour la liste des options\n" +" " + +#: quick_frontend/quick_app.py:97 +msgid "Select the profile to use" +msgstr "Veuillez sélectionner le profile à utiliser" + +#: quick_frontend/quick_app.py:107 +msgid "There is already one profile plugged (we are in single profile mode) !" +msgstr "Il y a déjà un profile utilisé (nous comme en mode profile unique) !" + +#: quick_frontend/quick_app.py:111 +msgid "The profile asked doesn't exist" +msgstr "Le profile demandé n'existe pas" + +#: quick_frontend/quick_app.py:114 +msgid "The profile is already plugged" +msgstr "Le profile est déjà utilisé" + +#: quick_frontend/quick_app.py:167 +msgid "This profile is not plugged" +msgstr "Ce profile n'est pas utilisé" + +#: quick_frontend/quick_app.py:178 +msgid "Connected" +msgstr "Connecté" + +#: quick_frontend/quick_app.py:187 +msgid "Disconnected" +msgstr "Déconnecté" + +#: quick_frontend/quick_app.py:221 +#, fuzzy, python-format +msgid "" +"presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%" +"(statuses)s) [profile:%(profile)s]" +msgstr "" +"Mise à jour de l'information de présence pour %(jid)s (show=%(show)s, " +"statuses=%(statuses)s)" + +#: quick_frontend/quick_app.py:242 +#, python-format +msgid "Watched jid [%s] is connected !" +msgstr "Le jid surveillé [%s] est connecté !" + +#: quick_frontend/quick_app.py:267 +#, python-format +msgid "Room [%(room_name)s] joined by %(profile)s, users presents:%(users)s" +msgstr "" +"%(profile)s a rejoint le salon [%(room_name)s], utilisateurs présents:%" +"(users)s" + +#: quick_frontend/quick_app.py:282 +#, python-format +msgid "user [%(user_nick)s] joined room [%(room_jid)s]" +msgstr "l'utilisateur [%(user_nick)s] a rejoint le salon [%(room_jid)s]" + +#: quick_frontend/quick_app.py:291 +#, python-format +msgid "user [%(user_nick)s] left room [%(room_jid)s]" +msgstr "l'utilisateur [%(user_nick)s] a quitté le salon [%(room_jid)s]" + +#: quick_frontend/quick_app.py:300 +#, python-format +msgid "new subject for room [%(room_jid)s]: %(subject)s" +msgstr "nouveau sujet pour le salon [%(room_jid)s]: %(subject)s" + +#: quick_frontend/quick_app.py:305 +msgid "Tarot Game Started \\o/" +msgstr "Jeu de Tarot commencé \\o/" + +#: quick_frontend/quick_app.py:308 +#, python-format +msgid "" +"new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %" +"(players)s" +msgstr "" +"nouveau jeu de Tarot lancé par [%(referee)s] dans le salon [%(room_jid)s] " +"avec %(players)s" + +#: quick_frontend/quick_app.py:313 +msgid "New Tarot Game" +msgstr "Nouveau jeu de Tarot" + +#: quick_frontend/quick_app.py:321 +#, fuzzy +msgid "Tarot: need to select a contrat" +msgstr "Vous n'avez sélectionné aucun contact !" + +#: quick_frontend/quick_app.py:328 +#, fuzzy +msgid "Show cards" +msgstr "Affichage du chat" + +#: quick_frontend/quick_app.py:335 +msgid "My turn to play" +msgstr "C'est à moi de jouer" + +#: quick_frontend/quick_app.py:343 +#, fuzzy +msgid "Tarot: score received" +msgstr "Formulaire reçu" + +#: quick_frontend/quick_app.py:350 +#, python-format +msgid "Card(s) played (%(player)s): %(cards)s" +msgstr "Carte(s) jouée(s) (%(player)s): %(cards)s" + +#: quick_frontend/quick_app.py:357 +#, python-format +msgid "Cards played are not valid: %s" +msgstr "Les cartes jouées sont invalides: %s" + +#: quick_frontend/quick_app.py:375 +#, python-format +msgid "The contact %s has accepted your subscription" +msgstr "Le contact %s a accepté votre inscription" + +#: quick_frontend/quick_app.py:375 quick_frontend/quick_app.py:381 +msgid "Subscription confirmation" +msgstr "Confirmation d'inscription" + +#: quick_frontend/quick_app.py:378 +#, python-format +msgid "The contact %s has refused your subscription" +msgstr "Le contact %s a refusé votre inscription" + +#: quick_frontend/quick_app.py:378 +msgid "Subscription refusal" +msgstr "Refus d'inscription" + +#: quick_frontend/quick_app.py:381 +#, python-format +msgid "" +"The contact %s wants to subscribe to your presence.\n" +"Do you accept ?" +msgstr "" +"Le contact %s veut s'inscrire à vos informations de présence\n" +"Acceptez vous ?" + +#: quick_frontend/quick_app.py:392 +#, python-format +msgid "param update: [%(namespace)s] %(name)s = %(value)s" +msgstr "Le paramètre [%(namespace)s] %(name)s vaut désormais %(value)s" + +#: quick_frontend/quick_app.py:394 +#, python-format +msgid "Changing JID to %s" +msgstr "Changement du JID pour %s" + +#: quick_frontend/quick_chat.py:47 +#, python-format +msgid "Adding users %s to room" +msgstr "Ajout de l'utilisateur %s dans le salon" + +#: quick_frontend/quick_chat.py:49 +msgid "[INTERNAL] trying to set presents nicks for a non group chat window" +msgstr "" +"[INTERNAL] tentative d'indiquer les utilisateurs présents pour une fenêtre " +"de chat qui n'est pas un chat de groupe" + +#: quick_frontend/quick_chat.py:55 wix/chat.py:138 +#, python-format +msgid "Replacing user %s" +msgstr "Remplacement de l'utilisateur %s" + +#: quick_frontend/quick_chat.py:57 wix/chat.py:140 +msgid "[INTERNAL] trying to replace user for a non group chat window" +msgstr "" +"[INTERNAL] tentative de remplacer un utilisateur pour une fenêtre de chat " +"qui n'est pas un chat de groupe" + +#: quick_frontend/quick_chat.py:70 +#, fuzzy, python-format +msgid "Removing user %s" +msgstr "suppression de %s" + +#: quick_frontend/quick_chat.py:72 +msgid "[INTERNAL] trying to remove user for a non group chat window" +msgstr "" +"[INTERNAL] tentative de supprimer un utilisateur pour une fenêtre de chat " +"qui n'est pas un chat de groupe" + +#: quick_frontend/quick_chat.py:79 +#, python-format +msgid "Setting subject to %s" +msgstr "Changement du sujet pour %s" + +#: quick_frontend/quick_chat.py:81 +msgid "[INTERNAL] trying to set subject for a non group chat window" +msgstr "" +"[INTERNAL] tentative de changer le sujet pour une fenêtre de chat qui n'est " +"pas un chat de groupe" + +#: quick_frontend/quick_chat.py:86 +msgid "now we print history" +msgstr "Maintenant on affiche l'historique" + +#: quick_frontend/quick_chat.py:122 +msgid "startGame is not implemented in this frontend" +msgstr "startGame n'est pas implémenté dans ce frontend" + +#: quick_frontend/quick_chat.py:127 +msgid "getGame is not implemented in this frontend" +msgstr "getGame n'est pas implémenté dans ce frontend" + +#: quick_frontend/quick_contact_list.py:33 +msgid "Contact List init" +msgstr "Initialisation de la liste de contacts" + +#: quick_frontend/quick_contact_management.py:67 +msgid "Trying to get attribute for an unknown contact" +msgstr "Tentative d'accès aux attributs d'un contact inconnu" + +#: quick_frontend/quick_contact_management.py:83 +msgid "INTERNAL ERROR: Key error" +msgstr "ERREUR INTERNE: erreur de clé" + +#: quick_frontend/quick_contact_management.py:95 +#, python-format +msgid "Trying to update an unknown contact: %s" +msgstr "Tentative de mise à jour d'un contact inconnu: %s" + +#: quick_frontend/quick_gateways.py:29 +msgid "" +"Be careful ! Gateways allow you to use an external IM (legacy IM), so you " +"can see your contact as jabber contacts.\n" +"But when you do this, all your messages go throught the external legacy IM " +"server, it is a huge privacy issue (i.e.: all your messages throught the " +"gateway can be monitored, recorded, analyzed by the external server, most of " +"time a private company)." +msgstr "" +"Soyez prudent ! Les transports vous permettent d'utiliser une messagerie " +"externe, de façon à pouvoir afficher vos contacts comme des contacts " +"jabber.\n" +"Mais si vous faites cela, tous vos messages passeront par les serveurs de la " +"messagerie externe, c'est un gros problème pour votre vie privée (comprenez: " +"tous vos messages à travers le transport pourront être affichés, " +"enregistrés, analysés par ces serveurs externes, la plupart du temps une " +"entreprise privée)." + +#: quick_frontend/quick_gateways.py:36 +msgid "Unknown IM" +msgstr "Messagerie inconnue" + +#: wix/card_game.py:95 +#, fuzzy +msgid "Contrat choosed" +msgstr "Contact choisi: %s" + +#: wix/card_game.py:233 +msgid "Écart" +msgstr "Écart" + +#: wix/chat.py:115 +msgid "configure chat window for Tarot game" +msgstr "Configuration de la fenêtre de chat pour un jeu de Tarot" + +#: wix/chat.py:167 +msgid "&SendFile\tCTRL-s" +msgstr "Envoi de fichier\tCTRL-s" + +#: wix/chat.py:167 +msgid " Send a file to contact" +msgstr "Envoi un fichier à un contact" + +#: wix/chat.py:168 +msgid "&Action" +msgstr "&Action" + +#: wix/chat.py:179 +msgid "Start &Tarot game\tCTRL-t" +msgstr "Lancer un jeu de &Tarot\tCTRL-t" + +#: wix/chat.py:179 +msgid " Start a Tarot card game" +msgstr " Commence un jeu de Tarot" + +#: wix/chat.py:180 +msgid "&Games" +msgstr "Jeux" + +#: wix/chat.py:248 +msgid "Send File" +msgstr "Envoi un fichier" + +#: wix/chat.py:249 +msgid "Choose a file to send" +msgstr "Veuillez choisir le fichier à envoyer" + +#: wix/chat.py:251 +#, python-format +msgid "filename: %s" +msgstr "nom du fichier: %s" + +#: wix/chat.py:254 wix/main_window.py:252 +msgid "File Transfer" +msgstr "Transfert de fichier" + +#: wix/chat.py:254 wix/main_window.py:252 +#, python-format +msgid "Copying %s" +msgstr "Copie de %s" + +#: wix/chat.py:257 +msgid "Starting Tarot game" +msgstr "Lancement d'un jeu de Tarot" + +#: wix/chat.py:258 +msgid "FIXME: temporary menu, must be changed" +msgstr "CORRIGEZ-MOI: menu temporaire, doit être remplacé" + +#: wix/constants.py:8 +#, fuzzy +msgid "offline" +msgstr "En ligne" + +#: wix/constants.py:9 +#, fuzzy +msgid "online" +msgstr "En ligne" + +#: wix/constants.py:11 +msgid "Online" +msgstr "En ligne" + +#: wix/constants.py:12 +msgid "Free for chat" +msgstr "Libre pour discuter" + +#: wix/constants.py:13 +msgid "AFK" +msgstr "Loin du clavier" + +#: wix/constants.py:14 +msgid "DND" +msgstr "Ne pas déranger" + +#: wix/constants.py:15 +msgid "Away" +msgstr "Absent" + +#: wix/contact_list.py:53 +#, python-format +msgid "update %s" +msgstr "mise à jour de %s" + +#: wix/contact_list.py:121 +#, python-format +msgid "adding %s" +msgstr "ajout de %s" + +#: wix/contact_list.py:139 +#, python-format +msgid "removing %s" +msgstr "suppression de %s" + +#: wix/gateways.py:106 +#, python-format +msgid "Opening gateways manager on [%s]" +msgstr "Ouverture du gestionnaire de transports pour [%s]" + +#: wix/gateways.py:166 wix/param.py:137 wix/profile.py:89 wix/xmlui.py:242 +msgid "close" +msgstr "fermeture" + +#: wix/main_window.py:96 +msgid "Wix jabber client" +msgstr "client jabber Wix" + +#: wix/main_window.py:121 +#, python-format +msgid "plugin profile %s" +msgstr "branchement du profil %s" + +#: wix/main_window.py:130 +msgid "Creating menus" +msgstr "Construction des menus" + +#: wix/main_window.py:132 +msgid "&Connect\tCTRL-c" +msgstr "&Connexion\tCTRL-c" + +#: wix/main_window.py:132 +msgid " Connect to the server" +msgstr " Connexion au serveur" + +#: wix/main_window.py:133 +msgid "&Disconnect\tCTRL-d" +msgstr "&Déconnexion\tCTRL-d" + +#: wix/main_window.py:133 +msgid " Disconnect from the server" +msgstr " Déconnexion du serveur" + +#: wix/main_window.py:134 +msgid "&Parameters" +msgstr "&Paramètres" + +#: wix/main_window.py:134 +msgid " Configure the program" +msgstr " Configurer l'application" + +#: wix/main_window.py:136 +msgid "A&bout" +msgstr "À propos" + +#: wix/main_window.py:136 +#, python-format +msgid " About %s" +msgstr " À propos %s" + +#: wix/main_window.py:137 +msgid "E&xit" +msgstr "Quitter" + +#: wix/main_window.py:137 +msgid " Terminate the program" +msgstr " Ferme l'application" + +#: wix/main_window.py:139 +msgid "&Add contact" +msgstr "&Ajouter un contact" + +#: wix/main_window.py:139 +msgid " Add a contact to your list" +msgstr " Ajouter un contact à votre liste" + +#: wix/main_window.py:140 +msgid "&Remove contact" +msgstr "Supp&rimer un contact" + +#: wix/main_window.py:140 +msgid " Remove the selected contact from your list" +msgstr " Supprime le contact sélectionné de votre liste" + +#: wix/main_window.py:142 +msgid "&Show profile" +msgstr "Afficher profile" + +#: wix/main_window.py:142 +msgid " Show contact's profile" +msgstr " Affiche le profile du contact" + +#: wix/main_window.py:144 +msgid "&Join Room" +msgstr "Re&joindre un salon" + +#: wix/main_window.py:144 +msgid " Join a Multi-User Chat room" +msgstr " Rejointre un salon de discussion" + +#: wix/main_window.py:145 +msgid "&Find Gateways" +msgstr "Trouver transports" + +#: wix/main_window.py:145 +msgid " Find gateways to legacy IM" +msgstr " Trouve les transports vers les messageries externes" + +#: wix/main_window.py:147 +msgid "&General" +msgstr "&Général" + +#: wix/main_window.py:148 +msgid "&Contacts" +msgstr "&Contacts" + +#: wix/main_window.py:149 +msgid "&Communication" +msgstr "&Communication" + +#: wix/main_window.py:238 +msgid "Confirmation asked" +msgstr "Confirmation demandée" + +#: wix/main_window.py:241 +msgid "File transfert confirmation asked" +msgstr "Demande de confirmation pour un transfert de fichier demandée" + +#: wix/main_window.py:242 +#, python-format +msgid "" +"The contact %(jid)s wants to send you the file %(filename)s\n" +"Do you accept ?" +msgstr "" +"Le contact %(jid)s veut vous envoyer le fichier %(filename)s\n" +"Êtes vous d'accord ?" + +#: wix/main_window.py:243 +msgid "File Request" +msgstr "Gestion de fichiers" + +#: wix/main_window.py:248 +msgid "Where do you want to save the file ?" +msgstr "Où voulez-vous sauvegarder le fichier ?" + +#: wix/main_window.py:261 +msgid "Yes/No confirmation asked" +msgstr "confirmation de type Oui/Non demandée" + +#: wix/main_window.py:263 wix/profile_manager.py:112 +msgid "Confirmation" +msgstr "Confirmation" + +#: wix/main_window.py:275 +#, python-format +msgid "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" +msgstr "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" + +#: wix/main_window.py:284 +msgid "Success" +msgstr "Succès" + +#: wix/main_window.py:303 +#, fuzzy +msgid "registration" +msgstr "Inscription" + +#: wix/main_window.py:351 +#, python-format +msgid "onContactActivated: %s" +msgstr "onContactActivated: %s" + +#: wix/main_window.py:369 +msgid "Status change request" +msgstr "Demande de changement de statut" + +#: wix/main_window.py:373 +msgid "Param request" +msgstr "Gestion des paramètres" + +#: wix/main_window.py:383 +#, python-format +msgid "%(name)s is a SàT (Salut à Toi) frontend\n" +msgstr "%(name)s est un frontend pour SàT (Salut à Toi)\n" + +#: wix/main_window.py:399 +msgid "Add contact request" +msgstr "Demande d'ajout de contact" + +#: wix/main_window.py:402 +msgid "name@server.tld" +msgstr "nom@serveur.ext" + +#: wix/main_window.py:415 +msgid "Remove contact request" +msgstr "Demande de suppression de contact" + +#: wix/main_window.py:418 wix/main_window.py:441 +msgid "You haven't selected any contact !" +msgstr "Vous n'avez sélectionné aucun contact !" + +#: wix/main_window.py:426 +#, python-format +msgid "Are you sure you want to delete %s from your roster list ?" +msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" + +#: wix/main_window.py:427 +msgid "Contact suppression" +msgstr "Suppression de contact" + +#: wix/main_window.py:438 +msgid "Show contact's profile request" +msgstr "Demande d'affichage du profile d'un contact" + +#: wix/main_window.py:454 +#, python-format +msgid "Profile received: [%s]" +msgstr "Profile reçu: [%s]" + +#: wix/main_window.py:485 +msgid "Exiting..." +msgstr "Sortie..." + +#: wix/main_window.py:491 +msgid "Tray Click" +msgstr "Clic sur l'icône de la barre de tâches" + +#: wix/param.py:32 +msgid "Configuration" +msgstr "Configuration" + +#: wix/param.py:83 +msgid "FIXME FIXME FIXME" +msgstr "CORRIGER-MOI" + +#: wix/profile_manager.py:47 +msgid "Profile:" +msgstr "Profile:" + +#: wix/profile_manager.py:60 +msgid "Login" +msgstr "Identifiant" + +#: wix/profile_manager.py:96 +msgid "Please enter the new profile name" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: wix/profile_manager.py:112 +#, python-format +msgid "Are you sure to delete the profile [%s]" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: wix/profile_manager.py:130 +msgid "You must select a profile or create a new one before connecting" +msgstr "" +"Vous devez sélectionner un profile ou en créer un nouveau avant de vous " +"connecter." + +#: wix/profile_manager.py:142 +#, fuzzy +msgid "Saving new JID and server" +msgstr "Sauvegarde du nouveau JID" + +#: wix/profile_manager.py:146 +msgid "Saving new password" +msgstr "Sauvegarde du nouveau mot de passe" + +#: wix/profile.py:35 +msgid "Full Name" +msgstr "Nom complet" + +#: wix/profile.py:36 +msgid "Nickname" +msgstr "Surnon" + +#: wix/profile.py:37 +msgid "Birthday" +msgstr "Date de naissance" + +#: wix/profile.py:38 +msgid "Phone #" +msgstr "N° de Tél:" + +#: wix/profile.py:39 +msgid "Website" +msgstr "Site Web" + +#: wix/profile.py:40 +msgid "E-mail" +msgstr "Courriel" + +#: wix/profile.py:41 +msgid "Avatar" +msgstr "Avatar" + +#: wix/xmlui.py:214 +msgid "Submitting form" +msgstr "Envoi du formulaire" + +#: wix/xmlui.py:236 +msgid "Cancelling form" +msgstr "Annulation du formulaire" + +#~ msgid "Contact List" +#~ msgstr "Liste de contacts" + +#~ msgid "Activating colors" +#~ msgstr "Activation des couleurs" + +#~ msgid "Deactivating colors" +#~ msgstr "Désactivation des couleurs" + +#~ msgid "hiding %s" +#~ msgstr "On cache %s" + +#~ msgid "showing %s" +#~ msgstr "On afficher %s" + +#~ msgid "FIXME: askConfirmation not implemented" +#~ msgstr "CORRIGEZ-MOI: askConfirmation n'est pas implémenté" + +#~ msgid "FIXME: actionResult not implemented" +#~ msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" + +#~ msgid "The profile is new, we create it" +#~ msgstr "Le profile est nouveau, on le créé"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/i18n/sat_frontend.po Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,1000 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-08-19 21:17+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: primitivus/primitivus:188 +msgid "Pleeeeasse, I can't even breathe !" +msgstr "" + +#: primitivus/primitivus:200 wix/profile.py:84 +msgid "General" +msgstr "" + +#: primitivus/primitivus:201 primitivus/profile_manager.py:50 +#: wix/profile_manager.py:70 +msgid "Connect" +msgstr "" + +#: primitivus/primitivus:202 +msgid "Disconnect" +msgstr "" + +#: primitivus/primitivus:203 +msgid "Parameters" +msgstr "" + +#: primitivus/primitivus:204 primitivus/primitivus:495 +msgid "About" +msgstr "" + +#: primitivus/primitivus:205 +msgid "Exit" +msgstr "" + +#: primitivus/primitivus:206 +msgid "Contact" +msgstr "" + +#: primitivus/primitivus:207 +msgid "Add contact" +msgstr "" + +#: primitivus/primitivus:208 +msgid "Remove contact" +msgstr "" + +#: primitivus/primitivus:209 +msgid "Communication" +msgstr "" + +#: primitivus/primitivus:210 +msgid "Join room" +msgstr "" + +#: primitivus/primitivus:211 +msgid "Find Gateways" +msgstr "" + +#: primitivus/primitivus:224 +msgid "Main menu" +msgstr "" + +#: primitivus/primitivus:295 primitivus/primitivus:323 +msgid "Chat menu" +msgstr "" + +#: primitivus/primitivus:369 wix/main_window.py:218 +#, python-format +msgid "unmanaged dialog type: %s" +msgstr "" + +#: primitivus/primitivus:384 +msgid "INTERNAL ERROR: Unexpected class for main widget's footer" +msgstr "" + +#: primitivus/primitivus:392 wix/main_window.py:277 +msgid "unknown id, ignoring" +msgstr "" + +#: primitivus/primitivus:398 wix/main_window.py:299 +msgid "XML user interface received" +msgstr "" + +#: primitivus/primitivus:401 wix/main_window.py:302 +msgid "Form" +msgstr "" + +#: primitivus/primitivus:403 wix/main_window.py:304 +msgid "Registration" +msgstr "" + +#: primitivus/primitivus:413 primitivus/primitivus:439 +#: primitivus/primitivus:449 primitivus/primitivus:489 +#: primitivus/gateways.py:52 wix/card_game.py:125 wix/main_window.py:292 +#: wix/main_window.py:419 wix/main_window.py:442 +msgid "Error" +msgstr "" + +#: primitivus/primitivus:427 wix/main_window.py:321 +#, python-format +msgid "FIXME FIXME FIXME: type [%s] not implemented" +msgstr "" + +#: primitivus/primitivus:437 primitivus/primitivus:447 wix/main_window.py:409 +#: wix/main_window.py:469 +#, python-format +msgid "'%s' is an invalid JID !" +msgstr "" + +#: primitivus/primitivus:453 wix/main_window.py:432 +#, python-format +msgid "Unsubscribing %s presence" +msgstr "" + +#: primitivus/primitivus:473 wix/main_window.py:462 +msgid "Entering a MUC room" +msgstr "" + +#: primitivus/primitivus:473 wix/main_window.py:461 +msgid "Please enter MUC's JID" +msgstr "" + +#: primitivus/primitivus:477 wix/main_window.py:472 +msgid "Find Gateways request" +msgstr "" + +#: primitivus/primitivus:483 wix/main_window.py:402 +msgid "Adding a contact" +msgstr "" + +#: primitivus/primitivus:483 wix/main_window.py:401 +msgid "Please enter new contact JID" +msgstr "" + +#: primitivus/primitivus:489 +msgid "You have not selected any contact to delete !" +msgstr "" + +#: primitivus/primitivus:491 +#, python-format +msgid "Are you sure you want to delete the contact [%s] ?" +msgstr "" + +#: primitivus/card_game.py:262 wix/card_game.py:103 +msgid "Please choose your contrat" +msgstr "" + +#: primitivus/card_game.py:277 wix/card_game.py:108 +msgid "You win \\o/" +msgstr "" + +#: primitivus/card_game.py:277 wix/card_game.py:108 +msgid "You loose :(" +msgstr "" + +#: primitivus/card_game.py:288 wix/card_game.py:125 +msgid "Cards played are invalid !" +msgstr "" + +#: primitivus/card_game.py:317 wix/card_game.py:233 +msgid "Do you put these cards in chien ?" +msgstr "" + +#: primitivus/chat.py:131 +msgid "Game" +msgstr "" + +#: primitivus/chat.py:134 +msgid "Action" +msgstr "" + +#: primitivus/chat.py:134 +msgid "Send file" +msgstr "" + +#: primitivus/chat.py:266 wix/chat.py:260 +msgid "Can't start game" +msgstr "" + +#: primitivus/chat.py:266 wix/chat.py:260 +msgid "You need to be exactly 4 peoples in the room to start a Tarot game" +msgstr "" + +#: primitivus/contact_list.py:39 +msgid "Contacts" +msgstr "" + +#: primitivus/custom_widgets.py:164 +msgid "WARNING: unknown text type" +msgstr "" + +#: primitivus/custom_widgets.py:739 primitivus/files_management.py:156 +#: primitivus/xmlui.py:178 primitivus/xmlui.py:186 +msgid "Cancel" +msgstr "" + +#: primitivus/custom_widgets.py:740 primitivus/custom_widgets.py:748 +msgid "Ok" +msgstr "" + +#: primitivus/custom_widgets.py:744 +msgid "Yes" +msgstr "" + +#: primitivus/custom_widgets.py:745 +msgid "No" +msgstr "" + +#: primitivus/custom_widgets.py:961 +msgid "INTERNAL ERROR: Tab not found" +msgstr "" + +#: primitivus/files_management.py:105 +msgid "Impossible to list directory" +msgstr "" + +#: primitivus/files_management.py:130 +msgid "Please select a file" +msgstr "" + +#: primitivus/files_management.py:137 +msgid "Path: " +msgstr "" + +#: primitivus/files_management.py:150 +msgid "Bookmarks" +msgstr "" + +#: primitivus/files_management.py:199 +msgid "No GTK bookmarks file found" +msgstr "" + +#: primitivus/files_management.py:209 +msgid "No KDE bookmarks file found" +msgstr "" + +#: primitivus/gateways.py:30 quick_frontend/quick_gateways.py:28 +#: wix/gateways.py:33 +msgid "Gateways manager" +msgstr "" + +#: primitivus/gateways.py:40 wix/gateways.py:88 +msgid "Use external XMPP server: " +msgstr "" + +#: primitivus/gateways.py:41 wix/gateways.py:91 +msgid "GO !" +msgstr "" + +#: primitivus/gateways.py:52 +msgid "You must enter an external server JID" +msgstr "" + +#: primitivus/gateways.py:70 wix/gateways.py:148 +msgid "Register" +msgstr "" + +#: primitivus/gateways.py:73 wix/gateways.py:152 +msgid "Unregister" +msgstr "" + +#: primitivus/profile_manager.py:36 +msgid "Login:" +msgstr "" + +#: primitivus/profile_manager.py:37 wix/profile_manager.py:67 +msgid "Password:" +msgstr "" + +#: primitivus/profile_manager.py:42 wix/profile_manager.py:52 +msgid "New" +msgstr "" + +#: primitivus/profile_manager.py:43 wix/profile_manager.py:53 +msgid "Delete" +msgstr "" + +#: primitivus/profile_manager.py:55 +msgid "Profile Manager" +msgstr "" + +#: primitivus/profile_manager.py:84 wix/profile_manager.py:96 +msgid "New profile" +msgstr "" + +#: primitivus/profile_manager.py:84 +msgid "Please enter a new profile name" +msgstr "" + +#: primitivus/profile_manager.py:88 +#, python-format +msgid "Are you sure you want to delete the profile %s ?" +msgstr "" + +#: primitivus/profile_manager.py:102 wix/profile_manager.py:130 +msgid "No profile selected" +msgstr "" + +#: primitivus/profile_manager.py:102 +msgid "You need to create and select a profile before connecting" +msgstr "" + +#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 +#: wix/profile_manager.py:133 +msgid "Bad profile name" +msgstr "" + +#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 +#: wix/profile_manager.py:133 +msgid "A profile name can't start with a @" +msgstr "" + +#: primitivus/progress.py:35 +msgid "Clear progress list" +msgstr "" + +#: primitivus/xmlui.py:67 wix/xmlui.py:61 +msgid "Unmanaged tag" +msgstr "" + +#: primitivus/xmlui.py:80 wix/xmlui.py:75 +msgid "text node has no child !" +msgstr "" + +#: primitivus/xmlui.py:105 wix/xmlui.py:107 +#, python-format +msgid "FIXME FIXME FIXME: type [%s] is not implemented" +msgstr "" + +#: primitivus/xmlui.py:135 wix/xmlui.py:138 +msgid "Unknown layout, using default one" +msgstr "" + +#: primitivus/xmlui.py:148 wix/xmlui.py:157 +msgid "Unknown tag" +msgstr "" + +#: primitivus/xmlui.py:176 wix/xmlui.py:179 +msgid "Submit" +msgstr "" + +#: primitivus/xmlui.py:185 +msgid "Save" +msgstr "" + +#: primitivus/xmlui.py:204 +#, python-format +msgid "INTERNAL ERROR: Unmanaged show_type (%s)" +msgstr "" + +#: primitivus/xmlui.py:245 wix/xmlui.py:230 +msgid "The form data is not sent back, the type is not managed properly" +msgstr "" + +#: quick_frontend/quick_app.py:44 +msgid "Can't connect to SàT backend, are you sure it's launched ?" +msgstr "" + +#: quick_frontend/quick_app.py:84 +#, python-format +msgid "Trying to plug an unknown profile (%s)" +msgstr "" + +#: quick_frontend/quick_app.py:90 +msgid "" +"\n" +" %prog [options]\n" +"\n" +" %prog --help for options list\n" +" " +msgstr "" + +#: quick_frontend/quick_app.py:97 +msgid "Select the profile to use" +msgstr "" + +#: quick_frontend/quick_app.py:107 +msgid "There is already one profile plugged (we are in single profile mode) !" +msgstr "" + +#: quick_frontend/quick_app.py:111 +msgid "The profile asked doesn't exist" +msgstr "" + +#: quick_frontend/quick_app.py:114 +msgid "The profile is already plugged" +msgstr "" + +#: quick_frontend/quick_app.py:167 +msgid "This profile is not plugged" +msgstr "" + +#: quick_frontend/quick_app.py:178 +msgid "Connected" +msgstr "" + +#: quick_frontend/quick_app.py:187 +msgid "Disconnected" +msgstr "" + +#: quick_frontend/quick_app.py:221 +#, python-format +msgid "" +"presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%" +"(statuses)s) [profile:%(profile)s]" +msgstr "" + +#: quick_frontend/quick_app.py:242 +#, python-format +msgid "Watched jid [%s] is connected !" +msgstr "" + +#: quick_frontend/quick_app.py:267 +#, python-format +msgid "Room [%(room_name)s] joined by %(profile)s, users presents:%(users)s" +msgstr "" + +#: quick_frontend/quick_app.py:282 +#, python-format +msgid "user [%(user_nick)s] joined room [%(room_jid)s]" +msgstr "" + +#: quick_frontend/quick_app.py:291 +#, python-format +msgid "user [%(user_nick)s] left room [%(room_jid)s]" +msgstr "" + +#: quick_frontend/quick_app.py:300 +#, python-format +msgid "new subject for room [%(room_jid)s]: %(subject)s" +msgstr "" + +#: quick_frontend/quick_app.py:305 +msgid "Tarot Game Started \\o/" +msgstr "" + +#: quick_frontend/quick_app.py:308 +#, python-format +msgid "" +"new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %" +"(players)s" +msgstr "" + +#: quick_frontend/quick_app.py:313 +msgid "New Tarot Game" +msgstr "" + +#: quick_frontend/quick_app.py:321 +msgid "Tarot: need to select a contrat" +msgstr "" + +#: quick_frontend/quick_app.py:328 +msgid "Show cards" +msgstr "" + +#: quick_frontend/quick_app.py:335 +msgid "My turn to play" +msgstr "" + +#: quick_frontend/quick_app.py:343 +msgid "Tarot: score received" +msgstr "" + +#: quick_frontend/quick_app.py:350 +#, python-format +msgid "Card(s) played (%(player)s): %(cards)s" +msgstr "" + +#: quick_frontend/quick_app.py:357 +#, python-format +msgid "Cards played are not valid: %s" +msgstr "" + +#: quick_frontend/quick_app.py:375 +#, python-format +msgid "The contact %s has accepted your subscription" +msgstr "" + +#: quick_frontend/quick_app.py:375 quick_frontend/quick_app.py:381 +msgid "Subscription confirmation" +msgstr "" + +#: quick_frontend/quick_app.py:378 +#, python-format +msgid "The contact %s has refused your subscription" +msgstr "" + +#: quick_frontend/quick_app.py:378 +msgid "Subscription refusal" +msgstr "" + +#: quick_frontend/quick_app.py:381 +#, python-format +msgid "" +"The contact %s wants to subscribe to your presence.\n" +"Do you accept ?" +msgstr "" + +#: quick_frontend/quick_app.py:392 +#, python-format +msgid "param update: [%(namespace)s] %(name)s = %(value)s" +msgstr "" + +#: quick_frontend/quick_app.py:394 +#, python-format +msgid "Changing JID to %s" +msgstr "" + +#: quick_frontend/quick_chat.py:47 +#, python-format +msgid "Adding users %s to room" +msgstr "" + +#: quick_frontend/quick_chat.py:49 +msgid "[INTERNAL] trying to set presents nicks for a non group chat window" +msgstr "" + +#: quick_frontend/quick_chat.py:55 wix/chat.py:138 +#, python-format +msgid "Replacing user %s" +msgstr "" + +#: quick_frontend/quick_chat.py:57 wix/chat.py:140 +msgid "[INTERNAL] trying to replace user for a non group chat window" +msgstr "" + +#: quick_frontend/quick_chat.py:70 +#, python-format +msgid "Removing user %s" +msgstr "" + +#: quick_frontend/quick_chat.py:72 +msgid "[INTERNAL] trying to remove user for a non group chat window" +msgstr "" + +#: quick_frontend/quick_chat.py:79 +#, python-format +msgid "Setting subject to %s" +msgstr "" + +#: quick_frontend/quick_chat.py:81 +msgid "[INTERNAL] trying to set subject for a non group chat window" +msgstr "" + +#: quick_frontend/quick_chat.py:86 +msgid "now we print history" +msgstr "" + +#: quick_frontend/quick_chat.py:122 +msgid "startGame is not implemented in this frontend" +msgstr "" + +#: quick_frontend/quick_chat.py:127 +msgid "getGame is not implemented in this frontend" +msgstr "" + +#: quick_frontend/quick_contact_list.py:33 +msgid "Contact List init" +msgstr "" + +#: quick_frontend/quick_contact_management.py:67 +msgid "Trying to get attribute for an unknown contact" +msgstr "" + +#: quick_frontend/quick_contact_management.py:83 +msgid "INTERNAL ERROR: Key error" +msgstr "" + +#: quick_frontend/quick_contact_management.py:95 +#, python-format +msgid "Trying to update an unknown contact: %s" +msgstr "" + +#: quick_frontend/quick_gateways.py:29 +msgid "" +"Be careful ! Gateways allow you to use an external IM (legacy IM), so you " +"can see your contact as jabber contacts.\n" +"But when you do this, all your messages go throught the external legacy IM " +"server, it is a huge privacy issue (i.e.: all your messages throught the " +"gateway can be monitored, recorded, analyzed by the external server, most of " +"time a private company)." +msgstr "" + +#: quick_frontend/quick_gateways.py:36 +msgid "Unknown IM" +msgstr "" + +#: wix/card_game.py:95 +msgid "Contrat choosed" +msgstr "" + +#: wix/card_game.py:233 +msgid "Écart" +msgstr "" + +#: wix/chat.py:115 +msgid "configure chat window for Tarot game" +msgstr "" + +#: wix/chat.py:167 +msgid "&SendFile\tCTRL-s" +msgstr "" + +#: wix/chat.py:167 +msgid " Send a file to contact" +msgstr "" + +#: wix/chat.py:168 +msgid "&Action" +msgstr "" + +#: wix/chat.py:179 +msgid "Start &Tarot game\tCTRL-t" +msgstr "" + +#: wix/chat.py:179 +msgid " Start a Tarot card game" +msgstr "" + +#: wix/chat.py:180 +msgid "&Games" +msgstr "" + +#: wix/chat.py:248 +msgid "Send File" +msgstr "" + +#: wix/chat.py:249 +msgid "Choose a file to send" +msgstr "" + +#: wix/chat.py:251 +#, python-format +msgid "filename: %s" +msgstr "" + +#: wix/chat.py:254 wix/main_window.py:252 +msgid "File Transfer" +msgstr "" + +#: wix/chat.py:254 wix/main_window.py:252 +#, python-format +msgid "Copying %s" +msgstr "" + +#: wix/chat.py:257 +msgid "Starting Tarot game" +msgstr "" + +#: wix/chat.py:258 +msgid "FIXME: temporary menu, must be changed" +msgstr "" + +#: wix/constants.py:8 +msgid "offline" +msgstr "" + +#: wix/constants.py:9 +msgid "online" +msgstr "" + +#: wix/constants.py:11 +msgid "Online" +msgstr "" + +#: wix/constants.py:12 +msgid "Free for chat" +msgstr "" + +#: wix/constants.py:13 +msgid "AFK" +msgstr "" + +#: wix/constants.py:14 +msgid "DND" +msgstr "" + +#: wix/constants.py:15 +msgid "Away" +msgstr "" + +#: wix/contact_list.py:53 +#, python-format +msgid "update %s" +msgstr "" + +#: wix/contact_list.py:121 +#, python-format +msgid "adding %s" +msgstr "" + +#: wix/contact_list.py:139 +#, python-format +msgid "removing %s" +msgstr "" + +#: wix/gateways.py:106 +#, python-format +msgid "Opening gateways manager on [%s]" +msgstr "" + +#: wix/gateways.py:166 wix/param.py:137 wix/profile.py:89 wix/xmlui.py:242 +msgid "close" +msgstr "" + +#: wix/main_window.py:96 +msgid "Wix jabber client" +msgstr "" + +#: wix/main_window.py:121 +#, python-format +msgid "plugin profile %s" +msgstr "" + +#: wix/main_window.py:130 +msgid "Creating menus" +msgstr "" + +#: wix/main_window.py:132 +msgid "&Connect\tCTRL-c" +msgstr "" + +#: wix/main_window.py:132 +msgid " Connect to the server" +msgstr "" + +#: wix/main_window.py:133 +msgid "&Disconnect\tCTRL-d" +msgstr "" + +#: wix/main_window.py:133 +msgid " Disconnect from the server" +msgstr "" + +#: wix/main_window.py:134 +msgid "&Parameters" +msgstr "" + +#: wix/main_window.py:134 +msgid " Configure the program" +msgstr "" + +#: wix/main_window.py:136 +msgid "A&bout" +msgstr "" + +#: wix/main_window.py:136 +#, python-format +msgid " About %s" +msgstr "" + +#: wix/main_window.py:137 +msgid "E&xit" +msgstr "" + +#: wix/main_window.py:137 +msgid " Terminate the program" +msgstr "" + +#: wix/main_window.py:139 +msgid "&Add contact" +msgstr "" + +#: wix/main_window.py:139 +msgid " Add a contact to your list" +msgstr "" + +#: wix/main_window.py:140 +msgid "&Remove contact" +msgstr "" + +#: wix/main_window.py:140 +msgid " Remove the selected contact from your list" +msgstr "" + +#: wix/main_window.py:142 +msgid "&Show profile" +msgstr "" + +#: wix/main_window.py:142 +msgid " Show contact's profile" +msgstr "" + +#: wix/main_window.py:144 +msgid "&Join Room" +msgstr "" + +#: wix/main_window.py:144 +msgid " Join a Multi-User Chat room" +msgstr "" + +#: wix/main_window.py:145 +msgid "&Find Gateways" +msgstr "" + +#: wix/main_window.py:145 +msgid " Find gateways to legacy IM" +msgstr "" + +#: wix/main_window.py:147 +msgid "&General" +msgstr "" + +#: wix/main_window.py:148 +msgid "&Contacts" +msgstr "" + +#: wix/main_window.py:149 +msgid "&Communication" +msgstr "" + +#: wix/main_window.py:238 +msgid "Confirmation asked" +msgstr "" + +#: wix/main_window.py:241 +msgid "File transfert confirmation asked" +msgstr "" + +#: wix/main_window.py:242 +#, python-format +msgid "" +"The contact %(jid)s wants to send you the file %(filename)s\n" +"Do you accept ?" +msgstr "" + +#: wix/main_window.py:243 +msgid "File Request" +msgstr "" + +#: wix/main_window.py:248 +msgid "Where do you want to save the file ?" +msgstr "" + +#: wix/main_window.py:261 +msgid "Yes/No confirmation asked" +msgstr "" + +#: wix/main_window.py:263 wix/profile_manager.py:112 +msgid "Confirmation" +msgstr "" + +#: wix/main_window.py:275 +#, python-format +msgid "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" +msgstr "" + +#: wix/main_window.py:284 +msgid "Success" +msgstr "" + +#: wix/main_window.py:303 +msgid "registration" +msgstr "" + +#: wix/main_window.py:351 +#, python-format +msgid "onContactActivated: %s" +msgstr "" + +#: wix/main_window.py:369 +msgid "Status change request" +msgstr "" + +#: wix/main_window.py:373 +msgid "Param request" +msgstr "" + +#: wix/main_window.py:383 +#, python-format +msgid "%(name)s is a SàT (Salut à Toi) frontend\n" +msgstr "" + +#: wix/main_window.py:399 +msgid "Add contact request" +msgstr "" + +#: wix/main_window.py:402 +msgid "name@server.tld" +msgstr "" + +#: wix/main_window.py:415 +msgid "Remove contact request" +msgstr "" + +#: wix/main_window.py:418 wix/main_window.py:441 +msgid "You haven't selected any contact !" +msgstr "" + +#: wix/main_window.py:426 +#, python-format +msgid "Are you sure you want to delete %s from your roster list ?" +msgstr "" + +#: wix/main_window.py:427 +msgid "Contact suppression" +msgstr "" + +#: wix/main_window.py:438 +msgid "Show contact's profile request" +msgstr "" + +#: wix/main_window.py:454 +#, python-format +msgid "Profile received: [%s]" +msgstr "" + +#: wix/main_window.py:485 +msgid "Exiting..." +msgstr "" + +#: wix/main_window.py:491 +msgid "Tray Click" +msgstr "" + +#: wix/param.py:32 +msgid "Configuration" +msgstr "" + +#: wix/param.py:83 +msgid "FIXME FIXME FIXME" +msgstr "" + +#: wix/profile_manager.py:47 +msgid "Profile:" +msgstr "" + +#: wix/profile_manager.py:60 +msgid "Login" +msgstr "" + +#: wix/profile_manager.py:96 +msgid "Please enter the new profile name" +msgstr "" + +#: wix/profile_manager.py:112 +#, python-format +msgid "Are you sure to delete the profile [%s]" +msgstr "" + +#: wix/profile_manager.py:130 +msgid "You must select a profile or create a new one before connecting" +msgstr "" + +#: wix/profile_manager.py:142 +msgid "Saving new JID and server" +msgstr "" + +#: wix/profile_manager.py:146 +msgid "Saving new password" +msgstr "" + +#: wix/profile.py:35 +msgid "Full Name" +msgstr "" + +#: wix/profile.py:36 +msgid "Nickname" +msgstr "" + +#: wix/profile.py:37 +msgid "Birthday" +msgstr "" + +#: wix/profile.py:38 +msgid "Phone #" +msgstr "" + +#: wix/profile.py:39 +msgid "Website" +msgstr "" + +#: wix/profile.py:40 +msgid "E-mail" +msgstr "" + +#: wix/profile.py:41 +msgid "Avatar" +msgstr "" + +#: wix/xmlui.py:214 +msgid "Submitting form" +msgstr "" + +#: wix/xmlui.py:236 +msgid "Cancelling form" +msgstr ""
--- a/frontends/jp/fr.po Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,164 +0,0 @@ -# JP French Translation. -# Copyright (C) 2009, 2010 Jérôme Poisson -# This file is distributed under the same license as the jp package. -# Jérôme Poisson <goffi@goffi.org>, 2009, 2010. -# Goffi <goffi@goffi.org>, 2010. -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 0.0.2\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-05 19:08+1100\n" -"PO-Revision-Date: 2010-03-05 19:24+1100\n" -"Last-Translator: Goffi <goffi@goffi.org>\n" -"Language-Team: French <goffi@goffi.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: jp:63 -msgid "" -"ProgressBar not available, please download it at http://pypi.python.org/pypi/" -"progressbar" -msgstr "" -"ProgressBar n'est pas disponible, veuillez le télécharger à http://pypi." -"python.org/pypi/progressbar" - -#: jp:64 -msgid "" -"Progress bar deactivated\n" -"--\n" -msgstr "" -"Barre de progression désactivée\n" -"--\n" - -#: jp:77 -msgid "" -"\n" -" %prog [options] [FILE1 FILE2 ...] JID\n" -" %prog -w [options] [JID1 JID2 ...]\n" -"\n" -" %prog --help for options list\n" -" " -msgstr "" -"\n" -" %prog [options] [FICHIER1 FICHIER2 ...] JID\n" -" %prog -w [options] [JID1 JID2 ...]\n" -"\n" -" %prog --help pour la liste des options\n" -" " - -#: jp:86 -msgid "Make a bzip2 tarball" -msgstr "Fait un fichier compressé bzip2" - -#: jp:88 -msgid "Wait for a file to be sent by a contact" -msgstr "Attend qu'un fichier soit envoyé par un contact" - -#: jp:90 -msgid "Accept multiple files (you'll have to stop manually)" -msgstr "" -"Accepte plusieurs fichiers (vous devrez arrêter le programme à la main)" - -#: jp:92 -msgid "Force overwritting of existing files" -msgstr "Force le remplacement des fichiers existants" - -#: jp:94 -msgid "Show progress bar" -msgstr "Affiche la barre de progression" - -#: jp:96 -msgid "" -"Separate xmpp messages: send one message per line instead of one message " -"alone." -msgstr "" -"Sépare les messages xmpp: envoi un message par ligne plutôt qu'un seul " -"message global." - -#: jp:98 -msgid "Add a new line at the beginning of the input (usefull for ascii art ;))" -msgstr "" -"Ajoute un saut de ligne au début de l'entrée (utile pour l'art ascii ;))" - -#: jp:103 -msgid "You must specify the destination JID (Jabber ID)" -msgstr "Vous devez préciser le JID (Jabber ID) de destination" - -#: jp:112 -#, python-format -msgid "%s is not a valid JID !" -msgstr "%s n'est pas un JID valide !" - -#: jp:118 -msgid "Option progress is not available, deactivated." -msgstr "" -"L'option « progress » (barre de progression) n'est pas disponible, elle est " -"désactivée." - -#: jp:131 -msgid "SAT is not conneted, please connect before using jp" -msgstr "SAT n'est pas connecté, veuillez le connecter avant d'utiliser jp" - -#: jp:155 -#, python-format -msgid "File [%s] doesn't exist !" -msgstr "Le fichier [%s] n'existe pas !" - -#: jp:158 -#, python-format -msgid "[%s] is a dir ! Please send files inside or use compression" -msgstr "" -"[%s] est un répertoire ! Veuillez envoyer les fichiers qu'il contient ou " -"utiliser la compression." - -#: jp:164 -#, python-format -msgid "tmp file (%s) already exists ! Please remove it" -msgstr "le fichier temporaire (%s) existe déjà ! Veuillez le supprimer" - -#: jp:166 -msgid "bz2 is an experimental option at an early dev stage, use with caution" -msgstr "" -"bz2 est une option expérimentale à un stade de développement peu avancé, " -"utilisez-là avec prudence" - -#: jp:168 -msgid "Starting compression, please wait..." -msgstr "Lancement de la compression, veuillez patienter..." - -#: jp:172 -#, python-format -msgid "Adding %s" -msgstr "Ajout de %s" - -#: jp:175 -msgid "OK !" -msgstr "C'est parti !" - -#: jp:196 -#, python-format -msgid "Accepted file [%(filename)s] from %(sender)s" -msgstr "Le fichier [%(filename)s] de %(sender)s a été accepté" - -#: jp:200 -#, python-format -msgid "" -"Refused file [%(filename)s] from %(sender)s: a file with the same name " -"already exist" -msgstr "" -"Le fichier [%(filename)s] de %(sender)s a été refusé: un fichier avec le " -"même nom existe déjà" - -#: jp:209 -msgid "FIXME: actionResult not implemented" -msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" - -#: jp:223 -msgid "Progress: " -msgstr "Progression: " - -#: jp:254 -msgid "User interruption: good bye" -msgstr "Interrompu par l'utilisateur: au revoir"
--- a/frontends/jp/jp Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,272 +0,0 @@ -#! /usr/bin/python -# -*- coding: utf-8 -*- - -""" -jp: a SAT command line tool -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/>. -""" - -#consts -name = u"jp" -about = name+u""" v%s (c) Jérôme Poisson (aka Goffi) 2009, 2010 - ---- -"""+name+u""" Copyright (C) 2009, 2010 Jérôme Poisson (aka Goffi) -This program comes with ABSOLUTELY NO WARRANTY; -This is free software, and you are welcome to redistribute it -under certain conditions. ---- - -This software is a command line tool for jabber -Get the latest version at http://www.goffi.org -""" - -global pbar_available -pbar_available = True #checked before using ProgressBar - -### logging ### -import logging -from logging import debug, info, error, warning -logging.basicConfig(level=logging.DEBUG, - format='%(message)s') -### - -import gettext -gettext.install('jp', "i18n", unicode=True) - -import sys -import os -from os.path import abspath, basename, dirname -from optparse import OptionParser -import pdb -from tools.jid import JID -import gobject -from sat_bridge_frontend.DBus import DBusBridgeFrontend,BridgeExceptionNoService -import tarfile -try: - from progressbar import ProgressBar, Percentage, Bar, ETA, FileTransferSpeed -except ImportError, e: - info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar')) - info (_('Progress bar deactivated\n--\n')) - pbar_available=False - - - - -class JP(): - def __init__(self): - try: - self.bridge=DBusBridgeFrontend() - except BridgeExceptionNoService: - print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) - import sys - sys.exit(1) - self.transfert_id = None - - def check_options(self): - """Check command line options""" - usage=_(""" - %prog [options] [FILE1 FILE2 ...] JID - %prog -w [options] [JID1 JID2 ...] - - %prog --help for options list - """) - version = unicode(self.bridge.getVersion()) - parser = OptionParser(usage=usage,version=about % version) - - parser.add_option("-p", "--profile", action="store", type="string", default='@DEFAULT@', - help=_("Use PROFILE profile key (default: %default)")) - parser.add_option("-b", "--bz2", action="store_true", default=False, - help=_("Make a bzip2 tarball")) - parser.add_option("-w", "--wait-file", action="store_true", default=False, - help=_("Wait for a file to be sent by a contact")) - parser.add_option("-m", "--multiple", action="store_true", default=False, - help=_("Accept multiple files (you'll have to stop manually)")) - parser.add_option("-f", "--force", action="store_true", default=False, - help=_("Force overwritting of existing files")) - parser.add_option("-g", "--progress", action="store_true", default=False, - help=_("Show progress bar")) - parser.add_option("-s", "--separate", action="store_true", default=False, - help=_("Separate xmpp messages: send one message per line instead of one message alone.")) - parser.add_option("-n", "--new-line", action="store_true", default=False, - help=_("Add a new line at the beginning of the input (usefull for ascii art ;))")) - - (self.options, args) = parser.parse_args() - - if len(args) < 1 and not self.options.wait_file: - parser.error(_("You must specify the destination JID (Jabber ID)").encode('utf-8')) - - if self.options.wait_file: - #several jid - self.dest_jids = args - else: - #one dest_jid, other args are files - self.dest_jid = JID(args[-1]) - if not self.dest_jid.is_valid: - error (_("%s is not a valid JID !"), self.dest_jid) - exit(1) - self.files = args[:-1] - - if not pbar_available and self.options.progress: - self.options.progress = False - error (_("Option progress is not available, deactivated.")) - - if self.options.progress or self.options.wait_file: - self.start_loop = True #We have to use loop for these options - else: - self.start_loop = False - - - return args - - def check_jabber_status(self): - """Check that jabber status is allright""" - - self.profile = self.bridge.getProfileName(self.options.profile) - if not self.profile: - error(_("The profile asked doesn't exist")) - exit(1) - - if not self.bridge.isConnected(self.profile): - error(_(u"SàT is not conneted, please connect before using jp")) - exit(1) - - - def send_stdin(self): - """Send incomming data on stdin to jabber contact""" - header = "\n" if self.options.new_line else "" - - if self.options.separate: #we send stdin in several messages - if header: - self.bridge.sendMessage(self.dest_jid, header, profile_key=self.profile) - while (True): - line = sys.stdin.readline() - if not line: - break - self.bridge.sendMessage(self.dest_jid, line.replace("\n",""), profile_key=self.profile) - else: - self.bridge.sendMessage(self.dest_jid, header + "".join(sys.stdin.readlines()), profile_key=self.profile) - - def send_files(self): - """Send files to jabber contact""" - - for file in self.files: - if not os.path.exists(file): - error (_("File [%s] doesn't exist !") % file) - exit(1) - if not self.options.bz2 and os.path.isdir(file): - error (_("[%s] is a dir ! Please send files inside or use compression") % file) - exit(1) - - if self.options.bz2: - tmpfile = (basename(self.files[0]) or basename(dirname(self.files[0])) ) + '.tar.bz2' #FIXME: tmp, need an algorithm to find a good name/path - if os.path.exists(tmpfile): - error (_("tmp file (%s) already exists ! Please remove it"), tmpfile) - exit(1) - warning(_("bz2 is an experimental option at an early dev stage, use with caution")) - #FIXME: check free space, writting perm, tmp dir, filename (watch for OS used) - info(_("Starting compression, please wait...")) - sys.stdout.flush() - bz2=tarfile.open(tmpfile, "w:bz2") - for file in self.files: - info(_("Adding %s"), file) - bz2.add(file) - bz2.close() - info(_("OK !")) - path = abspath(tmpfile) - self.transfert_id = self.bridge.sendFile(self.dest_jid, path, profile_key=self.profile) - else: - for file in self.files: - path = abspath(file) - self.transfert_id = self.bridge.sendFile(self.dest_jid, path, profile_key=self.profile) #FIXME: show progress only for last transfert_id - - #TODO: manage ProgressBar - - def askConfirmation(self, type, id, data): - """CB used for file transfert, accept files depending on parameters""" - answer_data={} - if type == "FILE_TRANSFERT": - if self.dest_jids and not data['from'] in self.dest_jids: - return #file is not sent by a filtered jid - - answer_data["dest_path"] = os.getcwd()+'/'+data['filename'] - - if self.options.force or not os.path.exists(answer_data["dest_path"]): - self.bridge.confirmationAnswer(id, True, answer_data) - info(_("Accepted file [%(filename)s] from %(sender)s") % {'filename':data['filename'], 'sender':data['from']}) - self.transfert_id = id - else: - self.bridge.confirmationAnswer(id, False, answer_data) - warning(_("Refused file [%(filename)s] from %(sender)s: a file with the same name already exist") % {'filename':data['filename'], 'sender':data['from']}) - - - if not self.options.multiple and not self.options.progress: - #we just accept one file - self.loop.quit() - - def actionResult(self, type, id, data): - #FIXME - info (_("FIXME: actionResult not implemented")) - - def wait_file(self): - """Wait for a file and write it on local dir""" - self.bridge.register("askConfirmation", self.askConfirmation, "request") - - def progressCB(self): - if self.transfert_id: - data = self.bridge.getProgress(self.transfert_id) - if data: - if not data['position']: - data['position'] = '0' - if not self.pbar: - #first answer, we must construct the bar - self.pbar = ProgressBar(int(data['size']),[_("Progress: "),Percentage()," ",Bar()," ",FileTransferSpeed()," ",ETA()]) - self.pbar.start() - - self.pbar.update(int(data['position'])) - elif self.pbar: - self.pbar.finish() - if not self.options.multiple: - self.loop.quit() - return False - - return True - - def go(self): - self.check_options() - self.check_jabber_status() - if self.options.wait_file: - self.wait_file() - else: - if not self.files: #we send message only if there are no files to send - self.send_stdin() - else: - self.send_files() - - if self.start_loop: - self.loop = gobject.MainLoop() - if self.options.progress: - self.pbar = None - gobject.timeout_add(10, self.progressCB) - try: - self.loop.run() - except KeyboardInterrupt: - info(_("User interruption: good bye")) - - -if __name__ == "__main__": - jp = JP() - jp.go()
--- a/frontends/jp/jp.po Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,142 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-03-05 19:08+1100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" -"Content-Transfer-Encoding: 8bit\n" - -#: jp:63 -msgid "" -"ProgressBar not available, please download it at http://pypi.python.org/pypi/" -"progressbar" -msgstr "" - -#: jp:64 -msgid "" -"Progress bar deactivated\n" -"--\n" -msgstr "" - -#: jp:77 -msgid "" -"\n" -" %prog [options] [FILE1 FILE2 ...] JID\n" -" %prog -w [options] [JID1 JID2 ...]\n" -"\n" -" %prog --help for options list\n" -" " -msgstr "" - -#: jp:86 -msgid "Make a bzip2 tarball" -msgstr "" - -#: jp:88 -msgid "Wait for a file to be sent by a contact" -msgstr "" - -#: jp:90 -msgid "Accept multiple files (you'll have to stop manually)" -msgstr "" - -#: jp:92 -msgid "Force overwritting of existing files" -msgstr "" - -#: jp:94 -msgid "Show progress bar" -msgstr "" - -#: jp:96 -msgid "" -"Separate xmpp messages: send one message per line instead of one message " -"alone." -msgstr "" - -#: jp:98 -msgid "Add a new line at the beginning of the input (usefull for ascii art ;))" -msgstr "" - -#: jp:103 -msgid "You must specify the destination JID (Jabber ID)" -msgstr "" - -#: jp:112 -#, python-format -msgid "%s is not a valid JID !" -msgstr "" - -#: jp:118 -msgid "Option progress is not available, deactivated." -msgstr "" - -#: jp:131 -msgid "SAT is not conneted, please connect before using jp" -msgstr "" - -#: jp:155 -#, python-format -msgid "File [%s] doesn't exist !" -msgstr "" - -#: jp:158 -#, python-format -msgid "[%s] is a dir ! Please send files inside or use compression" -msgstr "" - -#: jp:164 -#, python-format -msgid "tmp file (%s) already exists ! Please remove it" -msgstr "" - -#: jp:166 -msgid "bz2 is an experimental option at an early dev stage, use with caution" -msgstr "" - -#: jp:168 -msgid "Starting compression, please wait..." -msgstr "" - -#: jp:172 -#, python-format -msgid "Adding %s" -msgstr "" - -#: jp:175 -msgid "OK !" -msgstr "" - -#: jp:196 -#, python-format -msgid "Accepted file [%(filename)s] from %(sender)s" -msgstr "" - -#: jp:200 -#, python-format -msgid "" -"Refused file [%(filename)s] from %(sender)s: a file with the same name " -"already exist" -msgstr "" - -#: jp:209 -msgid "FIXME: actionResult not implemented" -msgstr "" - -#: jp:223 -msgid "Progress: " -msgstr "" - -#: jp:254 -msgid "User interruption: good bye" -msgstr ""
--- a/frontends/primitivus/card_game.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,335 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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/>. -""" - -import urwid -from tools.games import TarotCard -from quick_frontend.quick_card_game import QuickCardGame -from xmlui import XMLUI -from urwid_satext import sat_widgets - -class CardDisplayer(urwid.Text): - """Show a card""" - signals = ['click'] - - def __init__(self, card): - self.__selected = False - self.card = card - urwid.Text.__init__(self, card.getAttrText()) - - def selectable(self): - return True - - def keypress(self, size, key): - if key == ' ': - self.select(not self.__selected) - self._emit('click') - return key - - def mouse_event(self, size, event, button, x, y, focus): - if urwid.is_mouse_press(event) and button == 1: - self.select(not self.__selected) - self._emit('click') - return True - - return False - - def select(self, state=True): - self.__selected = state - attr,txt = self.card.getAttrText() - if self.__selected: - attr+='_selected' - self.set_text((attr,txt)) - self._invalidate() - - def isSelected(self): - return self.__selected - - def getCard(self): - return self.card - - def render(self, size, focus=False): - canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus)) - if focus: - canvas.set_cursor((0,0)) - return canvas - -class Hand(urwid.WidgetWrap): - """Used to display several cards, and manage a hand""" - signals = ['click'] - - def __init__(self, hand=[], selectable = False, on_click=None, user_data=None): - """@param hand: list of Card""" - self.__selectable = selectable - self.columns = urwid.Columns([],dividechars=1) - if on_click: - urwid.connect_signal(self, 'click', on_click, user_data) - if hand: - self.update(hand) - urwid.WidgetWrap.__init__(self, self.columns) - - def selectable(self): - return self.__selectable - - def keypress(self, size, key): - - if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]: - return self.columns.keypress(size,key) - else: - #No card displayed, we still have to manage the clicks - if key == ' ': - self._emit('click', None) - return key - - def getSelected(self): - """Return a list of selected cards""" - _selected = [] - for wid in self.columns.widget_list: - if isinstance(wid, CardDisplayer) and wid.isSelected(): - _selected.append(wid.getCard()) - return _selected - - def update(self, hand): - """Update the hand displayed in this widget - @param hand: list of Card""" - del self.columns.widget_list[:] - del self.columns.column_types[:] - self.columns.widget_list.append(urwid.Text('')) - self.columns.column_types.append(('weight',1)) - for card in hand: - widget = CardDisplayer(card) - self.columns.widget_list.append(widget) - self.columns.column_types.append(('fixed',3)) - urwid.connect_signal(widget, 'click', self.__onClick) - self.columns.widget_list.append(urwid.Text('')) - self.columns.column_types.append(('weight',1)) - self.columns.set_focus(1) - - def __onClick(self,card_wid): - self._emit('click', card_wid) - -class Card(TarotCard): - """This class is used to represent a card, logically - and give a text representation with attributes""" - SIZE = 3 #size of a displayed card - - def __init__(self, suit, value): - """@param file: path of the PNG file""" - TarotCard.__init__(self, (suit, value)) - - def getAttrText(self): - """return text representation of the card with attributes""" - try: - value = "%02i" % int(self.value) - except ValueError: - value = self.value[0].upper()+self.value[1] - if self.suit == "atout": - if self.value == "excuse": - suit = 'c' - else: - suit = 'A' - color = 'neutral' - elif self.suit == "pique": - suit = u'♠' - color = 'black' - elif self.suit == "trefle": - suit = u'♣' - color = 'black' - elif self.suit == "coeur": - suit = u'♥' - color = 'red' - elif self.suit == "carreau": - suit = u'♦' - color = 'red' - if self.bout: - color = 'special' - return ('card_%s' % color,u"%s%s" % (value,suit)) - - def getWidget(self): - """Return a widget representing the card""" - return CardDisplayer(self) - -class Table(urwid.FlowWidget): - """Represent the cards currently on the table""" - - def __init__(self): - self.top = self.left = self.bottom = self.right = None - - def putCard(self, location, card): - """Put a card on the table - @param location: where to put the card (top, left, bottom or right) - @param card: Card to play or None""" - assert location in ['top','left','bottom','right'] - assert isinstance(card,Card) or card == None - if [getattr(self, place) for place in ['top','left','bottom','right']].count(None) == 0: - #If the table is full of card, we remove them - self.top = self.left = self.bottom = self.right = None - setattr(self, location, card) - self._invalidate() - - def rows(self,size,focus=False): - return self.display_widget(size, focus).rows(size, focus) - - def render(self, size, focus=False): - return self.display_widget(size, focus).render(size, focus) - - def display_widget(self, size, focus): - cards={} - max_col, = size - separator = " - " - margin = max((max_col-Card.SIZE)/2,0) * ' ' - margin_center = max((max_col-Card.SIZE*2-len(separator))/2,0) * ' ' - for location in ['top', 'left', 'bottom', 'right']: - card = getattr(self,location) - cards[location] = card.getAttrText() if card else Card.SIZE * ' ' - render_wid = [urwid.Text([margin,cards['top']]), - urwid.Text([margin_center,cards['left'],separator,cards['right']]), - urwid.Text([margin,cards['bottom']])] - return urwid.Pile(render_wid) - - -class CardGame(QuickCardGame,urwid.WidgetWrap): - """Widget for card games""" - - def __init__(self, parent, referee, players, player_nick): - QuickCardGame.__init__(self, parent, referee, players, player_nick) - self.loadCards() - self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), 'center')]) - #self.parent.host.debug() - self.table = Table() - self.center = urwid.Columns([('fixed',len(self.left_nick),urwid.Filler(urwid.Text(self.left_nick))), - urwid.Filler(self.table), - ('fixed',len(self.right_nick),urwid.Filler(urwid.Text(self.right_nick))) - ]) - """urwid.Pile([urwid.Padding(self.top_card_wid,'center'), - urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)), - urwid.Padding(self.center_cards_wid,'center'), - ('fixed',len(self.right_nick),urwid.Text(self.right_nick)) - ]), - urwid.Padding(self.bottom_card_wid,'center') - ])""" - self.hand_wid = Hand(selectable = True, on_click = self.onClick) - self.main_frame = urwid.Frame(self.center,header=self.top, footer=self.hand_wid, focus_part='footer') - urwid.WidgetWrap.__init__(self,self.main_frame) - self.parent.host.bridge.tarotGameReady(player_nick, referee, profile_key = self.parent.host.profile) - - def loadCards(self): - """Load all the cards in memory""" - QuickCardGame.loadCards(self) - for value in map(str,range(1,22))+['excuse']: - card = Card('atout',value) - self.cards[card.suit, card.value]=card - self.deck.append(card) - for suit in ["pique", "coeur", "carreau", "trefle"]: - for value in map(str,range(1,11))+["valet","cavalier","dame","roi"]: - card = Card(suit,value) - self.cards[card.suit, card.value]=card - self.deck.append(card) - - def newGame(self, hand): - """Start a new game, with given hand""" - QuickCardGame.newGame(self, hand) - self.hand_wid.update(self.hand) - self.parent.host.redraw() - - def contratSelected(self, data): - """Called when the contrat has been choosed - @param data: form result""" - contrat = data[0][1] - QuickCardGame.contratSelected(self, contrat) - - def chooseContrat(self, xml_data): - """Called when the player as to select his contrat - @param xml_data: SàT xml representation of the form""" - misc = {'callback': self.contratSelected} - form = XMLUI(self.parent.host, xml_data, title = _('Please choose your contrat'), options = ['NO_CANCEL'], misc = misc) - form.show() - - def showCards(self, game_stage, cards, data): - """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" - QuickCardGame.showCards(self, game_stage, cards, data) - self.center.widget_list[1] = urwid.Filler(Hand(self.to_show)) - self.parent.host.redraw() - - def myTurn(self): - QuickCardGame.myTurn(self) - - def showScores(self, xml_data, winners, loosers): - """Called when the player as to select hist contrat - @param xml_data: SàT xml representation of the form""" - form = XMLUI(self.parent.host, xml_data, title = _('You win \o/') if self.player_nick in winners else _('You loose :('), options = ['NO_CANCEL']) - form.show() - - def invalidCards(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - QuickCardGame.invalidCards(self, phase, played_cards, invalid_cards) - self.hand_wid.update(self.hand) - if self._autoplay==None: #No dialog if there is autoplay - self.parent.host.notify(_('Cards played are invalid !')) - self.parent.host.redraw() - - def cardsPlayed(self, player, cards): - """A card has been played by player""" - QuickCardGame.cardsPlayed(self, player, cards) - self.table.putCard(self.getPlayerLocation(player),self.played[player]) - self.parent.host.redraw() - - ##EVENTS## - def onClick(self, hand, card_wid): - """Called when user do an action on the hand""" - if not self.state in ['play','ecart','wait_for_ecart']: - #it's not our turn, we ignore the click - card_wid.select(False) - return - if isinstance(self.center.widget_list[1].original_widget, Hand): #if we have a hand displayed - self.center.widget_list[1] = urwid.Filler(self.table) #we show again the table - if self.state == "chien": - self.to_show = [] - self.state = "wait" - elif self.state == "wait_for_ecart": - self.state = "ecart" - self.hand.extend(self.to_show) - self.hand.sort() - self.to_show = [] - self.hand_wid.update(self.hand) - if self.state == "ecart": - if len(self.hand_wid.getSelected()) == 6: - pop_up_widget = sat_widgets.ConfirmDialog(_("Do you put these cards in chien ?"), yes_cb=self.onEcartDone, no_cb=self.parent.host.removePopUp) - self.parent.host.showPopUp(pop_up_widget) - elif self.state == "play": - card = card_wid.getCard() - self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], profile_key = self.parent.host.profile) - self.hand.remove(card) - self.hand_wid.update(self.hand) - self.state = "wait" - - def onEcartDone(self,button): - """Called when player has finished is écart""" - ecart = [] - for card in self.hand_wid.getSelected(): - ecart.append((card.suit, card.value)) - self.hand.remove(card) - self.hand_wid.update(self.hand) - self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, ecart, profile_key = self.parent.host.profile) - self.state = "wait" - self.parent.host.removePopUp()
--- a/frontends/primitivus/chat.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,279 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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/>. -""" - -import urwid -from quick_frontend.quick_contact_list import QuickContactList -from quick_frontend.quick_chat import QuickChat -from urwid_satext import sat_widgets -import time -from tools.jid import JID -from card_game import CardGame -from urwid_satext.files_management import FileDialog - - -class ChatText(urwid.FlowWidget): - """Manage the printing of chat message""" - - def __init__(self, parent, timestamp, nick, my_mess, message, align='left'): - self.parent = parent - self.timestamp = time.localtime(timestamp) - self.nick = nick - self.my_mess = my_mess - self.message = unicode(message) - self.align = align - - def selectable(self): - return True - - def keypress(self, size, key): - return key - - def rows(self,size,focus=False): - return self.display_widget(size, focus).rows(size, focus) - - def render(self, size, focus=False): - canvas = urwid.CompositeCanvas(self.display_widget(size, focus).render(size, focus)) - if focus: - canvas.set_cursor(self.get_cursor_coords(size)) - return canvas - - def get_cursor_coords(self, size): - #(maxcol,) = size - return 0, 0 - - def display_widget(self, size, focus): - render_txt = [] - if self.parent.show_timestamp: - time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M" #if the message was sent before today, we print the full date - render_txt.append(('date',"[%s]" % time.strftime(time_format, self.timestamp).decode('utf-8'))) - if self.parent.show_short_nick: - render_txt.append(('my_nick' if self.my_mess else 'other_nick',"**" if self.my_mess else "*")) - else: - render_txt.append(('my_nick' if self.my_mess else 'other_nick',"[%s] " % self.nick)) - render_txt.append(self.message) - return urwid.Text(render_txt, align=self.align) - -class Chat(urwid.WidgetWrap, QuickChat): - - def __init__(self, target, host, type='one2one'): - self.target = target - QuickChat.__init__(self, target, host, type) - self.content = urwid.SimpleListWalker([]) - self.text_list = urwid.ListBox(self.content) - self.chat_widget = urwid.Frame(self.text_list) - self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) - self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) - self.pile = urwid.Pile([self.chat_colums]) - urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile)) - self.setType(type) - self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time - self.show_timestamp = True - self.show_short_nick = False - self.show_title = 1 #0: clip title; 1: full title; 2: no title - self.subject = None - - def keypress(self, size, key): - if key == "meta p": #user wants to (un)hide the presents panel - if self.type == 'group': - widgets = self.chat_colums.widget_list - if self.present_panel in widgets: - self.__removePresentPanel() - else: - self.__appendPresentPanel() - elif key == "meta t": #user wants to (un)hide timestamp - self.show_timestamp = not self.show_timestamp - for wid in self.content: - wid._invalidate() - elif key == "meta n": #user wants to (not) use short nick - self.show_short_nick = not self.show_short_nick - for wid in self.content: - wid._invalidate() - elif key == "meta l": #user wants to (un)hide widget decoration - show = not isinstance(self._w, sat_widgets.LabelLine) - self.showDecoration(show) - self._invalidate() - elif key == "meta s": #user wants to (un)hide group's subject or change its apperance - if self.subject: - self.show_title = (self.show_title + 1) % 3 - if self.show_title == 0: - self.setSubject(self.subject,'clip') - elif self.show_title == 1: - self.setSubject(self.subject,'space') - elif self.show_title == 2: - self.chat_widget.header = None - self._invalidate() - - - return super(Chat, self).keypress(size, key) - - def getMenu(self): - """Return Menu bar""" - menu = sat_widgets.Menu(self.host.loop) - if self.type == 'group': - game = _("Game") - menu.addMenu(game, "Tarot", self.onTarotRequest) - elif self.type == 'one2one': - menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest) - return menu - - def setType(self, type): - QuickChat.setType(self, type) - if type == 'one2one': - self.historyPrint(profile=self.host.profile) - elif type == 'group': - if len(self.chat_colums.widget_list) == 1: - present_widget = self.__buildPresentList() - self.present_panel = sat_widgets.VerticalSeparator(present_widget) - self.__appendPresentPanel() - - def __getDecoration(self, widget): - return sat_widgets.LabelLine(widget, sat_widgets.SurroundedText(unicode(self.target))) - - def showDecoration(self, show=True): - """Show/Hide the decoration around the chat window""" - if show: - main_widget = self.__getDecoration(self.pile) - else: - main_widget = self.pile - self._w = main_widget - - - def __buildPresentList(self): - self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText) - return self.present_wid - - def __appendPresentPanel(self): - self.chat_colums.widget_list.append(self.present_panel) - self.chat_colums.column_types.append(('weight', 2)) - - def __removePresentPanel(self): - self.chat_colums.set_focus(0) #necessary as the focus change to the next object, we can go out of range if we are on the last object of self.chat_colums - self.chat_colums.widget_list.remove(self.present_panel) - del self.chat_colums.column_types[-1] - - def __appendGamePanel(self, widget): - assert (len(self.pile.widget_list) == 1) - self.pile.widget_list.insert(0,widget) - self.pile.item_types.insert(0,('weight', 1)) - self.pile.widget_list.insert(1,urwid.Filler(urwid.Divider('-'))) - self.pile.item_types.insert(1,('fixed', 1)) - self.host.redraw() - - def __removeGamePanel(self): - assert (len(self.pile.widget_list) == 3) - self.pile.set_focus(0) #necessary as the focus change to the next object, we can go out of range if we are on the last object of self.chat_colums - del self.pile.widget_list[0] - del self.pile.item_types[0] - self.host.redraw() - - def setSubject(self, subject, wrap='space'): - """Set title for a group chat""" - QuickChat.setSubject(self, subject) - self.subject = subject - self.subj_wid = urwid.Text(unicode(subject.replace('\n','|') if wrap == 'clip' else subject ), - align='left' if wrap=='clip' else 'center',wrap=wrap) - self.chat_widget.header = urwid.AttrMap(self.subj_wid,'title') - self.host.redraw() - - def setPresents(self, param_nicks): - """Set the users presents in the contact list for a group chat - @param nicks: list of nicknames - """ - nicks = [unicode(nick) for nick in param_nicks] #FIXME: should be done in DBus bridge - nicks.sort() - QuickChat.setPresents(self, nicks) - self.present_wid.changeValues(nicks) - self.host.redraw() - - def replaceUser(self, param_nick): - """Add user if it is not in the group list""" - nick = unicode(param_nick) #FIXME: should be done in DBus bridge - if "facebook" in nick: - self.host.debug() - QuickChat.replaceUser(self, nick) - presents = self.present_wid.getAllValues() - if nick not in presents: - presents.append(nick) - presents.sort() - self.present_wid.changeValues(presents) - self.host.redraw() - - def removeUser(self, param_nick): - """Remove a user from the group list""" - nick = unicode(param_nick) #FIXME: should be done in DBus bridge - QuickChat.removeUser(self, nick) - self.present_wid.deleteValue(nick) - self.host.redraw() - - def printMessage(self, from_jid, msg, profile, timestamp=""): - assert isinstance(from_jid, JID) - try: - jid,nick,mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) - except TypeError: - return - my_jid = self.host.profiles[profile]['whoami'] - self.content.append(ChatText(self, timestamp or None, nick, mymess, msg)) - self.text_list.set_focus(len(self.content)-1) - self.host.redraw() - - def printInfo(self, msg, type='normal'): - """Print general info - @param msg: message to print - @type: one of: - normal: general info like "toto has joined the room" - me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" - """ - self.content.append(sat_widgets.ClickableText(msg)) - self.text_list.set_focus(len(self.content)-1) - self.host.redraw() - - def startGame(self, game_type, referee, players): - """Configure the chat window to start a game""" - if game_type=="Tarot": - try: - self.tarot_wid = CardGame(self, referee, players, self.nick) - self.__appendGamePanel(self.tarot_wid) - except e: - self.host.debug() - - def getGame(self, game_type): - """Return class managing the game type""" - #TODO: check that the game is launched, and manage errors - if game_type=="Tarot": - return self.tarot_wid - - #MENU EVENTS# - def onTarotRequest(self, menu): - if len(self.occupants) != 4: - self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp)) - else: - self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile) - - def onSendFileRequest(self, menu): - dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp) - self.host.showPopUp(dialog, 80, 80) - - #MISC EVENTS# - def onFileSelected(self, filepath): - self.host.removePopUp() - full_jid = self.host.CM.get_full(self.target) - id = self.host.bridge.sendFile(full_jid, filepath) - self.host.addProgress(id,filepath)
--- a/frontends/primitivus/contact_list.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,183 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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/>. -""" - -import urwid -from quick_frontend.quick_contact_list import QuickContactList -from tools.jid import JID -from urwid_satext import sat_widgets - - -class ContactList(urwid.WidgetWrap, QuickContactList): - signals = ['click','change'] - - def __init__(self, host, CM, on_click=None, on_change=None, user_data=None): - self.host = host - self.selected = None - self.groups={} - self.alert_jid=set() - - #we now build the widget - self.frame = urwid.Frame(self.__buildList()) - self.main_widget = sat_widgets.LabelLine(self.frame, sat_widgets.SurroundedText(_("Contacts"))) - urwid.WidgetWrap.__init__(self, self.main_widget) - if on_click: - urwid.connect_signal(self, 'click', on_click, user_data) - if on_change: - urwid.connect_signal(self, 'change', on_change, user_data) - QuickContactList.__init__(self, CM) - - def __contains__(self, jid): - for group in self.groups: - if jid.short in self.groups[group][1]: - return True - return False - - def setFocus(self, name): - """give focus to the first group or contact with the given name""" - idx = 0 - for widget in self.frame.body.body: - if widget.getValue() == name: - self.frame.body.set_focus(idx) - return - idx+=1 - - def putAlert(self, jid): - """Put an alert on the jid to get attention from user (e.g. for new message)""" - self.alert_jid.add(jid.short) - self.frame.body = self.__buildList() - self.host.redraw() - - def __groupClicked(self, group_wid): - group = self.groups[group_wid.getValue()] - group[0] = not group[0] - self.frame.body = self.__buildList() - self.host.redraw() - self.setFocus(group_wid.getValue()) - - def __contactClicked(self, contact_wid, selected): - self.selected = contact_wid.data - for widget in self.frame.body.body: - if widget.__class__ == sat_widgets.SelectableText: - widget.setState(widget.data == self.selected, invisible=True) - if self.selected in self.alert_jid: - self.alert_jid.remove(self.selected) - self.frame.body = self.__buildList() - self.host.redraw() - self._emit('click') - - def __buildContact(self, content, param_contacts): - """Add contact representation in widget list - @param content: widget list, e.g. SimpleListWalker - @param contacts: list of JID""" - contacts = list(param_contacts) - contacts.sort() - for contact in contacts: - jid=JID(contact) - name = self.CM.getAttr(jid,'name') - nick = self.CM.getAttr(jid,'nick') - display = nick or name or jid.node or jid.short - header = '(*) ' if contact in self.alert_jid else '' - widget = sat_widgets.SelectableText(('alert' if contact in self.alert_jid else 'default',display), - selected = contact==self.selected, header=header) - widget.data = contact - content.append(widget) - urwid.connect_signal(widget, 'change', self.__contactClicked) - - def __buildList(self): - """Build the main contact list widget""" - content = urwid.SimpleListWalker([]) - group_keys = self.groups.keys() - group_keys.sort() - for key in group_keys: - unfolded = self.groups[key][0] - if key!=None: - header = '[-]' if unfolded else '[+]' - widget = sat_widgets.ClickableText(key,header=header+' ') - content.append(widget) - urwid.connect_signal(widget, 'click', self.__groupClicked) - if unfolded: - self.__buildContact(content, self.groups[key][1]) - return urwid.ListBox(content) - - def unselectAll(self): - """Unselect all contacts""" - self.selected = None - for widget in self.frame.body.body: - if widget.__class__ == sat_widgets.SelectableText: - widget.setState(False, invisible=True) - - - def get_contact(self): - """Return contact currently selected""" - return self.selected - - def clear_contacts(self): - """clear all the contact list""" - self.groups={} - self.selected = None - self.unselectAll() - self.frame.body = self.__buildList() - self.host.redraw() - - def replace(self, jid, groups=[None]): - """add a contact to the list if doesn't exist, else update it""" - assert isinstance(groups, list) - assert isinstance(jid, JID) - if not groups: - groups=[None] - for group in groups: - if not self.groups.has_key(group): - self.groups[group] = [True,set()] #[unfold,list_of_contacts] - self.groups[group][1].add(jid.short) - self.frame.body = self.__buildList() - self.host.redraw() - - - """contacts = self.list_wid.getAllValues() - if jid.short not in contacts: - contacts.append(jid.short) - contacts.sort() - self.list_wid.changeValues(contacts) - self._emit('change')""" - - def disconnect(self, jid): - """mark a contact disconnected""" - self.remove(jid.short) - - def remove(self, param_jid): - """remove a contact from the list""" - groups_to_remove = [] - jid = JID(param_jid) - for group in self.groups: - contacts = self.groups[group][1] - if jid.short in contacts: - contacts.remove(jid.short) - if not len(contacts): - groups_to_remove.append(group) - for group in groups_to_remove: - del self.groups[group] - self.frame.body = self.__buildList() - self.host.redraw() - - def add(self, jid, param_groups=[None]): - """add a contact to the list""" - self.replace(jid,param_groups) -
--- a/frontends/primitivus/gateways.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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/>. -""" - -import urwid -from urwid_satext import sat_widgets -from tools.jid import JID -from quick_frontend.quick_gateways import QuickGatewaysManager - - -class GatewaysManager(urwid.WidgetWrap, QuickGatewaysManager): - - def __init__(self, host, gateways, title=_("Gateways manager"), server=None): - QuickGatewaysManager.__init__(self, host, gateways, server) - if server: - title+=" (%s)" % server - widget_list = urwid.SimpleListWalker([]) - widget_list.append(urwid.Text(self.WARNING_MSG)) - widget_list.append(urwid.Divider('-')) - for gateway in gateways: - self.addGateway(widget_list,gateway, gateways[gateway]) - widget_list.append(urwid.Divider()) - self.ext_serv = sat_widgets.AdvancedEdit(_("Use external XMPP server: ")) - go_button = sat_widgets.CustomButton( _("GO !"),self.browseExternalServer) - ext_serv_col = urwid.Columns([self.ext_serv,('fixed',go_button.getSize(),go_button)]) - widget_list.append(ext_serv_col) - list_wid = urwid.ListBox(widget_list) - decorated = sat_widgets.LabelLine(list_wid, sat_widgets.SurroundedText(title)) - urwid.WidgetWrap.__init__(self, decorated) - - def browseExternalServer(self, button): - """Open the gateway manager on given server""" - server = self.ext_serv.get_edit_text() - if not server: - popup = sat_widgets.Alert(_("Error"), _("You must enter an external server JID"), ok_cb=self.host.removePopUp) - self.host.showPopUp(popup) - return - id = self.host.bridge.findGateways(server, self.host.profile) - self.host.current_action_ids.add(id) - self.host.current_action_ids_cb[id] = self.host.onGatewaysFound - self.host.removeWindow() - - def addGateway(self, widget_list, gateway, param): - - widget_col = [] - widget_col.append(('weight',4,urwid.Text(unicode(param['name'])))) #FIXME: unicode to be remove when DBus bridge will not give dbus.String anymore - - #Then the transport type message - widget_col.append(('weight',6,urwid.Text(self.getGatewayDesc(param['type'])))) - - #The buttons - - reg_button = sat_widgets.CustomButton( _("Register"), self.onRegister) - reg_button.gateway_jid = JID(gateway) - widget_col.append(('fixed',reg_button.getSize(),reg_button)) - unreg_button = sat_widgets.CustomButton( _("Unregister"), self.onUnregister) - unreg_button.gateway_jid = JID(gateway) - widget_col.append(('fixed',unreg_button.getSize(),unreg_button)) - widget_list.append(urwid.Columns(widget_col,1)) - - def onRegister(self, button): - """Called when register button is clicked""" - gateway_jid = button.gateway_jid - id = self.host.bridge.in_band_register(gateway_jid, self.host.profile) - self.host.current_action_ids.add(id) - - def onUnregister(self, button): - """Called when unregister button is clicked""" - gateway_jid = button.gateway_jid - id = self.host.bridge.gatewayRegister("CANCEL",gateway_jid, None, self.host.profile) - self.host.current_action_ids.add(id)
--- a/frontends/primitivus/primitivus Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,509 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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 quick_frontend.quick_app import QuickApp -from quick_frontend.quick_chat_list import QuickChatList -from quick_frontend.quick_contact_list import QuickContactList -from quick_frontend.quick_contact_management import QuickContactManagement -import urwid -from profile_manager import ProfileManager -from contact_list import ContactList -from chat import Chat -from gateways import GatewaysManager -from urwid_satext import sat_widgets -import logging -from logging import debug, info, error -import sys, os -from tools.jid import JID -from xmlui import XMLUI -from progress import Progress - - -### logging configuration FIXME: put this elsewhere ### -logging.basicConfig(level=logging.CRITICAL, #TODO: configure it to put messages in a log file - format='%(message)s') -### - -const_APP_NAME = "Primitivus" -const_PALETTE = [('title', 'black', 'light gray', 'standout,underline'), - ('title_focus', 'white,bold', 'light gray', 'standout,underline'), - ('selected', 'default', 'dark red'), - ('selected_focus', 'default,bold', 'dark red'), - ('default', 'default', 'default'), - ('default_focus', 'default,bold', 'default'), - ('alert', 'default,underline', 'default'), - ('alert_focus', 'default,bold,underline', 'default'), - ('date', 'light gray', 'default'), - ('my_nick', 'dark red,bold', 'default'), - ('other_nick', 'dark cyan,bold', 'default'), - ('menubar', 'light gray,bold', 'dark red'), - ('menubar_focus', 'light gray,bold', 'dark green'), - ('selected_menu', 'light gray,bold', 'dark green'), - ('menuitem', 'light gray,bold', 'dark red'), - ('menuitem_focus', 'light gray,bold', 'dark green'), - ('notifs', 'black,bold', 'yellow'), - ('notifs_focus', 'dark red', 'yellow'), - ('card_neutral', 'dark gray', 'white', 'standout,underline'), - ('card_neutral_selected', 'dark gray', 'dark green', 'standout,underline'), - ('card_special', 'brown', 'white', 'standout,underline'), - ('card_special_selected', 'brown', 'dark green', 'standout,underline'), - ('card_red', 'dark red', 'white', 'standout,underline'), - ('card_red_selected', 'dark red', 'dark green', 'standout,underline'), - ('card_black', 'black', 'white', 'standout,underline'), - ('card_black_selected', 'black', 'dark green', 'standout,underline'), - ('directory', 'dark cyan, bold', 'default'), - ('directory_focus', 'dark cyan, bold', 'dark green'), - ('separator', 'brown', 'default'), - ('warning', 'light red', 'default'), - ('progress_normal', 'default', 'black'), - ('progress_complete', 'default', 'light red'), - ] - -class ChatList(QuickChatList): - """This class manage the list of chat windows""" - - def __init__(self, host): - QuickChatList.__init__(self, host) - - def createChat(self, target): - return Chat(target, self.host) - -class PrimitivusApp(QuickApp): - - def __init__(self): - self.CM = QuickContactManagement() #FIXME: not the best place - QuickApp.__init__(self) - - ## main loop setup ## - self.main_widget = ProfileManager(self) - self.loop = urwid.MainLoop(self.main_widget, const_PALETTE, event_loop=urwid.GLibEventLoop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler) - - ##misc setup## - self.chat_wins=ChatList(self) - self.notBar = sat_widgets.NotificationBar() - urwid.connect_signal(self.notBar,'change',self.onNotification) - self.progress_wid = Progress(self) - urwid.connect_signal(self.notBar.progress,'click',lambda x:self.addWindow(self.progress_wid)) - self.__saved_overlay = None - - def debug(self): - """convenient method to reset screen and launch p(u)db""" - try: - import pudb - pudb.set_trace() - except: - import os,pdb - os.system('reset') - print 'Entered debug mode' - pdb.set_trace() - - def writeLog(self, log, file_name='/tmp/primitivus_log'): - """method to write log in a temporary file, useful for debugging""" - with open(file_name, 'a') as f: - f.write(log+"\n") - - def redraw(self): - """redraw the screen""" - self.loop.draw_screen() - - def start(self): - self.i = 0 - self.loop.set_alarm_in(0,lambda a,b: self.postInit()) - self.loop.run() - - def inputFilter(self, input, raw): - if self.__saved_overlay and input != ['ctrl s']: - return - for i in input: - if isinstance(i,tuple): - if i[0] == 'mouse press': - if i[1] == 4: #Mouse wheel up - input[input.index(i)] = 'up' - if i[1] == 5: #Mouse wheel down - input[input.index(i)] = 'down' - return input - - def keyHandler(self, input): - if input == 'meta m': - """User want to (un)hide the menu roller""" - try: - if self.main_widget.header == None: - self.main_widget.header = self.menu_roller - else: - self.main_widget.header = None - except AttributeError: - pass - elif input == 'ctrl n': - """User wants to see next notification""" - self.notBar.showNext() - elif input == 'ctrl s': - """User wants to (un)hide overlay window""" - if isinstance(self.loop.widget,urwid.Overlay): - self.__saved_overlay = self.loop.widget - self.loop.widget = self.main_widget - else: - if self.__saved_overlay: - self.loop.widget = self.__saved_overlay - self.__saved_overlay = None - - elif input == 'ctrl d' and 'D' in self.bridge.getVersion(): #Debug only for dev versions - self.debug() - elif input == 'f2': #user wants to (un)hide the contact_list - try: - center_widgets = self.center_part.widget_list - if self.contactList in center_widgets: - self.center_part.set_focus(0) #necessary as the focus change to the next object, we can go out of range if we are on the last object of self.center_part - center_widgets.remove(self.contactList) - del self.center_part.column_types[0] - else: - center_widgets.insert(0, self.contactList) - self.center_part.column_types.insert(0, ('weight', 2)) - except AttributeError: - #The main widget is not built (probably in Profile Manager) - pass - elif input == 'window resize': - width,height = self.loop.screen_size - if height<=5 and width<=35: - if not 'save_main_widget' in dir(self): - self.save_main_widget = self.loop.widget - self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !"))) - else: - if 'save_main_widget' in dir(self): - self.loop.widget = self.save_main_widget - del self.save_main_widget - try: - return self.menu_roller.checkShortcuts(input) - except AttributeError: - return input - - def __buildMenuRoller(self): - menu = sat_widgets.Menu(self.loop) - general = _("General") - menu.addMenu(general, _("Connect"), self.onConnectRequest) - menu.addMenu(general, _("Disconnect"), self.onDisconnectRequest) - menu.addMenu(general, _("Parameters"), self.onParam) - menu.addMenu(general, _("About"), self.onAboutRequest) - menu.addMenu(general, _("Exit"), self.onExitRequest, 'ctrl x') - contact = _("Contact") - menu.addMenu(contact, _("Add contact"), self.onAddContactRequest) - menu.addMenu(contact, _("Remove contact"), self.onRemoveContactRequest) - communication = _("Communication") - menu.addMenu(communication, _("Join room"), self.onJoinRoomRequest, 'meta j') - menu.addMenu(communication, _("Find Gateways"), self.onFindGatewaysRequest, 'meta g') - #additionals menus - #FIXME: do this in a more generic way (in quickapp) - add_menus = self.bridge.getMenus() - def add_menu_cb(menu): - category, item = menu - id = self.bridge.callMenu(category, item, "NORMAL", self.profile) - self.current_action_ids.add(id) - for new_menu in add_menus: - category,item,type = new_menu - assert(type=="NORMAL") #TODO: manage other types - menu.addMenu(unicode(category), unicode(item), add_menu_cb) - - menu_roller = sat_widgets.MenuRoller([(_('Main menu'),menu)]) - return menu_roller - - def __buildMainWidget(self): - self.contactList = ContactList(self, self.CM, on_click = self.contactSelected, on_change=lambda w: self.redraw()) - #self.center_part = urwid.Columns([('weight',2,self.contactList),('weight',8,Chat('',self))]) - self.center_part = urwid.Columns([('weight',2,self.contactList), ('weight',8,urwid.Filler(urwid.Text('')))]) - self.editBar = sat_widgets.AdvancedEdit('> ') - self.editBar.setCompletionMethod(self._nick_completion) - urwid.connect_signal(self.editBar,'click',self.onTextEntered) - self.menu_roller = self.__buildMenuRoller() - self.main_widget = sat_widgets.FocusFrame(self.center_part, header=self.menu_roller, footer=self.editBar, focus_part='footer') - return self.main_widget - - def _nick_completion(self, text, completion_data): - """Completion method which complete pseudo in group chat - for params, see AdvancedEdit""" - contact = self.contactList.get_contact() ###Based on the fact that there is currently only one contact selectableat once - if contact: - chat = self.chat_wins[contact] - if chat.type != "group": - return text - space = text.rfind(" ") - start = text[space+1:] - nicks = list(chat.occupants) - nicks.sort() - try: - start_idx=nicks.index(completion_data['last_nick'])+1 - if start_idx == len(nicks): - start_idx = 0 - except (KeyError,ValueError): - start_idx = 0 - for idx in range(start_idx,len(nicks)) + range(0,start_idx): - if nicks[idx].lower().startswith(start.lower()): - completion_data['last_nick'] = nicks[idx] - return text[:space+1] + nicks[idx] + (': ' if space < 0 else '') - return text - - - - def plug_profile(self, profile_key='@DEFAULT@'): - self.loop.widget = self.__buildMainWidget() - QuickApp.plug_profile(self, profile_key) - - def removePopUp(self, widget=None): - "Remove current pop-up, and if there is other in queue, show it" - self.loop.widget = self.main_widget - next_popup = self.notBar.getNextPopup() - if next_popup: - #we still have popup to show, we display it - self.showPopUp(next_popup) - - def showPopUp(self, pop_up_widget, perc_width=40, perc_height=40): - "Show a pop-up window if possible, else put it in queue" - if not isinstance(self.loop.widget,urwid.Overlay): - display_widget = urwid.Overlay(pop_up_widget, self.main_widget, 'center', ('relative', perc_width), 'middle', ('relative', perc_height)) - self.loop.widget = display_widget - self.redraw() - else: - self.notBar.addPopUp(pop_up_widget) - - def notify(self, message): - """"Notify message to user via notification bar""" - self.notBar.addMessage(message) - - def addWindow(self, widget): - """Display a window if possible, - else add it in the notification bar queue - @param widget: BoxWidget""" - assert(len(self.center_part.widget_list)<=2) - wid_idx = len(self.center_part.widget_list)-1 - self.center_part.widget_list[wid_idx] = widget - self.menu_roller.removeMenu(_('Chat menu')) - self.contactList.unselectAll() - self.redraw() - - def removeWindow(self): - """Remove window showed on the right column""" - #TODO: to a better Window management than this crappy hack - assert(len(self.center_part.widget_list)<=2) - wid_idx = len(self.center_part.widget_list)-1 - self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text('')) - self.center_part.set_focus(0) - self.redraw() - - def addProgress (self, id, message): - """Follow a SàT progress bar - @param id: SàT id of the progression - @param message: message to show to identify the progression""" - self.progress_wid.addProgress(id, message) - - def setProgress(self, percentage): - """Set the progression shown in notification bar""" - self.notBar.setProgress(percentage) - - def contactSelected(self, contact_list): - contact = contact_list.get_contact() - if contact: - assert(len(self.center_part.widget_list)==2) - self.center_part.widget_list[1] = self.chat_wins[contact] - self.menu_roller.addMenu(_('Chat menu'), self.chat_wins[contact].getMenu()) - - def onTextEntered(self, editBar): - """Called when text is entered in the main edit bar""" - contact = self.contactList.get_contact() ###Based on the fact that there is currently only one contact selectableat once - if contact: - chat = self.chat_wins[contact] - self.bridge.sendMessage(contact, - editBar.get_edit_text(), - type = "groupchat" if chat.type == 'group' else "chat", - profile_key=self.profile) - editBar.set_edit_text('') - - def newMessage(self, from_jid, msg, type, to_jid, profile): - if not self.check_profile(profile): - return - QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile) - sender = JID(from_jid) - if JID(self.contactList.selected).short != sender.short: - self.contactList.putAlert(sender) - - def _dialogOkCb(self, widget, data): - self.removePopUp() - answer_cb = data[0] - answer_data = [data[1]] if data[1] else [] - answer_cb(True, *answer_data) - - def _dialogCancelCb(self, widget, data): - self.removePopUp() - answer_cb = data[0] - answer_data = [data[1]] if data[1] else [] - answer_cb(False, *answer_data) - - - def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None): - if type == 'info': - popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore - flags = wx.OK | wx.ICON_INFORMATION - elif type == 'error': - popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore - elif type == 'yes/no': - popup = sat_widgets.ConfirmDialog(unicode(message), - yes_cb=self._dialogOkCb, yes_value = (answer_cb, answer_data), - no_cb=self._dialogCancelCb, no_value = (answer_cb, answer_data)) - else: - popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore - error(_('unmanaged dialog type: %s'), type) - self.showPopUp(popup) - - def onNotification(self, notBar): - """Called when a new notification has been received""" - if not isinstance(self.main_widget, sat_widgets.FocusFrame): - #if we are not in the main configuration, we ignore the notifications bar - return - if isinstance(self.main_widget.footer,sat_widgets.AdvancedEdit): - if not self.notBar.canHide(): - #the notification bar is not visible and has usefull informations, we show it - pile = urwid.Pile([self.notBar, self.editBar]) - self.main_widget.footer = pile - else: - if not isinstance(self.main_widget.footer, urwid.Pile): - error(_("INTERNAL ERROR: Unexpected class for main widget's footer")) - assert(False) - if self.notBar.canHide(): - #No notification left, we can hide the bar - self.main_widget.footer = self.editBar - - def actionResult(self, type, id, data): - if not id in self.current_action_ids: - debug (_('unknown id, ignoring')) - return - if type == "SUPPRESS": - self.current_action_ids.remove(id) - elif type == "XMLUI": - self.current_action_ids.remove(id) - debug (_("XML user interface received")) - misc = {} - #FIXME FIXME FIXME: must clean all this crap ! - title = _('Form') - if data['type'] == 'registration': - title = _('Registration') - misc['target'] = data['target'] - misc['action_back'] = self.bridge.gatewayRegister - ui = XMLUI(self, title=title, xml_data = data['xml'], misc = misc) - if data['type'] == 'registration': - ui.show('popup') - else: - ui.show('window') - elif type == "ERROR": - self.current_action_ids.remove(id) - self.showPopUp(sat_widgets.Alert(_("Error"), unicode(data["message"]), ok_cb=self.removePopUp)) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore - elif type == "RESULT": - self.current_action_ids.remove(id) - if self.current_action_ids_cb.has_key(id): - callback = self.current_action_ids_cb[id] - del self.current_action_ids_cb[id] - callback(data) - elif type == "DICT_DICT": - self.current_action_ids.remove(id) - if self.current_action_ids_cb.has_key(id): - callback = self.current_action_ids_cb[id] - del self.current_action_ids_cb[id] - callback(data) - else: - error (_("FIXME FIXME FIXME: type [%s] not implemented") % type) - raise NotImplementedError - - ##DIALOGS CALLBACKS## - def onJoinRoom(self, button, edit): - self.removePopUp() - room_jid = JID(edit.get_edit_text()) - if room_jid.is_valid(): - self.bridge.joinMUC(room_jid.domain, room_jid.node, self.profiles[self.profile]['whoami'].node, self.profile) - else: - message = _("'%s' is an invalid JID !") % room_jid - error (message) - self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp)) - - def onAddContact(self, button, edit): - self.removePopUp() - jid=JID(edit.get_edit_text()) - if jid.is_valid(): - self.bridge.addContact(jid.short, profile_key=self.profile) - else: - message = _("'%s' is an invalid JID !") % jid - error (message) - self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp)) - - def onRemoveContact(self, button): - self.removePopUp() - info(_("Unsubscribing %s presence"),self.contactList.get_contact()) - self.bridge.delContact(self.contactList.get_contact(), profile_key=self.profile) - - #MENU EVENTS# - def onConnectRequest(self, menu): - self.bridge.connect(self.profile) - - def onDisconnectRequest(self, menu): - self.bridge.disconnect(self.profile) - - def onParam(self, menu): - params = XMLUI(self,xml_data=self.bridge.getParamsUI(self.profile)) - self.addWindow(params) - - def onExitRequest(self, menu): - QuickApp.onExit(self) - raise urwid.ExitMainLoop() - - def onJoinRoomRequest(self, menu): - """User wants to join a MUC room""" - pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt = 'room@muc_service.server.tld', cancel_cb=self.removePopUp, ok_cb=self.onJoinRoom) - self.showPopUp(pop_up_widget) - - def onFindGatewaysRequest(self, e): - debug(_("Find Gateways request")) - id = self.bridge.findGateways(self.profiles[self.profile]['whoami'].domain, self.profile) - self.current_action_ids.add(id) - self.current_action_ids_cb[id] = self.onGatewaysFound - - def onAddContactRequest(self, menu): - pop_up_widget = sat_widgets.InputDialog(_("Adding a contact"), _("Please enter new contact JID"), default_txt = 'name@server.tld', cancel_cb=self.removePopUp, ok_cb=self.onAddContact) - self.showPopUp(pop_up_widget) - - def onRemoveContactRequest(self, menu): - contact = self.contactList.get_contact() - if not contact: - self.showPopUp(sat_widgets.Alert(_("Error"), _("You have not selected any contact to delete !"), ok_cb=self.removePopUp)) - else: - pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the contact [%s] ?" % contact), yes_cb=self.onRemoveContact, no_cb=self.removePopUp) - self.showPopUp(pop_up_widget) - - def onAboutRequest(self, menu): - self.showPopUp(sat_widgets.Alert(_("About"), const_APP_NAME + " v" + self.bridge.getVersion(), ok_cb=self.removePopUp)) - - #MISC CALLBACKS# - - def onGatewaysFound(self, data): - """Called when SàT has found the server gateways""" - target = data['__private__']['target'] - del data['__private__'] - gatewayManager = GatewaysManager(self, data, server=target) - self.addWindow(gatewayManager) - -sat = PrimitivusApp() -sat.start() -
--- a/frontends/primitivus/profile_manager.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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/>. -""" - -import urwid -from urwid_satext.sat_widgets import Password,List,InputDialog,ConfirmDialog,Alert,FocusFrame -from tools.jid import JID - - -class ProfileManager(urwid.WidgetWrap): - - def __init__(self, host): - self.host = host - #profiles list - profiles = self.host.bridge.getProfilesList() - profiles.sort() - - #login & password box must be created before list because of onProfileChange - self.login_wid = urwid.Edit(_('Login:'),align='center') - self.pass_wid = Password(_('Password:'),align='center') - - self.list_profile = List(profiles, style=['single'], align='center', on_click=self.onProfileChange) - - #new & delete buttons - buttons = [urwid.Button(_("New"), self.onNewProfile), - urwid.Button(_("Delete"), self.onDeleteProfile)] - buttons_flow = urwid.GridFlow(buttons, max([len(button.get_label()) for button in buttons])+4, 1, 1, 'center') - - #second part: login information: - divider = urwid.Divider('-') - - #connect button - connect_button = urwid.Button(_("Connect"), self.onConnectProfile) - - #we now build the widget - body_content = urwid.SimpleListWalker([buttons_flow,self.list_profile,divider,self.login_wid, self.pass_wid, connect_button]) - frame_body = urwid.ListBox(body_content) - frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title')) - self.main_widget = urwid.LineBox(frame) - urwid.WidgetWrap.__init__(self, self.main_widget) - - def __refillProfiles(self): - """Update the list of profiles""" - profiles = self.host.bridge.getProfilesList() - profiles.sort() - self.list_profile.changeValues(profiles) - - def cancelDialog(self, button): - self.host.removePopUp() - - def newProfile(self, button, edit): - """Create the profile""" - name = edit.get_edit_text() - self.host.bridge.createProfile(name) - self.__refillProfiles() - #We select the profile created in the list - self.list_profile.selectValue(name) - self.host.removePopUp() - - def deleteProfile(self, button): - profile_name = self.list_profile.getSelectedValue() - if profile_name: - self.host.bridge.deleteProfile(profile_name) - self.__refillProfiles() - self.host.removePopUp() - - - def onNewProfile(self, e): - pop_up_widget = InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile) - self.host.showPopUp(pop_up_widget) - - def onDeleteProfile(self, e): - pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getSelectedValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile) - self.host.showPopUp(pop_up_widget) - - def onProfileChange(self, list_wid): - profile_name = list_wid.getSelectedValue() - if profile_name: - jabberID = self.host.bridge.getParamA("JabberID", "Connection", profile_key=profile_name) - password = self.host.bridge.getParamA("Password", "Connection", profile_key=profile_name) - self.login_wid.set_edit_text(jabberID) - self.pass_wid.set_edit_text(password) - - def onConnectProfile(self, button): - profile_name = self.list_profile.getSelectedValue() - if not profile_name: - pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog) - self.host.showPopUp(pop_up_widget) - elif profile_name[0] == '@': - pop_up_widget = Alert(_('Bad profile name'), _("A profile name can't start with a @"), ok_cb=self.cancelDialog) - self.host.showPopUp(pop_up_widget) - else: - profile = self.host.bridge.getProfileName(profile_name) - assert(profile) - #TODO: move this to quick_app - old_jid = self.host.bridge.getParamA("JabberID", "Connection", profile_key=profile) - old_pass = self.host.bridge.getParamA("Password", "Connection", profile_key=profile) - new_jid = self.login_wid.get_edit_text() - new_pass = self.pass_wid.get_edit_text() - - if old_jid != new_jid: - self.host.bridge.setParam("JabberID", new_jid, "Connection", profile) - self.host.bridge.setParam("Server", JID(new_jid).domain, "Connection", profile) - if old_pass != new_pass: - self.host.bridge.setParam("Password", new_pass, "Connection", profile) - self.host.plug_profile(profile) -
--- a/frontends/primitivus/progress.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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/>. -""" - -import urwid -from urwid_satext import sat_widgets -from tools.jid import JID - - -class Progress(urwid.WidgetWrap): - - def __init__(self, host): - self.host = host - self.progress_list = urwid.SimpleListWalker([]) - self.progress_dict = {} - listbox = urwid.ListBox(self.progress_list) - buttons = [] - buttons.append(sat_widgets.CustomButton(_('Clear progress list'), self.__onClear)) - max_len = max([button.getSize() for button in buttons]) - buttons_wid = urwid.GridFlow(buttons,max_len,1,0,'center') - main_wid = sat_widgets.FocusFrame(listbox, footer=buttons_wid) - urwid.WidgetWrap.__init__(self, main_wid) - - def addProgress(self, id, message): - mess_wid = urwid.Text(message) - progr_wid = urwid.ProgressBar('progress_normal', 'progress_complete') - column = urwid.Columns([mess_wid, progr_wid]) - self.progress_dict[id] = {'full':column,'progress':progr_wid,'state':'init'} - self.progress_list.append(column) - self.progressCB(self.host.loop, (id, message)) - - def progressCB(self, loop, data): - id, message = data - data = self.host.bridge.getProgress(id) - pbar = self.progress_dict[id]['progress'] - if data: - if self.progress_dict[id]['state'] == 'init': - #first answer, we must construct the bar - self.progress_dict[id]['state'] = 'progress' - pbar.done = float(data['size']) - - pbar.set_completion(float(data['position'])) - self.updateNotBar() - else: - if self.progress_dict[id]['state'] == 'progress': - self.progress_dict[id]['state'] = 'done' - pbar.set_completion(pbar.done) - self.updateNotBar() - return - - loop.set_alarm_in(1,self.progressCB, (id, message)) - - def __removeBar(self, id): - wid = self.progress_dict[id]['full'] - self.progress_list.remove(wid) - del(self.progress_dict[id]) - - def __onClear(self, button): - to_remove = [] - for id in self.progress_dict: - if self.progress_dict[id]['state'] == 'done': - to_remove.append(id) - for id in to_remove: - self.__removeBar(id) - self.updateNotBar() - - def updateNotBar(self): - if not self.progress_dict: - self.host.setProgress(None) - return - progress = 0 - nb_bars = 0 - for id in self.progress_dict: - pbar = self.progress_dict[id]['progress'] - progress += pbar.current/pbar.done*100 - nb_bars+=1 - av_progress = progress/float(nb_bars) - self.host.setProgress(av_progress) -
--- a/frontends/primitivus/xmlui.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,261 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Primitivus: a SAT frontend -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/>. -""" - -import urwid -from logging import debug, info, warning, error -from urwid_satext import sat_widgets -from xml.dom import minidom - -class Pairs(urwid.WidgetWrap): - - def __init__(self, weight_0='1', weight_1='1'): - self.idx = 0 - self.weight_0 = weight_0 - self.weight_1 = weight_1 - columns = urwid.Columns([urwid.Text(''), urwid.Text('')]) - #XXX: empty Text hack needed because Pile doesn't support empty list - urwid.WidgetWrap.__init__(self,columns) - - def append(self, widget): - pile = self._w.widget_list[self.idx] - if isinstance(pile, urwid.Text): - self._w.widget_list[self.idx] = urwid.Pile([widget]) - if self.idx == 1: - self._w.set_focus(1) - else: - pile.widget_list.append(widget) - pile.item_types.append(('weight',getattr(self,'weight_'+str(self.idx)))) - self.idx = (self.idx + 1) % 2 - -class InvalidXMLUI(Exception): - pass - -class XMLUI(urwid.WidgetWrap): - - def __init__(self, host, xml_data, title = None, options = [], misc={}): - self.host = host - self.title = title - self.options = options - self.misc = misc - self.__dest = "window" - self.ctrl_list = {} # usefull to access ctrl - widget = self.constructUI(xml_data) - urwid.WidgetWrap.__init__(self,widget) - - def __parseElems(self, node, parent): - """Parse elements inside a <layout> tags, and add them to the parent""" - for elem in node.childNodes: - if elem.nodeName != "elem": - message=_("Unmanaged tag") - error(message) - raise Exception(message) - id = elem.getAttribute("id") - name = elem.getAttribute("name") - type = elem.getAttribute("type") - value = elem.getAttribute("value") if elem.hasAttribute('value') else u'' - if type=="empty": - ctrl = urwid.Text('') - elif type=="text": - try: - value = elem.childNodes[0].wholeText - except IndexError: - warning (_("text node has no child !")) - ctrl = urwid.Text(value) - elif type=="label": - ctrl = urwid.Text(value+": ") - elif type=="string": - ctrl = sat_widgets.AdvancedEdit(edit_text = value) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - elif type=="password": - ctrl = sat_widgets.Password(edit_text = value) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - elif type=="textbox": - ctrl = sat_widgets.AdvancedEdit(edit_text = value, multiline=True) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - elif type=="bool": - ctrl = urwid.CheckBox('', state = value=="true") - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - elif type=="list": - style=[] if elem.getAttribute("multi")=='yes' else ['single'] - ctrl = sat_widgets.List(options=[option.getAttribute("value") for option in elem.getElementsByTagName("option")], style=style) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - elif type=="button": - callback_id = elem.getAttribute("callback_id") - ctrl = sat_widgets.CustomButton(value, on_press=self.onButtonPress) - ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")]) - else: - error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type) #FIXME ! - raise NotImplementedError - if self.type == 'param': - if isinstance(ctrl,urwid.Edit) or isinstance(ctrl,urwid.CheckBox): - urwid.connect_signal(ctrl,'change',self.onParamChange) - ctrl._param_category = self._current_category - ctrl._param_name = name - parent.append(ctrl) - - def __parseChilds(self, current, elem, wanted = ['layout'], data = None): - """Recursively parse childNodes of an elemen - @param current: widget container with 'append' method - @param elem: element from which childs will be parsed - @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant""" - for node in elem.childNodes: - if wanted and not node.nodeName in wanted: - raise InvalidXMLUI - if node.nodeName == "layout": - type = node.getAttribute('type') - if type == "tabs": - tab_cont = sat_widgets.TabsContainer() - self.__parseChilds(current, node, ['category'], tab_cont) - current.append(tab_cont) - elif type == "vertical": - self.__parseElems(node, current) - elif type == "pairs": - pairs = Pairs() - self.__parseElems(node, pairs) - current.append(pairs) - else: - warning(_("Unknown layout, using default one")) - self.__parseElems(node, current) - elif node.nodeName == "category": - name = node.getAttribute('name') - label = node.getAttribute('label') - if not name or not isinstance(data,sat_widgets.TabsContainer): - raise InvalidXMLUI - if self.type == 'param': - self._current_category = name #XXX: awful hack because params need category and we don't keep parent - tab_cont = data - listbox = tab_cont.addTab(label or name) - self.__parseChilds(listbox.body, node, ['layout']) - else: - message=_("Unknown tag") - error(message) - raise NotImplementedError - - def constructUI(self, xml_data): - - ret_wid = urwid.ListBox(urwid.SimpleListWalker([])) - - cat_dom = minidom.parseString(xml_data.encode('utf-8')) - top=cat_dom.documentElement - self.type = top.getAttribute("type") - self.title = top.getAttribute("title") or self.title - if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']: - raise InvalidXMLUI - - if self.type == 'param': - self.param_changed = set() - - self.__parseChilds(ret_wid.body, cat_dom.documentElement) - - assert ret_wid.body - - if isinstance(ret_wid.body[0],sat_widgets.TabsContainer): - ret_wid = ret_wid.body[0] #xxx: awfull hack cause TabsContainer is a BoxWidget, can't be inside a ListBox - - - if self.type == 'form': - buttons = [] - buttons.append(urwid.Button(_('Submit'),self.onFormSubmitted)) - if not 'NO_CANCEL' in self.options: - buttons.append(urwid.Button(_('Cancel'),self.onFormCancelled)) - max_len = max([len(button.get_label()) for button in buttons]) - grid_wid = urwid.GridFlow(buttons,max_len+4,1,0,'center') - ret_wid.body.append(grid_wid) - elif self.type == 'param': - assert(isinstance(ret_wid,sat_widgets.TabsContainer)) - buttons = [] - buttons.append(sat_widgets.CustomButton(_('Save'),self.onSaveParams)) - buttons.append(sat_widgets.CustomButton(_('Cancel'),lambda x:self.host.removeWindow())) - max_len = max([button.getSize() for button in buttons]) - grid_wid = urwid.GridFlow(buttons,max_len,1,0,'center') - ret_wid.addFooter(grid_wid) - return ret_wid - - def show(self,show_type = 'popup'): - """Show the constructed UI - @param show_type: how to show the UI: - - popup - - window""" - self.__dest = "popup" - decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or '')) - if show_type == 'popup': - self.host.showPopUp(decorated) - elif show_type == 'window': - self.host.addWindow(decorated) - else: - error(_('INTERNAL ERROR: Unmanaged show_type (%s)') % show_type) - assert(False) - self.host.redraw() - - - ##EVENTS## - - def onButtonPress(self, button): - callback_id, fields = button.param_id - data = {"callback_id":callback_id} - for field in fields: - ctrl = self.ctrl_list[field] - if isinstance(ctrl['control'],sat_widgets.List): - data[field] = '\t'.join(ctrl['control'].getSelectedValues()) - else: - data[field] = ctrl['control'].getValue() - - id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile) - self.host.current_action_ids.add(id) - - def onParamChange(self, widget, extra=None): - """Called when type is param and a widget to save is modified""" - assert(self.type == "param") - self.param_changed.add(widget) - - def onFormSubmitted(self, button): - data = [] - for ctrl_name in self.ctrl_list: - ctrl = self.ctrl_list[ctrl_name] - if isinstance(ctrl['control'], sat_widgets.List): - data.append((ctrl_name, ctrl['control'].getSelectedValue())) - elif isinstance(ctrl['control'], urwid.CheckBox): - data.append((ctrl_name, "true" if ctrl['control'].get_state() else "false")) - else: - data.append((ctrl_name, ctrl['control'].get_edit_text())) - if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned - raise NotImplementedError - self.host.debug() - elif self.misc.has_key('callback'): - self.misc['callback'](data) - else: - warning (_("The form data is not sent back, the type is not managed properly")) - self.host.removePopUp() - - def onFormCancelled(self, button): - if self.__dest == 'window': - self.host.removeWindow() - else: - self.host.removePopUp() - - def onSaveParams(self, button): - for ctrl in self.param_changed: - if isinstance(ctrl, urwid.CheckBox): - value = "true" if ctrl.get_state() else "false" - else: - value = ctrl.get_edit_text() - self.host.bridge.setParam(ctrl._param_name, value, ctrl._param_category, profile_key = self.host.profile) - self.host.removeWindow()
--- a/frontends/quick_frontend/quick_app.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,437 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -helper class for making a SAT frontend -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 tools.jid import JID -from sat_bridge_frontend.DBus import DBusBridgeFrontend,BridgeExceptionNoService -from optparse import OptionParser -import pdb - -import gettext -gettext.install('sat_frontend', "../i18n", unicode=True) - -class QuickApp(): - """This class contain the main methods needed for the frontend""" - - def __init__(self, single_profile=True): - self.rosterList = {} - self.profiles = {} - self.single_profile = single_profile - self.check_options() - - ## bridge ## - try: - self.bridge=DBusBridgeFrontend() - except BridgeExceptionNoService: - print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) - import sys - sys.exit(1) - self.bridge.register("connected", self.connected) - self.bridge.register("disconnected", self.disconnected) - self.bridge.register("newContact", self.newContact) - self.bridge.register("newMessage", self.newMessage) - self.bridge.register("newAlert", self.newAlert) - self.bridge.register("presenceUpdate", self.presenceUpdate) - self.bridge.register("roomJoined", self.roomJoined) - self.bridge.register("roomUserJoined", self.roomUserJoined) - self.bridge.register("roomUserLeft", self.roomUserLeft) - self.bridge.register("roomNewSubject", self.roomNewSubject) - self.bridge.register("tarotGameStarted", self.tarotGameStarted) - self.bridge.register("tarotGameNew", self.tarotGameNew) - self.bridge.register("tarotGameChooseContrat", self.tarotChooseContrat) - self.bridge.register("tarotGameShowCards", self.tarotShowCards) - self.bridge.register("tarotGameYourTurn", self.tarotMyTurn) - self.bridge.register("tarotGameScore", self.tarotScore) - self.bridge.register("tarotGameCardsPlayed", self.tarotCardsPlayed) - self.bridge.register("tarotGameInvalidCards", self.tarotInvalidCards) - self.bridge.register("subscribe", self.subscribe) - self.bridge.register("paramUpdate", self.paramUpdate) - self.bridge.register("contactDeleted", self.contactDeleted) - self.bridge.register("updatedValue", self.updatedValue, "request") - self.bridge.register("askConfirmation", self.askConfirmation, "request") - self.bridge.register("actionResult", self.actionResult, "request") - self.bridge.register("actionResultExt", self.actionResult, "request") - - self.current_action_ids = set() - self.current_action_ids_cb = {} - - def check_profile(self, profile): - """Tell if the profile is currently followed by the application""" - return profile in self.profiles.keys() - - def postInit(self): - """Must be called after initialization is done, do all automatic task (auto plug profile)""" - if self.options.profile: - if not self.bridge.getProfileName(self.options.profile): - error(_("Trying to plug an unknown profile (%s)" % self.options.profile)) - else: - self.plug_profile(self.options.profile) - - def check_options(self): - """Check command line options""" - usage=_(""" - %prog [options] - - %prog --help for options list - """) - parser = OptionParser(usage=usage) - - parser.add_option("-p", "--profile", help=_("Select the profile to use")) - - (self.options, args) = parser.parse_args() - if self.options.profile: - self.options.profile = self.options.profile.decode('utf-8') - return args - - def plug_profile(self, profile_key='@DEFAULT@'): - """Tell application which profile must be used""" - if self.single_profile and self.profiles: - error(_('There is already one profile plugged (we are in single profile mode) !')) - return - profile = self.bridge.getProfileName(profile_key) - if not profile: - error(_("The profile asked doesn't exist")) - return - if self.profiles.has_key(profile): - warning(_("The profile is already plugged")) - return - self.profiles[profile]={} - if self.single_profile: - self.profile = profile - - ###now we get the essential params### - self.profiles[profile]['whoami']=JID(self.bridge.getParamA("JabberID","Connection", profile)) - autoconnect = self.bridge.getParamA("autoconnect","Connection", profile) == "true" - self.profiles[profile]['watched']=self.bridge.getParamA("Watched", "Misc", profile).split() #TODO: put this in a plugin - - ## misc ## - self.profiles[profile]['onlineContact'] = set() #FIXME: temporary - - #TODO: gof: manage multi-profiles here - if not self.bridge.isConnected(profile): - self.setStatusOnline(False) - else: - self.setStatusOnline(True) - - ### now we fill the contact list ### - for contact in self.bridge.getContacts(profile): - self.newContact(contact[0], contact[1], contact[2], profile) - - presences = self.bridge.getPresenceStatus(profile) - for contact in presences: - for res in presences[contact]: - jabber_id = contact+('/'+res if res else '') - show = presences[contact][res][0] - priority = presences[contact][res][1] - statuses = presences[contact][res][2] - self.presenceUpdate(jabber_id, show, priority, statuses, profile) - - #The waiting subscription requests - waitingSub = self.bridge.getWaitingSub(profile) - for sub in waitingSub: - self.subscribe(waitingSub[sub], sub, profile) - - #Now we open the MUC window where we already are: - for room_args in self.bridge.getRoomJoined(profile): - self.roomJoined(*room_args, profile=profile) - - for subject_args in self.bridge.getRoomSubjects(profile): - self.roomNewSubject(*subject_args, profile=profile) - - if autoconnect and not self.bridge.isConnected(profile_key): - #Does the user want autoconnection ? - self.bridge.connect(profile_key) - - - def unplug_profile(self, profile): - """Tell the application to not follow anymore the profile""" - if not profile in self.profiles: - warning (_("This profile is not plugged")) - return - self.profiles.remove(profile) - - def clear_profile(self): - self.profiles.clear() - - def connected(self, profile): - """called when the connection is made""" - if not self.check_profile(profile): - return - debug(_("Connected")) - self.setStatusOnline(True) - - - def disconnected(self, profile): - """called when the connection is closed""" - if not self.check_profile(profile): - return - debug(_("Disconnected")) - self.CM.clear() - self.contactList.clear_contacts() - self.setStatusOnline(False) - - def newContact(self, JabberId, attributes, groups, profile): - if not self.check_profile(profile): - return - entity=JID(JabberId) - self.rosterList[entity.short]=(dict(attributes), list(groups)) - - def newMessage(self, from_jid, msg, type, to_jid, profile): - if not self.check_profile(profile): - return - sender=JID(from_jid) - addr=JID(to_jid) - win = addr if sender.short == self.profiles[profile]['whoami'].short else sender - self.current_action_ids = set() - self.current_action_ids_cb = {} - self.chat_wins[win.short].printMessage(sender, msg, profile) - - def newAlert(self, msg, title, alert_type, profile): - if not self.check_profile(profile): - return - assert alert_type in ['INFO','ERROR'] - self.showDialog(unicode(msg),unicode(title),alert_type.lower()) - - - def setStatusOnline(self, online=True): - pass - - def presenceUpdate(self, jabber_id, show, priority, statuses, profile): - if not self.check_profile(profile): - return - debug (_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]") % {'jid':jabber_id, 'show':show, 'priority':priority, 'statuses':statuses, 'profile':profile}); - from_jid=JID(jabber_id) - debug ("from_jid.short=%(from_jid)s whoami.short=%(whoami)s" % {'from_jid':from_jid.short, 'whoami':self.profiles[profile]['whoami'].short}) - - if from_jid.short==self.profiles[profile]['whoami'].short: - if not type: - self.setStatusOnline(True) - elif type=="unavailable": - self.setStatusOnline(False) - return - - if show != 'unavailable': - name="" - groups = [] - if self.rosterList.has_key(from_jid.short): - if self.rosterList[from_jid.short][0].has_key("name"): - name=self.rosterList[from_jid.short][0]["name"] - groups=self.rosterList[from_jid.short][1] - - #FIXME: must be moved in a plugin - if from_jid.short in self.profiles[profile]['watched'] and not from_jid.short in self.profiles[profile]['onlineContact']: - self.showAlert(_("Watched jid [%s] is connected !") % from_jid.short) - - self.profiles[profile]['onlineContact'].add(from_jid) #FIXME onlineContact is useless with CM, must be removed - self.CM.add(from_jid) - self.CM.update(from_jid, 'name', name) - self.CM.update(from_jid, 'show', show) - self.CM.update(from_jid, 'statuses', statuses) - self.CM.update(from_jid, 'groups', groups) - cache = self.bridge.getCardCache(from_jid) - if cache.has_key('nick'): - self.CM.update(from_jid, 'nick', cache['nick']) - if cache.has_key('avatar'): - self.CM.update(from_jid, 'avatar', self.bridge.getAvatarFile(cache['avatar'])) - self.contactList.replace(from_jid, self.CM.getAttr(from_jid, 'groups')) - - if show=="unavailable" and from_jid in self.profiles[profile]['onlineContact']: - self.profiles[profile]['onlineContact'].remove(from_jid) - self.CM.remove(from_jid) - if not self.CM.isConnected(from_jid): - self.contactList.disconnect(from_jid) - - def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile): - """Called when a MUC room is joined""" - if not self.check_profile(profile): - return - debug (_("Room [%(room_name)s] joined by %(profile)s, users presents:%(users)s") % {'room_name':room_id+'@'+room_service, 'profile': profile, 'users':room_nicks}) - room_jid=room_id+'@'+room_service - self.chat_wins[room_jid].setUserNick(user_nick) - self.chat_wins[room_jid].setType("group") - self.chat_wins[room_jid].id = room_jid - self.chat_wins[room_jid].setPresents(list(set([user_nick]+room_nicks))) - - - def roomUserJoined(self, room_id, room_service, user_nick, user_data, profile): - """Called when an user joined a MUC room""" - if not self.check_profile(profile): - return - room_jid=room_id+'@'+room_service - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].replaceUser(user_nick) - debug (_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick':user_nick, 'room_jid':room_jid}) - - def roomUserLeft(self, room_id, room_service, user_nick, user_data, profile): - """Called when an user joined a MUC room""" - if not self.check_profile(profile): - return - room_jid=room_id+'@'+room_service - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].removeUser(user_nick) - debug (_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick':user_nick, 'room_jid':room_jid}) - - def roomNewSubject(self, room_id, room_service, subject, profile): - """Called when subject of MUC room change""" - if not self.check_profile(profile): - return - room_jid=room_id+'@'+room_service - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].setSubject(subject) - debug (_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid':room_jid, "subject":subject}) - - def tarotGameStarted(self, room_jid, referee, players, profile): - if not self.check_profile(profile): - return - debug (_("Tarot Game Started \o/")) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].startGame("Tarot", referee, players) - debug (_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee':referee, 'room_jid':room_jid, 'players':[str(player) for player in players]}) - - def tarotGameNew(self, room_jid, hand, profile): - if not self.check_profile(profile): - return - debug (_("New Tarot Game")) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].getGame("Tarot").newGame(hand) - - def tarotChooseContrat(self, room_jid, xml_data, profile): - """Called when the player has to select his contrat""" - if not self.check_profile(profile): - return - debug (_("Tarot: need to select a contrat")) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].getGame("Tarot").chooseContrat(xml_data) - - def tarotShowCards(self, room_jid, game_stage, cards, data, profile): - if not self.check_profile(profile): - return - debug (_("Show cards")) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].getGame("Tarot").showCards(game_stage, cards, data) - - def tarotMyTurn(self, room_jid, profile): - if not self.check_profile(profile): - return - debug (_("My turn to play")) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].getGame("Tarot").myTurn() - - def tarotScore(self, room_jid, xml_data, winners, loosers, profile): - """Called when the game is finished and the score are updated""" - if not self.check_profile(profile): - return - debug (_("Tarot: score received")) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].getGame("Tarot").showScores(xml_data, winners, loosers) - - def tarotCardsPlayed(self, room_jid, player, cards, profile): - if not self.check_profile(profile): - return - debug (_("Card(s) played (%(player)s): %(cards)s") % {"player":player, "cards":cards}) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].getGame("Tarot").cardsPlayed(player, cards) - - def tarotInvalidCards(self, room_jid, phase, played_cards, invalid_cards, profile): - if not self.check_profile(profile): - return - debug (_("Cards played are not valid: %s") % invalid_cards) - if self.chat_wins.has_key(room_jid): - self.chat_wins[room_jid].getGame("Tarot").invalidCards(phase, played_cards, invalid_cards) - - def _subscribe_cb(self, answer, data): - entity, profile = data - if answer: - self.bridge.subscription("subscribed", entity.short, profile_key = profile) - else: - self.bridge.subscription("unsubscribed", entity.short, profile_key = profile) - - def subscribe(self, type, raw_jid, profile): - """Called when a subsciption management signal is received""" - if not self.check_profile(profile): - return - entity = JID(raw_jid) - if type=="subscribed": - # this is a subscription confirmation, we just have to inform user - self.showDialog(_("The contact %s has accepted your subscription") % entity.short, _('Subscription confirmation')) - elif type=="unsubscribed": - # this is a subscription refusal, we just have to inform user - self.showDialog(_("The contact %s has refused your subscription") % entity.short, _('Subscription refusal'), 'error') - elif type=="subscribe": - # this is a subscriptionn request, we have to ask for user confirmation - answer = self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.short, _('Subscription confirmation'), 'yes/no', answer_cb = self._subscribe_cb, answer_data=(entity, profile)) - - def showDialog(self, message, title, type="info", answer_cb = None): - raise NotImplementedError - - def showAlert(self, message): - pass #FIXME - - def paramUpdate(self, name, value, namespace, profile): - if not self.check_profile(profile): - return - debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace':namespace, 'name':name, 'value':value}) - if (namespace,name) == ("Connection", "JabberID"): - debug (_("Changing JID to %s"), value) - self.profiles[profile]['whoami']=JID(value) - elif (namespace,name) == ("Misc", "Watched"): - self.profiles[profile]['watched']=value.split() - - def contactDeleted(self, jid, profile): - if not self.check_profile(profile): - return - target = JID(jid) - self.CM.remove(target) - self.contactList.remove(self.CM.get_full(target)) - try: - self.profiles[profile]['onlineContact'].remove(target.short) - except KeyError: - pass - - def updatedValue(self, name, data): - if name == "card_nick": - target = JID(data['jid']) - if target in self.contactList: - self.CM.update(target, 'nick', data['nick']) - self.contactList.replace(target) - elif name == "card_avatar": - target = JID(data['jid']) - if target in self.contactList: - filename = self.bridge.getAvatarFile(data['avatar']) - self.CM.update(target, 'avatar', filename) - self.contactList.replace(target) - - def askConfirmation(self, type, id, data): - raise NotImplementedError - - def actionResult(self, type, id, data): - raise NotImplementedError - - def onExit(self): - """Must be called when the frontend is terminating""" - #TODO: mange multi-profile here - try: - autodisconnect = self.bridge.getParamA("autodisconnect","Connection", self.profile) == "true" - if autodisconnect and self.bridge.isConnected(self.profile): - #Does the user want autodisconnection ? - self.bridge.disconnect(self.profile) - except: - pass
--- a/frontends/quick_frontend/quick_card_game.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,156 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -helper class for making a SAT frontend -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 tools.jid import JID - - - -class QuickCardGame(): - - def __init__(self, parent, referee, players, player_nick): - self._autoplay = None #XXX: use 0 to activate fake play, None else - self.parent = parent - self.referee = referee - self.players = players - self.played = {} - for player in players: - self.played[player] = None - self.player_nick = player_nick - self.bottom_nick = unicode(self.player_nick) - idx = self.players.index(self.player_nick) - idx = (idx + 1) % len(self.players) - self.right_nick = unicode(self.players[idx]) - idx = (idx + 1) % len(self.players) - self.top_nick = unicode(self.players[idx]) - idx = (idx + 1) % len(self.players) - self.left_nick = unicode(self.players[idx]) - self.bottom_nick = unicode(player_nick) - self.selected = [] #Card choosed by the player (e.g. during ecart) - self.hand_size = 13 #number of cards in a hand - self.hand = [] - self.to_show = [] - self.state = None - - def getPlayerLocation(self, nick): - """return player location (top,bottom,left or right)""" - for location in ['top','left','bottom','right']: - if getattr(self,'%s_nick' % location) == nick: - return location - assert(False) - - def loadCards(self): - """Load all the cards in memory - @param dir: directory where the PNG files are""" - self.cards={} - self.deck=[] - self.cards["atout"]={} #As Tarot is a french game, it's more handy & logical to keep french names - self.cards["pique"]={} #spade - self.cards["coeur"]={} #heart - self.cards["carreau"]={} #diamond - self.cards["trefle"]={} #club - - def newGame(self, hand): - """Start a new game, with given hand""" - assert (len(self.hand) == 0) - for suit, value in hand: - self.hand.append(self.cards[suit, value]) - self.hand.sort() - self.state = "init" - - def contratSelected(self, contrat): - """Called when the contrat has been choosed - @param data: form result""" - self.parent.host.bridge.tarotGameContratChoosed(self.player_nick, self.referee, contrat or 'Passe', self.parent.host.profile) - - def chooseContrat(self, xml_data): - """Called when the player as to select his contrat - @param xml_data: SàT xml representation of the form""" - raise NotImplementedError - - def showCards(self, game_stage, cards, data): - """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" - self.to_show = [] - for suit, value in cards: - self.to_show.append(self.cards[suit, value]) - if game_stage == "chien" and data['attaquant'] == self.player_nick: - self.state = "wait_for_ecart" - else: - self.state = "chien" - - def myTurn(self): - """Called when we have to play :)""" - if self.state == "chien": - self.to_show = [] - self.state = "play" - self.__fakePlay() - - def __fakePlay(self): - """Convenience method for stupid autoplay - /!\ don't forgot to comment any interactive dialog for invalid card""" - if self._autoplay == None: - return - if self._autoplay >= len(self.hand): - self._autoplay = 0 - card = self.hand[self._autoplay] - self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], profile_key = self.parent.host.profile) - del self.hand[self._autoplay] - self.state = "wait" - self._autoplay+=1 - - def showScores(self, xml_data, winners, loosers): - """Called at the end of a game - @param xml_data: SàT xml representation of the scores - @param winners: list of winners' nicks - @param loosers: list of loosers' nicks""" - raise NotImplementedError - - def cardsPlayed(self, player, cards): - """A card has been played by player""" - if self.to_show: - self.to_show = [] - pl_cards = [] - if self.played[player] != None: #FIXME - for pl in self.played: - self.played[pl] = None - for suit, value in cards: - pl_cards.append(self.cards[suit, value]) - self.played[player] = pl_cards[0] - - def invalidCards(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - - if phase == "play": - self.state = "play" - elif phase == "ecart": - self.state = "ecart" - else: - error ('INTERNAL ERROR: unmanaged game phase') - - for suit, value in played_cards: - self.hand.append(self.cards[suit, value]) - - self.hand.sort() - self.__fakePlay() -
--- a/frontends/quick_frontend/quick_chat.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,127 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -helper class for making a SAT frontend -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 tools.jid import JID - - - -class QuickChat(): - - def __init__(self, target, host, type='one2one'): - self.target = target - self.host = host - self.type = type - self.id = "" - self.nick = None - self.occupants = set() - - def setType(self, type): - """Set the type of the chat - @param type: can be 'one2one' for single conversation or 'group' for chat à la IRC - """ - self.type = type - - def setPresents(self, nicks): - """Set the users presents in the contact list for a group chat - @param nicks: list of nicknames - """ - debug (_("Adding users %s to room") % nicks) - if self.type != "group": - error (_("[INTERNAL] trying to set presents nicks for a non group chat window")) - raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here - self.occupants.update(nicks) - - def replaceUser(self, nick): - """Add user if it is not in the group list""" - debug (_("Replacing user %s") % nick) - if self.type != "group": - error (_("[INTERNAL] trying to replace user for a non group chat window")) - raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here - len_before = len(self.occupants) - self.occupants.add(nick) - if len_before != len(self.occupants): - self.printInfo("=> %s has joined the room" % nick) - - def setUserNick(self, nick): - """Set the nick of the user, usefull for e.g. change the color of the user""" - self.nick = nick - - def removeUser(self, nick): - """Remove a user from the group list""" - debug(_("Removing user %s") % nick) - if self.type != "group": - error (_("[INTERNAL] trying to remove user for a non group chat window")) - raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here - self.occupants.remove(nick) - self.printInfo("<= %s has left the room" % nick) - - def setSubject(self, subject): - """Set title for a group chat""" - debug(_("Setting subject to %s") % subject) - if self.type != "group": - error (_("[INTERNAL] trying to set subject for a non group chat window")) - raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here - - def historyPrint(self, size=20, keep_last=False, profile='@NONE@'): - """Print the initial history""" - debug (_("now we print history")) - history=self.host.bridge.getHistory(self.host.profiles[profile]['whoami'].short, self.target, 20) - stamps=history.keys() - stamps.sort() - for stamp in stamps: - self.printMessage(JID(history[stamp][0]), history[stamp][1], profile, stamp) - if keep_last: ##FIXME hack for sortilege - self.last_history = stamps[-1] if stamps else None - - def _get_nick(self, jid): - """Return nick of this jid when possible""" - return jid.resource if self.type == "group" else (self.host.CM.getAttr(jid,'nick') or self.host.CM.getAttr(jid,'name') or jid.node) - - def printMessage(self, from_jid, msg, profile, timestamp): - """Print message in chat window. Must be implemented by child class""" - jid=JID(from_jid) - nick = self._get_nick(jid) - mymess = (jid.resource == self.nick) if self.type == "group" else (jid.short == self.host.profiles[profile]['whoami'].short) #mymess = True if message comes from local user - if msg.startswith('/me '): - self.printInfo('* %s %s' % (nick, msg[4:]),type='me') - return - return jid, nick, mymess - - def printInfo(self, msg, type='normal'): - """Print general info - @param msg: message to print - @type: one of: - normal: general info like "toto has joined the room" - me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" - """ - raise NotImplementedError - - - def startGame(self, game_type, referee, players): - """Configure the chat window to start a game""" - #No need to raise an error as game are not mandatory - warning(_('startGame is not implemented in this frontend')) - - def getGame(self, game_type): - """Return class managing the game type""" - #No need to raise an error as game are not mandatory - warning(_('getGame is not implemented in this frontend'))
--- a/frontends/quick_frontend/quick_chat_list.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -helper class for making a SAT frontend -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 tools.jid import JID - - -class QuickChatList(dict): - """This class is used to manage the list of chat windows. - It act as a dict, but create a chat window when the name is found for the first time.""" - - def __init__(self, host): - dict.__init__(self) - self.host = host - - def __getitem__(self,to_jid): - target=JID(to_jid) - if not self.has_key(target.short): - #we have to create the chat win - self[target.short] = self.createChat(target) - return dict.__getitem__(self,target.short) - - def createChat(self, target): - raise NotImplementedError
--- a/frontends/quick_frontend/quick_contact_list.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -helper class for making a SAT frontend -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 tools.jid import JID - - -class QuickContactList(): - """This class manage the visual representation of contacts""" - - def __init__(self, CM): - """ - @param CM: instance of QuickContactManagement - """ - debug(_("Contact List init")) - self.CM = CM - - def __contains__(self, jid): - raise NotImplementedError - - def clear_contacts(self, jid): - """Clear all the contact list""" - raise NotImplementedError - - def replace(self, jid, groups=None): - """add a contact to the list if doesn't exist, else update it""" - raise NotImplementedError - - def disconnect(self, jid): - """mark a contact disconnected""" - raise NotImplementedError - - def remove(self, jid): - """remove a contact from the list""" - raise NotImplementedError - - def add(self, jid, param_groups=None): - """add a contact to the list""" - raise NotImplementedError
--- a/frontends/quick_frontend/quick_contact_management.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -helper class for making a SAT frontend -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 tools.jid import JID -import pdb - - -class QuickContactManagement(): - """This helper class manage the contacts and ease the use of nicknames and shortcuts""" - ### FIXME: is SàT a better place for all this stuff ??? ### - - def __init__(self): - self.__contactlist = {} - - def clear(self): - """Clear all the contact list""" - self.__contactlist.clear() - - def add(self, entity): - """Add contact to the list, update resources""" - if not self.__contactlist.has_key(entity.short): - self.__contactlist[entity.short] = {'resources':[]} - if entity.resource in self.__contactlist[entity.short]['resources']: - self.__contactlist[entity.short]['resources'].remove(entity.resource) - self.__contactlist[entity.short]['resources'].append(entity.resource) - - def getContFromGroup(self, group): - """Return all contacts which are in given group""" - result = [] - for contact in self.__contactlist: - if self.__contactlist[contact].has_key('groups'): - if group in self.__contactlist[contact]['groups']: - result.append(JID(contact)) - return result - - def getAttr(self, entity, name): - """Return a specific attribute of contact, or all attributes - @param entity: jid of the contact - @param name: name of the attribute - @return: asked attribute""" - if self.__contactlist.has_key(entity.short): - if name == 'status': #FIXME: for the moment, we only use the first status - if self.__contactlist[entity.short]['statuses']: - return self.__contactlist[entity.short]['statuses'].values()[0] - if self.__contactlist[entity.short].has_key(name): - return self.__contactlist[entity.short][name] - else: - debug(_('Trying to get attribute for an unknown contact')) - return None - - def isConnected(self, entity): - """Tell if the contact is online""" - return self.__contactlist.has_key(entity.short) - - def remove(self, entity): - """remove resource. If no more resource is online or is no resource is specified, contact is deleted""" - try: - if entity.resource: - self.__contactlist[entity.short]['resources'].remove(entity.resource) - if not entity.resource or not self.__contactlist[entity.short]['resources']: - #no more resource available: the contact seems really disconnected - del self.__contactlist[entity.short] - except KeyError: - error(_('INTERNAL ERROR: Key error')) - raise - - def update(self, entity, key, value): - """Update attribute of contact - @param entity: jid of the contact - @param key: name of the attribute - @param value: value of the attribute - """ - if self.__contactlist.has_key(entity.short): - self.__contactlist[entity.short][key] = value - else: - debug (_('Trying to update an unknown contact: %s'), entity.short) - - def get_full(self, entity): - return entity.short+'/'+self.__contactlist[entity.short]['resources'][-1] -
--- a/frontends/quick_frontend/quick_gateways.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -helper class for making a SAT frontend -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 QuickGatewaysManager(): - - - def __init__(self, host, gateways, title=_("Gateways manager"), server=None): - self.WARNING_MSG = _(u"""Be careful ! Gateways allow you to use an external IM (legacy IM), so you can see your contact as jabber contacts. -But when you do this, all your messages go throught the external legacy IM server, it is a huge privacy issue (i.e.: all your messages throught the gateway can be monitored, recorded, analyzed by the external server, most of time a private company).""") - self.host = host - - def getGatewayDesc(self, gat_type): - """Return a human readable description of gateway type - @param gat_type: type of gateway, as given by SàT""" - desc = _('Unknown IM') - - if gat_type == 'irc': - desc = "Internet Relay Chat" - elif gat_type == 'xmpp': - desc = "XMPP" - elif gat_type == 'qq': - desc = "Tencent QQ" - elif gat_type == 'simple': - desc = "SIP/SIMPLE" - elif gat_type == 'icq': - desc = "ICQ" - elif gat_type == 'yahoo': - desc = "Yahoo! Messenger" - elif gat_type == 'gadu-gadu': - desc = "Gadu-Gadu" - elif gat_type == 'aim': - desc = "AOL Instant Messenger" - elif gat_type == 'msn': - desc = 'Windows Live Messenger' - - return desc
--- a/frontends/sat_bridge_frontend/DBus.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,182 +0,0 @@ -#!/usr/bin/python -#-*- coding: utf-8 -*- - -""" -SAT communication bridge -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_frontend import BridgeFrontend -import dbus, dbus.glib - -class BridgeExceptionNoService(Exception): - pass - -class DBusBridgeFrontend(BridgeFrontend): - def __init__(self): - try: - self.sessions_bus = dbus.SessionBus() - self.db_object = self.sessions_bus.get_object('org.goffi.SAT', - '/org/goffi/SAT/bridge') - self.db_comm_iface = dbus.Interface(self.db_object, - dbus_interface='org.goffi.SAT.communication') - self.db_req_iface = dbus.Interface(self.db_object, - dbus_interface='org.goffi.SAT.request') - except dbus.exceptions.DBusException,e: - if e._dbus_error_name=='org.freedesktop.DBus.Error.ServiceUnknown': - raise BridgeExceptionNoService - else: - raise e - #props = self.db_comm_iface.getProperties() - - def register(self, functionName, handler, iface="communication"): - if iface == "communication": - self.db_comm_iface.connect_to_signal(functionName, handler) - elif iface == "request": - self.db_req_iface.connect_to_signal(functionName, handler) - - def getVersion(self): - return self.db_req_iface.getVersion() - - def getProfileName(self, profile_key='@DEFAULT@'): - return self.db_req_iface.getProfileName(profile_key) - - def getProfilesList(self): - return self.db_req_iface.getProfilesList() - def createProfile(self, name): - return self.db_req_iface.createProfile(name) - - def deleteProfile(self, name): - return self.db_req_iface.deleteProfile(name) - - def connect(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.connect(profile_key) - - def disconnect(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.disconnect(profile_key) - - def isConnected(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.isConnected(profile_key) - - def getContacts(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.getContacts(profile_key) - - def getPresenceStatus(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.getPresenceStatus(profile_key) - - def getWaitingSub(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.getWaitingSub(profile_key) - - def sendMessage(self, to, message, type='chat', profile_key='@DEFAULT@'): - return self.db_comm_iface.sendMessage(to, message, type, profile_key) - - def setPresence(self, to="", show="", priority=0, statuses={}, profile_key='@DEFAULT@'): - return self.db_comm_iface.setPresence(to, show, priority, statuses, profile_key) - - def subscription(self, type, entity, profile_key='@DEFAULT@'): - return self.db_comm_iface.subscription(type, entity, profile_key) - - def setParam(self, name, value, category, profile_key='@DEFAULT@'): - return self.db_comm_iface.setParam(name, value, category, profile_key) - - def getParamA(self, name, category, profile_key='@DEFAULT@'): - return self.db_comm_iface.getParamA(name, category, profile_key) - - def getParamsUI(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.getParamsUI(profile_key) - - def getParams(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.getParams(profile_key) - - def getParamsForCategory(self, category, profile_key='@DEFAULT@'): - return self.db_comm_iface.getParamsForCategory(category, profile_key) - - def getParamsCategories(self): - return self.db_comm_iface.getParamsCategories() - - def getHistory(self, from_jid, to_jid, size): - return self.db_comm_iface.getHistory(from_jid, to_jid, size) - - def addContact(self, jid, profile_key='@DEFAULT@'): - return self.db_comm_iface.addContact(jid, profile_key) - - def delContact(self, jid, profile_key='@DEFAULT@'): - return self.db_comm_iface.delContact(jid, profile_key) - - def launchAction(self, type, data, profile_key='@DEFAULT@'): - return self.db_req_iface.launchAction(type, data, profile_key) - - def confirmationAnswer(self, id, accepted, data): - return self.db_req_iface.confirmationAnswer(id, accepted, data) - - def getProgress(self, id): - return self.db_req_iface.getProgress(id) - - def getMenus(self): - return self.db_req_iface.getMenus() - - def getMenuHelp(self, category, name, type="NORMAL"): - return self.db_req_iface.getMenuHelp(category, name, type) - - def callMenu(self, category, name, type="NORMAL", profile_key='@DEFAULT@'): - return self.db_req_iface.callMenu(category, name, type, profile_key) - -#methods from plugins - def getRoomJoined(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.getRoomJoined(profile_key) - - def getRoomSubjects(self, profile_key='@DEFAULT@'): - return self.db_comm_iface.getRoomSubjects(profile_key) - - def joinMUC(self, service, roomId, nick, profile_key='@DEFAULT@'): - return self.db_comm_iface.joinMUC(service, roomId, nick, profile_key) - - def tarotGameCreate(self, room_jid, players, profile_key='@DEFAULT@'): - return self.db_comm_iface.tarotGameCreate(room_jid, players, profile_key) - - def tarotGameReady(self, player, referee, profile_key='@DEFAULT@'): - return self.db_comm_iface.tarotGameReady(player, referee, profile_key) - - def tarotGameContratChoosed(self, player, referee, contrat, profile_key='@DEFAULT@'): - return self.db_comm_iface.tarotGameContratChoosed(player, referee, contrat, profile_key) - - def tarotGamePlayCards(self, player, referee, cards, profile_key='@DEFAULT@'): - return self.db_comm_iface.tarotGamePlayCards(player, referee, cards, profile_key) - - def sendFile(self, to, path, profile_key='@DEFAULT@'): - return self.db_comm_iface.sendFile(to, path, profile_key) - - def findGateways(self, target, profile_key='@DEFAULT@'): - return self.db_comm_iface.findGateways(target, profile_key) - - def getCard(self, target, profile_key='@DEFAULT@'): - return self.db_comm_iface.getCard(target, profile_key) - - def getCardCache(self, target): - return self.db_comm_iface.getCardCache(target) - - def getAvatarFile(self, hash): - return self.db_comm_iface.getAvatarFile(hash) - - def in_band_register(self, target, profile_key='@DEFAULT@'): - return self.db_comm_iface.in_band_register(target, profile_key) - - def gatewayRegister(self, action, target, data, profile_key='@DEFAULT@'): - if data == None: - data = [('', '')] #XXX: we have to do this awful hack because python dbus need to guess the signature - return self.db_req_iface.gatewayRegister(action, target, data, profile_key) - -
--- a/frontends/sat_bridge_frontend/bridge_frontend.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +0,0 @@ -#!/usr/bin/python -#-*- coding: utf-8 -*- - -""" -SAT communication bridge -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 BridgeFrontend: - def __init__(self): - print "Bridge frontend initialization" - - - def register(self, functionName, handler): - raise NotImplementedError
--- a/frontends/sat_frontend.po Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1000 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-08-19 21:17+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: primitivus/primitivus:188 -msgid "Pleeeeasse, I can't even breathe !" -msgstr "" - -#: primitivus/primitivus:200 wix/profile.py:84 -msgid "General" -msgstr "" - -#: primitivus/primitivus:201 primitivus/profile_manager.py:50 -#: wix/profile_manager.py:70 -msgid "Connect" -msgstr "" - -#: primitivus/primitivus:202 -msgid "Disconnect" -msgstr "" - -#: primitivus/primitivus:203 -msgid "Parameters" -msgstr "" - -#: primitivus/primitivus:204 primitivus/primitivus:495 -msgid "About" -msgstr "" - -#: primitivus/primitivus:205 -msgid "Exit" -msgstr "" - -#: primitivus/primitivus:206 -msgid "Contact" -msgstr "" - -#: primitivus/primitivus:207 -msgid "Add contact" -msgstr "" - -#: primitivus/primitivus:208 -msgid "Remove contact" -msgstr "" - -#: primitivus/primitivus:209 -msgid "Communication" -msgstr "" - -#: primitivus/primitivus:210 -msgid "Join room" -msgstr "" - -#: primitivus/primitivus:211 -msgid "Find Gateways" -msgstr "" - -#: primitivus/primitivus:224 -msgid "Main menu" -msgstr "" - -#: primitivus/primitivus:295 primitivus/primitivus:323 -msgid "Chat menu" -msgstr "" - -#: primitivus/primitivus:369 wix/main_window.py:218 -#, python-format -msgid "unmanaged dialog type: %s" -msgstr "" - -#: primitivus/primitivus:384 -msgid "INTERNAL ERROR: Unexpected class for main widget's footer" -msgstr "" - -#: primitivus/primitivus:392 wix/main_window.py:277 -msgid "unknown id, ignoring" -msgstr "" - -#: primitivus/primitivus:398 wix/main_window.py:299 -msgid "XML user interface received" -msgstr "" - -#: primitivus/primitivus:401 wix/main_window.py:302 -msgid "Form" -msgstr "" - -#: primitivus/primitivus:403 wix/main_window.py:304 -msgid "Registration" -msgstr "" - -#: primitivus/primitivus:413 primitivus/primitivus:439 -#: primitivus/primitivus:449 primitivus/primitivus:489 -#: primitivus/gateways.py:52 wix/card_game.py:125 wix/main_window.py:292 -#: wix/main_window.py:419 wix/main_window.py:442 -msgid "Error" -msgstr "" - -#: primitivus/primitivus:427 wix/main_window.py:321 -#, python-format -msgid "FIXME FIXME FIXME: type [%s] not implemented" -msgstr "" - -#: primitivus/primitivus:437 primitivus/primitivus:447 wix/main_window.py:409 -#: wix/main_window.py:469 -#, python-format -msgid "'%s' is an invalid JID !" -msgstr "" - -#: primitivus/primitivus:453 wix/main_window.py:432 -#, python-format -msgid "Unsubscribing %s presence" -msgstr "" - -#: primitivus/primitivus:473 wix/main_window.py:462 -msgid "Entering a MUC room" -msgstr "" - -#: primitivus/primitivus:473 wix/main_window.py:461 -msgid "Please enter MUC's JID" -msgstr "" - -#: primitivus/primitivus:477 wix/main_window.py:472 -msgid "Find Gateways request" -msgstr "" - -#: primitivus/primitivus:483 wix/main_window.py:402 -msgid "Adding a contact" -msgstr "" - -#: primitivus/primitivus:483 wix/main_window.py:401 -msgid "Please enter new contact JID" -msgstr "" - -#: primitivus/primitivus:489 -msgid "You have not selected any contact to delete !" -msgstr "" - -#: primitivus/primitivus:491 -#, python-format -msgid "Are you sure you want to delete the contact [%s] ?" -msgstr "" - -#: primitivus/card_game.py:262 wix/card_game.py:103 -msgid "Please choose your contrat" -msgstr "" - -#: primitivus/card_game.py:277 wix/card_game.py:108 -msgid "You win \\o/" -msgstr "" - -#: primitivus/card_game.py:277 wix/card_game.py:108 -msgid "You loose :(" -msgstr "" - -#: primitivus/card_game.py:288 wix/card_game.py:125 -msgid "Cards played are invalid !" -msgstr "" - -#: primitivus/card_game.py:317 wix/card_game.py:233 -msgid "Do you put these cards in chien ?" -msgstr "" - -#: primitivus/chat.py:131 -msgid "Game" -msgstr "" - -#: primitivus/chat.py:134 -msgid "Action" -msgstr "" - -#: primitivus/chat.py:134 -msgid "Send file" -msgstr "" - -#: primitivus/chat.py:266 wix/chat.py:260 -msgid "Can't start game" -msgstr "" - -#: primitivus/chat.py:266 wix/chat.py:260 -msgid "You need to be exactly 4 peoples in the room to start a Tarot game" -msgstr "" - -#: primitivus/contact_list.py:39 -msgid "Contacts" -msgstr "" - -#: primitivus/custom_widgets.py:164 -msgid "WARNING: unknown text type" -msgstr "" - -#: primitivus/custom_widgets.py:739 primitivus/files_management.py:156 -#: primitivus/xmlui.py:178 primitivus/xmlui.py:186 -msgid "Cancel" -msgstr "" - -#: primitivus/custom_widgets.py:740 primitivus/custom_widgets.py:748 -msgid "Ok" -msgstr "" - -#: primitivus/custom_widgets.py:744 -msgid "Yes" -msgstr "" - -#: primitivus/custom_widgets.py:745 -msgid "No" -msgstr "" - -#: primitivus/custom_widgets.py:961 -msgid "INTERNAL ERROR: Tab not found" -msgstr "" - -#: primitivus/files_management.py:105 -msgid "Impossible to list directory" -msgstr "" - -#: primitivus/files_management.py:130 -msgid "Please select a file" -msgstr "" - -#: primitivus/files_management.py:137 -msgid "Path: " -msgstr "" - -#: primitivus/files_management.py:150 -msgid "Bookmarks" -msgstr "" - -#: primitivus/files_management.py:199 -msgid "No GTK bookmarks file found" -msgstr "" - -#: primitivus/files_management.py:209 -msgid "No KDE bookmarks file found" -msgstr "" - -#: primitivus/gateways.py:30 quick_frontend/quick_gateways.py:28 -#: wix/gateways.py:33 -msgid "Gateways manager" -msgstr "" - -#: primitivus/gateways.py:40 wix/gateways.py:88 -msgid "Use external XMPP server: " -msgstr "" - -#: primitivus/gateways.py:41 wix/gateways.py:91 -msgid "GO !" -msgstr "" - -#: primitivus/gateways.py:52 -msgid "You must enter an external server JID" -msgstr "" - -#: primitivus/gateways.py:70 wix/gateways.py:148 -msgid "Register" -msgstr "" - -#: primitivus/gateways.py:73 wix/gateways.py:152 -msgid "Unregister" -msgstr "" - -#: primitivus/profile_manager.py:36 -msgid "Login:" -msgstr "" - -#: primitivus/profile_manager.py:37 wix/profile_manager.py:67 -msgid "Password:" -msgstr "" - -#: primitivus/profile_manager.py:42 wix/profile_manager.py:52 -msgid "New" -msgstr "" - -#: primitivus/profile_manager.py:43 wix/profile_manager.py:53 -msgid "Delete" -msgstr "" - -#: primitivus/profile_manager.py:55 -msgid "Profile Manager" -msgstr "" - -#: primitivus/profile_manager.py:84 wix/profile_manager.py:96 -msgid "New profile" -msgstr "" - -#: primitivus/profile_manager.py:84 -msgid "Please enter a new profile name" -msgstr "" - -#: primitivus/profile_manager.py:88 -#, python-format -msgid "Are you sure you want to delete the profile %s ?" -msgstr "" - -#: primitivus/profile_manager.py:102 wix/profile_manager.py:130 -msgid "No profile selected" -msgstr "" - -#: primitivus/profile_manager.py:102 -msgid "You need to create and select a profile before connecting" -msgstr "" - -#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 -#: wix/profile_manager.py:133 -msgid "Bad profile name" -msgstr "" - -#: primitivus/profile_manager.py:105 wix/profile_manager.py:101 -#: wix/profile_manager.py:133 -msgid "A profile name can't start with a @" -msgstr "" - -#: primitivus/progress.py:35 -msgid "Clear progress list" -msgstr "" - -#: primitivus/xmlui.py:67 wix/xmlui.py:61 -msgid "Unmanaged tag" -msgstr "" - -#: primitivus/xmlui.py:80 wix/xmlui.py:75 -msgid "text node has no child !" -msgstr "" - -#: primitivus/xmlui.py:105 wix/xmlui.py:107 -#, python-format -msgid "FIXME FIXME FIXME: type [%s] is not implemented" -msgstr "" - -#: primitivus/xmlui.py:135 wix/xmlui.py:138 -msgid "Unknown layout, using default one" -msgstr "" - -#: primitivus/xmlui.py:148 wix/xmlui.py:157 -msgid "Unknown tag" -msgstr "" - -#: primitivus/xmlui.py:176 wix/xmlui.py:179 -msgid "Submit" -msgstr "" - -#: primitivus/xmlui.py:185 -msgid "Save" -msgstr "" - -#: primitivus/xmlui.py:204 -#, python-format -msgid "INTERNAL ERROR: Unmanaged show_type (%s)" -msgstr "" - -#: primitivus/xmlui.py:245 wix/xmlui.py:230 -msgid "The form data is not sent back, the type is not managed properly" -msgstr "" - -#: quick_frontend/quick_app.py:44 -msgid "Can't connect to SàT backend, are you sure it's launched ?" -msgstr "" - -#: quick_frontend/quick_app.py:84 -#, python-format -msgid "Trying to plug an unknown profile (%s)" -msgstr "" - -#: quick_frontend/quick_app.py:90 -msgid "" -"\n" -" %prog [options]\n" -"\n" -" %prog --help for options list\n" -" " -msgstr "" - -#: quick_frontend/quick_app.py:97 -msgid "Select the profile to use" -msgstr "" - -#: quick_frontend/quick_app.py:107 -msgid "There is already one profile plugged (we are in single profile mode) !" -msgstr "" - -#: quick_frontend/quick_app.py:111 -msgid "The profile asked doesn't exist" -msgstr "" - -#: quick_frontend/quick_app.py:114 -msgid "The profile is already plugged" -msgstr "" - -#: quick_frontend/quick_app.py:167 -msgid "This profile is not plugged" -msgstr "" - -#: quick_frontend/quick_app.py:178 -msgid "Connected" -msgstr "" - -#: quick_frontend/quick_app.py:187 -msgid "Disconnected" -msgstr "" - -#: quick_frontend/quick_app.py:221 -#, python-format -msgid "" -"presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%" -"(statuses)s) [profile:%(profile)s]" -msgstr "" - -#: quick_frontend/quick_app.py:242 -#, python-format -msgid "Watched jid [%s] is connected !" -msgstr "" - -#: quick_frontend/quick_app.py:267 -#, python-format -msgid "Room [%(room_name)s] joined by %(profile)s, users presents:%(users)s" -msgstr "" - -#: quick_frontend/quick_app.py:282 -#, python-format -msgid "user [%(user_nick)s] joined room [%(room_jid)s]" -msgstr "" - -#: quick_frontend/quick_app.py:291 -#, python-format -msgid "user [%(user_nick)s] left room [%(room_jid)s]" -msgstr "" - -#: quick_frontend/quick_app.py:300 -#, python-format -msgid "new subject for room [%(room_jid)s]: %(subject)s" -msgstr "" - -#: quick_frontend/quick_app.py:305 -msgid "Tarot Game Started \\o/" -msgstr "" - -#: quick_frontend/quick_app.py:308 -#, python-format -msgid "" -"new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %" -"(players)s" -msgstr "" - -#: quick_frontend/quick_app.py:313 -msgid "New Tarot Game" -msgstr "" - -#: quick_frontend/quick_app.py:321 -msgid "Tarot: need to select a contrat" -msgstr "" - -#: quick_frontend/quick_app.py:328 -msgid "Show cards" -msgstr "" - -#: quick_frontend/quick_app.py:335 -msgid "My turn to play" -msgstr "" - -#: quick_frontend/quick_app.py:343 -msgid "Tarot: score received" -msgstr "" - -#: quick_frontend/quick_app.py:350 -#, python-format -msgid "Card(s) played (%(player)s): %(cards)s" -msgstr "" - -#: quick_frontend/quick_app.py:357 -#, python-format -msgid "Cards played are not valid: %s" -msgstr "" - -#: quick_frontend/quick_app.py:375 -#, python-format -msgid "The contact %s has accepted your subscription" -msgstr "" - -#: quick_frontend/quick_app.py:375 quick_frontend/quick_app.py:381 -msgid "Subscription confirmation" -msgstr "" - -#: quick_frontend/quick_app.py:378 -#, python-format -msgid "The contact %s has refused your subscription" -msgstr "" - -#: quick_frontend/quick_app.py:378 -msgid "Subscription refusal" -msgstr "" - -#: quick_frontend/quick_app.py:381 -#, python-format -msgid "" -"The contact %s wants to subscribe to your presence.\n" -"Do you accept ?" -msgstr "" - -#: quick_frontend/quick_app.py:392 -#, python-format -msgid "param update: [%(namespace)s] %(name)s = %(value)s" -msgstr "" - -#: quick_frontend/quick_app.py:394 -#, python-format -msgid "Changing JID to %s" -msgstr "" - -#: quick_frontend/quick_chat.py:47 -#, python-format -msgid "Adding users %s to room" -msgstr "" - -#: quick_frontend/quick_chat.py:49 -msgid "[INTERNAL] trying to set presents nicks for a non group chat window" -msgstr "" - -#: quick_frontend/quick_chat.py:55 wix/chat.py:138 -#, python-format -msgid "Replacing user %s" -msgstr "" - -#: quick_frontend/quick_chat.py:57 wix/chat.py:140 -msgid "[INTERNAL] trying to replace user for a non group chat window" -msgstr "" - -#: quick_frontend/quick_chat.py:70 -#, python-format -msgid "Removing user %s" -msgstr "" - -#: quick_frontend/quick_chat.py:72 -msgid "[INTERNAL] trying to remove user for a non group chat window" -msgstr "" - -#: quick_frontend/quick_chat.py:79 -#, python-format -msgid "Setting subject to %s" -msgstr "" - -#: quick_frontend/quick_chat.py:81 -msgid "[INTERNAL] trying to set subject for a non group chat window" -msgstr "" - -#: quick_frontend/quick_chat.py:86 -msgid "now we print history" -msgstr "" - -#: quick_frontend/quick_chat.py:122 -msgid "startGame is not implemented in this frontend" -msgstr "" - -#: quick_frontend/quick_chat.py:127 -msgid "getGame is not implemented in this frontend" -msgstr "" - -#: quick_frontend/quick_contact_list.py:33 -msgid "Contact List init" -msgstr "" - -#: quick_frontend/quick_contact_management.py:67 -msgid "Trying to get attribute for an unknown contact" -msgstr "" - -#: quick_frontend/quick_contact_management.py:83 -msgid "INTERNAL ERROR: Key error" -msgstr "" - -#: quick_frontend/quick_contact_management.py:95 -#, python-format -msgid "Trying to update an unknown contact: %s" -msgstr "" - -#: quick_frontend/quick_gateways.py:29 -msgid "" -"Be careful ! Gateways allow you to use an external IM (legacy IM), so you " -"can see your contact as jabber contacts.\n" -"But when you do this, all your messages go throught the external legacy IM " -"server, it is a huge privacy issue (i.e.: all your messages throught the " -"gateway can be monitored, recorded, analyzed by the external server, most of " -"time a private company)." -msgstr "" - -#: quick_frontend/quick_gateways.py:36 -msgid "Unknown IM" -msgstr "" - -#: wix/card_game.py:95 -msgid "Contrat choosed" -msgstr "" - -#: wix/card_game.py:233 -msgid "Écart" -msgstr "" - -#: wix/chat.py:115 -msgid "configure chat window for Tarot game" -msgstr "" - -#: wix/chat.py:167 -msgid "&SendFile\tCTRL-s" -msgstr "" - -#: wix/chat.py:167 -msgid " Send a file to contact" -msgstr "" - -#: wix/chat.py:168 -msgid "&Action" -msgstr "" - -#: wix/chat.py:179 -msgid "Start &Tarot game\tCTRL-t" -msgstr "" - -#: wix/chat.py:179 -msgid " Start a Tarot card game" -msgstr "" - -#: wix/chat.py:180 -msgid "&Games" -msgstr "" - -#: wix/chat.py:248 -msgid "Send File" -msgstr "" - -#: wix/chat.py:249 -msgid "Choose a file to send" -msgstr "" - -#: wix/chat.py:251 -#, python-format -msgid "filename: %s" -msgstr "" - -#: wix/chat.py:254 wix/main_window.py:252 -msgid "File Transfer" -msgstr "" - -#: wix/chat.py:254 wix/main_window.py:252 -#, python-format -msgid "Copying %s" -msgstr "" - -#: wix/chat.py:257 -msgid "Starting Tarot game" -msgstr "" - -#: wix/chat.py:258 -msgid "FIXME: temporary menu, must be changed" -msgstr "" - -#: wix/constants.py:8 -msgid "offline" -msgstr "" - -#: wix/constants.py:9 -msgid "online" -msgstr "" - -#: wix/constants.py:11 -msgid "Online" -msgstr "" - -#: wix/constants.py:12 -msgid "Free for chat" -msgstr "" - -#: wix/constants.py:13 -msgid "AFK" -msgstr "" - -#: wix/constants.py:14 -msgid "DND" -msgstr "" - -#: wix/constants.py:15 -msgid "Away" -msgstr "" - -#: wix/contact_list.py:53 -#, python-format -msgid "update %s" -msgstr "" - -#: wix/contact_list.py:121 -#, python-format -msgid "adding %s" -msgstr "" - -#: wix/contact_list.py:139 -#, python-format -msgid "removing %s" -msgstr "" - -#: wix/gateways.py:106 -#, python-format -msgid "Opening gateways manager on [%s]" -msgstr "" - -#: wix/gateways.py:166 wix/param.py:137 wix/profile.py:89 wix/xmlui.py:242 -msgid "close" -msgstr "" - -#: wix/main_window.py:96 -msgid "Wix jabber client" -msgstr "" - -#: wix/main_window.py:121 -#, python-format -msgid "plugin profile %s" -msgstr "" - -#: wix/main_window.py:130 -msgid "Creating menus" -msgstr "" - -#: wix/main_window.py:132 -msgid "&Connect\tCTRL-c" -msgstr "" - -#: wix/main_window.py:132 -msgid " Connect to the server" -msgstr "" - -#: wix/main_window.py:133 -msgid "&Disconnect\tCTRL-d" -msgstr "" - -#: wix/main_window.py:133 -msgid " Disconnect from the server" -msgstr "" - -#: wix/main_window.py:134 -msgid "&Parameters" -msgstr "" - -#: wix/main_window.py:134 -msgid " Configure the program" -msgstr "" - -#: wix/main_window.py:136 -msgid "A&bout" -msgstr "" - -#: wix/main_window.py:136 -#, python-format -msgid " About %s" -msgstr "" - -#: wix/main_window.py:137 -msgid "E&xit" -msgstr "" - -#: wix/main_window.py:137 -msgid " Terminate the program" -msgstr "" - -#: wix/main_window.py:139 -msgid "&Add contact" -msgstr "" - -#: wix/main_window.py:139 -msgid " Add a contact to your list" -msgstr "" - -#: wix/main_window.py:140 -msgid "&Remove contact" -msgstr "" - -#: wix/main_window.py:140 -msgid " Remove the selected contact from your list" -msgstr "" - -#: wix/main_window.py:142 -msgid "&Show profile" -msgstr "" - -#: wix/main_window.py:142 -msgid " Show contact's profile" -msgstr "" - -#: wix/main_window.py:144 -msgid "&Join Room" -msgstr "" - -#: wix/main_window.py:144 -msgid " Join a Multi-User Chat room" -msgstr "" - -#: wix/main_window.py:145 -msgid "&Find Gateways" -msgstr "" - -#: wix/main_window.py:145 -msgid " Find gateways to legacy IM" -msgstr "" - -#: wix/main_window.py:147 -msgid "&General" -msgstr "" - -#: wix/main_window.py:148 -msgid "&Contacts" -msgstr "" - -#: wix/main_window.py:149 -msgid "&Communication" -msgstr "" - -#: wix/main_window.py:238 -msgid "Confirmation asked" -msgstr "" - -#: wix/main_window.py:241 -msgid "File transfert confirmation asked" -msgstr "" - -#: wix/main_window.py:242 -#, python-format -msgid "" -"The contact %(jid)s wants to send you the file %(filename)s\n" -"Do you accept ?" -msgstr "" - -#: wix/main_window.py:243 -msgid "File Request" -msgstr "" - -#: wix/main_window.py:248 -msgid "Where do you want to save the file ?" -msgstr "" - -#: wix/main_window.py:261 -msgid "Yes/No confirmation asked" -msgstr "" - -#: wix/main_window.py:263 wix/profile_manager.py:112 -msgid "Confirmation" -msgstr "" - -#: wix/main_window.py:275 -#, python-format -msgid "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" -msgstr "" - -#: wix/main_window.py:284 -msgid "Success" -msgstr "" - -#: wix/main_window.py:303 -msgid "registration" -msgstr "" - -#: wix/main_window.py:351 -#, python-format -msgid "onContactActivated: %s" -msgstr "" - -#: wix/main_window.py:369 -msgid "Status change request" -msgstr "" - -#: wix/main_window.py:373 -msgid "Param request" -msgstr "" - -#: wix/main_window.py:383 -#, python-format -msgid "%(name)s is a SàT (Salut à Toi) frontend\n" -msgstr "" - -#: wix/main_window.py:399 -msgid "Add contact request" -msgstr "" - -#: wix/main_window.py:402 -msgid "name@server.tld" -msgstr "" - -#: wix/main_window.py:415 -msgid "Remove contact request" -msgstr "" - -#: wix/main_window.py:418 wix/main_window.py:441 -msgid "You haven't selected any contact !" -msgstr "" - -#: wix/main_window.py:426 -#, python-format -msgid "Are you sure you want to delete %s from your roster list ?" -msgstr "" - -#: wix/main_window.py:427 -msgid "Contact suppression" -msgstr "" - -#: wix/main_window.py:438 -msgid "Show contact's profile request" -msgstr "" - -#: wix/main_window.py:454 -#, python-format -msgid "Profile received: [%s]" -msgstr "" - -#: wix/main_window.py:485 -msgid "Exiting..." -msgstr "" - -#: wix/main_window.py:491 -msgid "Tray Click" -msgstr "" - -#: wix/param.py:32 -msgid "Configuration" -msgstr "" - -#: wix/param.py:83 -msgid "FIXME FIXME FIXME" -msgstr "" - -#: wix/profile_manager.py:47 -msgid "Profile:" -msgstr "" - -#: wix/profile_manager.py:60 -msgid "Login" -msgstr "" - -#: wix/profile_manager.py:96 -msgid "Please enter the new profile name" -msgstr "" - -#: wix/profile_manager.py:112 -#, python-format -msgid "Are you sure to delete the profile [%s]" -msgstr "" - -#: wix/profile_manager.py:130 -msgid "You must select a profile or create a new one before connecting" -msgstr "" - -#: wix/profile_manager.py:142 -msgid "Saving new JID and server" -msgstr "" - -#: wix/profile_manager.py:146 -msgid "Saving new password" -msgstr "" - -#: wix/profile.py:35 -msgid "Full Name" -msgstr "" - -#: wix/profile.py:36 -msgid "Nickname" -msgstr "" - -#: wix/profile.py:37 -msgid "Birthday" -msgstr "" - -#: wix/profile.py:38 -msgid "Phone #" -msgstr "" - -#: wix/profile.py:39 -msgid "Website" -msgstr "" - -#: wix/profile.py:40 -msgid "E-mail" -msgstr "" - -#: wix/profile.py:41 -msgid "Avatar" -msgstr "" - -#: wix/xmlui.py:214 -msgid "Submitting form" -msgstr "" - -#: wix/xmlui.py:236 -msgid "Cancelling form" -msgstr ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/bridge/DBus.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,182 @@ +#!/usr/bin/python +#-*- coding: utf-8 -*- + +""" +SAT communication bridge +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_frontend import BridgeFrontend +import dbus, dbus.glib + +class BridgeExceptionNoService(Exception): + pass + +class DBusBridgeFrontend(BridgeFrontend): + def __init__(self): + try: + self.sessions_bus = dbus.SessionBus() + self.db_object = self.sessions_bus.get_object('org.goffi.SAT', + '/org/goffi/SAT/bridge') + self.db_comm_iface = dbus.Interface(self.db_object, + dbus_interface='org.goffi.SAT.communication') + self.db_req_iface = dbus.Interface(self.db_object, + dbus_interface='org.goffi.SAT.request') + except dbus.exceptions.DBusException,e: + if e._dbus_error_name=='org.freedesktop.DBus.Error.ServiceUnknown': + raise BridgeExceptionNoService + else: + raise e + #props = self.db_comm_iface.getProperties() + + def register(self, functionName, handler, iface="communication"): + if iface == "communication": + self.db_comm_iface.connect_to_signal(functionName, handler) + elif iface == "request": + self.db_req_iface.connect_to_signal(functionName, handler) + + def getVersion(self): + return self.db_req_iface.getVersion() + + def getProfileName(self, profile_key='@DEFAULT@'): + return self.db_req_iface.getProfileName(profile_key) + + def getProfilesList(self): + return self.db_req_iface.getProfilesList() + def createProfile(self, name): + return self.db_req_iface.createProfile(name) + + def deleteProfile(self, name): + return self.db_req_iface.deleteProfile(name) + + def connect(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.connect(profile_key) + + def disconnect(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.disconnect(profile_key) + + def isConnected(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.isConnected(profile_key) + + def getContacts(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.getContacts(profile_key) + + def getPresenceStatus(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.getPresenceStatus(profile_key) + + def getWaitingSub(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.getWaitingSub(profile_key) + + def sendMessage(self, to, message, type='chat', profile_key='@DEFAULT@'): + return self.db_comm_iface.sendMessage(to, message, type, profile_key) + + def setPresence(self, to="", show="", priority=0, statuses={}, profile_key='@DEFAULT@'): + return self.db_comm_iface.setPresence(to, show, priority, statuses, profile_key) + + def subscription(self, type, entity, profile_key='@DEFAULT@'): + return self.db_comm_iface.subscription(type, entity, profile_key) + + def setParam(self, name, value, category, profile_key='@DEFAULT@'): + return self.db_comm_iface.setParam(name, value, category, profile_key) + + def getParamA(self, name, category, profile_key='@DEFAULT@'): + return self.db_comm_iface.getParamA(name, category, profile_key) + + def getParamsUI(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.getParamsUI(profile_key) + + def getParams(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.getParams(profile_key) + + def getParamsForCategory(self, category, profile_key='@DEFAULT@'): + return self.db_comm_iface.getParamsForCategory(category, profile_key) + + def getParamsCategories(self): + return self.db_comm_iface.getParamsCategories() + + def getHistory(self, from_jid, to_jid, size): + return self.db_comm_iface.getHistory(from_jid, to_jid, size) + + def addContact(self, jid, profile_key='@DEFAULT@'): + return self.db_comm_iface.addContact(jid, profile_key) + + def delContact(self, jid, profile_key='@DEFAULT@'): + return self.db_comm_iface.delContact(jid, profile_key) + + def launchAction(self, type, data, profile_key='@DEFAULT@'): + return self.db_req_iface.launchAction(type, data, profile_key) + + def confirmationAnswer(self, id, accepted, data): + return self.db_req_iface.confirmationAnswer(id, accepted, data) + + def getProgress(self, id): + return self.db_req_iface.getProgress(id) + + def getMenus(self): + return self.db_req_iface.getMenus() + + def getMenuHelp(self, category, name, type="NORMAL"): + return self.db_req_iface.getMenuHelp(category, name, type) + + def callMenu(self, category, name, type="NORMAL", profile_key='@DEFAULT@'): + return self.db_req_iface.callMenu(category, name, type, profile_key) + +#methods from plugins + def getRoomJoined(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.getRoomJoined(profile_key) + + def getRoomSubjects(self, profile_key='@DEFAULT@'): + return self.db_comm_iface.getRoomSubjects(profile_key) + + def joinMUC(self, service, roomId, nick, profile_key='@DEFAULT@'): + return self.db_comm_iface.joinMUC(service, roomId, nick, profile_key) + + def tarotGameCreate(self, room_jid, players, profile_key='@DEFAULT@'): + return self.db_comm_iface.tarotGameCreate(room_jid, players, profile_key) + + def tarotGameReady(self, player, referee, profile_key='@DEFAULT@'): + return self.db_comm_iface.tarotGameReady(player, referee, profile_key) + + def tarotGameContratChoosed(self, player, referee, contrat, profile_key='@DEFAULT@'): + return self.db_comm_iface.tarotGameContratChoosed(player, referee, contrat, profile_key) + + def tarotGamePlayCards(self, player, referee, cards, profile_key='@DEFAULT@'): + return self.db_comm_iface.tarotGamePlayCards(player, referee, cards, profile_key) + + def sendFile(self, to, path, profile_key='@DEFAULT@'): + return self.db_comm_iface.sendFile(to, path, profile_key) + + def findGateways(self, target, profile_key='@DEFAULT@'): + return self.db_comm_iface.findGateways(target, profile_key) + + def getCard(self, target, profile_key='@DEFAULT@'): + return self.db_comm_iface.getCard(target, profile_key) + + def getCardCache(self, target): + return self.db_comm_iface.getCardCache(target) + + def getAvatarFile(self, hash): + return self.db_comm_iface.getAvatarFile(hash) + + def in_band_register(self, target, profile_key='@DEFAULT@'): + return self.db_comm_iface.in_band_register(target, profile_key) + + def gatewayRegister(self, action, target, data, profile_key='@DEFAULT@'): + if data == None: + data = [('', '')] #XXX: we have to do this awful hack because python dbus need to guess the signature + return self.db_req_iface.gatewayRegister(action, target, data, profile_key) + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/bridge/bridge_frontend.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,28 @@ +#!/usr/bin/python +#-*- coding: utf-8 -*- + +""" +SAT communication bridge +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 BridgeFrontend: + def __init__(self): + print "Bridge frontend initialization" + + + def register(self, functionName, handler): + raise NotImplementedError
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/jp/fr.po Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,164 @@ +# JP French Translation. +# Copyright (C) 2009, 2010 Jérôme Poisson +# This file is distributed under the same license as the jp package. +# Jérôme Poisson <goffi@goffi.org>, 2009, 2010. +# Goffi <goffi@goffi.org>, 2010. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.0.2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-05 19:08+1100\n" +"PO-Revision-Date: 2010-03-05 19:24+1100\n" +"Last-Translator: Goffi <goffi@goffi.org>\n" +"Language-Team: French <goffi@goffi.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: jp:63 +msgid "" +"ProgressBar not available, please download it at http://pypi.python.org/pypi/" +"progressbar" +msgstr "" +"ProgressBar n'est pas disponible, veuillez le télécharger à http://pypi." +"python.org/pypi/progressbar" + +#: jp:64 +msgid "" +"Progress bar deactivated\n" +"--\n" +msgstr "" +"Barre de progression désactivée\n" +"--\n" + +#: jp:77 +msgid "" +"\n" +" %prog [options] [FILE1 FILE2 ...] JID\n" +" %prog -w [options] [JID1 JID2 ...]\n" +"\n" +" %prog --help for options list\n" +" " +msgstr "" +"\n" +" %prog [options] [FICHIER1 FICHIER2 ...] JID\n" +" %prog -w [options] [JID1 JID2 ...]\n" +"\n" +" %prog --help pour la liste des options\n" +" " + +#: jp:86 +msgid "Make a bzip2 tarball" +msgstr "Fait un fichier compressé bzip2" + +#: jp:88 +msgid "Wait for a file to be sent by a contact" +msgstr "Attend qu'un fichier soit envoyé par un contact" + +#: jp:90 +msgid "Accept multiple files (you'll have to stop manually)" +msgstr "" +"Accepte plusieurs fichiers (vous devrez arrêter le programme à la main)" + +#: jp:92 +msgid "Force overwritting of existing files" +msgstr "Force le remplacement des fichiers existants" + +#: jp:94 +msgid "Show progress bar" +msgstr "Affiche la barre de progression" + +#: jp:96 +msgid "" +"Separate xmpp messages: send one message per line instead of one message " +"alone." +msgstr "" +"Sépare les messages xmpp: envoi un message par ligne plutôt qu'un seul " +"message global." + +#: jp:98 +msgid "Add a new line at the beginning of the input (usefull for ascii art ;))" +msgstr "" +"Ajoute un saut de ligne au début de l'entrée (utile pour l'art ascii ;))" + +#: jp:103 +msgid "You must specify the destination JID (Jabber ID)" +msgstr "Vous devez préciser le JID (Jabber ID) de destination" + +#: jp:112 +#, python-format +msgid "%s is not a valid JID !" +msgstr "%s n'est pas un JID valide !" + +#: jp:118 +msgid "Option progress is not available, deactivated." +msgstr "" +"L'option « progress » (barre de progression) n'est pas disponible, elle est " +"désactivée." + +#: jp:131 +msgid "SAT is not conneted, please connect before using jp" +msgstr "SAT n'est pas connecté, veuillez le connecter avant d'utiliser jp" + +#: jp:155 +#, python-format +msgid "File [%s] doesn't exist !" +msgstr "Le fichier [%s] n'existe pas !" + +#: jp:158 +#, python-format +msgid "[%s] is a dir ! Please send files inside or use compression" +msgstr "" +"[%s] est un répertoire ! Veuillez envoyer les fichiers qu'il contient ou " +"utiliser la compression." + +#: jp:164 +#, python-format +msgid "tmp file (%s) already exists ! Please remove it" +msgstr "le fichier temporaire (%s) existe déjà ! Veuillez le supprimer" + +#: jp:166 +msgid "bz2 is an experimental option at an early dev stage, use with caution" +msgstr "" +"bz2 est une option expérimentale à un stade de développement peu avancé, " +"utilisez-là avec prudence" + +#: jp:168 +msgid "Starting compression, please wait..." +msgstr "Lancement de la compression, veuillez patienter..." + +#: jp:172 +#, python-format +msgid "Adding %s" +msgstr "Ajout de %s" + +#: jp:175 +msgid "OK !" +msgstr "C'est parti !" + +#: jp:196 +#, python-format +msgid "Accepted file [%(filename)s] from %(sender)s" +msgstr "Le fichier [%(filename)s] de %(sender)s a été accepté" + +#: jp:200 +#, python-format +msgid "" +"Refused file [%(filename)s] from %(sender)s: a file with the same name " +"already exist" +msgstr "" +"Le fichier [%(filename)s] de %(sender)s a été refusé: un fichier avec le " +"même nom existe déjà" + +#: jp:209 +msgid "FIXME: actionResult not implemented" +msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" + +#: jp:223 +msgid "Progress: " +msgstr "Progression: " + +#: jp:254 +msgid "User interruption: good bye" +msgstr "Interrompu par l'utilisateur: au revoir"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/jp/jp Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,272 @@ +#! /usr/bin/python +# -*- coding: utf-8 -*- + +""" +jp: a SAT command line tool +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/>. +""" + +#consts +name = u"jp" +about = name+u""" v%s (c) Jérôme Poisson (aka Goffi) 2009, 2010 + +--- +"""+name+u""" Copyright (C) 2009, 2010 Jérôme Poisson (aka Goffi) +This program comes with ABSOLUTELY NO WARRANTY; +This is free software, and you are welcome to redistribute it +under certain conditions. +--- + +This software is a command line tool for jabber +Get the latest version at http://www.goffi.org +""" + +global pbar_available +pbar_available = True #checked before using ProgressBar + +### logging ### +import logging +from logging import debug, info, error, warning +logging.basicConfig(level=logging.DEBUG, + format='%(message)s') +### + +import gettext +gettext.install('jp', "i18n", unicode=True) + +import sys +import os +from os.path import abspath, basename, dirname +from optparse import OptionParser +import pdb +from tools.jid import JID +import gobject +from sat_bridge_frontend.DBus import DBusBridgeFrontend,BridgeExceptionNoService +import tarfile +try: + from progressbar import ProgressBar, Percentage, Bar, ETA, FileTransferSpeed +except ImportError, e: + info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar')) + info (_('Progress bar deactivated\n--\n')) + pbar_available=False + + + + +class JP(): + def __init__(self): + try: + self.bridge=DBusBridgeFrontend() + except BridgeExceptionNoService: + print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) + import sys + sys.exit(1) + self.transfert_id = None + + def check_options(self): + """Check command line options""" + usage=_(""" + %prog [options] [FILE1 FILE2 ...] JID + %prog -w [options] [JID1 JID2 ...] + + %prog --help for options list + """) + version = unicode(self.bridge.getVersion()) + parser = OptionParser(usage=usage,version=about % version) + + parser.add_option("-p", "--profile", action="store", type="string", default='@DEFAULT@', + help=_("Use PROFILE profile key (default: %default)")) + parser.add_option("-b", "--bz2", action="store_true", default=False, + help=_("Make a bzip2 tarball")) + parser.add_option("-w", "--wait-file", action="store_true", default=False, + help=_("Wait for a file to be sent by a contact")) + parser.add_option("-m", "--multiple", action="store_true", default=False, + help=_("Accept multiple files (you'll have to stop manually)")) + parser.add_option("-f", "--force", action="store_true", default=False, + help=_("Force overwritting of existing files")) + parser.add_option("-g", "--progress", action="store_true", default=False, + help=_("Show progress bar")) + parser.add_option("-s", "--separate", action="store_true", default=False, + help=_("Separate xmpp messages: send one message per line instead of one message alone.")) + parser.add_option("-n", "--new-line", action="store_true", default=False, + help=_("Add a new line at the beginning of the input (usefull for ascii art ;))")) + + (self.options, args) = parser.parse_args() + + if len(args) < 1 and not self.options.wait_file: + parser.error(_("You must specify the destination JID (Jabber ID)").encode('utf-8')) + + if self.options.wait_file: + #several jid + self.dest_jids = args + else: + #one dest_jid, other args are files + self.dest_jid = JID(args[-1]) + if not self.dest_jid.is_valid: + error (_("%s is not a valid JID !"), self.dest_jid) + exit(1) + self.files = args[:-1] + + if not pbar_available and self.options.progress: + self.options.progress = False + error (_("Option progress is not available, deactivated.")) + + if self.options.progress or self.options.wait_file: + self.start_loop = True #We have to use loop for these options + else: + self.start_loop = False + + + return args + + def check_jabber_status(self): + """Check that jabber status is allright""" + + self.profile = self.bridge.getProfileName(self.options.profile) + if not self.profile: + error(_("The profile asked doesn't exist")) + exit(1) + + if not self.bridge.isConnected(self.profile): + error(_(u"SàT is not conneted, please connect before using jp")) + exit(1) + + + def send_stdin(self): + """Send incomming data on stdin to jabber contact""" + header = "\n" if self.options.new_line else "" + + if self.options.separate: #we send stdin in several messages + if header: + self.bridge.sendMessage(self.dest_jid, header, profile_key=self.profile) + while (True): + line = sys.stdin.readline() + if not line: + break + self.bridge.sendMessage(self.dest_jid, line.replace("\n",""), profile_key=self.profile) + else: + self.bridge.sendMessage(self.dest_jid, header + "".join(sys.stdin.readlines()), profile_key=self.profile) + + def send_files(self): + """Send files to jabber contact""" + + for file in self.files: + if not os.path.exists(file): + error (_("File [%s] doesn't exist !") % file) + exit(1) + if not self.options.bz2 and os.path.isdir(file): + error (_("[%s] is a dir ! Please send files inside or use compression") % file) + exit(1) + + if self.options.bz2: + tmpfile = (basename(self.files[0]) or basename(dirname(self.files[0])) ) + '.tar.bz2' #FIXME: tmp, need an algorithm to find a good name/path + if os.path.exists(tmpfile): + error (_("tmp file (%s) already exists ! Please remove it"), tmpfile) + exit(1) + warning(_("bz2 is an experimental option at an early dev stage, use with caution")) + #FIXME: check free space, writting perm, tmp dir, filename (watch for OS used) + info(_("Starting compression, please wait...")) + sys.stdout.flush() + bz2=tarfile.open(tmpfile, "w:bz2") + for file in self.files: + info(_("Adding %s"), file) + bz2.add(file) + bz2.close() + info(_("OK !")) + path = abspath(tmpfile) + self.transfert_id = self.bridge.sendFile(self.dest_jid, path, profile_key=self.profile) + else: + for file in self.files: + path = abspath(file) + self.transfert_id = self.bridge.sendFile(self.dest_jid, path, profile_key=self.profile) #FIXME: show progress only for last transfert_id + + #TODO: manage ProgressBar + + def askConfirmation(self, type, id, data): + """CB used for file transfert, accept files depending on parameters""" + answer_data={} + if type == "FILE_TRANSFERT": + if self.dest_jids and not data['from'] in self.dest_jids: + return #file is not sent by a filtered jid + + answer_data["dest_path"] = os.getcwd()+'/'+data['filename'] + + if self.options.force or not os.path.exists(answer_data["dest_path"]): + self.bridge.confirmationAnswer(id, True, answer_data) + info(_("Accepted file [%(filename)s] from %(sender)s") % {'filename':data['filename'], 'sender':data['from']}) + self.transfert_id = id + else: + self.bridge.confirmationAnswer(id, False, answer_data) + warning(_("Refused file [%(filename)s] from %(sender)s: a file with the same name already exist") % {'filename':data['filename'], 'sender':data['from']}) + + + if not self.options.multiple and not self.options.progress: + #we just accept one file + self.loop.quit() + + def actionResult(self, type, id, data): + #FIXME + info (_("FIXME: actionResult not implemented")) + + def wait_file(self): + """Wait for a file and write it on local dir""" + self.bridge.register("askConfirmation", self.askConfirmation, "request") + + def progressCB(self): + if self.transfert_id: + data = self.bridge.getProgress(self.transfert_id) + if data: + if not data['position']: + data['position'] = '0' + if not self.pbar: + #first answer, we must construct the bar + self.pbar = ProgressBar(int(data['size']),[_("Progress: "),Percentage()," ",Bar()," ",FileTransferSpeed()," ",ETA()]) + self.pbar.start() + + self.pbar.update(int(data['position'])) + elif self.pbar: + self.pbar.finish() + if not self.options.multiple: + self.loop.quit() + return False + + return True + + def go(self): + self.check_options() + self.check_jabber_status() + if self.options.wait_file: + self.wait_file() + else: + if not self.files: #we send message only if there are no files to send + self.send_stdin() + else: + self.send_files() + + if self.start_loop: + self.loop = gobject.MainLoop() + if self.options.progress: + self.pbar = None + gobject.timeout_add(10, self.progressCB) + try: + self.loop.run() + except KeyboardInterrupt: + info(_("User interruption: good bye")) + + +if __name__ == "__main__": + jp = JP() + jp.go()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/jp/jp.po Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,142 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-05 19:08+1100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: jp:63 +msgid "" +"ProgressBar not available, please download it at http://pypi.python.org/pypi/" +"progressbar" +msgstr "" + +#: jp:64 +msgid "" +"Progress bar deactivated\n" +"--\n" +msgstr "" + +#: jp:77 +msgid "" +"\n" +" %prog [options] [FILE1 FILE2 ...] JID\n" +" %prog -w [options] [JID1 JID2 ...]\n" +"\n" +" %prog --help for options list\n" +" " +msgstr "" + +#: jp:86 +msgid "Make a bzip2 tarball" +msgstr "" + +#: jp:88 +msgid "Wait for a file to be sent by a contact" +msgstr "" + +#: jp:90 +msgid "Accept multiple files (you'll have to stop manually)" +msgstr "" + +#: jp:92 +msgid "Force overwritting of existing files" +msgstr "" + +#: jp:94 +msgid "Show progress bar" +msgstr "" + +#: jp:96 +msgid "" +"Separate xmpp messages: send one message per line instead of one message " +"alone." +msgstr "" + +#: jp:98 +msgid "Add a new line at the beginning of the input (usefull for ascii art ;))" +msgstr "" + +#: jp:103 +msgid "You must specify the destination JID (Jabber ID)" +msgstr "" + +#: jp:112 +#, python-format +msgid "%s is not a valid JID !" +msgstr "" + +#: jp:118 +msgid "Option progress is not available, deactivated." +msgstr "" + +#: jp:131 +msgid "SAT is not conneted, please connect before using jp" +msgstr "" + +#: jp:155 +#, python-format +msgid "File [%s] doesn't exist !" +msgstr "" + +#: jp:158 +#, python-format +msgid "[%s] is a dir ! Please send files inside or use compression" +msgstr "" + +#: jp:164 +#, python-format +msgid "tmp file (%s) already exists ! Please remove it" +msgstr "" + +#: jp:166 +msgid "bz2 is an experimental option at an early dev stage, use with caution" +msgstr "" + +#: jp:168 +msgid "Starting compression, please wait..." +msgstr "" + +#: jp:172 +#, python-format +msgid "Adding %s" +msgstr "" + +#: jp:175 +msgid "OK !" +msgstr "" + +#: jp:196 +#, python-format +msgid "Accepted file [%(filename)s] from %(sender)s" +msgstr "" + +#: jp:200 +#, python-format +msgid "" +"Refused file [%(filename)s] from %(sender)s: a file with the same name " +"already exist" +msgstr "" + +#: jp:209 +msgid "FIXME: actionResult not implemented" +msgstr "" + +#: jp:223 +msgid "Progress: " +msgstr "" + +#: jp:254 +msgid "User interruption: good bye" +msgstr ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/card_game.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,335 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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/>. +""" + +import urwid +from tools.games import TarotCard +from quick_frontend.quick_card_game import QuickCardGame +from xmlui import XMLUI +from urwid_satext import sat_widgets + +class CardDisplayer(urwid.Text): + """Show a card""" + signals = ['click'] + + def __init__(self, card): + self.__selected = False + self.card = card + urwid.Text.__init__(self, card.getAttrText()) + + def selectable(self): + return True + + def keypress(self, size, key): + if key == ' ': + self.select(not self.__selected) + self._emit('click') + return key + + def mouse_event(self, size, event, button, x, y, focus): + if urwid.is_mouse_press(event) and button == 1: + self.select(not self.__selected) + self._emit('click') + return True + + return False + + def select(self, state=True): + self.__selected = state + attr,txt = self.card.getAttrText() + if self.__selected: + attr+='_selected' + self.set_text((attr,txt)) + self._invalidate() + + def isSelected(self): + return self.__selected + + def getCard(self): + return self.card + + def render(self, size, focus=False): + canvas = urwid.CompositeCanvas(urwid.Text.render(self, size, focus)) + if focus: + canvas.set_cursor((0,0)) + return canvas + +class Hand(urwid.WidgetWrap): + """Used to display several cards, and manage a hand""" + signals = ['click'] + + def __init__(self, hand=[], selectable = False, on_click=None, user_data=None): + """@param hand: list of Card""" + self.__selectable = selectable + self.columns = urwid.Columns([],dividechars=1) + if on_click: + urwid.connect_signal(self, 'click', on_click, user_data) + if hand: + self.update(hand) + urwid.WidgetWrap.__init__(self, self.columns) + + def selectable(self): + return self.__selectable + + def keypress(self, size, key): + + if CardDisplayer in [wid.__class__ for wid in self.columns.widget_list]: + return self.columns.keypress(size,key) + else: + #No card displayed, we still have to manage the clicks + if key == ' ': + self._emit('click', None) + return key + + def getSelected(self): + """Return a list of selected cards""" + _selected = [] + for wid in self.columns.widget_list: + if isinstance(wid, CardDisplayer) and wid.isSelected(): + _selected.append(wid.getCard()) + return _selected + + def update(self, hand): + """Update the hand displayed in this widget + @param hand: list of Card""" + del self.columns.widget_list[:] + del self.columns.column_types[:] + self.columns.widget_list.append(urwid.Text('')) + self.columns.column_types.append(('weight',1)) + for card in hand: + widget = CardDisplayer(card) + self.columns.widget_list.append(widget) + self.columns.column_types.append(('fixed',3)) + urwid.connect_signal(widget, 'click', self.__onClick) + self.columns.widget_list.append(urwid.Text('')) + self.columns.column_types.append(('weight',1)) + self.columns.set_focus(1) + + def __onClick(self,card_wid): + self._emit('click', card_wid) + +class Card(TarotCard): + """This class is used to represent a card, logically + and give a text representation with attributes""" + SIZE = 3 #size of a displayed card + + def __init__(self, suit, value): + """@param file: path of the PNG file""" + TarotCard.__init__(self, (suit, value)) + + def getAttrText(self): + """return text representation of the card with attributes""" + try: + value = "%02i" % int(self.value) + except ValueError: + value = self.value[0].upper()+self.value[1] + if self.suit == "atout": + if self.value == "excuse": + suit = 'c' + else: + suit = 'A' + color = 'neutral' + elif self.suit == "pique": + suit = u'♠' + color = 'black' + elif self.suit == "trefle": + suit = u'♣' + color = 'black' + elif self.suit == "coeur": + suit = u'♥' + color = 'red' + elif self.suit == "carreau": + suit = u'♦' + color = 'red' + if self.bout: + color = 'special' + return ('card_%s' % color,u"%s%s" % (value,suit)) + + def getWidget(self): + """Return a widget representing the card""" + return CardDisplayer(self) + +class Table(urwid.FlowWidget): + """Represent the cards currently on the table""" + + def __init__(self): + self.top = self.left = self.bottom = self.right = None + + def putCard(self, location, card): + """Put a card on the table + @param location: where to put the card (top, left, bottom or right) + @param card: Card to play or None""" + assert location in ['top','left','bottom','right'] + assert isinstance(card,Card) or card == None + if [getattr(self, place) for place in ['top','left','bottom','right']].count(None) == 0: + #If the table is full of card, we remove them + self.top = self.left = self.bottom = self.right = None + setattr(self, location, card) + self._invalidate() + + def rows(self,size,focus=False): + return self.display_widget(size, focus).rows(size, focus) + + def render(self, size, focus=False): + return self.display_widget(size, focus).render(size, focus) + + def display_widget(self, size, focus): + cards={} + max_col, = size + separator = " - " + margin = max((max_col-Card.SIZE)/2,0) * ' ' + margin_center = max((max_col-Card.SIZE*2-len(separator))/2,0) * ' ' + for location in ['top', 'left', 'bottom', 'right']: + card = getattr(self,location) + cards[location] = card.getAttrText() if card else Card.SIZE * ' ' + render_wid = [urwid.Text([margin,cards['top']]), + urwid.Text([margin_center,cards['left'],separator,cards['right']]), + urwid.Text([margin,cards['bottom']])] + return urwid.Pile(render_wid) + + +class CardGame(QuickCardGame,urwid.WidgetWrap): + """Widget for card games""" + + def __init__(self, parent, referee, players, player_nick): + QuickCardGame.__init__(self, parent, referee, players, player_nick) + self.loadCards() + self.top = urwid.Pile([urwid.Padding(urwid.Text(self.top_nick), 'center')]) + #self.parent.host.debug() + self.table = Table() + self.center = urwid.Columns([('fixed',len(self.left_nick),urwid.Filler(urwid.Text(self.left_nick))), + urwid.Filler(self.table), + ('fixed',len(self.right_nick),urwid.Filler(urwid.Text(self.right_nick))) + ]) + """urwid.Pile([urwid.Padding(self.top_card_wid,'center'), + urwid.Columns([('fixed',len(self.left_nick),urwid.Text(self.left_nick)), + urwid.Padding(self.center_cards_wid,'center'), + ('fixed',len(self.right_nick),urwid.Text(self.right_nick)) + ]), + urwid.Padding(self.bottom_card_wid,'center') + ])""" + self.hand_wid = Hand(selectable = True, on_click = self.onClick) + self.main_frame = urwid.Frame(self.center,header=self.top, footer=self.hand_wid, focus_part='footer') + urwid.WidgetWrap.__init__(self,self.main_frame) + self.parent.host.bridge.tarotGameReady(player_nick, referee, profile_key = self.parent.host.profile) + + def loadCards(self): + """Load all the cards in memory""" + QuickCardGame.loadCards(self) + for value in map(str,range(1,22))+['excuse']: + card = Card('atout',value) + self.cards[card.suit, card.value]=card + self.deck.append(card) + for suit in ["pique", "coeur", "carreau", "trefle"]: + for value in map(str,range(1,11))+["valet","cavalier","dame","roi"]: + card = Card(suit,value) + self.cards[card.suit, card.value]=card + self.deck.append(card) + + def newGame(self, hand): + """Start a new game, with given hand""" + QuickCardGame.newGame(self, hand) + self.hand_wid.update(self.hand) + self.parent.host.redraw() + + def contratSelected(self, data): + """Called when the contrat has been choosed + @param data: form result""" + contrat = data[0][1] + QuickCardGame.contratSelected(self, contrat) + + def chooseContrat(self, xml_data): + """Called when the player as to select his contrat + @param xml_data: SàT xml representation of the form""" + misc = {'callback': self.contratSelected} + form = XMLUI(self.parent.host, xml_data, title = _('Please choose your contrat'), options = ['NO_CANCEL'], misc = misc) + form.show() + + def showCards(self, game_stage, cards, data): + """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" + QuickCardGame.showCards(self, game_stage, cards, data) + self.center.widget_list[1] = urwid.Filler(Hand(self.to_show)) + self.parent.host.redraw() + + def myTurn(self): + QuickCardGame.myTurn(self) + + def showScores(self, xml_data, winners, loosers): + """Called when the player as to select hist contrat + @param xml_data: SàT xml representation of the form""" + form = XMLUI(self.parent.host, xml_data, title = _('You win \o/') if self.player_nick in winners else _('You loose :('), options = ['NO_CANCEL']) + form.show() + + def invalidCards(self, phase, played_cards, invalid_cards): + """Invalid cards have been played + @param phase: phase of the game + @param played_cards: all the cards played + @param invalid_cards: cards which are invalid""" + QuickCardGame.invalidCards(self, phase, played_cards, invalid_cards) + self.hand_wid.update(self.hand) + if self._autoplay==None: #No dialog if there is autoplay + self.parent.host.notify(_('Cards played are invalid !')) + self.parent.host.redraw() + + def cardsPlayed(self, player, cards): + """A card has been played by player""" + QuickCardGame.cardsPlayed(self, player, cards) + self.table.putCard(self.getPlayerLocation(player),self.played[player]) + self.parent.host.redraw() + + ##EVENTS## + def onClick(self, hand, card_wid): + """Called when user do an action on the hand""" + if not self.state in ['play','ecart','wait_for_ecart']: + #it's not our turn, we ignore the click + card_wid.select(False) + return + if isinstance(self.center.widget_list[1].original_widget, Hand): #if we have a hand displayed + self.center.widget_list[1] = urwid.Filler(self.table) #we show again the table + if self.state == "chien": + self.to_show = [] + self.state = "wait" + elif self.state == "wait_for_ecart": + self.state = "ecart" + self.hand.extend(self.to_show) + self.hand.sort() + self.to_show = [] + self.hand_wid.update(self.hand) + if self.state == "ecart": + if len(self.hand_wid.getSelected()) == 6: + pop_up_widget = sat_widgets.ConfirmDialog(_("Do you put these cards in chien ?"), yes_cb=self.onEcartDone, no_cb=self.parent.host.removePopUp) + self.parent.host.showPopUp(pop_up_widget) + elif self.state == "play": + card = card_wid.getCard() + self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], profile_key = self.parent.host.profile) + self.hand.remove(card) + self.hand_wid.update(self.hand) + self.state = "wait" + + def onEcartDone(self,button): + """Called when player has finished is écart""" + ecart = [] + for card in self.hand_wid.getSelected(): + ecart.append((card.suit, card.value)) + self.hand.remove(card) + self.hand_wid.update(self.hand) + self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, ecart, profile_key = self.parent.host.profile) + self.state = "wait" + self.parent.host.removePopUp()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/chat.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,279 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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/>. +""" + +import urwid +from quick_frontend.quick_contact_list import QuickContactList +from quick_frontend.quick_chat import QuickChat +from urwid_satext import sat_widgets +import time +from tools.jid import JID +from card_game import CardGame +from urwid_satext.files_management import FileDialog + + +class ChatText(urwid.FlowWidget): + """Manage the printing of chat message""" + + def __init__(self, parent, timestamp, nick, my_mess, message, align='left'): + self.parent = parent + self.timestamp = time.localtime(timestamp) + self.nick = nick + self.my_mess = my_mess + self.message = unicode(message) + self.align = align + + def selectable(self): + return True + + def keypress(self, size, key): + return key + + def rows(self,size,focus=False): + return self.display_widget(size, focus).rows(size, focus) + + def render(self, size, focus=False): + canvas = urwid.CompositeCanvas(self.display_widget(size, focus).render(size, focus)) + if focus: + canvas.set_cursor(self.get_cursor_coords(size)) + return canvas + + def get_cursor_coords(self, size): + #(maxcol,) = size + return 0, 0 + + def display_widget(self, size, focus): + render_txt = [] + if self.parent.show_timestamp: + time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M" #if the message was sent before today, we print the full date + render_txt.append(('date',"[%s]" % time.strftime(time_format, self.timestamp).decode('utf-8'))) + if self.parent.show_short_nick: + render_txt.append(('my_nick' if self.my_mess else 'other_nick',"**" if self.my_mess else "*")) + else: + render_txt.append(('my_nick' if self.my_mess else 'other_nick',"[%s] " % self.nick)) + render_txt.append(self.message) + return urwid.Text(render_txt, align=self.align) + +class Chat(urwid.WidgetWrap, QuickChat): + + def __init__(self, target, host, type='one2one'): + self.target = target + QuickChat.__init__(self, target, host, type) + self.content = urwid.SimpleListWalker([]) + self.text_list = urwid.ListBox(self.content) + self.chat_widget = urwid.Frame(self.text_list) + self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) + self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) + self.pile = urwid.Pile([self.chat_colums]) + urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile)) + self.setType(type) + self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time + self.show_timestamp = True + self.show_short_nick = False + self.show_title = 1 #0: clip title; 1: full title; 2: no title + self.subject = None + + def keypress(self, size, key): + if key == "meta p": #user wants to (un)hide the presents panel + if self.type == 'group': + widgets = self.chat_colums.widget_list + if self.present_panel in widgets: + self.__removePresentPanel() + else: + self.__appendPresentPanel() + elif key == "meta t": #user wants to (un)hide timestamp + self.show_timestamp = not self.show_timestamp + for wid in self.content: + wid._invalidate() + elif key == "meta n": #user wants to (not) use short nick + self.show_short_nick = not self.show_short_nick + for wid in self.content: + wid._invalidate() + elif key == "meta l": #user wants to (un)hide widget decoration + show = not isinstance(self._w, sat_widgets.LabelLine) + self.showDecoration(show) + self._invalidate() + elif key == "meta s": #user wants to (un)hide group's subject or change its apperance + if self.subject: + self.show_title = (self.show_title + 1) % 3 + if self.show_title == 0: + self.setSubject(self.subject,'clip') + elif self.show_title == 1: + self.setSubject(self.subject,'space') + elif self.show_title == 2: + self.chat_widget.header = None + self._invalidate() + + + return super(Chat, self).keypress(size, key) + + def getMenu(self): + """Return Menu bar""" + menu = sat_widgets.Menu(self.host.loop) + if self.type == 'group': + game = _("Game") + menu.addMenu(game, "Tarot", self.onTarotRequest) + elif self.type == 'one2one': + menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest) + return menu + + def setType(self, type): + QuickChat.setType(self, type) + if type == 'one2one': + self.historyPrint(profile=self.host.profile) + elif type == 'group': + if len(self.chat_colums.widget_list) == 1: + present_widget = self.__buildPresentList() + self.present_panel = sat_widgets.VerticalSeparator(present_widget) + self.__appendPresentPanel() + + def __getDecoration(self, widget): + return sat_widgets.LabelLine(widget, sat_widgets.SurroundedText(unicode(self.target))) + + def showDecoration(self, show=True): + """Show/Hide the decoration around the chat window""" + if show: + main_widget = self.__getDecoration(self.pile) + else: + main_widget = self.pile + self._w = main_widget + + + def __buildPresentList(self): + self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText) + return self.present_wid + + def __appendPresentPanel(self): + self.chat_colums.widget_list.append(self.present_panel) + self.chat_colums.column_types.append(('weight', 2)) + + def __removePresentPanel(self): + self.chat_colums.set_focus(0) #necessary as the focus change to the next object, we can go out of range if we are on the last object of self.chat_colums + self.chat_colums.widget_list.remove(self.present_panel) + del self.chat_colums.column_types[-1] + + def __appendGamePanel(self, widget): + assert (len(self.pile.widget_list) == 1) + self.pile.widget_list.insert(0,widget) + self.pile.item_types.insert(0,('weight', 1)) + self.pile.widget_list.insert(1,urwid.Filler(urwid.Divider('-'))) + self.pile.item_types.insert(1,('fixed', 1)) + self.host.redraw() + + def __removeGamePanel(self): + assert (len(self.pile.widget_list) == 3) + self.pile.set_focus(0) #necessary as the focus change to the next object, we can go out of range if we are on the last object of self.chat_colums + del self.pile.widget_list[0] + del self.pile.item_types[0] + self.host.redraw() + + def setSubject(self, subject, wrap='space'): + """Set title for a group chat""" + QuickChat.setSubject(self, subject) + self.subject = subject + self.subj_wid = urwid.Text(unicode(subject.replace('\n','|') if wrap == 'clip' else subject ), + align='left' if wrap=='clip' else 'center',wrap=wrap) + self.chat_widget.header = urwid.AttrMap(self.subj_wid,'title') + self.host.redraw() + + def setPresents(self, param_nicks): + """Set the users presents in the contact list for a group chat + @param nicks: list of nicknames + """ + nicks = [unicode(nick) for nick in param_nicks] #FIXME: should be done in DBus bridge + nicks.sort() + QuickChat.setPresents(self, nicks) + self.present_wid.changeValues(nicks) + self.host.redraw() + + def replaceUser(self, param_nick): + """Add user if it is not in the group list""" + nick = unicode(param_nick) #FIXME: should be done in DBus bridge + if "facebook" in nick: + self.host.debug() + QuickChat.replaceUser(self, nick) + presents = self.present_wid.getAllValues() + if nick not in presents: + presents.append(nick) + presents.sort() + self.present_wid.changeValues(presents) + self.host.redraw() + + def removeUser(self, param_nick): + """Remove a user from the group list""" + nick = unicode(param_nick) #FIXME: should be done in DBus bridge + QuickChat.removeUser(self, nick) + self.present_wid.deleteValue(nick) + self.host.redraw() + + def printMessage(self, from_jid, msg, profile, timestamp=""): + assert isinstance(from_jid, JID) + try: + jid,nick,mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) + except TypeError: + return + my_jid = self.host.profiles[profile]['whoami'] + self.content.append(ChatText(self, timestamp or None, nick, mymess, msg)) + self.text_list.set_focus(len(self.content)-1) + self.host.redraw() + + def printInfo(self, msg, type='normal'): + """Print general info + @param msg: message to print + @type: one of: + normal: general info like "toto has joined the room" + me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" + """ + self.content.append(sat_widgets.ClickableText(msg)) + self.text_list.set_focus(len(self.content)-1) + self.host.redraw() + + def startGame(self, game_type, referee, players): + """Configure the chat window to start a game""" + if game_type=="Tarot": + try: + self.tarot_wid = CardGame(self, referee, players, self.nick) + self.__appendGamePanel(self.tarot_wid) + except e: + self.host.debug() + + def getGame(self, game_type): + """Return class managing the game type""" + #TODO: check that the game is launched, and manage errors + if game_type=="Tarot": + return self.tarot_wid + + #MENU EVENTS# + def onTarotRequest(self, menu): + if len(self.occupants) != 4: + self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp)) + else: + self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile) + + def onSendFileRequest(self, menu): + dialog = FileDialog(ok_cb=self.onFileSelected, cancel_cb=self.host.removePopUp) + self.host.showPopUp(dialog, 80, 80) + + #MISC EVENTS# + def onFileSelected(self, filepath): + self.host.removePopUp() + full_jid = self.host.CM.get_full(self.target) + id = self.host.bridge.sendFile(full_jid, filepath) + self.host.addProgress(id,filepath)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/contact_list.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,183 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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/>. +""" + +import urwid +from quick_frontend.quick_contact_list import QuickContactList +from tools.jid import JID +from urwid_satext import sat_widgets + + +class ContactList(urwid.WidgetWrap, QuickContactList): + signals = ['click','change'] + + def __init__(self, host, CM, on_click=None, on_change=None, user_data=None): + self.host = host + self.selected = None + self.groups={} + self.alert_jid=set() + + #we now build the widget + self.frame = urwid.Frame(self.__buildList()) + self.main_widget = sat_widgets.LabelLine(self.frame, sat_widgets.SurroundedText(_("Contacts"))) + urwid.WidgetWrap.__init__(self, self.main_widget) + if on_click: + urwid.connect_signal(self, 'click', on_click, user_data) + if on_change: + urwid.connect_signal(self, 'change', on_change, user_data) + QuickContactList.__init__(self, CM) + + def __contains__(self, jid): + for group in self.groups: + if jid.short in self.groups[group][1]: + return True + return False + + def setFocus(self, name): + """give focus to the first group or contact with the given name""" + idx = 0 + for widget in self.frame.body.body: + if widget.getValue() == name: + self.frame.body.set_focus(idx) + return + idx+=1 + + def putAlert(self, jid): + """Put an alert on the jid to get attention from user (e.g. for new message)""" + self.alert_jid.add(jid.short) + self.frame.body = self.__buildList() + self.host.redraw() + + def __groupClicked(self, group_wid): + group = self.groups[group_wid.getValue()] + group[0] = not group[0] + self.frame.body = self.__buildList() + self.host.redraw() + self.setFocus(group_wid.getValue()) + + def __contactClicked(self, contact_wid, selected): + self.selected = contact_wid.data + for widget in self.frame.body.body: + if widget.__class__ == sat_widgets.SelectableText: + widget.setState(widget.data == self.selected, invisible=True) + if self.selected in self.alert_jid: + self.alert_jid.remove(self.selected) + self.frame.body = self.__buildList() + self.host.redraw() + self._emit('click') + + def __buildContact(self, content, param_contacts): + """Add contact representation in widget list + @param content: widget list, e.g. SimpleListWalker + @param contacts: list of JID""" + contacts = list(param_contacts) + contacts.sort() + for contact in contacts: + jid=JID(contact) + name = self.CM.getAttr(jid,'name') + nick = self.CM.getAttr(jid,'nick') + display = nick or name or jid.node or jid.short + header = '(*) ' if contact in self.alert_jid else '' + widget = sat_widgets.SelectableText(('alert' if contact in self.alert_jid else 'default',display), + selected = contact==self.selected, header=header) + widget.data = contact + content.append(widget) + urwid.connect_signal(widget, 'change', self.__contactClicked) + + def __buildList(self): + """Build the main contact list widget""" + content = urwid.SimpleListWalker([]) + group_keys = self.groups.keys() + group_keys.sort() + for key in group_keys: + unfolded = self.groups[key][0] + if key!=None: + header = '[-]' if unfolded else '[+]' + widget = sat_widgets.ClickableText(key,header=header+' ') + content.append(widget) + urwid.connect_signal(widget, 'click', self.__groupClicked) + if unfolded: + self.__buildContact(content, self.groups[key][1]) + return urwid.ListBox(content) + + def unselectAll(self): + """Unselect all contacts""" + self.selected = None + for widget in self.frame.body.body: + if widget.__class__ == sat_widgets.SelectableText: + widget.setState(False, invisible=True) + + + def get_contact(self): + """Return contact currently selected""" + return self.selected + + def clear_contacts(self): + """clear all the contact list""" + self.groups={} + self.selected = None + self.unselectAll() + self.frame.body = self.__buildList() + self.host.redraw() + + def replace(self, jid, groups=[None]): + """add a contact to the list if doesn't exist, else update it""" + assert isinstance(groups, list) + assert isinstance(jid, JID) + if not groups: + groups=[None] + for group in groups: + if not self.groups.has_key(group): + self.groups[group] = [True,set()] #[unfold,list_of_contacts] + self.groups[group][1].add(jid.short) + self.frame.body = self.__buildList() + self.host.redraw() + + + """contacts = self.list_wid.getAllValues() + if jid.short not in contacts: + contacts.append(jid.short) + contacts.sort() + self.list_wid.changeValues(contacts) + self._emit('change')""" + + def disconnect(self, jid): + """mark a contact disconnected""" + self.remove(jid.short) + + def remove(self, param_jid): + """remove a contact from the list""" + groups_to_remove = [] + jid = JID(param_jid) + for group in self.groups: + contacts = self.groups[group][1] + if jid.short in contacts: + contacts.remove(jid.short) + if not len(contacts): + groups_to_remove.append(group) + for group in groups_to_remove: + del self.groups[group] + self.frame.body = self.__buildList() + self.host.redraw() + + def add(self, jid, param_groups=[None]): + """add a contact to the list""" + self.replace(jid,param_groups) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/gateways.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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/>. +""" + +import urwid +from urwid_satext import sat_widgets +from tools.jid import JID +from quick_frontend.quick_gateways import QuickGatewaysManager + + +class GatewaysManager(urwid.WidgetWrap, QuickGatewaysManager): + + def __init__(self, host, gateways, title=_("Gateways manager"), server=None): + QuickGatewaysManager.__init__(self, host, gateways, server) + if server: + title+=" (%s)" % server + widget_list = urwid.SimpleListWalker([]) + widget_list.append(urwid.Text(self.WARNING_MSG)) + widget_list.append(urwid.Divider('-')) + for gateway in gateways: + self.addGateway(widget_list,gateway, gateways[gateway]) + widget_list.append(urwid.Divider()) + self.ext_serv = sat_widgets.AdvancedEdit(_("Use external XMPP server: ")) + go_button = sat_widgets.CustomButton( _("GO !"),self.browseExternalServer) + ext_serv_col = urwid.Columns([self.ext_serv,('fixed',go_button.getSize(),go_button)]) + widget_list.append(ext_serv_col) + list_wid = urwid.ListBox(widget_list) + decorated = sat_widgets.LabelLine(list_wid, sat_widgets.SurroundedText(title)) + urwid.WidgetWrap.__init__(self, decorated) + + def browseExternalServer(self, button): + """Open the gateway manager on given server""" + server = self.ext_serv.get_edit_text() + if not server: + popup = sat_widgets.Alert(_("Error"), _("You must enter an external server JID"), ok_cb=self.host.removePopUp) + self.host.showPopUp(popup) + return + id = self.host.bridge.findGateways(server, self.host.profile) + self.host.current_action_ids.add(id) + self.host.current_action_ids_cb[id] = self.host.onGatewaysFound + self.host.removeWindow() + + def addGateway(self, widget_list, gateway, param): + + widget_col = [] + widget_col.append(('weight',4,urwid.Text(unicode(param['name'])))) #FIXME: unicode to be remove when DBus bridge will not give dbus.String anymore + + #Then the transport type message + widget_col.append(('weight',6,urwid.Text(self.getGatewayDesc(param['type'])))) + + #The buttons + + reg_button = sat_widgets.CustomButton( _("Register"), self.onRegister) + reg_button.gateway_jid = JID(gateway) + widget_col.append(('fixed',reg_button.getSize(),reg_button)) + unreg_button = sat_widgets.CustomButton( _("Unregister"), self.onUnregister) + unreg_button.gateway_jid = JID(gateway) + widget_col.append(('fixed',unreg_button.getSize(),unreg_button)) + widget_list.append(urwid.Columns(widget_col,1)) + + def onRegister(self, button): + """Called when register button is clicked""" + gateway_jid = button.gateway_jid + id = self.host.bridge.in_band_register(gateway_jid, self.host.profile) + self.host.current_action_ids.add(id) + + def onUnregister(self, button): + """Called when unregister button is clicked""" + gateway_jid = button.gateway_jid + id = self.host.bridge.gatewayRegister("CANCEL",gateway_jid, None, self.host.profile) + self.host.current_action_ids.add(id)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/primitivus Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,509 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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 quick_frontend.quick_app import QuickApp +from quick_frontend.quick_chat_list import QuickChatList +from quick_frontend.quick_contact_list import QuickContactList +from quick_frontend.quick_contact_management import QuickContactManagement +import urwid +from profile_manager import ProfileManager +from contact_list import ContactList +from chat import Chat +from gateways import GatewaysManager +from urwid_satext import sat_widgets +import logging +from logging import debug, info, error +import sys, os +from tools.jid import JID +from xmlui import XMLUI +from progress import Progress + + +### logging configuration FIXME: put this elsewhere ### +logging.basicConfig(level=logging.CRITICAL, #TODO: configure it to put messages in a log file + format='%(message)s') +### + +const_APP_NAME = "Primitivus" +const_PALETTE = [('title', 'black', 'light gray', 'standout,underline'), + ('title_focus', 'white,bold', 'light gray', 'standout,underline'), + ('selected', 'default', 'dark red'), + ('selected_focus', 'default,bold', 'dark red'), + ('default', 'default', 'default'), + ('default_focus', 'default,bold', 'default'), + ('alert', 'default,underline', 'default'), + ('alert_focus', 'default,bold,underline', 'default'), + ('date', 'light gray', 'default'), + ('my_nick', 'dark red,bold', 'default'), + ('other_nick', 'dark cyan,bold', 'default'), + ('menubar', 'light gray,bold', 'dark red'), + ('menubar_focus', 'light gray,bold', 'dark green'), + ('selected_menu', 'light gray,bold', 'dark green'), + ('menuitem', 'light gray,bold', 'dark red'), + ('menuitem_focus', 'light gray,bold', 'dark green'), + ('notifs', 'black,bold', 'yellow'), + ('notifs_focus', 'dark red', 'yellow'), + ('card_neutral', 'dark gray', 'white', 'standout,underline'), + ('card_neutral_selected', 'dark gray', 'dark green', 'standout,underline'), + ('card_special', 'brown', 'white', 'standout,underline'), + ('card_special_selected', 'brown', 'dark green', 'standout,underline'), + ('card_red', 'dark red', 'white', 'standout,underline'), + ('card_red_selected', 'dark red', 'dark green', 'standout,underline'), + ('card_black', 'black', 'white', 'standout,underline'), + ('card_black_selected', 'black', 'dark green', 'standout,underline'), + ('directory', 'dark cyan, bold', 'default'), + ('directory_focus', 'dark cyan, bold', 'dark green'), + ('separator', 'brown', 'default'), + ('warning', 'light red', 'default'), + ('progress_normal', 'default', 'black'), + ('progress_complete', 'default', 'light red'), + ] + +class ChatList(QuickChatList): + """This class manage the list of chat windows""" + + def __init__(self, host): + QuickChatList.__init__(self, host) + + def createChat(self, target): + return Chat(target, self.host) + +class PrimitivusApp(QuickApp): + + def __init__(self): + self.CM = QuickContactManagement() #FIXME: not the best place + QuickApp.__init__(self) + + ## main loop setup ## + self.main_widget = ProfileManager(self) + self.loop = urwid.MainLoop(self.main_widget, const_PALETTE, event_loop=urwid.GLibEventLoop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler) + + ##misc setup## + self.chat_wins=ChatList(self) + self.notBar = sat_widgets.NotificationBar() + urwid.connect_signal(self.notBar,'change',self.onNotification) + self.progress_wid = Progress(self) + urwid.connect_signal(self.notBar.progress,'click',lambda x:self.addWindow(self.progress_wid)) + self.__saved_overlay = None + + def debug(self): + """convenient method to reset screen and launch p(u)db""" + try: + import pudb + pudb.set_trace() + except: + import os,pdb + os.system('reset') + print 'Entered debug mode' + pdb.set_trace() + + def writeLog(self, log, file_name='/tmp/primitivus_log'): + """method to write log in a temporary file, useful for debugging""" + with open(file_name, 'a') as f: + f.write(log+"\n") + + def redraw(self): + """redraw the screen""" + self.loop.draw_screen() + + def start(self): + self.i = 0 + self.loop.set_alarm_in(0,lambda a,b: self.postInit()) + self.loop.run() + + def inputFilter(self, input, raw): + if self.__saved_overlay and input != ['ctrl s']: + return + for i in input: + if isinstance(i,tuple): + if i[0] == 'mouse press': + if i[1] == 4: #Mouse wheel up + input[input.index(i)] = 'up' + if i[1] == 5: #Mouse wheel down + input[input.index(i)] = 'down' + return input + + def keyHandler(self, input): + if input == 'meta m': + """User want to (un)hide the menu roller""" + try: + if self.main_widget.header == None: + self.main_widget.header = self.menu_roller + else: + self.main_widget.header = None + except AttributeError: + pass + elif input == 'ctrl n': + """User wants to see next notification""" + self.notBar.showNext() + elif input == 'ctrl s': + """User wants to (un)hide overlay window""" + if isinstance(self.loop.widget,urwid.Overlay): + self.__saved_overlay = self.loop.widget + self.loop.widget = self.main_widget + else: + if self.__saved_overlay: + self.loop.widget = self.__saved_overlay + self.__saved_overlay = None + + elif input == 'ctrl d' and 'D' in self.bridge.getVersion(): #Debug only for dev versions + self.debug() + elif input == 'f2': #user wants to (un)hide the contact_list + try: + center_widgets = self.center_part.widget_list + if self.contactList in center_widgets: + self.center_part.set_focus(0) #necessary as the focus change to the next object, we can go out of range if we are on the last object of self.center_part + center_widgets.remove(self.contactList) + del self.center_part.column_types[0] + else: + center_widgets.insert(0, self.contactList) + self.center_part.column_types.insert(0, ('weight', 2)) + except AttributeError: + #The main widget is not built (probably in Profile Manager) + pass + elif input == 'window resize': + width,height = self.loop.screen_size + if height<=5 and width<=35: + if not 'save_main_widget' in dir(self): + self.save_main_widget = self.loop.widget + self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !"))) + else: + if 'save_main_widget' in dir(self): + self.loop.widget = self.save_main_widget + del self.save_main_widget + try: + return self.menu_roller.checkShortcuts(input) + except AttributeError: + return input + + def __buildMenuRoller(self): + menu = sat_widgets.Menu(self.loop) + general = _("General") + menu.addMenu(general, _("Connect"), self.onConnectRequest) + menu.addMenu(general, _("Disconnect"), self.onDisconnectRequest) + menu.addMenu(general, _("Parameters"), self.onParam) + menu.addMenu(general, _("About"), self.onAboutRequest) + menu.addMenu(general, _("Exit"), self.onExitRequest, 'ctrl x') + contact = _("Contact") + menu.addMenu(contact, _("Add contact"), self.onAddContactRequest) + menu.addMenu(contact, _("Remove contact"), self.onRemoveContactRequest) + communication = _("Communication") + menu.addMenu(communication, _("Join room"), self.onJoinRoomRequest, 'meta j') + menu.addMenu(communication, _("Find Gateways"), self.onFindGatewaysRequest, 'meta g') + #additionals menus + #FIXME: do this in a more generic way (in quickapp) + add_menus = self.bridge.getMenus() + def add_menu_cb(menu): + category, item = menu + id = self.bridge.callMenu(category, item, "NORMAL", self.profile) + self.current_action_ids.add(id) + for new_menu in add_menus: + category,item,type = new_menu + assert(type=="NORMAL") #TODO: manage other types + menu.addMenu(unicode(category), unicode(item), add_menu_cb) + + menu_roller = sat_widgets.MenuRoller([(_('Main menu'),menu)]) + return menu_roller + + def __buildMainWidget(self): + self.contactList = ContactList(self, self.CM, on_click = self.contactSelected, on_change=lambda w: self.redraw()) + #self.center_part = urwid.Columns([('weight',2,self.contactList),('weight',8,Chat('',self))]) + self.center_part = urwid.Columns([('weight',2,self.contactList), ('weight',8,urwid.Filler(urwid.Text('')))]) + self.editBar = sat_widgets.AdvancedEdit('> ') + self.editBar.setCompletionMethod(self._nick_completion) + urwid.connect_signal(self.editBar,'click',self.onTextEntered) + self.menu_roller = self.__buildMenuRoller() + self.main_widget = sat_widgets.FocusFrame(self.center_part, header=self.menu_roller, footer=self.editBar, focus_part='footer') + return self.main_widget + + def _nick_completion(self, text, completion_data): + """Completion method which complete pseudo in group chat + for params, see AdvancedEdit""" + contact = self.contactList.get_contact() ###Based on the fact that there is currently only one contact selectableat once + if contact: + chat = self.chat_wins[contact] + if chat.type != "group": + return text + space = text.rfind(" ") + start = text[space+1:] + nicks = list(chat.occupants) + nicks.sort() + try: + start_idx=nicks.index(completion_data['last_nick'])+1 + if start_idx == len(nicks): + start_idx = 0 + except (KeyError,ValueError): + start_idx = 0 + for idx in range(start_idx,len(nicks)) + range(0,start_idx): + if nicks[idx].lower().startswith(start.lower()): + completion_data['last_nick'] = nicks[idx] + return text[:space+1] + nicks[idx] + (': ' if space < 0 else '') + return text + + + + def plug_profile(self, profile_key='@DEFAULT@'): + self.loop.widget = self.__buildMainWidget() + QuickApp.plug_profile(self, profile_key) + + def removePopUp(self, widget=None): + "Remove current pop-up, and if there is other in queue, show it" + self.loop.widget = self.main_widget + next_popup = self.notBar.getNextPopup() + if next_popup: + #we still have popup to show, we display it + self.showPopUp(next_popup) + + def showPopUp(self, pop_up_widget, perc_width=40, perc_height=40): + "Show a pop-up window if possible, else put it in queue" + if not isinstance(self.loop.widget,urwid.Overlay): + display_widget = urwid.Overlay(pop_up_widget, self.main_widget, 'center', ('relative', perc_width), 'middle', ('relative', perc_height)) + self.loop.widget = display_widget + self.redraw() + else: + self.notBar.addPopUp(pop_up_widget) + + def notify(self, message): + """"Notify message to user via notification bar""" + self.notBar.addMessage(message) + + def addWindow(self, widget): + """Display a window if possible, + else add it in the notification bar queue + @param widget: BoxWidget""" + assert(len(self.center_part.widget_list)<=2) + wid_idx = len(self.center_part.widget_list)-1 + self.center_part.widget_list[wid_idx] = widget + self.menu_roller.removeMenu(_('Chat menu')) + self.contactList.unselectAll() + self.redraw() + + def removeWindow(self): + """Remove window showed on the right column""" + #TODO: to a better Window management than this crappy hack + assert(len(self.center_part.widget_list)<=2) + wid_idx = len(self.center_part.widget_list)-1 + self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text('')) + self.center_part.set_focus(0) + self.redraw() + + def addProgress (self, id, message): + """Follow a SàT progress bar + @param id: SàT id of the progression + @param message: message to show to identify the progression""" + self.progress_wid.addProgress(id, message) + + def setProgress(self, percentage): + """Set the progression shown in notification bar""" + self.notBar.setProgress(percentage) + + def contactSelected(self, contact_list): + contact = contact_list.get_contact() + if contact: + assert(len(self.center_part.widget_list)==2) + self.center_part.widget_list[1] = self.chat_wins[contact] + self.menu_roller.addMenu(_('Chat menu'), self.chat_wins[contact].getMenu()) + + def onTextEntered(self, editBar): + """Called when text is entered in the main edit bar""" + contact = self.contactList.get_contact() ###Based on the fact that there is currently only one contact selectableat once + if contact: + chat = self.chat_wins[contact] + self.bridge.sendMessage(contact, + editBar.get_edit_text(), + type = "groupchat" if chat.type == 'group' else "chat", + profile_key=self.profile) + editBar.set_edit_text('') + + def newMessage(self, from_jid, msg, type, to_jid, profile): + if not self.check_profile(profile): + return + QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile) + sender = JID(from_jid) + if JID(self.contactList.selected).short != sender.short: + self.contactList.putAlert(sender) + + def _dialogOkCb(self, widget, data): + self.removePopUp() + answer_cb = data[0] + answer_data = [data[1]] if data[1] else [] + answer_cb(True, *answer_data) + + def _dialogCancelCb(self, widget, data): + self.removePopUp() + answer_cb = data[0] + answer_data = [data[1]] if data[1] else [] + answer_cb(False, *answer_data) + + + def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None): + if type == 'info': + popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore + flags = wx.OK | wx.ICON_INFORMATION + elif type == 'error': + popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore + elif type == 'yes/no': + popup = sat_widgets.ConfirmDialog(unicode(message), + yes_cb=self._dialogOkCb, yes_value = (answer_cb, answer_data), + no_cb=self._dialogCancelCb, no_value = (answer_cb, answer_data)) + else: + popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore + error(_('unmanaged dialog type: %s'), type) + self.showPopUp(popup) + + def onNotification(self, notBar): + """Called when a new notification has been received""" + if not isinstance(self.main_widget, sat_widgets.FocusFrame): + #if we are not in the main configuration, we ignore the notifications bar + return + if isinstance(self.main_widget.footer,sat_widgets.AdvancedEdit): + if not self.notBar.canHide(): + #the notification bar is not visible and has usefull informations, we show it + pile = urwid.Pile([self.notBar, self.editBar]) + self.main_widget.footer = pile + else: + if not isinstance(self.main_widget.footer, urwid.Pile): + error(_("INTERNAL ERROR: Unexpected class for main widget's footer")) + assert(False) + if self.notBar.canHide(): + #No notification left, we can hide the bar + self.main_widget.footer = self.editBar + + def actionResult(self, type, id, data): + if not id in self.current_action_ids: + debug (_('unknown id, ignoring')) + return + if type == "SUPPRESS": + self.current_action_ids.remove(id) + elif type == "XMLUI": + self.current_action_ids.remove(id) + debug (_("XML user interface received")) + misc = {} + #FIXME FIXME FIXME: must clean all this crap ! + title = _('Form') + if data['type'] == 'registration': + title = _('Registration') + misc['target'] = data['target'] + misc['action_back'] = self.bridge.gatewayRegister + ui = XMLUI(self, title=title, xml_data = data['xml'], misc = misc) + if data['type'] == 'registration': + ui.show('popup') + else: + ui.show('window') + elif type == "ERROR": + self.current_action_ids.remove(id) + self.showPopUp(sat_widgets.Alert(_("Error"), unicode(data["message"]), ok_cb=self.removePopUp)) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore + elif type == "RESULT": + self.current_action_ids.remove(id) + if self.current_action_ids_cb.has_key(id): + callback = self.current_action_ids_cb[id] + del self.current_action_ids_cb[id] + callback(data) + elif type == "DICT_DICT": + self.current_action_ids.remove(id) + if self.current_action_ids_cb.has_key(id): + callback = self.current_action_ids_cb[id] + del self.current_action_ids_cb[id] + callback(data) + else: + error (_("FIXME FIXME FIXME: type [%s] not implemented") % type) + raise NotImplementedError + + ##DIALOGS CALLBACKS## + def onJoinRoom(self, button, edit): + self.removePopUp() + room_jid = JID(edit.get_edit_text()) + if room_jid.is_valid(): + self.bridge.joinMUC(room_jid.domain, room_jid.node, self.profiles[self.profile]['whoami'].node, self.profile) + else: + message = _("'%s' is an invalid JID !") % room_jid + error (message) + self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp)) + + def onAddContact(self, button, edit): + self.removePopUp() + jid=JID(edit.get_edit_text()) + if jid.is_valid(): + self.bridge.addContact(jid.short, profile_key=self.profile) + else: + message = _("'%s' is an invalid JID !") % jid + error (message) + self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp)) + + def onRemoveContact(self, button): + self.removePopUp() + info(_("Unsubscribing %s presence"),self.contactList.get_contact()) + self.bridge.delContact(self.contactList.get_contact(), profile_key=self.profile) + + #MENU EVENTS# + def onConnectRequest(self, menu): + self.bridge.connect(self.profile) + + def onDisconnectRequest(self, menu): + self.bridge.disconnect(self.profile) + + def onParam(self, menu): + params = XMLUI(self,xml_data=self.bridge.getParamsUI(self.profile)) + self.addWindow(params) + + def onExitRequest(self, menu): + QuickApp.onExit(self) + raise urwid.ExitMainLoop() + + def onJoinRoomRequest(self, menu): + """User wants to join a MUC room""" + pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt = 'room@muc_service.server.tld', cancel_cb=self.removePopUp, ok_cb=self.onJoinRoom) + self.showPopUp(pop_up_widget) + + def onFindGatewaysRequest(self, e): + debug(_("Find Gateways request")) + id = self.bridge.findGateways(self.profiles[self.profile]['whoami'].domain, self.profile) + self.current_action_ids.add(id) + self.current_action_ids_cb[id] = self.onGatewaysFound + + def onAddContactRequest(self, menu): + pop_up_widget = sat_widgets.InputDialog(_("Adding a contact"), _("Please enter new contact JID"), default_txt = 'name@server.tld', cancel_cb=self.removePopUp, ok_cb=self.onAddContact) + self.showPopUp(pop_up_widget) + + def onRemoveContactRequest(self, menu): + contact = self.contactList.get_contact() + if not contact: + self.showPopUp(sat_widgets.Alert(_("Error"), _("You have not selected any contact to delete !"), ok_cb=self.removePopUp)) + else: + pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the contact [%s] ?" % contact), yes_cb=self.onRemoveContact, no_cb=self.removePopUp) + self.showPopUp(pop_up_widget) + + def onAboutRequest(self, menu): + self.showPopUp(sat_widgets.Alert(_("About"), const_APP_NAME + " v" + self.bridge.getVersion(), ok_cb=self.removePopUp)) + + #MISC CALLBACKS# + + def onGatewaysFound(self, data): + """Called when SàT has found the server gateways""" + target = data['__private__']['target'] + del data['__private__'] + gatewayManager = GatewaysManager(self, data, server=target) + self.addWindow(gatewayManager) + +sat = PrimitivusApp() +sat.start() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/profile_manager.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,124 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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/>. +""" + +import urwid +from urwid_satext.sat_widgets import Password,List,InputDialog,ConfirmDialog,Alert,FocusFrame +from tools.jid import JID + + +class ProfileManager(urwid.WidgetWrap): + + def __init__(self, host): + self.host = host + #profiles list + profiles = self.host.bridge.getProfilesList() + profiles.sort() + + #login & password box must be created before list because of onProfileChange + self.login_wid = urwid.Edit(_('Login:'),align='center') + self.pass_wid = Password(_('Password:'),align='center') + + self.list_profile = List(profiles, style=['single'], align='center', on_click=self.onProfileChange) + + #new & delete buttons + buttons = [urwid.Button(_("New"), self.onNewProfile), + urwid.Button(_("Delete"), self.onDeleteProfile)] + buttons_flow = urwid.GridFlow(buttons, max([len(button.get_label()) for button in buttons])+4, 1, 1, 'center') + + #second part: login information: + divider = urwid.Divider('-') + + #connect button + connect_button = urwid.Button(_("Connect"), self.onConnectProfile) + + #we now build the widget + body_content = urwid.SimpleListWalker([buttons_flow,self.list_profile,divider,self.login_wid, self.pass_wid, connect_button]) + frame_body = urwid.ListBox(body_content) + frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title')) + self.main_widget = urwid.LineBox(frame) + urwid.WidgetWrap.__init__(self, self.main_widget) + + def __refillProfiles(self): + """Update the list of profiles""" + profiles = self.host.bridge.getProfilesList() + profiles.sort() + self.list_profile.changeValues(profiles) + + def cancelDialog(self, button): + self.host.removePopUp() + + def newProfile(self, button, edit): + """Create the profile""" + name = edit.get_edit_text() + self.host.bridge.createProfile(name) + self.__refillProfiles() + #We select the profile created in the list + self.list_profile.selectValue(name) + self.host.removePopUp() + + def deleteProfile(self, button): + profile_name = self.list_profile.getSelectedValue() + if profile_name: + self.host.bridge.deleteProfile(profile_name) + self.__refillProfiles() + self.host.removePopUp() + + + def onNewProfile(self, e): + pop_up_widget = InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile) + self.host.showPopUp(pop_up_widget) + + def onDeleteProfile(self, e): + pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getSelectedValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile) + self.host.showPopUp(pop_up_widget) + + def onProfileChange(self, list_wid): + profile_name = list_wid.getSelectedValue() + if profile_name: + jabberID = self.host.bridge.getParamA("JabberID", "Connection", profile_key=profile_name) + password = self.host.bridge.getParamA("Password", "Connection", profile_key=profile_name) + self.login_wid.set_edit_text(jabberID) + self.pass_wid.set_edit_text(password) + + def onConnectProfile(self, button): + profile_name = self.list_profile.getSelectedValue() + if not profile_name: + pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog) + self.host.showPopUp(pop_up_widget) + elif profile_name[0] == '@': + pop_up_widget = Alert(_('Bad profile name'), _("A profile name can't start with a @"), ok_cb=self.cancelDialog) + self.host.showPopUp(pop_up_widget) + else: + profile = self.host.bridge.getProfileName(profile_name) + assert(profile) + #TODO: move this to quick_app + old_jid = self.host.bridge.getParamA("JabberID", "Connection", profile_key=profile) + old_pass = self.host.bridge.getParamA("Password", "Connection", profile_key=profile) + new_jid = self.login_wid.get_edit_text() + new_pass = self.pass_wid.get_edit_text() + + if old_jid != new_jid: + self.host.bridge.setParam("JabberID", new_jid, "Connection", profile) + self.host.bridge.setParam("Server", JID(new_jid).domain, "Connection", profile) + if old_pass != new_pass: + self.host.bridge.setParam("Password", new_pass, "Connection", profile) + self.host.plug_profile(profile) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/progress.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,96 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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/>. +""" + +import urwid +from urwid_satext import sat_widgets +from tools.jid import JID + + +class Progress(urwid.WidgetWrap): + + def __init__(self, host): + self.host = host + self.progress_list = urwid.SimpleListWalker([]) + self.progress_dict = {} + listbox = urwid.ListBox(self.progress_list) + buttons = [] + buttons.append(sat_widgets.CustomButton(_('Clear progress list'), self.__onClear)) + max_len = max([button.getSize() for button in buttons]) + buttons_wid = urwid.GridFlow(buttons,max_len,1,0,'center') + main_wid = sat_widgets.FocusFrame(listbox, footer=buttons_wid) + urwid.WidgetWrap.__init__(self, main_wid) + + def addProgress(self, id, message): + mess_wid = urwid.Text(message) + progr_wid = urwid.ProgressBar('progress_normal', 'progress_complete') + column = urwid.Columns([mess_wid, progr_wid]) + self.progress_dict[id] = {'full':column,'progress':progr_wid,'state':'init'} + self.progress_list.append(column) + self.progressCB(self.host.loop, (id, message)) + + def progressCB(self, loop, data): + id, message = data + data = self.host.bridge.getProgress(id) + pbar = self.progress_dict[id]['progress'] + if data: + if self.progress_dict[id]['state'] == 'init': + #first answer, we must construct the bar + self.progress_dict[id]['state'] = 'progress' + pbar.done = float(data['size']) + + pbar.set_completion(float(data['position'])) + self.updateNotBar() + else: + if self.progress_dict[id]['state'] == 'progress': + self.progress_dict[id]['state'] = 'done' + pbar.set_completion(pbar.done) + self.updateNotBar() + return + + loop.set_alarm_in(1,self.progressCB, (id, message)) + + def __removeBar(self, id): + wid = self.progress_dict[id]['full'] + self.progress_list.remove(wid) + del(self.progress_dict[id]) + + def __onClear(self, button): + to_remove = [] + for id in self.progress_dict: + if self.progress_dict[id]['state'] == 'done': + to_remove.append(id) + for id in to_remove: + self.__removeBar(id) + self.updateNotBar() + + def updateNotBar(self): + if not self.progress_dict: + self.host.setProgress(None) + return + progress = 0 + nb_bars = 0 + for id in self.progress_dict: + pbar = self.progress_dict[id]['progress'] + progress += pbar.current/pbar.done*100 + nb_bars+=1 + av_progress = progress/float(nb_bars) + self.host.setProgress(av_progress) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/primitivus/xmlui.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,261 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Primitivus: a SAT frontend +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/>. +""" + +import urwid +from logging import debug, info, warning, error +from urwid_satext import sat_widgets +from xml.dom import minidom + +class Pairs(urwid.WidgetWrap): + + def __init__(self, weight_0='1', weight_1='1'): + self.idx = 0 + self.weight_0 = weight_0 + self.weight_1 = weight_1 + columns = urwid.Columns([urwid.Text(''), urwid.Text('')]) + #XXX: empty Text hack needed because Pile doesn't support empty list + urwid.WidgetWrap.__init__(self,columns) + + def append(self, widget): + pile = self._w.widget_list[self.idx] + if isinstance(pile, urwid.Text): + self._w.widget_list[self.idx] = urwid.Pile([widget]) + if self.idx == 1: + self._w.set_focus(1) + else: + pile.widget_list.append(widget) + pile.item_types.append(('weight',getattr(self,'weight_'+str(self.idx)))) + self.idx = (self.idx + 1) % 2 + +class InvalidXMLUI(Exception): + pass + +class XMLUI(urwid.WidgetWrap): + + def __init__(self, host, xml_data, title = None, options = [], misc={}): + self.host = host + self.title = title + self.options = options + self.misc = misc + self.__dest = "window" + self.ctrl_list = {} # usefull to access ctrl + widget = self.constructUI(xml_data) + urwid.WidgetWrap.__init__(self,widget) + + def __parseElems(self, node, parent): + """Parse elements inside a <layout> tags, and add them to the parent""" + for elem in node.childNodes: + if elem.nodeName != "elem": + message=_("Unmanaged tag") + error(message) + raise Exception(message) + id = elem.getAttribute("id") + name = elem.getAttribute("name") + type = elem.getAttribute("type") + value = elem.getAttribute("value") if elem.hasAttribute('value') else u'' + if type=="empty": + ctrl = urwid.Text('') + elif type=="text": + try: + value = elem.childNodes[0].wholeText + except IndexError: + warning (_("text node has no child !")) + ctrl = urwid.Text(value) + elif type=="label": + ctrl = urwid.Text(value+": ") + elif type=="string": + ctrl = sat_widgets.AdvancedEdit(edit_text = value) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + elif type=="password": + ctrl = sat_widgets.Password(edit_text = value) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + elif type=="textbox": + ctrl = sat_widgets.AdvancedEdit(edit_text = value, multiline=True) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + elif type=="bool": + ctrl = urwid.CheckBox('', state = value=="true") + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + elif type=="list": + style=[] if elem.getAttribute("multi")=='yes' else ['single'] + ctrl = sat_widgets.List(options=[option.getAttribute("value") for option in elem.getElementsByTagName("option")], style=style) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + elif type=="button": + callback_id = elem.getAttribute("callback_id") + ctrl = sat_widgets.CustomButton(value, on_press=self.onButtonPress) + ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")]) + else: + error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type) #FIXME ! + raise NotImplementedError + if self.type == 'param': + if isinstance(ctrl,urwid.Edit) or isinstance(ctrl,urwid.CheckBox): + urwid.connect_signal(ctrl,'change',self.onParamChange) + ctrl._param_category = self._current_category + ctrl._param_name = name + parent.append(ctrl) + + def __parseChilds(self, current, elem, wanted = ['layout'], data = None): + """Recursively parse childNodes of an elemen + @param current: widget container with 'append' method + @param elem: element from which childs will be parsed + @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant""" + for node in elem.childNodes: + if wanted and not node.nodeName in wanted: + raise InvalidXMLUI + if node.nodeName == "layout": + type = node.getAttribute('type') + if type == "tabs": + tab_cont = sat_widgets.TabsContainer() + self.__parseChilds(current, node, ['category'], tab_cont) + current.append(tab_cont) + elif type == "vertical": + self.__parseElems(node, current) + elif type == "pairs": + pairs = Pairs() + self.__parseElems(node, pairs) + current.append(pairs) + else: + warning(_("Unknown layout, using default one")) + self.__parseElems(node, current) + elif node.nodeName == "category": + name = node.getAttribute('name') + label = node.getAttribute('label') + if not name or not isinstance(data,sat_widgets.TabsContainer): + raise InvalidXMLUI + if self.type == 'param': + self._current_category = name #XXX: awful hack because params need category and we don't keep parent + tab_cont = data + listbox = tab_cont.addTab(label or name) + self.__parseChilds(listbox.body, node, ['layout']) + else: + message=_("Unknown tag") + error(message) + raise NotImplementedError + + def constructUI(self, xml_data): + + ret_wid = urwid.ListBox(urwid.SimpleListWalker([])) + + cat_dom = minidom.parseString(xml_data.encode('utf-8')) + top=cat_dom.documentElement + self.type = top.getAttribute("type") + self.title = top.getAttribute("title") or self.title + if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']: + raise InvalidXMLUI + + if self.type == 'param': + self.param_changed = set() + + self.__parseChilds(ret_wid.body, cat_dom.documentElement) + + assert ret_wid.body + + if isinstance(ret_wid.body[0],sat_widgets.TabsContainer): + ret_wid = ret_wid.body[0] #xxx: awfull hack cause TabsContainer is a BoxWidget, can't be inside a ListBox + + + if self.type == 'form': + buttons = [] + buttons.append(urwid.Button(_('Submit'),self.onFormSubmitted)) + if not 'NO_CANCEL' in self.options: + buttons.append(urwid.Button(_('Cancel'),self.onFormCancelled)) + max_len = max([len(button.get_label()) for button in buttons]) + grid_wid = urwid.GridFlow(buttons,max_len+4,1,0,'center') + ret_wid.body.append(grid_wid) + elif self.type == 'param': + assert(isinstance(ret_wid,sat_widgets.TabsContainer)) + buttons = [] + buttons.append(sat_widgets.CustomButton(_('Save'),self.onSaveParams)) + buttons.append(sat_widgets.CustomButton(_('Cancel'),lambda x:self.host.removeWindow())) + max_len = max([button.getSize() for button in buttons]) + grid_wid = urwid.GridFlow(buttons,max_len,1,0,'center') + ret_wid.addFooter(grid_wid) + return ret_wid + + def show(self,show_type = 'popup'): + """Show the constructed UI + @param show_type: how to show the UI: + - popup + - window""" + self.__dest = "popup" + decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or '')) + if show_type == 'popup': + self.host.showPopUp(decorated) + elif show_type == 'window': + self.host.addWindow(decorated) + else: + error(_('INTERNAL ERROR: Unmanaged show_type (%s)') % show_type) + assert(False) + self.host.redraw() + + + ##EVENTS## + + def onButtonPress(self, button): + callback_id, fields = button.param_id + data = {"callback_id":callback_id} + for field in fields: + ctrl = self.ctrl_list[field] + if isinstance(ctrl['control'],sat_widgets.List): + data[field] = '\t'.join(ctrl['control'].getSelectedValues()) + else: + data[field] = ctrl['control'].getValue() + + id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile) + self.host.current_action_ids.add(id) + + def onParamChange(self, widget, extra=None): + """Called when type is param and a widget to save is modified""" + assert(self.type == "param") + self.param_changed.add(widget) + + def onFormSubmitted(self, button): + data = [] + for ctrl_name in self.ctrl_list: + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl['control'], sat_widgets.List): + data.append((ctrl_name, ctrl['control'].getSelectedValue())) + elif isinstance(ctrl['control'], urwid.CheckBox): + data.append((ctrl_name, "true" if ctrl['control'].get_state() else "false")) + else: + data.append((ctrl_name, ctrl['control'].get_edit_text())) + if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned + raise NotImplementedError + self.host.debug() + elif self.misc.has_key('callback'): + self.misc['callback'](data) + else: + warning (_("The form data is not sent back, the type is not managed properly")) + self.host.removePopUp() + + def onFormCancelled(self, button): + if self.__dest == 'window': + self.host.removeWindow() + else: + self.host.removePopUp() + + def onSaveParams(self, button): + for ctrl in self.param_changed: + if isinstance(ctrl, urwid.CheckBox): + value = "true" if ctrl.get_state() else "false" + else: + value = ctrl.get_edit_text() + self.host.bridge.setParam(ctrl._param_name, value, ctrl._param_category, profile_key = self.host.profile) + self.host.removeWindow()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/quick_frontend/quick_app.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,437 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +helper class for making a SAT frontend +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 tools.jid import JID +from sat_bridge_frontend.DBus import DBusBridgeFrontend,BridgeExceptionNoService +from optparse import OptionParser +import pdb + +import gettext +gettext.install('sat_frontend', "../i18n", unicode=True) + +class QuickApp(): + """This class contain the main methods needed for the frontend""" + + def __init__(self, single_profile=True): + self.rosterList = {} + self.profiles = {} + self.single_profile = single_profile + self.check_options() + + ## bridge ## + try: + self.bridge=DBusBridgeFrontend() + except BridgeExceptionNoService: + print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) + import sys + sys.exit(1) + self.bridge.register("connected", self.connected) + self.bridge.register("disconnected", self.disconnected) + self.bridge.register("newContact", self.newContact) + self.bridge.register("newMessage", self.newMessage) + self.bridge.register("newAlert", self.newAlert) + self.bridge.register("presenceUpdate", self.presenceUpdate) + self.bridge.register("roomJoined", self.roomJoined) + self.bridge.register("roomUserJoined", self.roomUserJoined) + self.bridge.register("roomUserLeft", self.roomUserLeft) + self.bridge.register("roomNewSubject", self.roomNewSubject) + self.bridge.register("tarotGameStarted", self.tarotGameStarted) + self.bridge.register("tarotGameNew", self.tarotGameNew) + self.bridge.register("tarotGameChooseContrat", self.tarotChooseContrat) + self.bridge.register("tarotGameShowCards", self.tarotShowCards) + self.bridge.register("tarotGameYourTurn", self.tarotMyTurn) + self.bridge.register("tarotGameScore", self.tarotScore) + self.bridge.register("tarotGameCardsPlayed", self.tarotCardsPlayed) + self.bridge.register("tarotGameInvalidCards", self.tarotInvalidCards) + self.bridge.register("subscribe", self.subscribe) + self.bridge.register("paramUpdate", self.paramUpdate) + self.bridge.register("contactDeleted", self.contactDeleted) + self.bridge.register("updatedValue", self.updatedValue, "request") + self.bridge.register("askConfirmation", self.askConfirmation, "request") + self.bridge.register("actionResult", self.actionResult, "request") + self.bridge.register("actionResultExt", self.actionResult, "request") + + self.current_action_ids = set() + self.current_action_ids_cb = {} + + def check_profile(self, profile): + """Tell if the profile is currently followed by the application""" + return profile in self.profiles.keys() + + def postInit(self): + """Must be called after initialization is done, do all automatic task (auto plug profile)""" + if self.options.profile: + if not self.bridge.getProfileName(self.options.profile): + error(_("Trying to plug an unknown profile (%s)" % self.options.profile)) + else: + self.plug_profile(self.options.profile) + + def check_options(self): + """Check command line options""" + usage=_(""" + %prog [options] + + %prog --help for options list + """) + parser = OptionParser(usage=usage) + + parser.add_option("-p", "--profile", help=_("Select the profile to use")) + + (self.options, args) = parser.parse_args() + if self.options.profile: + self.options.profile = self.options.profile.decode('utf-8') + return args + + def plug_profile(self, profile_key='@DEFAULT@'): + """Tell application which profile must be used""" + if self.single_profile and self.profiles: + error(_('There is already one profile plugged (we are in single profile mode) !')) + return + profile = self.bridge.getProfileName(profile_key) + if not profile: + error(_("The profile asked doesn't exist")) + return + if self.profiles.has_key(profile): + warning(_("The profile is already plugged")) + return + self.profiles[profile]={} + if self.single_profile: + self.profile = profile + + ###now we get the essential params### + self.profiles[profile]['whoami']=JID(self.bridge.getParamA("JabberID","Connection", profile)) + autoconnect = self.bridge.getParamA("autoconnect","Connection", profile) == "true" + self.profiles[profile]['watched']=self.bridge.getParamA("Watched", "Misc", profile).split() #TODO: put this in a plugin + + ## misc ## + self.profiles[profile]['onlineContact'] = set() #FIXME: temporary + + #TODO: gof: manage multi-profiles here + if not self.bridge.isConnected(profile): + self.setStatusOnline(False) + else: + self.setStatusOnline(True) + + ### now we fill the contact list ### + for contact in self.bridge.getContacts(profile): + self.newContact(contact[0], contact[1], contact[2], profile) + + presences = self.bridge.getPresenceStatus(profile) + for contact in presences: + for res in presences[contact]: + jabber_id = contact+('/'+res if res else '') + show = presences[contact][res][0] + priority = presences[contact][res][1] + statuses = presences[contact][res][2] + self.presenceUpdate(jabber_id, show, priority, statuses, profile) + + #The waiting subscription requests + waitingSub = self.bridge.getWaitingSub(profile) + for sub in waitingSub: + self.subscribe(waitingSub[sub], sub, profile) + + #Now we open the MUC window where we already are: + for room_args in self.bridge.getRoomJoined(profile): + self.roomJoined(*room_args, profile=profile) + + for subject_args in self.bridge.getRoomSubjects(profile): + self.roomNewSubject(*subject_args, profile=profile) + + if autoconnect and not self.bridge.isConnected(profile_key): + #Does the user want autoconnection ? + self.bridge.connect(profile_key) + + + def unplug_profile(self, profile): + """Tell the application to not follow anymore the profile""" + if not profile in self.profiles: + warning (_("This profile is not plugged")) + return + self.profiles.remove(profile) + + def clear_profile(self): + self.profiles.clear() + + def connected(self, profile): + """called when the connection is made""" + if not self.check_profile(profile): + return + debug(_("Connected")) + self.setStatusOnline(True) + + + def disconnected(self, profile): + """called when the connection is closed""" + if not self.check_profile(profile): + return + debug(_("Disconnected")) + self.CM.clear() + self.contactList.clear_contacts() + self.setStatusOnline(False) + + def newContact(self, JabberId, attributes, groups, profile): + if not self.check_profile(profile): + return + entity=JID(JabberId) + self.rosterList[entity.short]=(dict(attributes), list(groups)) + + def newMessage(self, from_jid, msg, type, to_jid, profile): + if not self.check_profile(profile): + return + sender=JID(from_jid) + addr=JID(to_jid) + win = addr if sender.short == self.profiles[profile]['whoami'].short else sender + self.current_action_ids = set() + self.current_action_ids_cb = {} + self.chat_wins[win.short].printMessage(sender, msg, profile) + + def newAlert(self, msg, title, alert_type, profile): + if not self.check_profile(profile): + return + assert alert_type in ['INFO','ERROR'] + self.showDialog(unicode(msg),unicode(title),alert_type.lower()) + + + def setStatusOnline(self, online=True): + pass + + def presenceUpdate(self, jabber_id, show, priority, statuses, profile): + if not self.check_profile(profile): + return + debug (_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]") % {'jid':jabber_id, 'show':show, 'priority':priority, 'statuses':statuses, 'profile':profile}); + from_jid=JID(jabber_id) + debug ("from_jid.short=%(from_jid)s whoami.short=%(whoami)s" % {'from_jid':from_jid.short, 'whoami':self.profiles[profile]['whoami'].short}) + + if from_jid.short==self.profiles[profile]['whoami'].short: + if not type: + self.setStatusOnline(True) + elif type=="unavailable": + self.setStatusOnline(False) + return + + if show != 'unavailable': + name="" + groups = [] + if self.rosterList.has_key(from_jid.short): + if self.rosterList[from_jid.short][0].has_key("name"): + name=self.rosterList[from_jid.short][0]["name"] + groups=self.rosterList[from_jid.short][1] + + #FIXME: must be moved in a plugin + if from_jid.short in self.profiles[profile]['watched'] and not from_jid.short in self.profiles[profile]['onlineContact']: + self.showAlert(_("Watched jid [%s] is connected !") % from_jid.short) + + self.profiles[profile]['onlineContact'].add(from_jid) #FIXME onlineContact is useless with CM, must be removed + self.CM.add(from_jid) + self.CM.update(from_jid, 'name', name) + self.CM.update(from_jid, 'show', show) + self.CM.update(from_jid, 'statuses', statuses) + self.CM.update(from_jid, 'groups', groups) + cache = self.bridge.getCardCache(from_jid) + if cache.has_key('nick'): + self.CM.update(from_jid, 'nick', cache['nick']) + if cache.has_key('avatar'): + self.CM.update(from_jid, 'avatar', self.bridge.getAvatarFile(cache['avatar'])) + self.contactList.replace(from_jid, self.CM.getAttr(from_jid, 'groups')) + + if show=="unavailable" and from_jid in self.profiles[profile]['onlineContact']: + self.profiles[profile]['onlineContact'].remove(from_jid) + self.CM.remove(from_jid) + if not self.CM.isConnected(from_jid): + self.contactList.disconnect(from_jid) + + def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile): + """Called when a MUC room is joined""" + if not self.check_profile(profile): + return + debug (_("Room [%(room_name)s] joined by %(profile)s, users presents:%(users)s") % {'room_name':room_id+'@'+room_service, 'profile': profile, 'users':room_nicks}) + room_jid=room_id+'@'+room_service + self.chat_wins[room_jid].setUserNick(user_nick) + self.chat_wins[room_jid].setType("group") + self.chat_wins[room_jid].id = room_jid + self.chat_wins[room_jid].setPresents(list(set([user_nick]+room_nicks))) + + + def roomUserJoined(self, room_id, room_service, user_nick, user_data, profile): + """Called when an user joined a MUC room""" + if not self.check_profile(profile): + return + room_jid=room_id+'@'+room_service + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].replaceUser(user_nick) + debug (_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick':user_nick, 'room_jid':room_jid}) + + def roomUserLeft(self, room_id, room_service, user_nick, user_data, profile): + """Called when an user joined a MUC room""" + if not self.check_profile(profile): + return + room_jid=room_id+'@'+room_service + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].removeUser(user_nick) + debug (_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick':user_nick, 'room_jid':room_jid}) + + def roomNewSubject(self, room_id, room_service, subject, profile): + """Called when subject of MUC room change""" + if not self.check_profile(profile): + return + room_jid=room_id+'@'+room_service + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].setSubject(subject) + debug (_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid':room_jid, "subject":subject}) + + def tarotGameStarted(self, room_jid, referee, players, profile): + if not self.check_profile(profile): + return + debug (_("Tarot Game Started \o/")) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].startGame("Tarot", referee, players) + debug (_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee':referee, 'room_jid':room_jid, 'players':[str(player) for player in players]}) + + def tarotGameNew(self, room_jid, hand, profile): + if not self.check_profile(profile): + return + debug (_("New Tarot Game")) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].getGame("Tarot").newGame(hand) + + def tarotChooseContrat(self, room_jid, xml_data, profile): + """Called when the player has to select his contrat""" + if not self.check_profile(profile): + return + debug (_("Tarot: need to select a contrat")) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].getGame("Tarot").chooseContrat(xml_data) + + def tarotShowCards(self, room_jid, game_stage, cards, data, profile): + if not self.check_profile(profile): + return + debug (_("Show cards")) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].getGame("Tarot").showCards(game_stage, cards, data) + + def tarotMyTurn(self, room_jid, profile): + if not self.check_profile(profile): + return + debug (_("My turn to play")) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].getGame("Tarot").myTurn() + + def tarotScore(self, room_jid, xml_data, winners, loosers, profile): + """Called when the game is finished and the score are updated""" + if not self.check_profile(profile): + return + debug (_("Tarot: score received")) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].getGame("Tarot").showScores(xml_data, winners, loosers) + + def tarotCardsPlayed(self, room_jid, player, cards, profile): + if not self.check_profile(profile): + return + debug (_("Card(s) played (%(player)s): %(cards)s") % {"player":player, "cards":cards}) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].getGame("Tarot").cardsPlayed(player, cards) + + def tarotInvalidCards(self, room_jid, phase, played_cards, invalid_cards, profile): + if not self.check_profile(profile): + return + debug (_("Cards played are not valid: %s") % invalid_cards) + if self.chat_wins.has_key(room_jid): + self.chat_wins[room_jid].getGame("Tarot").invalidCards(phase, played_cards, invalid_cards) + + def _subscribe_cb(self, answer, data): + entity, profile = data + if answer: + self.bridge.subscription("subscribed", entity.short, profile_key = profile) + else: + self.bridge.subscription("unsubscribed", entity.short, profile_key = profile) + + def subscribe(self, type, raw_jid, profile): + """Called when a subsciption management signal is received""" + if not self.check_profile(profile): + return + entity = JID(raw_jid) + if type=="subscribed": + # this is a subscription confirmation, we just have to inform user + self.showDialog(_("The contact %s has accepted your subscription") % entity.short, _('Subscription confirmation')) + elif type=="unsubscribed": + # this is a subscription refusal, we just have to inform user + self.showDialog(_("The contact %s has refused your subscription") % entity.short, _('Subscription refusal'), 'error') + elif type=="subscribe": + # this is a subscriptionn request, we have to ask for user confirmation + answer = self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.short, _('Subscription confirmation'), 'yes/no', answer_cb = self._subscribe_cb, answer_data=(entity, profile)) + + def showDialog(self, message, title, type="info", answer_cb = None): + raise NotImplementedError + + def showAlert(self, message): + pass #FIXME + + def paramUpdate(self, name, value, namespace, profile): + if not self.check_profile(profile): + return + debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace':namespace, 'name':name, 'value':value}) + if (namespace,name) == ("Connection", "JabberID"): + debug (_("Changing JID to %s"), value) + self.profiles[profile]['whoami']=JID(value) + elif (namespace,name) == ("Misc", "Watched"): + self.profiles[profile]['watched']=value.split() + + def contactDeleted(self, jid, profile): + if not self.check_profile(profile): + return + target = JID(jid) + self.CM.remove(target) + self.contactList.remove(self.CM.get_full(target)) + try: + self.profiles[profile]['onlineContact'].remove(target.short) + except KeyError: + pass + + def updatedValue(self, name, data): + if name == "card_nick": + target = JID(data['jid']) + if target in self.contactList: + self.CM.update(target, 'nick', data['nick']) + self.contactList.replace(target) + elif name == "card_avatar": + target = JID(data['jid']) + if target in self.contactList: + filename = self.bridge.getAvatarFile(data['avatar']) + self.CM.update(target, 'avatar', filename) + self.contactList.replace(target) + + def askConfirmation(self, type, id, data): + raise NotImplementedError + + def actionResult(self, type, id, data): + raise NotImplementedError + + def onExit(self): + """Must be called when the frontend is terminating""" + #TODO: mange multi-profile here + try: + autodisconnect = self.bridge.getParamA("autodisconnect","Connection", self.profile) == "true" + if autodisconnect and self.bridge.isConnected(self.profile): + #Does the user want autodisconnection ? + self.bridge.disconnect(self.profile) + except: + pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/quick_frontend/quick_card_game.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,156 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +helper class for making a SAT frontend +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 tools.jid import JID + + + +class QuickCardGame(): + + def __init__(self, parent, referee, players, player_nick): + self._autoplay = None #XXX: use 0 to activate fake play, None else + self.parent = parent + self.referee = referee + self.players = players + self.played = {} + for player in players: + self.played[player] = None + self.player_nick = player_nick + self.bottom_nick = unicode(self.player_nick) + idx = self.players.index(self.player_nick) + idx = (idx + 1) % len(self.players) + self.right_nick = unicode(self.players[idx]) + idx = (idx + 1) % len(self.players) + self.top_nick = unicode(self.players[idx]) + idx = (idx + 1) % len(self.players) + self.left_nick = unicode(self.players[idx]) + self.bottom_nick = unicode(player_nick) + self.selected = [] #Card choosed by the player (e.g. during ecart) + self.hand_size = 13 #number of cards in a hand + self.hand = [] + self.to_show = [] + self.state = None + + def getPlayerLocation(self, nick): + """return player location (top,bottom,left or right)""" + for location in ['top','left','bottom','right']: + if getattr(self,'%s_nick' % location) == nick: + return location + assert(False) + + def loadCards(self): + """Load all the cards in memory + @param dir: directory where the PNG files are""" + self.cards={} + self.deck=[] + self.cards["atout"]={} #As Tarot is a french game, it's more handy & logical to keep french names + self.cards["pique"]={} #spade + self.cards["coeur"]={} #heart + self.cards["carreau"]={} #diamond + self.cards["trefle"]={} #club + + def newGame(self, hand): + """Start a new game, with given hand""" + assert (len(self.hand) == 0) + for suit, value in hand: + self.hand.append(self.cards[suit, value]) + self.hand.sort() + self.state = "init" + + def contratSelected(self, contrat): + """Called when the contrat has been choosed + @param data: form result""" + self.parent.host.bridge.tarotGameContratChoosed(self.player_nick, self.referee, contrat or 'Passe', self.parent.host.profile) + + def chooseContrat(self, xml_data): + """Called when the player as to select his contrat + @param xml_data: SàT xml representation of the form""" + raise NotImplementedError + + def showCards(self, game_stage, cards, data): + """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" + self.to_show = [] + for suit, value in cards: + self.to_show.append(self.cards[suit, value]) + if game_stage == "chien" and data['attaquant'] == self.player_nick: + self.state = "wait_for_ecart" + else: + self.state = "chien" + + def myTurn(self): + """Called when we have to play :)""" + if self.state == "chien": + self.to_show = [] + self.state = "play" + self.__fakePlay() + + def __fakePlay(self): + """Convenience method for stupid autoplay + /!\ don't forgot to comment any interactive dialog for invalid card""" + if self._autoplay == None: + return + if self._autoplay >= len(self.hand): + self._autoplay = 0 + card = self.hand[self._autoplay] + self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], profile_key = self.parent.host.profile) + del self.hand[self._autoplay] + self.state = "wait" + self._autoplay+=1 + + def showScores(self, xml_data, winners, loosers): + """Called at the end of a game + @param xml_data: SàT xml representation of the scores + @param winners: list of winners' nicks + @param loosers: list of loosers' nicks""" + raise NotImplementedError + + def cardsPlayed(self, player, cards): + """A card has been played by player""" + if self.to_show: + self.to_show = [] + pl_cards = [] + if self.played[player] != None: #FIXME + for pl in self.played: + self.played[pl] = None + for suit, value in cards: + pl_cards.append(self.cards[suit, value]) + self.played[player] = pl_cards[0] + + def invalidCards(self, phase, played_cards, invalid_cards): + """Invalid cards have been played + @param phase: phase of the game + @param played_cards: all the cards played + @param invalid_cards: cards which are invalid""" + + if phase == "play": + self.state = "play" + elif phase == "ecart": + self.state = "ecart" + else: + error ('INTERNAL ERROR: unmanaged game phase') + + for suit, value in played_cards: + self.hand.append(self.cards[suit, value]) + + self.hand.sort() + self.__fakePlay() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/quick_frontend/quick_chat.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,127 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +helper class for making a SAT frontend +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 tools.jid import JID + + + +class QuickChat(): + + def __init__(self, target, host, type='one2one'): + self.target = target + self.host = host + self.type = type + self.id = "" + self.nick = None + self.occupants = set() + + def setType(self, type): + """Set the type of the chat + @param type: can be 'one2one' for single conversation or 'group' for chat à la IRC + """ + self.type = type + + def setPresents(self, nicks): + """Set the users presents in the contact list for a group chat + @param nicks: list of nicknames + """ + debug (_("Adding users %s to room") % nicks) + if self.type != "group": + error (_("[INTERNAL] trying to set presents nicks for a non group chat window")) + raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here + self.occupants.update(nicks) + + def replaceUser(self, nick): + """Add user if it is not in the group list""" + debug (_("Replacing user %s") % nick) + if self.type != "group": + error (_("[INTERNAL] trying to replace user for a non group chat window")) + raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here + len_before = len(self.occupants) + self.occupants.add(nick) + if len_before != len(self.occupants): + self.printInfo("=> %s has joined the room" % nick) + + def setUserNick(self, nick): + """Set the nick of the user, usefull for e.g. change the color of the user""" + self.nick = nick + + def removeUser(self, nick): + """Remove a user from the group list""" + debug(_("Removing user %s") % nick) + if self.type != "group": + error (_("[INTERNAL] trying to remove user for a non group chat window")) + raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here + self.occupants.remove(nick) + self.printInfo("<= %s has left the room" % nick) + + def setSubject(self, subject): + """Set title for a group chat""" + debug(_("Setting subject to %s") % subject) + if self.type != "group": + error (_("[INTERNAL] trying to set subject for a non group chat window")) + raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here + + def historyPrint(self, size=20, keep_last=False, profile='@NONE@'): + """Print the initial history""" + debug (_("now we print history")) + history=self.host.bridge.getHistory(self.host.profiles[profile]['whoami'].short, self.target, 20) + stamps=history.keys() + stamps.sort() + for stamp in stamps: + self.printMessage(JID(history[stamp][0]), history[stamp][1], profile, stamp) + if keep_last: ##FIXME hack for sortilege + self.last_history = stamps[-1] if stamps else None + + def _get_nick(self, jid): + """Return nick of this jid when possible""" + return jid.resource if self.type == "group" else (self.host.CM.getAttr(jid,'nick') or self.host.CM.getAttr(jid,'name') or jid.node) + + def printMessage(self, from_jid, msg, profile, timestamp): + """Print message in chat window. Must be implemented by child class""" + jid=JID(from_jid) + nick = self._get_nick(jid) + mymess = (jid.resource == self.nick) if self.type == "group" else (jid.short == self.host.profiles[profile]['whoami'].short) #mymess = True if message comes from local user + if msg.startswith('/me '): + self.printInfo('* %s %s' % (nick, msg[4:]),type='me') + return + return jid, nick, mymess + + def printInfo(self, msg, type='normal'): + """Print general info + @param msg: message to print + @type: one of: + normal: general info like "toto has joined the room" + me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" + """ + raise NotImplementedError + + + def startGame(self, game_type, referee, players): + """Configure the chat window to start a game""" + #No need to raise an error as game are not mandatory + warning(_('startGame is not implemented in this frontend')) + + def getGame(self, game_type): + """Return class managing the game type""" + #No need to raise an error as game are not mandatory + warning(_('getGame is not implemented in this frontend'))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/quick_frontend/quick_chat_list.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +helper class for making a SAT frontend +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 tools.jid import JID + + +class QuickChatList(dict): + """This class is used to manage the list of chat windows. + It act as a dict, but create a chat window when the name is found for the first time.""" + + def __init__(self, host): + dict.__init__(self) + self.host = host + + def __getitem__(self,to_jid): + target=JID(to_jid) + if not self.has_key(target.short): + #we have to create the chat win + self[target.short] = self.createChat(target) + return dict.__getitem__(self,target.short) + + def createChat(self, target): + raise NotImplementedError
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/quick_frontend/quick_contact_list.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +helper class for making a SAT frontend +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 tools.jid import JID + + +class QuickContactList(): + """This class manage the visual representation of contacts""" + + def __init__(self, CM): + """ + @param CM: instance of QuickContactManagement + """ + debug(_("Contact List init")) + self.CM = CM + + def __contains__(self, jid): + raise NotImplementedError + + def clear_contacts(self, jid): + """Clear all the contact list""" + raise NotImplementedError + + def replace(self, jid, groups=None): + """add a contact to the list if doesn't exist, else update it""" + raise NotImplementedError + + def disconnect(self, jid): + """mark a contact disconnected""" + raise NotImplementedError + + def remove(self, jid): + """remove a contact from the list""" + raise NotImplementedError + + def add(self, jid, param_groups=None): + """add a contact to the list""" + raise NotImplementedError
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/quick_frontend/quick_contact_management.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +helper class for making a SAT frontend +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 tools.jid import JID +import pdb + + +class QuickContactManagement(): + """This helper class manage the contacts and ease the use of nicknames and shortcuts""" + ### FIXME: is SàT a better place for all this stuff ??? ### + + def __init__(self): + self.__contactlist = {} + + def clear(self): + """Clear all the contact list""" + self.__contactlist.clear() + + def add(self, entity): + """Add contact to the list, update resources""" + if not self.__contactlist.has_key(entity.short): + self.__contactlist[entity.short] = {'resources':[]} + if entity.resource in self.__contactlist[entity.short]['resources']: + self.__contactlist[entity.short]['resources'].remove(entity.resource) + self.__contactlist[entity.short]['resources'].append(entity.resource) + + def getContFromGroup(self, group): + """Return all contacts which are in given group""" + result = [] + for contact in self.__contactlist: + if self.__contactlist[contact].has_key('groups'): + if group in self.__contactlist[contact]['groups']: + result.append(JID(contact)) + return result + + def getAttr(self, entity, name): + """Return a specific attribute of contact, or all attributes + @param entity: jid of the contact + @param name: name of the attribute + @return: asked attribute""" + if self.__contactlist.has_key(entity.short): + if name == 'status': #FIXME: for the moment, we only use the first status + if self.__contactlist[entity.short]['statuses']: + return self.__contactlist[entity.short]['statuses'].values()[0] + if self.__contactlist[entity.short].has_key(name): + return self.__contactlist[entity.short][name] + else: + debug(_('Trying to get attribute for an unknown contact')) + return None + + def isConnected(self, entity): + """Tell if the contact is online""" + return self.__contactlist.has_key(entity.short) + + def remove(self, entity): + """remove resource. If no more resource is online or is no resource is specified, contact is deleted""" + try: + if entity.resource: + self.__contactlist[entity.short]['resources'].remove(entity.resource) + if not entity.resource or not self.__contactlist[entity.short]['resources']: + #no more resource available: the contact seems really disconnected + del self.__contactlist[entity.short] + except KeyError: + error(_('INTERNAL ERROR: Key error')) + raise + + def update(self, entity, key, value): + """Update attribute of contact + @param entity: jid of the contact + @param key: name of the attribute + @param value: value of the attribute + """ + if self.__contactlist.has_key(entity.short): + self.__contactlist[entity.short][key] = value + else: + debug (_('Trying to update an unknown contact: %s'), entity.short) + + def get_full(self, entity): + return entity.short+'/'+self.__contactlist[entity.short]['resources'][-1] +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/quick_frontend/quick_gateways.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +helper class for making a SAT frontend +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 QuickGatewaysManager(): + + + def __init__(self, host, gateways, title=_("Gateways manager"), server=None): + self.WARNING_MSG = _(u"""Be careful ! Gateways allow you to use an external IM (legacy IM), so you can see your contact as jabber contacts. +But when you do this, all your messages go throught the external legacy IM server, it is a huge privacy issue (i.e.: all your messages throught the gateway can be monitored, recorded, analyzed by the external server, most of time a private company).""") + self.host = host + + def getGatewayDesc(self, gat_type): + """Return a human readable description of gateway type + @param gat_type: type of gateway, as given by SàT""" + desc = _('Unknown IM') + + if gat_type == 'irc': + desc = "Internet Relay Chat" + elif gat_type == 'xmpp': + desc = "XMPP" + elif gat_type == 'qq': + desc = "Tencent QQ" + elif gat_type == 'simple': + desc = "SIP/SIMPLE" + elif gat_type == 'icq': + desc = "ICQ" + elif gat_type == 'yahoo': + desc = "Yahoo! Messenger" + elif gat_type == 'gadu-gadu': + desc = "Gadu-Gadu" + elif gat_type == 'aim': + desc = "AOL Instant Messenger" + elif gat_type == 'msn': + desc = 'Windows Live Messenger' + + return desc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/card_game.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,254 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + + + +import wx +import os.path, glob +import pdb +from logging import debug, info, error +from tools.jid import JID +from tools.games import TarotCard +from quick_frontend.quick_card_game import QuickCardGame +from xmlui import XMLUI + +CARD_WIDTH = 74 +CARD_HEIGHT = 136 +MIN_WIDTH = 950 #Minimum size of the panel +MIN_HEIGHT = 500 + +class WxCard(TarotCard): + """This class is used to represent a card, graphically and logically""" + + def __init__(self, file): + """@param file: path of the PNG file""" + self.bitmap = wx.Image(file).ConvertToBitmap() + root_name = os.path.splitext(os.path.basename(file))[0] + suit,value = root_name.split('_') + TarotCard.__init__(self, (suit, value)) + print "Carte:",suit, value #, self.bout + + def draw(self, dc, x, y): + """Draw the card on the device context + @param dc: device context + @param x: abscissa + @param y: ordinate""" + dc.DrawBitmap(self.bitmap, x, y, True) + + +class CardPanel(QuickCardGame,wx.Panel): + """This class is used to display the cards""" + + def __init__(self, parent, referee, players, player_nick): + QuickCardGame.__init__(self, parent, referee, players, player_nick) + wx.Panel.__init__(self, parent) + self.SetMinSize(wx.Size(MIN_WIDTH, MIN_HEIGHT)) + self.loadCards("images/cards/") + self.mouse_over_card = None #contain the card to highlight + self.visible_size = CARD_WIDTH/2 #number of pixels visible for cards + self.hand = [] + self.to_show = [] + self.state = None + self.SetBackgroundColour(wx.GREEN) + self.Bind(wx.EVT_SIZE, self.onResize) + self.Bind(wx.EVT_PAINT, self.onPaint) + self.Bind(wx.EVT_MOTION, self.onMouseMove) + self.Bind(wx.EVT_LEFT_UP, self.onMouseClick) + self.parent.host.bridge.tarotGameReady(player_nick, referee, profile_key = self.parent.host.profile) + + def loadCards(self, dir): + """Load all the cards in memory + @param dir: directory where the PNG files are""" + QuickCardGame.loadCards(self) + for file in glob.glob(dir+'/*_*.png'): + card = WxCard(file) + self.cards[card.suit, card.value]=card + self.deck.append(card) + + def newGame(self, hand): + """Start a new game, with given hand""" + QuickCardGame.newGame(self, hand) + self._recalc_ori() + self.Refresh() + + def contratSelected(self, data): + """Called when the contrat has been choosed + @param data: form result""" + debug (_("Contrat choosed")) + contrat = data[0][1] + QuickCardGame.contratSelected(self, contrat) + + def chooseContrat(self, xml_data): + """Called when the player as to select his contrat + @param xml_data: SàT xml representation of the form""" + misc = {'callback': self.contratSelected} + form = XMLUI(self.parent.host, xml_data, title = _('Please choose your contrat'), options = ['NO_CANCEL'], misc = misc) + + def showScores(self, xml_data, winners, loosers): + """Called when the player as to select hist contrat + @param xml_data: SàT xml representation of the form""" + form = XMLUI(self.parent.host, xml_data, title = _('You win \o/') if self.player_nick in winners else _('You loose :('), options = ['NO_CANCEL']) + + def cardsPlayed(self, player, cards): + """A card has been played by player""" + QuickCardGame.cardsPlayed(self, player, cards) + self.Refresh() + + def invalidCards(self, phase, played_cards, invalid_cards): + """Invalid cards have been played + @param phase: phase of the game + @param played_cards: all the cards played + @param invalid_cards: cards which are invalid""" + QuickCardGame.invalidCards(self, phase, played_cards, invalid_cards) + + self._recalc_ori() + self.Refresh() + if self._autoplay==None: #No dialog if there is autoplay + wx.MessageDialog(self, _("Cards played are invalid !"), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal() + + def _is_on_hand(self, pos_x, pos_y): + """Return True if the coordinate are on the hand cards""" + if pos_x > self.orig_x and pos_y > self.orig_y \ + and pos_x < self.orig_x + (len(self.hand)+1) * self.visible_size \ + and pos_y < self.end_y: + return True + return False + + def onResize(self, event): + self._recalc_ori() + + def _recalc_ori(self): + """Recalculate origins of hand, must be call when hand size change""" + self.orig_x = (self.GetSizeTuple()[0]-(len(self.hand)+1)*self.visible_size)/2 #where we start to draw cards + self.orig_y = self.GetSizeTuple()[1] - CARD_HEIGHT - 20 + self.end_y = self.orig_y + CARD_HEIGHT + + def onPaint(self, event): + dc = wx.PaintDC(self) + + #We print the names to know who play where TODO: print avatars when available + max_x, max_y = self.GetSize() + border = 10 #border between nick and end of panel + right_y = left_y = 200 + right_width, right_height = dc.GetTextExtent(self.right_nick) + right_x = max_x - right_width - border + left_x = border + top_width, top_height = dc.GetTextExtent(self.top_nick) + top_x = (max_x - top_width) / 2 + top_y = border + dc.DrawText(self.right_nick, right_x, right_y) + dc.DrawText(self.top_nick, top_x, top_y) + dc.DrawText(self.left_nick, left_x, left_y) + + #We draw the played cards: + center_y = 200 #ordinate used as center point + left_x = (max_x - CARD_WIDTH)/2 - CARD_WIDTH - 5 + right_x = (max_x/2) + (CARD_WIDTH/2) + 5 + left_y = right_y = center_y - CARD_HEIGHT/2 + top_x = bottom_x = (max_x - CARD_WIDTH)/2 + top_y = center_y - CARD_HEIGHT - 5 + bottom_y = center_y + 5 + for side in ['left', 'top', 'right', 'bottom']: + card = self.played[getattr(self, side+'_nick')] + if card != None: + card.draw(dc,locals()[side+'_x'], locals()[side+'_y']) + + x=self.orig_x + for card in self.hand: + if (self.state == "play" or self.state == "ecart") and card == self.mouse_over_card \ + or self.state == "ecart" and card in self.selected: + y = self.orig_y - 30 + else: + y = self.orig_y + + card.draw(dc,x,y) + x+=self.visible_size + + if self.to_show: + """There are cards to display in the middle""" + size = len(self.to_show)*(CARD_WIDTH+10)-10 + x = (max_x - size)/2 + for card in self.to_show: + card.draw(dc, x, 150) + x+=CARD_WIDTH+10 + + def onMouseMove(self, event): + pos_x,pos_y = event.GetPosition() + if self._is_on_hand(pos_x, pos_y): + try: + self.mouse_over_card = self.hand[(pos_x-self.orig_x)/self.visible_size] + except IndexError: + self.mouse_over_card = self.hand[-1] + self.Refresh() + else: + self.mouse_over_card = None + self.Refresh() + + def onMouseClick(self, event): + print "mouse click:",event.GetPosition() + pos_x,pos_y = event.GetPosition() + + if self.state == "chien": + self.to_show = [] + self.state = "wait" + return + elif self.state == "wait_for_ecart": + self.state = "ecart" + self.hand.extend(self.to_show) + self.hand.sort() + self.to_show = [] + self._recalc_ori() + self.Refresh() + return + + if self._is_on_hand(pos_x, pos_y): + idx = (pos_x-self.orig_x)/self.visible_size + if idx == len(self.hand): + idx-=1 + if self.hand[idx] == self.mouse_over_card: + if self.state == "ecart": + if self.hand[idx] in self.selected: + self.selected.remove(self.hand[idx]) + else: + self.selected.append(self.hand[idx]) + if len(self.selected) == 6: #TODO: use variable here, as chien len can change with variants + dlg = wx.MessageDialog(self, _("Do you put these cards in chien ?"), _(u"Écart"), wx.YES_NO | wx.ICON_QUESTION) + answer = dlg.ShowModal() + if answer == wx.ID_YES: + ecart = [] + for card in self.selected: + ecart.append((card.suit, card.value)) + self.hand.remove(card) + del self.selected[:] + self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, ecart, profile_key = self.parent.host.profile) + self.state = "wait" + + self._recalc_ori() + self.Refresh() + if self.state == "play": + card = self.hand[idx] + self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], profile_key = self.parent.host.profile) + del self.hand[idx] + self.state = "wait" + self._recalc_ori() + self.Refresh() + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/chat.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + + + +import wx +import os.path +import time +import pdb +from logging import debug, info, error, warning +from tools.jid import JID +from quick_frontend.quick_chat import QuickChat +from contact_list import ContactList +from card_game import CardPanel + + +idSEND = 1 +idTAROT = 2 + +class Chat(wx.Frame, QuickChat): + """The chat Window for one to one conversations""" + + def __init__(self, target, host, type='one2one'): + wx.Frame.__init__(self, None, title=target, pos=(0,0), size=(400,200)) + QuickChat.__init__(self, target, host, type) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.sizer) + + self.splitter = wx.SplitterWindow(self, -1) + self.sizer.Add(self.splitter, 1, flag = wx.EXPAND) + + self.conv_panel = wx.Panel(self.splitter) + self.conv_panel.sizer = wx.BoxSizer(wx.VERTICAL) + self.subjectBox = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_READONLY) + self.chatWindow = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_MULTILINE | wx.TE_RICH | wx.TE_READONLY) + self.textBox = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_PROCESS_ENTER) + self.conv_panel.sizer.Add(self.subjectBox, flag=wx.EXPAND) + self.conv_panel.sizer.Add(self.chatWindow, 1, flag=wx.EXPAND) + self.conv_panel.sizer.Add(self.textBox, 0, flag=wx.EXPAND) + self.conv_panel.SetSizer(self.conv_panel.sizer) + self.splitter.Initialize(self.conv_panel) + self.SetMenuBar(wx.MenuBar()) + + #events + self.Bind(wx.EVT_CLOSE, self.onClose, self) + self.Bind(wx.EVT_TEXT_ENTER, self.onEnterPressed, self.textBox) + + #fonts + self.font={} + self.font["points"] = self.chatWindow.GetFont().GetPointSize() + self.font["family"] = self.chatWindow.GetFont().GetFamily() + + + #misc + self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time + self.setType(self.type) + self.textBox.SetFocus() + self.Hide() #We hide because of the show toggle + + def __createPresents(self): + """Create a list of present people in a group chat""" + self.present_panel = wx.Panel(self.splitter) + self.present_panel.sizer = wx.BoxSizer(wx.VERTICAL) + self.present_panel.SetBackgroundColour(wx.BLUE) + self.present_panel.presents = ContactList(self.present_panel, self.host, type='nicks') + self.present_panel.presents.SetMinSize(wx.Size(80,20)) + self.present_panel.sizer.Add(self.present_panel.presents, 1, wx.EXPAND) + self.present_panel.SetSizer(self.present_panel.sizer) + self.splitter.SplitVertically(self.present_panel, self.conv_panel, 80) + + def setType(self, type): + QuickChat.setType(self, type) + if type is 'group' and not self.splitter.IsSplit(): + self.__createPresents() + self.subjectBox.Show() + self.__eraseMenus() + self.__createMenus_group() + self.sizer.Layout() + elif type is 'one2one' and self.splitter.IsSplit(): + self.splitter.Unsplit(self.present_panel) + del self.present_panel + self.GetMenuBar().Show() + self.subjectBox.Hide() + self.__eraseMenus() + self.__createMenus_O2O() + self.nick = None + else: + self.subjectBox.Hide() + self.__eraseMenus() + self.__createMenus_O2O() + self.historyPrint(profile=self.host.profile) + + def startGame(self, game_type, referee, players): + """Configure the chat window to start a game""" + if game_type=="Tarot": + debug (_("configure chat window for Tarot game")) + self.tarot_panel = CardPanel(self, referee, players, self.nick) + self.sizer.Prepend(self.tarot_panel, 0, flag=wx.EXPAND) + self.sizer.Layout() + self.Fit() + + def getGame(self, game_type): + """Return class managing the game type""" + #TODO: check that the game is launched, and manage errors + if game_type=="Tarot": + return self.tarot_panel + + + def setPresents(self, nicks): + """Set the users presents in the contact list for a group chat + @param nicks: list of nicknames + """ + QuickChat.setPresents(self, nicks) + for nick in nicks: + self.present_panel.presents.replace(nick) + + def replaceUser(self, nick): + """Add user if it is not in the group list""" + debug (_("Replacing user %s") % nick) + if self.type != "group": + error (_("[INTERNAL] trying to replace user for a non group chat window")) + return + QuickChat.replaceUser(self, nick) + self.present_panel.presents.replace(nick) + + def removeUser(self, nick): + """Remove a user from the group list""" + QuickChat.removeUser(self, nick) + self.present_panel.presents.remove(nick) + + def setSubject(self, subject): + """Set title for a group chat""" + QuickChat.setSubject(self, subject) + self.subjectBox.SetValue(subject) + + def __eraseMenus(self): + """erase all menus""" + menuBar = self.GetMenuBar() + for i in range(menuBar.GetMenuCount()): + menuBar.Remove(i) + + def __createMenus_O2O(self): + """create menu bar for one 2 one chat""" + info("Creating menus") + self.__eraseMenus() + menuBar = self.GetMenuBar() + actionMenu = wx.Menu() + actionMenu.Append(idSEND, _("&SendFile CTRL-s"),_(" Send a file to contact")) + menuBar.Append(actionMenu,_("&Action")) + + #events + wx.EVT_MENU(self, idSEND, self.onSendFile) + + def __createMenus_group(self): + """create menu bar for group chat""" + info("Creating menus") + self.__eraseMenus() + menuBar = self.GetMenuBar() + actionMenu = wx.Menu() + actionMenu.Append(idTAROT, _("Start &Tarot game CTRL-t"),_(" Start a Tarot card game")) #tmp + menuBar.Append(actionMenu,_("&Games")) + + #events + wx.EVT_MENU(self, idTAROT, self.onStartTarot) + + def __del__(self): + wx.Frame.__del__(self) + + def onClose(self, event): + """Close event: we only hide the frame.""" + event.Veto() + self.Hide() + + def onEnterPressed(self, event): + """Behaviour when enter pressed in send line.""" + self.host.bridge.sendMessage(self.target.short if self.type=='group' else self.target, + event.GetString(), + type = "groupchat" if self.type=='group' else "chat", + profile_key=self.host.profile) + self.textBox.Clear() + + def __blink(self): + """Do wizzz and buzzz to show window to user or + at least inform him of something new""" + #TODO: use notification system + if not self.IsActive(): + self.RequestUserAttention() + if not self.IsShown(): + self.Show() + + def printMessage(self, from_jid, msg, profile, timestamp=""): + """Print the message with differents colors depending on where it comes from.""" + try: + jid,nick,mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) + except TypeError: + return + print "printMessage, jid=",jid,"type=",self.type + _font_bold = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.BOLD) + _font_normal = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.NORMAL) + _font_italic = wx.Font(self.font["points"], self.font["family"], wx.ITALIC if mymess else wx.NORMAL, wx.NORMAL) + self.chatWindow.SetDefaultStyle(wx.TextAttr("GREY", font=_font_normal)) + msg_time = time.localtime(timestamp or None) + time_format = "%c" if msg_time < self.day_change else "%H:%M" #if the message was sent before today, we print the full date + self.chatWindow.AppendText("[%s]" % time.strftime(time_format, msg_time )) + self.chatWindow.SetDefaultStyle(wx.TextAttr( "BLACK" if mymess else "BLUE", font=_font_bold)) + self.chatWindow.AppendText("[%s] " % nick) + self.chatWindow.SetDefaultStyle(wx.TextAttr("BLACK", font=_font_italic)) + self.chatWindow.AppendText("%s\n" % msg) + if not mymess: + self.__blink() + + def printInfo(self, msg, type='normal'): + """Print general info + @param msg: message to print + @type: one of: + normal: general info like "toto has joined the room" + me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" + """ + _font_bold = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.BOLD) + _font_normal = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.NORMAL) + self.chatWindow.SetDefaultStyle(wx.TextAttr("BLACK", font=_font_bold if type == 'normal' else _font_normal)) + self.chatWindow.AppendText("%s\n" % msg) + if type=="me": + self.__blink() + + ### events ### + + def onSendFile(self, e): + debug(_("Send File")) + filename = wx.FileSelector(_("Choose a file to send"), flags = wx.FD_FILE_MUST_EXIST) + if filename: + debug(_("filename: %s"),filename) + full_jid = self.host.CM.get_full(self.target) + id = self.host.bridge.sendFile(full_jid, filename) + self.host.waitProgress(id, _("File Transfer"), _("Copying %s") % os.path.basename(filename)) + + def onStartTarot(self, e): + debug (_("Starting Tarot game")) + warning (_("FIXME: temporary menu, must be changed")) + if len(self.occupants) != 4: + err_dlg = wx.MessageDialog(self, _("You need to be exactly 4 peoples in the room to start a Tarot game"), _("Can't start game"), style = wx.OK | wx.ICON_ERROR) #FIXME: gof: temporary only, need to choose the people with who the game has to be started + err_dlg.ShowModal() + else: + self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/constants.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,15 @@ +import sys +import __builtin__ + +__builtin__.__dict__['APP_NAME'] = "Wix" +__builtin__.__dict__['LICENCE_PATH'] = "../../COPYING" +__builtin__.__dict__['IMAGE_DIR'] = sys.path[0]+'/images' + +__builtin__.__dict__['msgOFFLINE'] = _("offline") +__builtin__.__dict__['msgONLINE'] = _("online") +__builtin__.__dict__['const_DEFAULT_GROUP'] = "Unclassed" +__builtin__.__dict__['const_STATUS'] = [("", _("Online"), None), + ("chat", _("Free for chat"), "green"), + ("away", _("AFK"), "brown"), + ("dnd", _("DND"), "red"), + ("xa", _("Away"), "red")]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/contact_list.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,182 @@ +import wx +from quick_frontend.quick_contact_list import QuickContactList +from logging import debug, info, error +from cgi import escape +from tools.jid import JID + + +class Group(unicode): + """Class used to recognize groups""" + +class Contact(unicode): + """Class used to recognize groups""" + +class ContactList(wx.SimpleHtmlListBox, QuickContactList): + """Customized control to manage contacts.""" + + def __init__(self, parent, host, type="JID"): + """init the contact list + @param parent: WxWidgets parent of the widget + @param host: wix main app class + @param type: type of contact list: "JID" for the usual big jid contact list + "CUSTOM" for a customized contact list (self.__presentItem must then be overrided) + """ + wx.SimpleHtmlListBox.__init__(self, parent, -1) + QuickContactList.__init__(self, host.CM) + self.host = host + self.type = type + self.__typeSwitch() + self.groups = {} #list contacts in each groups, key = group + self.Bind(wx.EVT_LISTBOX, self.onSelected) + self.Bind(wx.EVT_LISTBOX_DCLICK, self.onActivated) + + def __contains__(self, jid): + return bool(self.__find_idx(jid)) + + def __typeSwitch(self): + if self.type == "JID": + self.__presentItem = self.__presentItemJID + elif type != "CUSTOM": + self.__presentItem = self.__presentItemDefault + + def __find_idx(self, entity): + """Find indexes of given contact (or groups) in contact list, manage jid + @return: list of indexes""" + result=[] + for i in range(self.GetCount()): + if (type(entity) == JID and type(self.GetClientData(i)) == JID and self.GetClientData(i).short == entity.short) or\ + self.GetClientData(i) == entity: + result.append(i) + return result + + def replace(self, contact, groups=None): + debug(_("update %s") % contact) + if not self.__find_idx(contact): + self.add(contact, groups) + else: + for i in self.__find_idx(contact): + self.SetString(i, self.__presentItem(contact)) + + def disconnect(self, contact): + self.remove(contact) #for now, we only show online contacts + + def __eraseGroup(self, group): + """Erase all contacts in group + @param group: group to erase + @return: True if something as been erased""" + erased = False + indexes = self.__find_idx(group) + for idx in indexes: + while idx<self.GetCount()-1 and type(self.GetClientData(idx+1)) != Group: + erased = True + self.Delete(idx+1) + return erased + + + def __presentGroup(self, group): + """Make a nice presentation for the contact groups""" + html = u"""-- [%s] --""" % group + + return html + + def __presentItemDefault(self, contact): + """Make a basic presentation of string contacts in the list.""" + return contact + + def __presentItemJID(self, jid): + """Make a nice presentation of the contact in the list for JID contacts.""" + name = self.CM.getAttr(jid,'name') + nick = self.CM.getAttr(jid,'nick') + show = filter(lambda x:x[0]==self.CM.getAttr(jid,'show'), const_STATUS)[0] + #show[0]==shortcut + #show[1]==human readable + #show[2]==color (or None) + show_html = "<font color='%s'>[%s]</font>" % (show[2], show[1]) if show[2] else "" + status = self.CM.getAttr(jid,'status') or '' + avatar = self.CM.getAttr(jid,'avatar') or IMAGE_DIR+'/empty_avatar.png' + + html = """ + <table border='0'> + <td> + <img height='64' width='64' src='%s' /> + </td> + <td> + <b>%s</b> %s<br /> + <i>%s</i> + </td> + </table> + """ % (avatar, + escape(nick or name or jid.node or jid.short), + show_html, + escape(status)) + + return html + + def clear_contacts(self): + """Clear all the contact list""" + self.Clear() + + def add(self, contact, groups = None): + """add a contact to the list""" + debug (_("adding %s"),contact) + if not groups: + idx = self.Insert(self.__presentItem(contact), 0, contact) + else: + for group in groups: + indexes = self.__find_idx(group) + gp_idx = 0 + if not indexes: #this is a new group, we have to create it + gp_idx = self.Append(self.__presentGroup(group), Group(group)) + else: + gp_idx = indexes[0] + + self.Insert(self.__presentItem(contact), gp_idx+1, contact) + + + + def remove(self, contact): + """remove a contact from the list""" + debug (_("removing %s"), contact) + list_idx = self.__find_idx(contact) + list_idx.reverse() #as we make some deletions, we have to reverse the order + for i in list_idx: + self.Delete(i) + + def onSelected(self, event): + """Called when a contact is selected.""" + data = self.getSelection() + if data == None: #we have a group + first_visible = self.GetVisibleBegin() + group = self.GetClientData(self.GetSelection()) + erased = self.__eraseGroup(group) + if not erased: #the group was already erased, we can add again the contacts + contacts = self.CM.getContFromGroup(group) + contacts.sort() + id_insert = self.GetSelection()+1 + for contact in contacts: + self.Insert(self.__presentItem(contact), id_insert, contact) + self.SetSelection(wx.NOT_FOUND) + self.ScrollToLine(first_visible) + event.Skip(False) + else: + event.Skip() + + def onActivated(self, event): + """Called when a contact is clicked or activated with keyboard.""" + data = self.getSelection() + self.onActivatedCB(data) + event.Skip() + + def getSelection(self): + """Return the selected contact, or an empty string if there is not""" + if self.GetSelection() == wx.NOT_FOUND: + return None + data = self.GetClientData(self.GetSelection()) + if type(data) == Group: + return None + return data + + def registerActivatedCB(self, cb): + """Register a callback with manage contact activation.""" + self.onActivatedCB=cb +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/gateways.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + + + +import wx +import pdb +from xml.dom import minidom +from logging import debug, info, error +from tools.jid import JID +from quick_frontend.quick_gateways import QuickGatewaysManager + +class GatewaysManager(wx.Frame,QuickGatewaysManager): + + def __init__(self, host, gateways, title=_("Gateways manager"), server=None): + wx.Frame.__init__(self, None, title=title) + QuickGatewaysManager.__init__(self, host, gateways, server) + + if server: + self.SetTitle(title+" (%s)" % server) + + #Fonts + self.normal_font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL) + self.bold_font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD) + self.italic_font = wx.Font(8, wx.DEFAULT, wx.FONTSTYLE_ITALIC, wx.NORMAL) + self.button_font = wx.Font(6, wx.DEFAULT, wx.NORMAL, wx.BOLD) + + + self.modified = {} # dict of modified data (i.e. what we have to save) + self.ctl_list = {} # usefull to access ctrl, key = (name, category) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + warning = wx.TextCtrl(self, -1, value=self.WARNING_MSG, style = wx.TE_MULTILINE | + wx.TE_READONLY | + wx.TE_LEFT) + warning.SetFont(self.bold_font) + self.sizer.Add(warning, 0, wx.EXPAND) + warning.ShowPosition(0) + self.panel = wx.Panel(self) + self.panel.sizer = wx.FlexGridSizer(cols=5) + self.panel.SetSizer(self.panel.sizer) + self.panel.SetAutoLayout(True) + self.sizer.Add(self.panel, 1, flag=wx.EXPAND) + self.SetSizer(self.sizer) + self.SetAutoLayout(True) + + #events + self.Bind(wx.EVT_CLOSE, self.onClose, self) + + self.MakeModal() + self.panel.sizer.Add(wx.Window(self.panel, -1)) + title_name = wx.StaticText(self.panel, -1, "Name") + title_name.SetFont(self.bold_font) + title_type = wx.StaticText(self.panel, -1, "Type") + title_type.SetFont(self.bold_font) + self.panel.sizer.Add(title_name) + self.panel.sizer.Add(title_type) + self.panel.sizer.Add(wx.Window(self.panel, -1)) + self.panel.sizer.Add(wx.Window(self.panel, -1)) + + for gateway in gateways: + self.addGateway(gateway, gateways[gateway]) + + self.ext_server_panel = wx.Panel(self) + self.ext_server_panel.sizer = wx.BoxSizer(wx.HORIZONTAL) + self.ext_server_panel.SetSizer(self.ext_server_panel.sizer) + self.ext_server_panel.SetAutoLayout(True) + self.sizer.Add(self.ext_server_panel, 0, flag=wx.EXPAND) + + ext_server_label = wx.StaticText(self.ext_server_panel, -1, _("Use external XMPP server: ")) + ext_server_label.SetFont(wx.ITALIC_FONT) + self.ext_server_text = wx.TextCtrl(self.ext_server_panel, -1) + ext_server_button = wx.Button(self.ext_server_panel, -1, _("GO !")) + self.ext_server_panel.Bind(wx.EVT_BUTTON, self.browseExternalServer, ext_server_button) + + self.ext_server_panel.sizer.Add(ext_server_label) + self.ext_server_panel.sizer.Add(self.ext_server_text, 1, flag=wx.EXPAND) + self.ext_server_panel.sizer.Add(ext_server_button) + + #self.panel.sizer.Fit(self) + self.sizer.Fit(self) + + self.Show() + + def browseExternalServer(self, event): + """Open the gateway manager on given server""" + server = self.ext_server_text.GetValue() + debug(_("Opening gateways manager on [%s]") % server) + id = self.host.bridge.findGateways(server, self.host.profile) + self.host.current_action_ids.add(id) + self.host.current_action_ids_cb[id] = self.host.onGatewaysFound + self.MakeModal(False) + self.Destroy() + + + def addGateway(self, gateway, param): + + #First The icon + isz = (16,16) + im_icon = wx.StaticBitmap(self.panel, -1, wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, wx.ART_TOOLBAR, isz)) + + #Then the name + + label=wx.StaticText(self.panel, -1, param['name']) + label.SetFont(self.normal_font) + + #Then the transport type message + type_label_txt = self.getGatewayDesc(param['type']) + + type_label = wx.StaticText(self.panel, -1, type_label_txt) + type_label.SetFont(self.italic_font) + + #The buttons + def register_cb(event): + """Called when register button is clicked""" + gateway_jid = event.GetEventObject().gateway_jid + id = self.host.bridge.in_band_register(gateway_jid, self.host.profile) + self.host.current_action_ids.add(id) + self.MakeModal(False) + self.Destroy() + + def unregister_cb(event): + """Called when unregister button is clicked""" + gateway_jid = event.GetEventObject().gateway_jid + id = self.host.bridge.gatewayRegister("CANCEL",gateway_jid, None, self.host.profile) + self.host.current_action_ids.add(id) + self.MakeModal(False) + self.Destroy() + + reg_button = wx.Button(self.panel, -1, _("Register"), size=wx.Size(-1, 8)) + reg_button.SetFont(self.button_font) + reg_button.gateway_jid = JID(gateway) + self.panel.Bind(wx.EVT_BUTTON, register_cb, reg_button) + unreg_button = wx.Button(self.panel, -1, _("Unregister"), size=wx.Size(-1, 8)) + unreg_button.SetFont(self.button_font) + unreg_button.gateway_jid = JID(gateway) + self.panel.Bind(wx.EVT_BUTTON, unregister_cb, unreg_button) + + self.panel.sizer.Add(im_icon) + self.panel.sizer.Add(label) + self.panel.sizer.Add(type_label) + self.panel.sizer.Add(reg_button, 1, wx.EXPAND) + self.panel.sizer.Add(unreg_button, 1, wx.EXPAND) + + + def onClose(self, event): + """Close event""" + debug(_("close")) + self.MakeModal(False) + event.Skip() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/cards/.size Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,1 @@ +Biggest size: 74X136
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/cards/COPYING Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,1 @@ +The work in this directory is licensed under the Creative Commons BY-SA License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/; or, (b) send a letter to Creative Commons, 171 2nd Street, Suite 300, San Francisco, California, 94105, USA.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/cards/README Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,3 @@ +The Tarot cards pictures come from the deck found on wikicommons (the script used to cut them is available at frontend/wix/images/split_card.sh), and available at http://upload.wikimedia.org/wikipedia/commons/8/8d/Tarotcards.jpg under Creative Commons BY-SA. The authors shown on the site are Piast and Svick. The splitted cards are, according to the license, available under the same conditions. +Notice that the Creative Common BY-SA apply only to this directory +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/crystal/COPYING Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/crystal/COPYING.LESSER Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/crystal/README Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,3 @@ +These pictures come from Crystal clear, were made by Everaldo Coelho (www.everaldo.com) and YellowIcon (www.yellowicon.com). They are under LGPL V3 licence (see COPYING.LESSER). + +Found on http://commons.wikimedia.org/wiki/File:Crystal_Clear_app_browser.png and http://commons.wikimedia.org/wiki/File:Crystal_Clear_app_cache.png .
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/crystal/tray_icon.xpm Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,309 @@ +/* XPM */ +static char *tray_icon[] = { +/* columns rows colors chars-per-pixel */ +"128 128 175 2", +" c #03060F", +". c #030A1D", +"X c #030D27", +"o c #050F29", +"O c #0B1121", +"+ c #08122A", +"@ c #151A26", +"# c #171A28", +"$ c #1B1F2B", +"% c #1A223E", +"& c #20263A", +"* c #24293B", +"= c #061741", +"- c #061843", +"; c #091E56", +": c #091F59", +"> c #142244", +", c #1B2540", +"< c #09215D", +"1 c #202F5A", +"2 c #263257", +"3 c #2A3450", +"4 c #24335C", +"5 c #2A3658", +"6 c #2F3A5A", +"7 c #363B50", +"8 c #383D52", +"9 c #0B276D", +"0 c #0B286F", +"q c #192E63", +"w c #1E3265", +"e c #0C2970", +"r c #0E2E7D", +"t c #102F7E", +"y c #1B3474", +"u c #11317F", +"i c #19347B", +"p c #1E387A", +"a c #213876", +"s c #293F7C", +"d c #2F447E", +"f c #30437B", +"g c #36497D", +"h c #404867", +"j c #434C68", +"k c #4A526C", +"l c #404C74", +"z c #445075", +"x c #153481", +"c c #193783", +"v c #1C3A85", +"b c #203E87", +"n c #223E88", +"m c #25428A", +"M c #2A468D", +"N c #2D498E", +"B c #314B8E", +"V c #2F4990", +"C c #324D91", +"Z c #365093", +"A c #3B5595", +"S c #3E5897", +"D c #3E5698", +"F c #3F5898", +"G c #465787", +"H c #4C5980", +"J c #4C5C8A", +"K c #505C82", +"L c #445993", +"P c #4B5F97", +"I c #40579B", +"U c #445B9C", +"Y c #485F9D", +"T c #556086", +"R c #5A6284", +"E c #47609C", +"W c #4A629E", +"Q c #51639B", +"! c #5A6B9F", +"~ c #606E9E", +"^ c #6A779C", +"/ c #6E789D", +"( c #495EA0", +") c #4D63A1", +"_ c #5066A2", +"` c #556AA4", +"' c #596EA6", +"] c #5769A9", +"[ c #5B6EAA", +"{ c #5C72A7", +"} c #5E72AB", +"| c #606FA2", +" . c #6573A1", +".. c #6A77A5", +"X. c #6F7BA7", +"o. c #6275AB", +"O. c #6579AC", +"+. c #6A7DAE", +"@. c #727CA5", +"#. c #6574B1", +"$. c #6778B0", +"%. c #6C7DB2", +"&. c #6F7DB8", +"*. c #707FB6", +"=. c #717EB9", +"-. c #6F81B1", +";. c #7384B4", +":. c #7986B6", +">. c #7789B6", +",. c #7989B7", +"<. c #7583B9", +"1. c #7984BD", +"2. c #7C8CB9", +"3. c #7F90BA", +"4. c #7C87C0", +"5. c #7E89C0", +"6. c #818EBC", +"7. c #8594BD", +"8. c #8896BF", +"9. c #8898BF", +"0. c #828DC2", +"q. c #8691C4", +"w. c #8A95C5", +"e. c #8C9AC2", +"r. c #8D97C8", +"t. c #8E98C8", +"y. c #919EC4", +"u. c #939CCA", +"i. c #94A1C6", +"p. c #98A6C7", +"a. c #97A2CA", +"s. c #9AA5CB", +"d. c #9DAACA", +"f. c #9EA6D0", +"g. c #9FA8D1", +"h. c #A1ADCD", +"j. c #A5B1CF", +"k. c #A4ACD3", +"l. c #A7B1D1", +"z. c #ABB3D4", +"x. c #AEB8D3", +"c. c #AEB6D8", +"v. c #B3BCD6", +"b. c #B0B7D9", +"n. c #B5BBDA", +"m. c #B8BFDD", +"M. c #B7C0D8", +"N. c #BBC2DD", +"B. c #BFC8DD", +"V. c #BFC4E0", +"C. c #C0C7DF", +"Z. c #C2CADE", +"A. c #C1C6E1", +"S. c #C5CBE2", +"D. c #C9CEE5", +"F. c #CDD2E5", +"G. c #CFD3E8", +"H. c #D0D6E6", +"J. c #D2D8E7", +"K. c #D2D6E9", +"L. c #D5DAEA", +"P. c #DADDED", +"I. c #DDE1EE", +"U. c #DFE2F0", +"Y. c #E1E5EF", +"T. c #E3E5F1", +"R. c #E6E9F3", +"E. c #EAECF5", +"W. c #EEF0F6", +"Q. c #EFF1F8", +"!. c #F0F2F7", +"~. c #F3F5F9", +"^. c #F7F8FB", +"/. c #FDFDFE", +"(. c None", +/* pixels */ +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r r r r r r r r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r r r u r r u r r r r r r t r r r r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r r r r r r r A W W W W W W r u r r r t r r r r r r t r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r v W 9.p.Z.Z./././././././././././././.Y.S.j.8.{ A r t r r r t t r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r S ,.v.Y././././././././././././././././././././././././././.Z.i.{ v r t r u r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t r t n $.j.W././././././././././././././././././././././././././././././././././.H.>.N e r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r t r r t ` c./.^./././.^.^.^./.^./.^./././././././.^./././.^./.^././.^././././././.^./././.^.^.H.>.M r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t t M 7.P.^.^.^.^.^././.~.^.^.^.^.^.^./././.^././.^.^.^.^.~.^.^.^.~.~.^.^.^././.^.^.^.^.^.~.^.^.^.^.^.x.E r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r t t r M d.~.~.~.^.~.~.~.^.^.^.~.^.!.!.!.~.~.~.^.^./././.^.!.!.~.!.!.!.!.~.~.~.~.~.!.~.~.!.~.^.~.~.!.!.~.~.~.~.~.S.O.r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r M p.!.E.W.~.^.~.~.~.Q.W.~.W.W.W.W.W.W.!.W.!.^./.^.^.^./.W.W.W.!.!.!.!.E.!.!.E.W.W.W.W.!.!.~.!.W.W.W.W.~.W.W.W.W.W.H.E r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t r ,.I.E.E.E.E.~.~.~.~.~.Q.E.W.~.E.E.E.R.W.E.E.E.!.^.~.~.!.W.^.E.E.E.E.R.E.E.E.E.E.E.E.E.E.E.E.!.!.E.E.!.E.~.E.E.W.~.W.E.E.v.M r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r L F.R.R.E.R.R.E.!.~.~.~.~.Q.~.~.~.E.R.R.R.R.R.R.R.~.R.E.W.R.R.~.R.R.E.R.E.E.R.R.R.R.R.R.R.R.R.R.E.E.R.R.E.R.W.R.R.W.~.~.~.R.R.R.,.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r >.R.E.Y.R.Y.Y.Y.T.W.!.Q.Q.Q.Q.W.~.W.E.Y.Y.Y.R.Y.Y.W.R.Y.R.Y.Y.Y.R.^.Y.Y.Y.Y.Y.Y.Y.Y.R.Y.Y.R.Y.R.R.W.E.Y.Y.W.R.Y.Y.Y.R.W.E.E.~.E.Y.Y.B.Z r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r c m.R.Q.Q.R.U.U.U.U.T.W.W.Q.Q.E.Q.W.!.!.R.Y.Y.Y.Y.Y.I.W.Y.Y.Y.Y.Y.Y.Y.!.Y.Y.Y.Y.Y.I.I.Y.I.Y.I.Y.Y.R.E.E.Y.Y.!.!.Y.I.I.I.I.I.Y.I.W.W.W.T.I.J.{ r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r C J.E.E.E.E.E.T.P.P.I.E.E.W.Q.Q.Q.E.W.W.W.!.Y.Y.J.Y.Y.J.W.I.J.I.I.I.I.I.E.E.I.J.J.I.I.I.I.I.J.I.Y.Y.R.R.R.Y.Y.!.!.Y.I.I.I.I.Y.W.I.E.E.W.I.T.I.I.3.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r { J.R.E.R.E.E.E.E.U.P.U.E.E.E.E.E.Q.E.E.W.W.E.E.J.Y.J.J.Y.I.I.J.I.J.J.J.J.I.!.J.I.I.J.I.J.I.J.I.J.I.J.I.I.I.I.Y.R.E.E.L.P.L.L.W.E.I.R.T.I.R.E.I.I.I.9.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r ' I.R.R.R.R.R.R.E.R.R.K.T.E.E.E.E.E.E.E.E.E.W.E.E.!.J.J.J.!.J.J.J.J.J.H.J.J.J.~.J.J.J.J.J.J.J.J.J.J.J.J.J.Y.E.E.Y.Y.E.E.I.K.L.J.E.E.Y.R.E.R.R.R.R.Y.L.L.9.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r ` P.R.T.R.T.T.R.R.R.R.R.T.R.R.R.E.E.R.E.E.E.E.E.E.W.E.J.H.H.R.H.H.H.H.H.H.H.H.H.R.Y.H.H.H.H.H.H.H.H.H.H.H.J.Y.W.W.R.Y.W.W.I.K.K.T.E.E.R.R.R.R.E.R.R.R.R.L.G.7.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t C K.T.T.T.T.T.T.T.R.T.R.R.R.R.R.R.R.R.E.E.R.E.E.R.R.!.Y.H.Z.J.Y.Z.H.F.H.F.F.Z.H.H.J.R.H.F.F.H.H.Z.H.F.F.H.H.H.J.R.!.Y.W.R.R.P.K.T.R.R.R.R.R.R.^./.T.T.R.T.T.L.F.7.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r c K.U.U.U.U.T.U.T.T.T.T.T.T.T.R.R.T.R.R.R.R.R.R.R.R.I.H.J.H.Z.Y.H.Z.H.F.Z.F.Z.Z.H.Z.H.!.H.Z.Z.H.Z.Z.H.Z.Z.Z.Z.F.H.I.J.Y.R.E.R.R.R.R.R.R.R.R.T.~.R.~.T.T.T.U.T.I.J.S.! r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r x b.I.I.I.I.U.I.U.U.T.T.T.T.T.T.T.T.R.T.R.T.R.G.F.H.Z.H.Y.R.R.H.I.Z.Z.Z.Z.Z.Z.Z.Z.B.Z.Z.W.Z.Z.Z.Z.Z.Z.Z.Z.Z.Z.Z.B.H.J.I.J.I.Y.R.R.R.R.T.R.T.T.Q.R.Q.E.T.I.T.U.I.I.I.H.Z.C r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.0 r x x w.P.P.P.P.P.U.I.U.U.U.U.U.U.U.T.T.T.T.T.T.T.Y.L.B.B.B.I.R.R.R.I.I.Z.B.Z.B.Z.Z.B.B.B.B.B.Y.H.Z.Z.Z.Z.B.Z.B.Z.B.Z.B.Z.J.J.L.L.R.R.R.R.T.T.T.T.Q.E.T.^.T.U.U.U.I.U.U.P.U.D.f.x r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.x x r x [ K.P.P.I.P.I.P.P.P.U.P.U.U.U.U.T.R.Q.E.Q.E.E.~.~.Y.I.H.Z.I.I.R.I.R.Z.M.B.B.M.B.M.B.B.B.B.H.H.M.B.M.B.B.M.B.B.M.B.B.B.Z.H.G.J.Y.Y.Y.T.R.T.T.R.!.T.R.Q.T.I.I.I.U.P.P.P.P.P.A.o.r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.x x x ( b.P.L.L.L.P.P.P.P.U.P.P.P.I.U.R.T.U.U.T.T.T.T.Y.Y.R.Y.I.I.H.J.W.!.^.W.F.M.M.M.M.M.M.M.M.M.B.J.M.M.M.M.M.M.M.M.M.M.M.M.M.Z.G.J.J.T.T.T.T.T.T.~.T.I.^.U.U.I.U.P.P.P.P.P.P.P.L.x.n r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.0 x x v 0.L.L.L.L.L.K.P.P.P.P.P.P.P.I.I.U.U.U.U.P.T.T.Y.L.P.I.R.Y.R.F.B.W./././.!.J.B.v.M.M.M.x.M.M.M.E.M.M.M.x.M.M.M.x.M.M.M.M.M.M.S.F.J.T.T.T.T.T.~.T.U.R.Q.I.I.P.P.U.P.P.P.P.K.P.L.G.,.r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r x r c =.b.K.L.L.K.L.L.P.U.P.P.P.P.P.P.U.P.I.U.U.I.U.I.M.v.Z.Y.I.Z.M.x.B.^./././.E.Z.J.I.J.M.x.M.M.M.B.Y.Z.Z.x.M.M.x.x.M.x.M.M.M.I.I.I.I.T.T.T.U.I.~.Y.I.I.^.U.P.P.P.P.P.P.L.P.L.L.L.L.K.S.c r r r (.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.x r r #.4.K.K.G.K.K.K.R.T.P.K.P.K.P.L.P.P.P.I.P.U.I.I.B.x.x.x.F.x.x.x.x.M.E./.^.^.I.x.M.M.M.H.J.Z.M.Z.R.^.!.Y.Z.j.x.x.x.j.x.M.x.j.I.R.Y.I.T.T.T.U.I.F.Z.I.E.W.P.D.D.P.P.P.L.P.L.L.K.L.K.J.J.} r r r (.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r x r D 1.f.G.G.G.G.K.E.P.K.K.K.L.P.P.P.L.P.P.P.P.P.G.I.x.j.x.j.j.x.x.j.j.x.B.!.R.Y.M.M.h.j.M.h.x.M.J.^.^././.^.I.M.j.j.j.x.j.j.j.j.R.Y.Y.Y.Y.Y.U.F.E.B.S.I.!.D.P.P.A.L.L.P.L.K.L.K.K.K.K.K.J.m.c r r r (.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r x M =.1.b.G.D.G.P.R.G.K.K.K.K.K.P.K.P.L.P.P.P.P.F.l.k.j.j.j.j.j.j.j.j.j.j.j.J.M.j.d.j.j.j.j.j.j.j.Z.^././././.R.M.j.j.j.j.j.j.j.j.Y.Y.Y.Y.Y.Y.b.I.F.Z.Z.W.S.z.K.P.D.A.P.P.K.L.L.K.K.K.K.K.G.G.$.r r r r (.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r x r ] 1.1.D.D.D.K.E.G.G.G.G.G.K.K.K.P.K.L.L.L.P.P.D.h.h.h.h.h.d.h.h.h.d.h.h.h.H.d.h.h.h.d.h.h.h.h.h.F.~././././.W.M.h.h.h.h.h.h.d.h.T.T.T.T.T.U.F.Y.v.h.N.E.j.z.D.P.D.j.L.K.N.D.H.H.G.G.G.F.F.D.f.Z x x r (.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r x ( =.1.r.S.D.D.T.D.G.D.D.G.G.G.K.K.K.P.L.K.P.L.L.m.d.d.d.d.d.d.d.p.d.d.d.d.p.M.p.d.d.d.d.d.d.d.p.p.M.R./././.^.Y.I.M.d.d.d.d.d.d.x.T.T.T.U.U.R.F.N.v.l.L.B.C.K.L.K.l.d.S.L.N.h.G.G.G.G.F.F.D.D.c.=.x x r r (.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r v =.4.=.u.S.S.U.D.D.D.D.G.G.G.G.G.K.K.K.L.P.L.L.D.s.p.p.p.p.p.p.p.p.p.p.p.p.M.j.p.p.p.p.p.p.p.p.p.p.d.Z.R.~.~.I.M.p.M.J.M.p.p.p.s.p.h.x.x.N.A.^.I.I.I.Y.~.P.D.z.V.x.s.p.d.F.Z.v.l.S.G.G.F.D.D.D.b.1.I x r r (.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.r r r ) 1.1.1.f.A.T.G.A.D.D.D.D.G.D.G.G.G.K.K.K.L.K.P.S.i.p.i.p.i.i.p.i.p.i.p.p.p.j.p.p.p.p.p.i.p.i.p.p.p.p.d.x.Z.Y.M.p.p.i.p.M.H.d.i.p.i.u.K.T.T.~.T.I.I.P.~.Y.P.D.l.u.i.i.p.i.l.J.G.l.p.V.D.D.D.D.D.A.4.=.x r r r (.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.r r c =.4.=.1.f.G.U.A.A.D.A.D.G.D.D.G.G.G.K.K.K.K.l.h.d.i.i.i.i.i.i.e.p.i.9.p.i.p.B.i.9.i.p.p.9.p.i.i.i.i.i.y.p.i.Y.i.i.i.i.e.i.p.F.B.i.i.M.Y.T.E.R.U.I.P.T.W.P.P.D.n.x.i.e.i.y.y.z.x.i.i.i.i.D.D.D.S.S.4.4.] x r r (.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.r r r U 4.=.4.1.c.Q.D.V.D.A.D.D.D.D.D.G.G.G.G.G.K.n.y.e.9.y.9.e.9.y.i.9.9.p.i.9.9.9.M.p.i.9.9.p.9.9.e.i.e.e.e.9.e.e.Y.e.9.e.e.e.e.e.9.x.P.M.T.T.R.^.R.E.E.T.^.P.P.P.P.K.K.z.e.N.Z.N.n.d.i.t.e.d.D.S.S.A.A.4.4.4.v r r r (.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.r r r U =.4.4.=.n.E.V.V.A.A.D.D.A.D.D.D.G.K.G.G.K.y.8.8.9.9.9.9.9.9.9.9.9.9.9.9.9.9.J.3.3.9.9.9.9.9.9.9.9.9.9.9.9.9.B.j.9.9.e.9.9.9.9.9.y.R.!.T.!.!.~.~.^.^.~.U.P.P.P.K.K.K.D.J.G.G.G.F.D.D.D.D.D.A.A.S.A.5.4.4.( x x r (.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.r r r x U 4.4.=.4.m.L.V.V.A.A.A.A.D.D.D.n.u.u.u.K.G.8.8.8.9.9.7.8.8.9.7.9.3.9.9.9.9.3.J.3.9.9.9.3.9.9.9.3.9.9.9.9.9.9.Z.j.9.9.9.9.7.9.9.7.e.T.E.^.^.^././././.!.T.P.L.L.P.L.K.G.G.G.F.F.F.D.D.A.D.D.A.S.A.A.5.4.4.4.c x r r (.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.r r r r D 4.4.=.4.U.V.V.V.V.V.V.M.e.9.3.5.3.q.3.F.H.7.3.7.7.7.7.7.7.7.7.3.3.3.3.3.9.3.B.3.3.3.3.3.3.3.3.3.7.7.7.7.7.7.M.M.7.7.7.7.7.7.7.7.B.T.R.^./././././././.E.T.L.L.K.K.K.K.K.G.D.G.D.D.D.S.S.A.A.A.A.A.4.4.4.1.( x r r (.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.r r r r r x } 4.4.4.Q.m.m.V.V.V.M.3.3.3.3.3.3.3.3.N.G.3.3.3.3.3.3.3.3.3.3.3.3.3.3.3.3.3.M.3.3.3.3.3.3.3.3.3.3.3.3.3.,.3.p.B.3.3.3.3.3.3.7.j.R.R.^.~./././././././.~.T.L.L.L.K.K.H.G.G.G.D.D.D.S.S.A.M.V.A.V.c.0.4.4.1.#.x r r r (.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.r r r r r r b -.4.3.U.m.m.V.V.V.x.>.3.>.2.>.3.>.3.i.v.>.3.>.,.,.3.,.,.2.2.,.3.>.>.>.3.>.p.3.3.3.3.3.3.3.>.3.>.3.>.3.,.,.p.Z.2.,.,.2.>.,.e.R.T.^.E.~./././././././.^.T.P.P.K.G.G.G.G.D.D.D.D.D.S.S.A.k.l.V.V.k.0.4.4.4.1.Z x r r (.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.r r r r r r x Z ;.3.V.n.m.m.V.M.d.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.-.3.i.p.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.p.H.>.>.,.>.>.7.Y.R.!.E.R.~./././././././.~.T.L.K.K.K.G.F.F.D.D.D.D.S.S.A.A.c.2.n.N.f.0.0.4.4.=.[ x r r (.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.r r r r r r r r r x i.g.g.b.V.M.M.u.<.;.;.;.;.>.>.l.N.F.D.p.>.>.-.;.-.>.>.-.>.-.-.-.-.>.p.>.-.>.>.-.>.-.-.-.-.>.-.-.-.>.-.>.I.>.;.;.;.1.L.E.R.~.R.R.Q.^././././././.Q.U.K.K.K.G.G.G.D.D.D.D.A.A.A.V.V.V.a.u.N.r.0.0.4.1.1.1.c r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.r r r r r r r r r r 5.[ r.n.n.M.V.i.-.-.-.e.c.x.3.i.z.V.F.G.V.e.>.-.-.-.-.-.-.-.-.-.-.-.j.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.I.-.-.-.-.z.E.R.Q.R.T.T.E.~././././.^.^.~.P.L.K.K.G.G.G.D.D.S.D.A.D.A.V.V.V.V.0.z.0.0.0.4.4.1.1.A r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.r r r r r r r r r r e.) b g.M.n.M.M.r.-.>.M.V.N.-.-.-.-.l.F.G.H.J.B.x.-.-.-.-.-.-.-.-.-.9.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.J.+.+.-.+.z.R.~.E.R.R.T.T.!./.~.^.~.~.R.R.Q.K.K.K.G.F.F.D.D.D.S.S.A.A.V.V.m.m.5.u.q.0.0.4.4.1.1.] r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.r r r r r r r r r r 5.E x ) f.n.b.V.M.z.M.V.V.d.-.O.O.O.-.e.p.V.H.H.F.-.-.M.>.-.-.-.-.-.9.-.` -.-.-.-.-.! -.-.-.-.-.{ ^ -.-.^.+.+.+.O.x.W.^.R.R.R.R.R.^.R.R.R.R.T.P.P.R.~.K.G.G.F.D.D.D.S.S.V.V.V.V.V.m.m.k.} q.q.0.5.1.1.1.1.r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r r r r r q.>.r c O.3.M.M.M.V.M.V.V.n.O.e.>.O.O.O.-.F.G.G.N.O.n.L.i.O.O.O.O.O.e.-.{ -.{ { { -.{ -.{ { -.{ -.-.-.` I.-.-.-.Q d.!.E.R.R.R.R.~.Q.U.U.U.P.P.L.L.K.R.~.K.D.G.D.D.D.D.A.V.V.V.V.m.m.m.k.Z V =.0.5.4.4.4.1.V x x x (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r r r r r >.9.r t F { -.e.b.b.k.V.V.V.n.S.V.>.} } -.i.x.2.} { 7.p.-.{ } { { { { O.{ ' -.{ -.{ { ! -.{ { { ' ` -.-.Z.-._ -._ j.E.E.E.R.R.R.^.T.U.P.U.P.P.P.K.K.G.P.Q.G.D.D.S.A.A.A.V.V.V.m.m.m.n.t.4.) ] 0.5.4.4.1.1.Z x 0 x (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r x r r r r r r r E j.r r c ' { { { { { -.e.B.V.V.z.{ { { { { { { { { { ' ' { { { { { { { { { { { { { { { { { { { { -._ -.P.E -.E -.M.W.E.R.R.R.^.E.U.I.P.P.P.P.K.K.K.K.G.P.~.G.D.S.A.A.V.V.V.m.m.m.n.g.r.r.4.m 4.4.4.4.=.&.I x x x (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r r r r r M Z.r r r N ' { ' ' ' ` ' V.V.V.-.{ { ' { +.' ' ' { { { ' { { { ' ! { { { { ` { { ' ' ' ' ' ' ! ' ' ` _ J.-.E -.E 3.W.E.E.R.Q.~.T.T.I.I.P.P.L.P.L.G.G.G.G.U.E.S.S.D.A.V.V.m.m.m.m.b.t.r.r.q.M c #.4.4.( x x x 0 x (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r r r r r r /.t t r r S ` ` ` ` ` ` N.V.V.z.' ` _ -.V.V.s.;.O.] _ ] ` ` ` ! ` ` ` ` ` ` ` ` ` ` ` ` _ ! ' ` ` ` ` J.>.` ` ! -.W.E.R.E./.T.T.U.P.I.P.P.K.L.G.K.G.G.D.D.U.E.A.V.V.V.V.V.m.m.n.f.t.r.r.q.&.x x m c x x r r r r (.(.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r r Z.W r r r x W ` _ _ _ _ d.N.V.V.x.` O.B.D.F.F.G.G.K.F.<._ _ _ ` W ` ` W _ _ W ` W ` W ! _ _ W ` ` W _ B.>._ _ _ _ 9.E.E.^.Q.T.T.U.U.P.P.P.K.L.K.G.G.D.D.S.S.U.U.V.V.V.m.n.n.n.g.u.t.r.r.q.q.A x [ } I x r r r r x (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r t 7.9.r r r r p E W W ) W 5.N.V.V.V.c.x.S.D.F.F.G.G.K.L.L.i.W W W W W W W W _ _ W W W W W E _ W _ ` ' ` M.>.W _ W W _ 3.!.^.T.T.U.U.U.I.L.P.L.L.G.G.G.D.D.S.S.A.E.L.V.m.m.m.n.k.f.u.t.r.r.r.q.4.&.4.4.} x r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r r A J.r r r r r m W W E E E ' p.V.V.V.V.S.S.S.F.G.G.G.K.L.B.E E W W >.E W W E W W W W E E E E E E -.-.-.M.H.E E W E E ` Y.E.R.U.U.U.P.P.P.L.K.G.G.G.D.D.D.S.S.A.V.Q.D.m.m.n.k.f.u.t.r.r.r.q.q.5.5.4.4.] x r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r x r r r r r r r r I.N r r r r r m E E E U E U O.k.V.V.S.S.D.D.F.G.G.K.L.B.e.i.;.' e.U E E E E F E E E E E E E W -.-.-.M.p.S E E E { B.N.g.U.U.U.U.U.P.L.L.J.G.G.G.D.S.S.A.A.V.V.V.E.V.n.k.f.f.f.t.t.r.r.q.q.0.0.4.4.C r r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r r r p.9.r r r r r r b A S F U F F 2.V.V.A.A.D.D.F.G.G.G.K.L.I.L.I.L.g.F F F F F F S S F F S I F F S F ] U.3.S S ` -.5.R.-.-.g.U.U.I.I.L.B.l.z.G.G.D.D.D.S.A.A.V.V.V.S.R.c.f.f.u.u.u.t.r.r.r.q.0.5.4.=.x r r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r t r r r A I.r r r r r r u i A S A A S } V.V.V.A.D.A.F.F.G.K.K.L.L.L.I.I.U.{ S S A F F A S S A S A S ) [ ) i.B.3._ { { -.U.5.;.-.-.d.p.;.' E ` D } 1.A.D.D.S.S.A.A.V.m.m.m.P.A.f.f.f.u.u.u.r.r.q.q.0.0.4.) r r r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r t r r r r r u U.N r r r r r r r r C A A _ l.m.V.V.A.A.A.D.D.D.G.G.J.J.J.I.I.E.L.F A A A A A Z A A A F } } ] { e.M.9.` -.-.M.N.-.-.$.' A A Z A A A A Z A A #.S.S.A.A.A.V.V.n.c.k.R.k.f.u.f.u.t.r.r.r.0.0.5.&.v r r r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r -.h.r r r r r r r r r b Z q.M.N.V.V.V.A.S.D.D.D.G.G.K.J.L.L.I.U.U.} Z Z Z Z Z Z Z Z Z E _ Z Z p.Z d.-.S o.>.U.-.#.-.$.{ Z Z Z Z Z Z ) _ W Z =.S.S.A.V.V.m.n.c.k.k.b.R.f.a.u.u.r.r.q.q.q.0.[ x r r r x r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r b U.b r r r r r r r r t c s.n.n.n.V.V.A.V.D.D.D.D.G.K.J.L.L.U.U.I.l.' C C Z C V C C C C C C e.W C p.-.[ } L.;.` } } _ Z C C ) ] U Z _ $.A C &.A.A.V.V.m.n.c.k.k.k.f.G.A.u.u.u.r.r.r.q.0.( x r r r r r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r r 7.9.r r r r r r r r r r w.c.n.m.V.V.V.V.A.A.D.D.D.G.K.K.L.L.U.U.U.I.M.>.-.E N N N N N N -.>.C A d.;.-.j.V.N V I V N N N N Z _ ' D C Z A O.0.V.V.V.n.c.c.k.k.k.f.f.f.E.f.u.u.r.r.q.5.D x x r r r r r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r t r r r r r r r r r b I.b r r r r r r r r b 3.l.z.b.m.V.V.V.V.A.D.D.D.D.G.G.K.U.U.L.U.I.I.U.T.U.i.Z M M M E L.C -.3.J.V.p.Q.E M N _ S M M M V Z Z C M M M C } #.m.m.n.b.c.c.k.k.k.k.f.f.n.L.u.u.r.r.q.&.x r r r r r r r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r u r r r r 9.j.r r r r r r r r M k.k.c.c.b.m.V.V.V.A.S.S.D.D.G.G.G.U.U.L.P.I.I.I.T.T.T.P.-.m M B.;.;.v.I.~.W.~.H.>.Z U ' ` n n V ] ] ] [ A m m m M M #.M.b.c.c.c.k.k.k.g.g.f.u.U.b.t.r.q.r.n x x x 0 x x x x x x x 0 (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r r r v U.N r r r r r r r t u.k.k.k.c.c.b.m.m.A.A.A.D.D.D.G.G.P.R.L.P.P.P.P.U.P.T.T.T.d.3.B.{ v.E././././.W.B.+.] _ ] V n D ] ] ] ] ] Z I D m x x f.c.c.c.c.k.k.g.g.f.f.u.b.R.t.r.r.) x x 0 x x x x 0 0 x x 0 x (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r b r r r r r r r r r -.B.r r r r r r r t V k.c.k.c.c.b.n.m.m.V.A.A.A.D.D.G.P.R.K.K.L.L.I.P.U.U.U.U.M.R.A 5.I././././././.R.k.o._ _ V v ( ] ] ] ] ] ( ) ) ( Z x ) b.c.k.c.k.g.g.f.f.f.u.u.L.c.r.r.&.x x x x x x x x x 0 x x x (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r m r r r r r r r r r r B.-.r r r r t r t t u.k.c.k.c.b.b.m.m.m.V.V.A.D.D.D.P.R.G.K.K.L.L.L.P.P.U.U.Q.3.N h.E././././././.~.N.&.( ( I v ( ( ) ) ( ( ) ( ( ) ( A x g.k.c.k.k.k.g.f.a.u.f.u.k.U.r.q.q.x x 0 x x x x 0 x x x x x (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r r M r r r r r r r r r r M U.v r r r r r r r ] k.z.c.c.b.b.n.m.m.m.V.V.A.A.D.P.T.G.G.K.K.K.L.L.L.P.R.B.x M d.W.^./././././.~.N.%.) ( ( v I I I ( ( ( ( ( ( ( ( ( n 5.c.k.k.k.g.g.g.f.a.a.u.t.U.g.q.r.x x x x x x x x r r r r r (.(.(.", +"(.(.(.(.(.(.(.u r r r r r r r C r r r r r r r r r r r >.M.r r r r r r r n k.k.z.k.b.b.b.n.m.m.V.V.V.A.A.L.T.D.G.G.K.K.K.L.L.P.^.S v N p.U././././././.W.n.[ U U I ( ( ( ( ( I ( ( ( ( ( ( ( ( 4.c.c.k.k.g.g.g.u.a.u.u.u.b.L.q.q.c x x x v x x x r r r r r (.(.(.", +"(.(.(.(.(.(.(.r r r r r r r u C c r r r r r r r r r r r B.>.r r r r r r r 0.k.c.k.c.b.b.n.n.m.m.V.V.A.A.K.T.D.D.D.G.K.K.L.P.R.p.C C A 3.H././././././.I.u.) I U I I ( I I ( I ( I ( ( ( ) ( ] r.k.k.g.g.g.f.f.u.a.u.u.t.r.E.t.5.n x x x n n n x r r r r r (.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r C b r r r r r r r r r r r b !.A r r r r r x V u.k.k.c.c.b.b.n.m.m.V.V.A.A.K.T.D.G.K.K.L.L.L.I.R.S C C C W M.^.Q././.^.T.n.1.) ( ( ( I I I ( I ( ( ( ( I ( ( [ k.c.c.k.g.g.f.f.f.u.u.u.t.r.r.V.n.q.n x x x n =.#.x r r r r (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r C M r r r r r r r r r r r r W R.b r r r r r r c 1.k.c.c.c.b.b.n.m.m.m.V.V.K.Q.G.G.G.J.K.K.K.^.O.C B C B L ~.h.M.R.I.Z.z.5.$.} } } _ ( I I I ( ( I I ( ( ( ( #.z.z.g.k.g.g.f.f.u.u.u.u.r.r.w.a.I.5.n r r ( =.1.[ r r r r r (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r C C r r r r r r r r r r r r r 3.B.r r r r r r r r [ k.k.c.b.b.b.n.n.V.V.D.P.Q.G.D.G.G.G.K.R.~.B Z C C C B.p.S -.B.i.7.-.#.} } } } } } ] ( I I I ( ( I ( I ( #.k.z.k.k.g.g.f.f.u.u.u.u.r.r.w.q.U.} x r &.r.r.1.[ r r r r r (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r N S u u r r r r r r r r r r r r M.k.t t r r r r r r U k.c.c.c.c.n.V.V.A.S.G.Q.D.D.G.D.G.L.^.A.C B C C -.I.C C B F.2.} } } } } } } } #.} #.] ( I I I ( ( ( I } k.k.g.g.g.g.u.a.u.u.r.r.r.q.w.q.M.' r V 4.u.w.1.I r r r r r (.(.(.(.", +"(.(.(.(.(.(.(.(.r r r r r r r m S b r r r r r r r r r r r r r b J.O.r r r r r r x Z c.k.c.c.b.V.V.V.A.A.S.Q.S.D.D.D.D.^.R.&.C C C S !.{ C C B F.%.} } o.} #.} } } } } } [ [ ) I I I I ( I ( f.k.g.g.f.f.f.f.u.u.r.r.r.r.5.#.) i.x c 0.u.0.1.n r r r r r (.(.(.(.", +"(.(.(.(.(.(.(.(.(.u u r r r r b S C r r r r u r r r r u r r r r v W.+.r r r r r r V k.k.k.n.m.V.m.V.A.A.A.Q.S.S.S.D.P.^.M.B C C C B.p.B C C C F.} } } } } } } #.} } #.} } #.] ] ( I I ( ( I &.g.g.g.f.u.u.u.u.t.t.r.r.q.5.c x F.r n q.u.0.=.x r r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.r r r r r r c S S u r r r r r r r r r r r r r r A W.' r r r r x A k.k.n.n.m.m.V.V.V.V.A./.S.S.S.D.^.L.-.C C C -.I.C C C C C F.} } } } } } } } [ #.[ } } } } #.] ( I I I ( I u.g.g.f.a.f.u.t.t.r.r.q.r.5.x x V.x #.u.u.4.] r r r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.u r r r r r u S S m r r r r r r r r r r r r r r r A Y.A r r r r ] k.b.n.m.n.m.m.m.V.V.V./.A.A.Z.R.I.e.A B C S !.{ B C V C V P.) } } } } } } } } } #.} } } } } [ ] ( I I I I q.f.f.u.f.u.u.t.t.r.r.q.q.5.x x t.] 6.u.u.4.C r r r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.r r r r r r r A S A r r r r r r r r r r r r r r r r { !.A r r x ] c.b.b.b.n.m.m.m.m.m.V.~.A.A.F.R.S C C B B B.p.B C C Z V Z /.( } #.} } } #.} } } } } } } } [ #.#.] ( ( I U <.f.a.u.u.u.u.t.t.q.q.0.5.4.x x 4.] #.u.r.$.r r r r r r (.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.r r r r r r N S S m r r r r r u r r r r r r r r t r -.W.E r r O.c.c.c.n.b.n.m.m.m.m.V.E.V.V.Q.;.C C B B ;.I.C C C C C C C R.A } } } } } } } } } } } } } } } } } } ' U U I #.f.a.a.u.u.r.r.r.w.q.5.4.A r r { s.A q.w.U r r r r r (.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.r r r r r r b S S Z r r r r r r r r r r r r r r r r r ' W.-.m -.k.c.c.b.c.n.b.n.n.m.m.Q.G.L.D.C C C Z W R.W C C Z Z C C B F.C Z } } } } #.} #.} } } #.} } [ } ` A ) _ U ( ] u.a.u.u.r.r.r.q.q.q.q.E r r r W h.L Y _ M r r r r r (.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.u t r r r r u S S S b r r r r r r r r u r r r r r r r r A W.3.7.z.c.c.c.c.n.n.V.V.A.S.Q.K.^.) C C Y ` G.e.C Z Z Z Z Z C W H.Z Z D } } } } } } } } } ] } } ) Q S C B Z C D I 5.a.u.r.r.r.r.q.q.5.5.v r r r W d.D I u r r r r r r (.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.t r r r r r Z S S Z u r r r r r r r r r r r r t r r r u ' W.F.l.l.c.k.c.m.V.S.F.G.G.Q.~.x.W A A [ l.G.A A Z Z A Z Z C -.H.Z Z Z [ #.} } } } } #.} [ ( ) A C C C C V V x c m [ t.r.r.r.q.w.w.5.A r r r r -.h.L C t r r r r r (.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.r r r r r r m S S S m r r r r r r r r r r r t t r r r m C W U.U.k.k.c.m.V.G.L.P.U.I.W.~.e.%.} _ 3.E._ A A A A A A S Z -.j.A Z A ) [ #.o.} #.} } } } ) C C B Z B B C C b r x M r.r.r.q.q.5.5.Z x r r r m -.d.F m t r r r t r (.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.r r r r u u S S S S i r r r r r r r r r r r r r t u C N Z r.R.Q.c.n.V.F.P.R.E.Q.W./.I.x.h.8.>.Y.i.F S F S S S S S S 9.h.A A A F #.o.#.O.} #.} #._ Z C C C C B Z C C N r x M r.q.q.q.q.4.m x r r r u S 9.h.S u r t r r r (.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.r r r t r r C S S S Z u r r r r r r r r r r t r r m N C C q.k.L./.A.F.P.E.~.^.^././.W.G.N.z.Z.G.$.) F I F F A S S S h.h.F F S S ` #.#.O.#.#.] ) Z C C C B C C B C C C b x V q.q.5.q.4.c r r r r r Z S d.-.Z r r r r r r (.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r b S S S S M r u r r r r r r r r r r r N C Z E a.g.k.G.^.R.E.~.^././././.^.E.L.S.^.7.[ E U U E E E E E S h.3.U L I I D Y ) _ F A A A A A Z Z Z C C C C C C N x v ) Z M m x r r x r r N S S H.` m r r r r r (.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.r r t r r r A S S S S b r r r r r r r r r r r c V C Z %.f.f.k.c.K.^.^.^././././././.~.Y.^.Z.;.' ] Y Y E U E E U U Z.;.U U U U U F U F F F S S A A Z Z Z Z Z C V Z C N x x 0 x x x x x r r r v F F S E.A r r r r r r (.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r m S S S S A x r r r r r r r r r r b Z Z Z 1.s.f.k.n.D.E.^./././././././.~.^.R.l.,.*.%.O.Y E E ) E W W J.>.E E E U U U ' $.$.F F S S A D A Z Z Z Z Z V C C x x x r x 0 0 x r r i A S S } M.m r r r r r (.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r u A S S S S A c r r r r r r r r r N Z Z A q.f.f.k.n.D.R.^././././././././.^.J.d.w.;.%.%.o._ ) ) E W ) I.W W W E E Y U $.$.} U F F L F S S D Z Z A Z Z C Z c x x r x x x x r u A S S S x.>.u r r r r r (.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r m S S S F F A x r r r r r r x x V Z A A 2.s.u.k.n.D.R.~././././././././.E.G.s.;.;.&.%.%.-.} ) ) ) ) /.) W W W W Y ) O.%.' U F ) U I S S D D A A Z Z Z Z n x x r 0 x 0 x u Z S S S W R.m r r r r r (.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.0 r r r r r Z S S A S S A c r r r x r x x Z A F F [ t.a.f.z.D.U.T.~././././././.^.Y.Z.h.-.] o.*.*.&.-.%.' ) { I.Q _ Q ) W ) W ) o.) ) E E U U U S S D A A A A Z Z c 0 x x x x 0 x Z S S S S d.d.u r r r r 0 (.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r b S S F F S S A x r t x x x x A D F F D r.a.s.k.V.L.J.~././././././.^.J.v.e.-.` ' %.*.<.&.&.o.` 2.L._ ` _ _ _ _ _ ) W ) E W W E E U U U F F S A A Z Z V x x x x x x v S S S S ` R.N r r r r r (.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.< u r r r r C S S S S S F D v x x c x c F F F ( ] u.u.u.k.c.m.S.^.^.~.~././.^./.~.N.3.O.` ' ' O.%.%.$.} ` 6.v.` ' _ ` _ _ _ Q _ ) ) W W E W E Y U U F F F F A A V x x x x x V I S S S S J.-.r r r r r : (.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.e r r r r u A S S S S D F F M x c c v F U U ' r.u.u.u.u.k.a.A.~.J.Y.E./.E.I.R.B.W.I.8.' ] ' &.<.} ' ` ' x.x.' ' ` ' ` ` ` ' _ ) _ ) ) W W W E Y U U U F F S A V x x x c I Z I S S S h.M.u r r r r 0 (.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r m A S S S U F E U A v v v A U Y ) r.u.r.u.u.u.:.!.v.v.Z.!.E.Z.Z.E.y.;.h.E.L.8.' ;.;.' { ' o.v.y.' ' ] ' ' ] ' ` ` ` _ _ _ _ ) ) W W U E U U F F S V c c V I I I I S S 7.I.N r r r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.+ r r r r r B S S F U U U E W W N m A E Y W 5.r.r.r.t.u.v.Y.6.p.D.!.h.l.p./.;.O.[ } p.E.Y.s.%.o.o.} | L.8.' | o.' ' ' ' ' ` ` ` ` _ _ _ ) ) W W W U E U U F V n I U P I S L S 9.I.S r r r r r - (.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.9 r r r r u Z L I E U E E W W _ U Z Y W W ;.w.r.r.t.u.~.2.O.e.^.y.3.>.,.R.:.o.o.O.o.o.y.P.E.b.;.O.o.E.o.o.o.} ' [ | ' ' ' { ` ' _ _ ` _ Q W W W W E U Y U ( ( ( ( U U I E M.G.I x r r r r < (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.9 r r r r c F U U E W W W _ _ Q ' { W ) o.r.r.r.r.Z.Z.' O.^.y.O.$.O. .F.y.o.o.O.O.O.O.O.:.N.~.F.h.E.O.O.o.o.o.o.o.o.o.' { ' ' ' ` ` _ _ _ _ W W W E _ O.] ( ( ( E U ;.R.d.A c r r r r 9 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.e r r t t b U E W W W _ _ _ ` ` -.-.} ` r.r.r.u.^.*.' L.z.o.o.O.} O.x.b.O.+.O.O.O.O.O.+.O.O.h.P.^.Z.h.+.O.O.o.o.o.o.[ { ' ' ' ' ` ' ` ` _ Q ) { +.%.O._ W W W >.J.H...F n r r r r e (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.. r t x x c N E W W Q _ _ ` ' ` O.;.;.;.1.r.r.N.S.o.Z.F.o.o.O.O.O.O.r.P.O.O.O.+.%.%.+.O.+.+.%.l.B.h.Z.^.P.v.8.O.O.o.o.o.o.o.' ' ' ' ` ` ` { -.-.-.-.{ ! O.d.R.J.>.E U n r r r r r . (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.X t x x c v N W _ ` _ ` ` ' { { ;.>.,.,.u.s.Q.u.z.Y.+.O.$.O.+.O.O.;.^.+.+.X.*.+.*.X.*.+.*.+.Z.d.+.O.+.8.z.H.^.T.Z.v.8.6.o.' { { { { %.;.>.;.;.p.j.n.R.I.d.O.W W E m x x r r e o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.o t c c v m Z W ` ` ` ' ' { { %.,.,.,.2.N.T.A.^.q.O.O.O.O.%.O.+.%.W.2.-.*.*.*.*.*.*.-.*.*.R.6.*.;.%.%.%.O.O.2.p.n.F.P.^./.P.L.L.I.I.I.I.^.W.I.x.e.} ` ` _ W W N c x x x r o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.o i v b n m Z ` ' ` ' ' ' o.o.,.,.2.2.L.P.^.s.7.6.:.*.O.X.*.*.*.Z.h.;.;.=.*.:.X.-.;.;.2.~.;.X.;.X.*.*.X.-.+.O.O.O.;.;.2.6.s.d.d.h.9.,.,.;.;.[ ' { ` ` _ _ C v v c x t o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.+ c m m m m A ` ] { o.o.o.o.%.3.6.6.Y.^.s.7.7.8.8.8.9.7.6.:.:.l.H.;.;.=.@.:.:.;.:.:.h.J.;.;.;.:.*.*.*.;.,.2.7.8.7.7.7.7.6.7.6.2.2.2.,.,.O.{ { ` ` ` _ C n n v v x o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.X a n M M V A ` [ [ o.O.O.O.>.6.6.y.9.8.8.8.8.e.y.e.y.8.9.y.h.!.8.8.8.8.8.6.6.6.2.Z.N.8.6.7.8.8.y.y.e.e.w.9.8.8.8.7.7.7.7.2.2.2.2.2.-. .{ } { { ` C M n n b p . (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(. a M N V C Z ` o. .O.O.$.+.2.7.7.7.8.8.8.e.e.e.e.y.y.y.y.y.~.h.u.y.i.i.i.i.i.i.E.z.u.y.y.y.y.y.9.y.y.e.e.9.e.8.8.7.7.7.7.7.6.2.:.o.o.{ } { W C M M m n y (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.w C C C A A ( o.$.O.%.+.-.7.7.8.8.w.e.e.e.y.y.y.y.y.s.y.L.Z.y.y.h.i.p.i.s.h.~.p.p.u.i.y.y.p.y.y.y.y.y.e.e.w.e.8.8.7.7.7.6.,.O.O.o.o.o.E V V N M M w (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.> N C A A A P o.+.+.-.-.;.7.8.e.e.e.y.y.y.y.y.y.y.y.i.m.E.h.s.y.s.s.s.s.S.J.p.u.p.s.s.y.y.s.y.y.y.y.y.e.e.e.8.q.8.7.7.6.+.+.$.O.] I Z Z C V m > (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.O f A A I I U ] +.*.;.;.;.7.e.8.y.y.y.y.y.u.y.s.p.d.p.^.l.h.s.s.s.s.h.E.c.y.h.h.y.p.i.s.u.a.i.y.y.y.y.e.e.9.w.8.8.2.%.%.%.O.` D A Z Z C s . (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.2 A I U U P W o.;.;.:.:.7.y.y.y.u.y.p.s.s.s.p.s.s.J.P.v.x.h.s.s.x.~.s.h.h.y.h.s.s.d.p.u.p.i.y.y.y.y.e.e.e.e.,.;.;.+.o.P I L A D B 4 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.@ g Y Y Y _ _ ] $.:.:.2.6.y.y.i.i.s.p.d.s.s.j.Z.~././.^.^.~.Y.~.T.v.z.d.d.d.d.s.s.s.s.s.p.p.y.y.m.x.9.9.6.;.*.O._ Y Y U I A f O (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.% L W _ _ ` ` +.P.N.9.7.y.i.s.s.s.s.l.D.E.E.E.~.^.E.W.W.!.^.E.E.E.E.L.F.Z.x.h.s.s.s.y.p.l.L.E.E.E.J.v.:.' Q ) Y Y Y L % (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.5 P ` ` ] ' %.s.Z.m.d.8.y.s.h.Z.I.I.Y.I.P.I./.E.I.Y.^.R.Y.I.I.I.I.I.I.P.P.Z.x.s.h.S.P.P.L.H.H.C.y.$.` _ _ ) L 2 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.6 Q [ | | o.O.6.s.M.x.M.S.F.F.H.H.H.H.H.P.^.^.^.Y.H.H.H.H.H.H.H.H.H.H.F.H.D.H.D.Z.C.v.p.6.' | ' ` ` P 5 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.3 J o.O.$.O.@.X.4.8.h.c.v.N.C.Z.Z.Z.Z.Z.H.S.S.S.S.S.S.S.S.Z.Z.Z.A.A.N.C.v.s.8.:.O.o.o. .} { G 3 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.# l | %.X.=.@.=.=.:.6.8.9.h.h.c.c.m.m.N.N.N.m.m.N.M.n.v.z.k.s.t.6.@.@.@.*.+.$.$.O.! g @ (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.* K / 1.=.=.:.:.:.6.6.6.6.6.w.6.8.8.8.8.8.8.6.6.6.6.:.:.:.1.1.:.@.*.+.~ H & (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.$ j R / 6.6.6.6.6.6.6.6.8.8.8.7.6.8.6.8.6.6.6.6.4.:.:.1.X.R h $ (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.8 k R @.@.6.8.e.w.8.r.8.y.8.8.8.6./ / R k 8 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(." +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/crystal/tray_icon_warning.xpm Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,390 @@ +/* XPM */ +static char *tray_icon_warning[] = { +/* columns rows colors chars-per-pixel */ +"128 128 256 2", +" c black", +". c #110700", +"X c #130800", +"o c #220D00", +"O c #2A1200", +"+ c #331500", +"@ c #3F1A00", +"# c #441D00", +"$ c #491F00", +"% c #4B2000", +"& c #5A2600", +"* c #572600", +"= c #632B00", +"- c #6E2F00", +"; c #702F00", +": c #743300", +"> c #7A3500", +", c #843900", +"< c #873B00", +"1 c #913F00", +"2 c #954200", +"3 c #9C4400", +"4 c #8F4100", +"5 c #A54700", +"6 c #AD4C00", +"7 c #A44900", +"8 c #B64E00", +"9 c #B84F00", +"0 c #B45300", +"q c #BB5300", +"w c #BF5900", +"e c #B55608", +"r c #C15700", +"t c #C45A00", +"y c #CA5E00", +"u c #C46200", +"i c #CD6200", +"p c #CD6B00", +"a c #C66900", +"s c #D16400", +"d c #D46C00", +"f c #D96A00", +"g c #CD7400", +"h c #CD7A00", +"j c #D47300", +"k c #D37A00", +"l c #DB7D00", +"z c #DB7500", +"x c #C96510", +"c c #DB7310", +"v c #D46E10", +"b c #E27B00", +"n c #E07600", +"m c #DB7A20", +"M c #C77333", +"N c #CC7127", +"B c #DB8200", +"V c #DC8A00", +"C c #D38700", +"Z c #DC9400", +"A c #DB9800", +"S c #E48400", +"D c #E68A00", +"F c #EA8D00", +"G c #E39300", +"H c #EC9400", +"J c #E39D00", +"K c #EC9B00", +"L c #F39B00", +"P c #F29700", +"I c #DFA600", +"U c #E4A400", +"Y c #E5AB00", +"T c #ECA900", +"R c #F5A300", +"E c #FAA400", +"W c #F4AB00", +"Q c #FFAC00", +"! c #E6B200", +"~ c #EAB200", +"^ c #ECBB00", +"/ c #E6B900", +"( c #FFB300", +") c #F4BB00", +"_ c #FFBB00", +"` c #F6B200", +"' c #E5AB1C", +"] c #DD8230", +"[ c #E08630", +"{ c #EAC200", +"} c #EECB00", +"| c #F4C400", +" . c #FFC300", +".. c #F2CC00", +"X. c #FFCB00", +"o. c #EFD500", +"O. c #FFD300", +"+. c #F4DA00", +"@. c #FFDB00", +"#. c #F9D806", +"$. c #FFDD1C", +"%. c #FFDC14", +"&. c #F6E100", +"*. c #FFE300", +"=. c #FEEB00", +"-. c #FCE708", +";. c #FEF400", +":. c #FFFF00", +">. c #FFFE0C", +",. c #FFEA19", +"<. c #FFFF1B", +"1. c #FFFA14", +"2. c #FEDE34", +"3. c #FFDE29", +"4. c #FFE42D", +"5. c #FFE826", +"6. c #FFFF24", +"7. c #FFFE2B", +"8. c #FFF227", +"9. c #FFE43B", +"0. c #FFE636", +"q. c #FFFE34", +"w. c #FFFE3A", +"e. c #FFF535", +"r. c #CA834E", +"t. c #CE884E", +"y. c #D38645", +"u. c #CA8453", +"i. c #C8875A", +"p. c #CA8A5C", +"a. c #D69753", +"s. c #E39952", +"d. c #DCA755", +"f. c #DDA249", +"g. c #E6B54D", +"h. c #E3A95C", +"j. c #C78B66", +"k. c #C78D6C", +"l. c #C98A60", +"z. c #CC9369", +"x. c #D59869", +"c. c #C79174", +"v. c #C7957C", +"b. c #CD9B78", +"n. c #D3A979", +"m. c #DBB373", +"M. c #E9AA6F", +"N. c #E2BD6A", +"B. c #E3BB63", +"V. c #E7B07F", +"C. c #F7D64E", +"Z. c #F6D95A", +"A. c #EEC854", +"S. c #FFE54F", +"D. c #FFE847", +"F. c #FFFE45", +"G. c #FFFE4B", +"H. c #FFF545", +"J. c #FFE753", +"K. c #FFE956", +"L. c #FFEB5B", +"P. c #FCE55E", +"I. c #FFFE5C", +"U. c #FFF65A", +"Y. c #F2D868", +"T. c #E7C976", +"R. c #ECD173", +"E. c #FFED63", +"W. c #FCE865", +"Q. c #FFF067", +"!. c #FFFF63", +"~. c #FFF16C", +"^. c #FFFD6C", +"/. c #FFF473", +"(. c #FFF67C", +"). c #FFFF79", +"_. c #F7E679", +"`. c #C89A84", +"'. c #C89D8B", +"]. c #C79781", +"[. c #CEA288", +"{. c #D6A88A", +"}. c #CBA393", +"|. c #CCA79B", +" X c #CCA89D", +".X c #CDA996", +"XX c #D5AC95", +"oX c #D8B599", +"OX c #DBBD89", +"+X c #EBBB8F", +"@X c #E5BD9F", +"#X c #CEACA3", +"$X c #D0B2AB", +"%X c #D6B9A9", +"&X c #D4B9B3", +"*X c #D6BDB9", +"=X c #D3B8B1", +"-X c #ECD886", +";X c #E7CA94", +":X c #F1E183", +">X c #FFF983", +",X c #FFFB8B", +"<X c #FCF385", +"1X c #FFFD94", +"2X c #FFFF9B", +"3X c #F8F191", +"4X c #F4EC9B", +"5X c #DCC3A8", +"6X c #D9C3BC", +"7X c #E8C7AE", +"8X c #E8DAA7", +"9X c #E3CEBB", +"0X c #E2CAB5", +"qX c #EAD6BB", +"wX c #F4D8BF", +"eX c #F2CFAF", +"rX c #FFFFA3", +"tX c #FFFFAC", +"yX c #F6F0A9", +"uX c #FFFFB3", +"iX c #FFFFBB", +"pX c #F8F4B8", +"aX c #F0E7B1", +"sX c #DFC99D", +"dX c #DAC6C3", +"fX c #DDCCCA", +"gX c #DCC9C4", +"hX c #E0CEC3", +"jX c #E7D8C8", +"kX c #E2D3D3", +"lX c #E6D9D9", +"zX c #E8DCDC", +"xX c #EADBD2", +"cX c #EDE2CC", +"vX c #EFE7C6", +"bX c #F4E5CD", +"nX c #FFFFC4", +"mX c #FFFFCB", +"MX c #F5F0C1", +"NX c #EEE5D7", +"BX c #F4EADC", +"VX c #F3E8D4", +"CX c #F9F5D6", +"ZX c #FFFFD3", +"AX c #F9F4DA", +"SX c #FFFFDD", +"DX c #F6F0DA", +"FX c #F9F5E0", +"GX c #FFFFE3", +"HX c #FBF4ED", +"JX c #FFFFEB", +"KX c #FFFFF4", +"LX c #FFFFFE", +"PX c #FCF7F4", +"IX c #F8EFE8", +"UX c None", +/* pixels */ +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXy y y i s s i s i i i i i i y y y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXy y s s s i s s i i s i i i i i i i i i y i y s i y y y UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s i s i s s s i l V V Z Y ~ ! Y J V V B p i i y i y y y i y y t UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s s i s s V U { +.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.+...! Z j y i y y i i y w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s s s s V ^ &.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.=.{ A p y y y y y y UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s s s Z } ;.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.&.I j u y y y t q UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s s B { ;.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.} A y y y y y 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s i J +.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.;.I a y y y y 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s i U =.:.:.:.:.:.:.:.:.:.:.:.:.:.:.w.!.).tXtXSXJXJXGXJXJXGXJXJXJXiXtX1X).w.<.:.:.:.:.:.:.:.:.:.:.:.:.:.;.Y a y y y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s Z =.:.:.:.:.:.:.:.:.:.:.:.>.F.>XiXGXGXGXJXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXmX2X!.<.:.:.:.:.:.:.:.:.:.:.:.;.Y a y t y q UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s z +.:.:.:.:.:.:.:.:.:.:.>.!.tXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXnX).7.:.:.:.:.:.:.:.:.:.:.=.V y t y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s ~ :.:.:.:.:.:.:.:.:.:.F.2XGXSXGXGXSXGXGXGXGXSXGXGXSXGXGXGXSXGXGXSXGXGXGXGXGXGXSXGXSXGXGXGXGXGXSXGXnX!.>.:.:.:.:.:.:.:.:.:.o.g t t t 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s B &.:.:.:.:.:.:.:.:.>.^.nXSXGXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXGXSX,X<.:.:.:.:.:.:.:.:.;.U y t t w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s Y :.:.:.:.:.:.:.:.>.^.mXSXSXSXSXSXSXSXSXSXSXBXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXlXzXzXFXGX,X<.:.:.:.:.:.:.:.:.} a r t t UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s d o.:.:.:.:.:.:.:.:.!.mXSXSXZXSXSXZXSXZXSXZXZXAXzXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXzXZXSXZX,X>.:.:.:.:.:.:.:.=.k t t t UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s B &.:.:.:.:.:.:.:.q.iXSXZXZXSXZXSXSXZXZXSXZXSXZXNXlXzXFXFXFXFXAXFXFXFXFXFXFXFXAXFXAXFXFXFXFXFXFXFXFXFXFXAXFXFXFXFXlXlXCXZXSXZXmXI.:.:.:.:.:.:.:.:.A t r y UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s V ;.:.:.:.:.:.:.>.).ZXZXZXZXZXZXZXZXZXZXZXZXSXZXZXxXkXNXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXNXkXkXCXZXZXZXZXZXtX7.:.:.:.:.:.:.:.^ y r y 6 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s U :.:.:.:.:.:.:.q.iXZXZXZXmXZXZXZXZXZXZXZXZXZXZXZXCXkXkXDXGXGXGXGXGXGXGXGXGXSXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXAXkXkXxXZXZXZXZXZXZXZXZX^.:.:.:.:.:.:.:.{ r r t 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s J :.:.:.:.:.:.:.^.mXZXmXZXZXZXZXmXZXZXZXZXmXZXZXZXmXcXkXkXSXGXGXSXGXSXGXGXGXGXGXGXSXSXGXGXSXGXGXGXGXGXSXSXGXSXGXSXGXlXkXkXCXmXZXZXZXZXmXZXZXmX2X>.:.:.:.:.:.:.{ t t r 3 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s d J :.:.:.:.:.:.>.1XmXZXmXmXZXmXmXZXmXmXmXmXmXmXZXmXmXmXkXfXxXSXSXGXSXGXGXSXSXGXSXSXGXGXGXSXSXSXGXSXSXGXSXGXSXGXGXSXGXNXfXfXvXmXmXZXmXZXmXmXZXmXmXmXiX6.:.:.:.:.:.:.{ r t r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s U :.:.:.:.:.:.<.rXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXfXfXBXGXSXSXGXSXSXSXGXSXSXGXSXSXSXGXSXGXSXSXGXSXSXSXGXSXSXSXAXfXfXxXmXmXmXmXmXmXmXmXmXmXmXmXmXiXG.:.:.:.:.:.:.{ t y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s Z :.:.:.:.:.:.6.iXnXmXmXmXmXnXmXnXmXmXmXnXnXmXmXmXmXnXmXvXfXfXAXSXGXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXGXSXSXjXfXfXMXmXmXmXmXmXmXnXmXnXmXnXmXmXmXnXG.:.:.:.:.:.:.{ r t w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXi s s B ;.:.:.:.:.:.6.iXnXmXnXnXnXmXnXmXnXnXnXnXmXmXnXnXnXnXnXnXjXdXhXSXSXSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXSXSXSXSXNXdXdXvXnXnXnXnXnXnXnXnXmXnXmXnXnXnXnXmXnXG.:.:.:.:.:.:.Y r r q UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXs s j ;.:.:.:.:.:.6.uXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXdXdXxXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXCXdXdXjXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXG.:.:.:.:.:.:.A r r 8 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXs s s &.:.:.:.:.:.6.iXnXnXnXnXiXnXiXnXnXiXnXnXnXnXnXiXnXnXnXnXnXMXdX6XDXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXgXdXgXMXnXnXiXnXnXnXnXnXiXnXnXnXnXnXnXiXnXnXnXnXF.:.:.:.:.:.;.h r r 3 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXi s s ^ :.:.:.:.:.<.uXiXnXiXnXiXiXiXiXnXiXiXiXiXnXnXiXiXiXiXnXiXnXjX6XdXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXcXdX6XqXnXiXiXnXiXiXiXnXiXiXiXiXiXiXiXiXnXiXiXiXiXiXF.:.:.:.:.:.&.u r r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXi s s V :.:.:.:.:.>.1XiXiXiXiXiXiXiXiXnXiXiXiXiXiXiXiXiXiXiXiXiXiXiX6X6XjXSXSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXAX*X6X9XiXiXiXiXiXiXiXiXnXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiX6.:.:.:.:.:.{ w r 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXs s d ;.:.:.:.:.:.>XiXuXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXpX*X*XcXSXSXSXSXSXSXSXSXSXSXSXSXSXZXSXSXSXSXSXSXSXSXZXSXSXhX*X*XpXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXtX<.:.:.:.:.:.V r r 2 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXs s s o.:.:.:.:.:.I.iXiXiXuXiXiXiXuXiXuXiXiXiXiXuXiXuXiXiXiXiXiXiXiXaX*X*XZXSXSXSXSXSXSXZXSXSXZXSXSXSXSXSXSXSXSXSXSXZXSXSXSXcX*X*XqXiXiXiXuXiXiXiXiXuXiXiXuXiXiXiXiXiXiXiXuXiXiXiXiXiXiX1X:.:.:.:.:.;.u r r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXs s V :.:.:.:.:.7.iXuXuXiXiXuXuXiXuXuXuXuXiXiXuXuXuXiXiXuXuXuXiXuXuX6X&X6XSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXSXSXZXZXSXSXSXSXSXAX*X=X6XuXuXuXuXuXuXiXuXuXiXiXuXuXuXuXiXuXiXuXiXuXiXuXiXiXuXuXiX!.:.:.:.:.:./ q r 6 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXs s i =.:.:.:.:.>.2XuXuXuXtXuXuXuXuXuXuXuXiXtXuXuXuXuXtXuXuXuXuXuXuXuX=X&XcXSXSXSXSXSXSXSXSXZXSXSXSXZXSXSXSXSXSXSXSXSXSXSXSXgX=X=XpXuXuXuXuXuXuXtXuXuXuXtXuXuXiXuXuXtXuXuXuXtXuXuXtXuXuXuXuXuX7.:.:.:.:.:.C w w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXi s s ~ :.:.:.:.:.!.uXuXuXuXuXuXuXtXuXtXuXuXtXuXuXuXuXuXuXuXuXuXuXtXuXaX=X$XCXSXSXSXSXSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXVX=X=X8XuXuXuXuXuXuXuXuXtXiXtXuXuXuXtXtXuXuXuXtXuXuXuXuXuXuXuXuXuXuX2X>.:.:.:.:.o.q w 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXi s d ;.:.:.:.:.7.uXtXtXtXtXtXtXtXtXuXtXtXuXtXtXtXtXtXtXtXtXtXtXtXtXtX0X%X&XSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXZXSXSXSXSXSXSXSXCX%X$X5XpXuXtXtXuXtXtXtXuXtXtXtXtXtXuXtXtXtXtXuXtXtXtXtXtXtXtXtXtXtXtXtX!.:.:.:.:.:.A q r , UXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXy s s ! :.:.:.:.:.,XtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtX%X$X9XSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXjX$X$XaXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtX6.:.:.:.:.=.q r 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXi s d ;.:.:.:.:.G.tXtXtXrXtXtXtXtXrXtXtXtXtXrXtXrXtXtXtXrXtXtXtXtXtXrXyX$X$XVXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXbX$X$X5XrXrXtXtXrXrXtXtXtXtXrXtXtXtXtXtXrXtXtXrXtXrXtXtXtXtXtXrXtXtXtXrXtXtX).:.:.:.:.:.Z q w > UXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXy s s ! :.:.:.:.>.2XrXtXrXtXrXrXrXrXtXrXtXtXrXtXrXtXtXrXtXrXtXrXtXrXrXtX8X$X#XSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSX&X#X$XrXtXrXtXrXtXrXtXrXrXrXtXrXtXtXrXtXrXtXtXtXrXrXtXrXrXrXrXrXtXrXtXrXtXrXtX7.:.:.:.:.=.r q 6 UXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXs s s ;.:.:.:.:.I.rXrXrXrXrXtXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXtXrX%X#X6XSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXSXjX#X#X8XrXrXrXrXrXrXrXtXrXrXrXrXrXrXrXrXrXrXrXrXrXrXtXrXrXrXtXrXtXrXrXrXrXrXrXrX1X:.:.:.:.:.C r w UXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXs s A :.:.:.:.>.2XrXrXrXrXrXrXrXrXrXrXrXrXrXrX2XrXrXrXrXrXrXrXrXrXrXrXrX X#XjXSXSXSXGXSXGXSXSXSXGXSXSXSXSXSXGXGXSXSXSXSXCX#X#X5XrXrXrXrXrXrXrX2XrXrXrXrXrXrXrXrXrXrX2XrXrXrXrXrXrXrXrXrXrXrXrXrXrX2XrXrXrXrXw.:.:.:.:.o.r q 3 UXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXi s s &.:.:.:.:.F.2XrX2X2XrX2XrXrX2XrXrX2X2XrXrXrXrX2XrX2XrX2XrXrX2XrX2X8X#X#XCXSXSXSXSXSXGXSXGXSXSXSXGXGXGXSXSXSXSXGXGXSX9X X#X4XrX2X2XrX2X2XrX2XrXrXrXrX2XrX2X2XrX2XrX2XrX2XrXrX2XrXrX2XrXrX2XrXrXrX2XrX2XrX).:.:.:.:.:.a w q UXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXi s l :.:.:.:.:.,X2X2X2XrX2X2X2X2X2X2X2X2XrX2X2X2X2X2X2X2X2X2X2X2X2X2X2XsX|.$XSXSXSXSXGXSXSXSXGXGXSXGXSXSXSXSXSXSXGXSXSXCX X X X X X X X X X|.|. X X X XoX4X2XrX2X2X2XrX2XrX2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X6.:.:.:.:.I r q > UXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXs s ~ :.:.:.:.q.2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2XXX|.9XSXGXGXSXGXGXSXGXSXGXSXGXSXGXGXGXGXSXGXGXSXAX|.|.|. X|. X X|.|.|.|.|.|.|.|. XsX2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X!.:.:.:.:.=.q w 3 UXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXi s s =.:.;.:.:.^.2X2X2X1X2X2X2X2X1X2X1X2X2X2X2X2X2X2X1X2X2X2X2X2X2X2X1X4X|.|.bXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXSXGXGXGXGXGXGXAXbXVXbXcXVXVXVXVXVXVXVXVXbX9X}.|.2X2X2X2X1X2X2X2X2X2X2X2X2X2X2X2X2X2X1X2X2X2X2X2X2X2X2X>.:.:.:.:.a w r UXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXi s B :.:.:.:.>.1X1X2X2X1X1X1X1X1X1X2X1X1X1X1X1X1X2X1X1X2X2X1X1X1X1X1X1XsX}.|.GXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXSXGXSXGXGX%X}..X1X1X1X2X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X2X1X1X1X1Xq.;.:.:.:.! q q : UXUXUXUXUXUXUXUXUXUX", +"UXUXUXs s Y :.:.;.:.w.1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1XXX}.5XGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXqX}.}.sX1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X^.;.:.;.:.+.q w 1 UXUXUXUXUXUXUXUXUXUX", +"UXUXt s s +.;.;.:.;.^.1X1X1X,X1X1X,X1X1X1X1X1X1X1X,X1X1X,X1X1X1X1X,X1X1X1X1X3X}.}.jXJXGXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXCX.X}.XX1X1X,X1X1X1X1X1X3X1X,X1X1X1X1X1X1X1X1X1X1X1X1X1X,X1X1X1X1X>.;.:.;.:.a q q UXUXUXUXUXUXUXUXUXUX", +"UXUXi i d :.;.:.;.>.,X1X,X1X1X,X,X1X1X,X1X,X1X,X1X,X,X,X1X,X1X,X,X1X1X,X,X,X;X}.}.AXGXJXGXGXGXJXGXJXJXGXGXJXGXJXJXGXGXJXGXJXJXGXGXGXGXJXGXGXJXGXJXGXGXJXGX%X}.'.3X,X3X1X,X1X,X1X,X1X1X,X1X,X1X,X,X1X1X,X1X,X1X,X,X1X1X,X,X1Xe.:.;.;.;.Z q q UXUXUXUXUXUXUXUXUXUX", +"UXUXs i V ;.;.;.;.e.,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X1X,X,X,X,X,X,X,X,X,X1XOX'.#XGXJXGXGXJXGXJXJXGXGXGXJXGXJXGXGXGXJXGXJXGXGXGXJXJXJXGXJXGXGXJXGXJXJXJXcX'.[.;X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,XU.;.;.:.;./ r q ; UXUXUXUXUXUXUXUXUX", +"UXUXs s Y ;.;.;.;.G.,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X>X,X,X,X,X,X,X,X'.'.9XGXJXJXJXJXJXGXJXGXJXJXJXJXJXGXJXJXGXJXJXGXJXJXJXJXJXGXGXJXJXJXJXJXGXFX}.'.[.,X,X,X>X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X>X;.;.:.;.+.q q < UXUXUXUXUXUXUXUXUX", +"UXr i s ..:.;.;.;.^.>X,X,X>X>X,X,X>X,X>X>X,X>X,X>X,X>X,X,X,X,X>X,X>X>X,X,X:X'.`.jXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJX5X`.'.:X,X,X>X,X,X>X,X>X,X>X,X>X,X>X,X>X,X,X>X,X>X,X>X,X,X>X,X>X,X>X,X>X1.;.;.;.;.q q 7 UXUXUXUXUXUXUXUXUX", +"UXt s i =.;.;.;.;.>X>X>X>X,X>X>X>X,X>X>X>X>X,X>X>X>X,X>X>X,X>X>X,X>X>X>X>X>X[.`.`.`.`.`.`.`.`.`.`.'.`.'.`.`.XXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXVX`.`.OX>X>X>X>X>X,X,X>X>X>X>X>X>X>X,X>X>X>X>X>X>X,X>X>X>X>X>X>X,X>X>X>X>Xe.;.;.;.;.h q q UXUXUXUXUXUXUXUXUX", +"UXt s i ;.;.;.;.8.>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X-X`.].`.`.`.`.`.].].].].].`.`.].'.JXJXJXJXKXJXKXJXJXJXJXKXJXJXJXJXJXJXGX}.].`.<X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>XG.;.;.;.;.A q r UXUXUXUXUXUXUXUXUX", +"UXi s B ;.=.=.;.e.>X>X>X>X>X>X(.>X>X>X(.>X>X>X>X>X(.>X>X>X>X>X(.>X>X>X>X(.>X(.<X:X:X-X:X:X:X:X:X-X_.:XT.].v.9XJXJXKXKXJXKXJXKXJXJXJXKXJXKXJXKXKXKX5X].].R.>X>X(.>X>X(.>X>X>X>X>X>X>X>X(.>X>X(.>X>X(.>X>X>X>X>X>X>X>X>X>X>X>X(.Q.=.;.;.;.I q q % UXUXUXUXUXUXUXUX", +"UXs s V ;.;.;.=.H.(.(.>X(.(.>X(.(.(.>X(.(.(.<X(.>X(.(.>X(.(.>X(.(.(.(.(.>X>X(.(.(.(.(.(.>X(.(.>X(.>X).{.v.[.HXKXKXJXKXKXJXKXKXKXKXKXJXKXKXJXKXJXBX].v.OX>X>X(.>X(.(.>X>X(.(.(.>X>X(.(.<X(.>X>X(.(.<X(.(.(.>X(.(.(.(.>X(.(.>X(.~.;.=.=.;.{ q q > UXUXUXUXUXUXUXUX", +"UXs i T =.;.=.=.U.>X(.(.(.(.(.(.(.(.(.>X(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(._.].].hXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXXXv.`._.(.(.>X(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.;.=.=.;.{ q q , UXUXUXUXUXUXUXUX", +"UXi i U ;.=.=.;.U.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.b.v.[.KXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKX0Xv.v.T.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.-.;.=.=.@.9 q > UXUXUXUXUXUXUXUX", +"UXy s ^ =.=.=.=.,.~.(./.(.(.(.).(././.(.(././.(.(.(.(.(.(.(.(.(.(.(.(.(./.(.(././.(.(././.(.(./.(.R.v.v.jXKXKXLXKXKXKXKXKXKXKXKXKXKXKXLXKXKXIX`.v.n./././.(./.(.(.(.(.).(.(././.(./.(./.(.(.(./.(./.(.(.(.(.(./.(.(.(.)./.(./.e.;.=.=.;.=.q q 3 UXUXUXUXUXUXUXUX", +"UXs i ..=.=.=.=.;.,./././././././.(././.(./././././././././././././././.(./././.(./.(./.(././././.b.c.`.LXKXLXKXLXLXKXLXLXKXLXLXLXLXKXLXKXLXXXc.c._./.(./.(./././././././././.(./.(.(././././././.(./././././.(././././.(./.8.=.=.=.=.=.=.q 9 3 UXUXUXUXUXUXUXUX", +"q i s | =.=.=.=.=.=.,.~././././././././././././././././././././././././././././././././././././.R.c.c.jXKXLXKXLXLXLXKXLXLXKXKXLXLXKXLXLXKXhXc.k.T././././././././././././././././././././././././././././././././././././.5.=.=.=.=.=.=.=.9 9 3 UXUXUXUXUXUXUXUX", +"0 i i | =.=.=.=.=.=.=.-.U././.~.~./././.~./././.~././././.~.~./.~././././.~./././.~./././.~././.b.c.`.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXb.c.n.~./.~././././.~././.~././.~./././.~././././.~./././.~././.~.~./././.~.~.8.=.*.=.=.=.=.=.=.9 q 5 UXUXUXUXUXUXUXUX", +"0 i i ..=.=.=.*.=.=.=.=.-.K.~./.~./.~.~.~./.~.~./.~.~.~.~.~./.~./.~.~.~.~.~./.~.~.~./.~.~.~.~.R.k.c.xXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXoXc.k.R.~.~.~./.~.~.~./.~.~./.~.~.~.~.~./.~.~.~.~.~./.~.~.~./.~.~.~./.~.~.~.E.,.*.-.*.=.=.*.=.*.=.9 q 3 UXUXUXUXUXUXUXUX", +"0 s i | =.*.*.=.=.*.*.*.=.*.9.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.z.k.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXxXk.k.N.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.L.-.*.=.=.=.*.=.=.*.=.=.9 q 3 UXUXUXUXUXUXUXUX", +"0 s i | =.*.*.*.=.*.*.=.*.=.*.5.Q.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.E.~.~.~.Q.~.E.~.E.~.~.~.~.~.N.j.k.NXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXb.k.k.m.N.N.N.N.N.N.T.T.L.~.E.~.~.Q.~.~.~.E.~.E.~.~.E.~.~.E.~.~.~.Q.~.E.~.9.=.=.=.*.*.*.=.*.*.*.=.*.9 q 3 UXUXUXUXUXUXUXUX", +"6 y i | *.*.=.=.*.=.*.*.=.*.*.=.-.S.E.~.E.Q.E.Q.Q.E.Q.~.E.Q.E.E.Q.Q.Q.~.Q.Q.~.~.~.~.E.E.W.W.z.j.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXNXj.j.j.j.j.j.j.j.k.j.j.j.B.~.E.E.~.Q.E.Q.~.~.Q.Q.Q.~.~.E.~.~.E.Q.Q.Q.Q.L.5.*.=.*.*.*.=.*.=.*.=.*.*.=.9 9 3 UXUXUXUXUXUXUXUX", +"UXi s ~ *.*.*.*.@.*.*.*.@.*.*.*.*.*.0.U.E.E.~.E.E.E.E.E.E.Q.E.~.E.E.E.E.E.E.E.E.E.E.E.~.E.N.j.j.zXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXoX{.{.{.{.{.{.{.{.{.v.j.j.Y.E.E.E.E.E.Q.E.~.E.E.E.E.E.E.E.E.E.E.Q.Q.D.*.=.*.*.*.*.*.*.*.*.*.*.*.*.=.9 9 3 UXUXUXUXUXUXUXUX", +"UXi y U *.@.*.*.*.*.*.*.*.*.*.*.*.*.*.-.D.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.l.l.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXjXi.j.Y.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.L.5.@.*.*.*.*.*.*.@.*.*.*.@.*.*.*...9 9 > UXUXUXUXUXUXUXUX", +"UXi y U *.@.*.*.*.*.@.*.@.*.*.*.*.*.@.*.*.,.L.E.E.E.L.E.E.L.E.E.E.E.L.E.E.E.L.E.E.E.E.L.B.l.l.NXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXk.l.l.P.E.L.E.E.E.E.L.L.E.E.E.L.E.E.E.E.E.0.*.*.*.*.@.*.*.@.*.*.*.*.@.*.*.*.@.^ q 9 > UXUXUXUXUXUXUXUX", +"UXi y V *.*.*.@.@.*.@.*.*.*.@.*.@.*.@.*.@.*.@.4.K.L.L.L.L.L.L.L.L.E.L.L.L.L.L.E.L.L.L.P.l.i.oXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX{.i.l.Z.L.L.L.E.L.L.L.L.L.L.L.L.L.L.L.E.9.-.*.@.@.*.*.*.*.@.*.@.@.@.*.*.*.@.*.*.^ q 9 - UXUXUXUXUXUXUXUX", +"UXy i l *.@.@.*.@.*.*.@.*.@.@.*.@.*.*.@.@.*.*.@.-.4.L.L.L.L.L.L.L.L.L.L.L.L.L.L.L.L.L.d.i.i.IXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX7Xi.i.h.L.L.L.L.L.L.L.L.L.L.L.L.L.L.K.9.-.@.@.@.*.*.*.@.@.*.*.@.*.*.@.@.*.@.*.@.@.A q 9 # UXUXUXUXUXUXUXUX", +"UXw y i @.*.@.@.*.@.@.*.@.*.@.@.*.@.*.@.*.@.@.@.@.*.@.4.K.K.K.L.K.L.L.L.K.L.K.L.L.L.Z.p.i.oXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXBXi.i.a.L.L.L.L.K.K.L.L.L.L.L.L.L.K.9.%.*.@.@.*.*.@.@.@.@.*.@.@.*.@.@.*.*.@.*.@.@.*.V 0 q O UXUXUXUXUXUXUXUX", +"UX0 y i O.@.@.*.@.@.@.@.@.@.@.@.@.@.@.@.@.@.*.@.@.@.@.@.@.5.D.K.K.K.K.K.K.L.J.L.J.K.d.i.u.HXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXz.u.p.Z.K.K.J.K.K.K.K.K.K.K.K.J.4.#.*.@.@.@.@.@.*.@.@.@.*.@.@.@.*.@.@.@.*.@.@.@.*.@.g 9 q UXUXUXUXUXUXUXUXUX", +"UX6 y s ) @.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.%.0.S.J.J.J.J.J.J.J.J.a.i.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX{.u.t.A.J.J.J.J.J.J.J.J.J.J.9.5.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.9 q 1 UXUXUXUXUXUXUXUXUX", +"UXUXi y J @.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.$.0.J.J.J.J.J.J.d.u.t.u.u.u.u.u.u.u.x.IXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX0Xu.u.d.J.J.J.J.J.J.J.J.2.5.#.@.O.@.@.@.@.@.@.@.@.@.@.@.@.O.O.@.@.@.@.@.@.O.O.@.@.@.@.) 9 9 < UXUXUXUXUXUXUXUXUX", +"UXUXi y V @.O.@.@.@.O.@.@.@.@.@.@.@.O.@.@.O.@.@.@.@.O.@.O.@.@.O.O.@.@.O.O.%.3.9.S.J.C.u.r.r.r.r.u.r.r.r.r.9XLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXBXp.r.a.C.S.S.S.S.D.0.$.@.#.O.O.@.O.@.@.@.@.O.O.@.@.O.@.@.@.@.@.@.@.O.@.@.@.@.@.@.@.@.@.U 8 q = UXUXUXUXUXUXUXUXUX", +"UXUXw y i O.@.O.@.@.O.@.@.@.@.@.O.@.@.O.O.@.@.O.@.@.O.@.@.@.@.@.@.O.@.@.@.@.O.@.@.$.3.2.g.g.g.g.g.g.f.r.r.IXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXx.r.r.C.S.9.2.$.%.@.@.O.@.@.@.@.@.@.O.@.@.@.O.@.@.O.@.@.O.@.O.@.@.O.@.@.O.@.O.@.@.O.@.O.C 8 8 + UXUXUXUXUXUXUXUXUX", +"UXUX6 i y | O.@.O.@.@.O.O.O.O.O.@.@.O.@.@.O.@.O.O.@.@.O.O.O.O.O.O.@.O.O.O.@.@.O.O.@.O.@.@.%.%.3.2.2.f.r.p.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX{.M N ' %.@.@.O.@.O.O.@.@.O.O.O.O.O.O.@.@.O.O.@.O.@.@.O.@.O.O.@.O.@.@.O.@.O.O.@.O.@.@.O.O.u 8 8 UXUXUXUXUXUXUXUXUXUX", +"UXUXUXy y J O.@.@.O.O.@.O.@.O.@.O.O.@.O.@.O.O.@.O.O.O.@.O.@.O.@.O.@.O.@.O.O.O.O.@.O.@.O.O.O.O.O.O.O.t 8 x.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX7X8 8 a O.@.O.@.O.O.O.O.O.O.@.O.@.O.@.O.O.O.O.@.O.@.O.O.@.O.@.@.O.@.O.O.@.O.@.O.O.@.O.O.@.^ q 9 < UXUXUXUXUXUXUXUXUXUX", +"UXUXUXy y z O.O.O.O.O.O.O.O.O.@.O.O.O.O.O.O.O.O.@.O.O.O.O.O.O.O.O.O.O.O.@.@.O.O.O.O.O.O.O.@.@.O.O.| 9 9 @XLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXIXe 0 u X.O.O.O.O.O.@.O.@.O.O.O.O.O.O.@.O.@.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.@.O.O.O.O.O.A 8 8 = UXUXUXUXUXUXUXUXUXUX", +"UXUXUX0 y y | O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.U 9 q HXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXM q 9 ) O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.a 8 8 . UXUXUXUXUXUXUXUXUXUX", +"UXUXUX2 y y U O.X.O.O.O.O.O.O.X.O.O.O.X.O.O.O.O.X.O.O.O.O.O.X.O.O.O.O.O.O.O.X.O.O.O.X.O.O.O.X.O.#.k q M LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXx.q q Z O.X.O.O.O.X.O.O.O.O.O.O.O.O.O.O.X.O.O.X.O.O.O.X.O.O.O.X.O.O.O.O.O.X.O.O.O.O.X.O.O.O.| 8 9 1 UXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXi y z O.O.O.O.X.X.O.O.X.O.X.O.X.O.X.O.O.O.X.X.O.O.O.O.O.O.X.X.O.O.O.O.O.X.O.O.O.O.O.O.O.X.t r x.LXLXLXLXLXLXLXLXLXLXLXLXLXLXjXr q g X.O.O.O.O.O.O.O.X.X.X.O.O.X.O.O.O.X.X.O.O.O.X.O.O.O.X.O.O.O.X.O.O.X.O.O.X.O.O.X.O.O.O.Z 9 8 = UXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUX6 y y ` X.O.X.O.O.O.X.O.O.X.O.O.X.O.O.X.O.O.O.X.X.O.X.X.O.O.O.X.X.O.X.X.O.X.X.O.X.X.O.X.) r r 7XLXLXLXLXLXLXLXLXLXLXLXLXLXHXy q r X.O.X.X.O.X.O.X.O.O.O.O.X.X.O.X.X.O.O.O.O.X.X.O.X.X.O.X.O.O.X.O.X.O.X.O.X.O.O.X.O.X.O.X.w q 6 X UXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXi y V X.O.O.X.X.X.O.X.O.X.O.O.O.X.X.O.X.X.O.X.X.O.X.O.X.X.O.X.O.X.X.O.X.O.O.X.O.O.X.O.J r r LXLXLXLXLXLXLXLXLXLXLXLXLXLXy.r r ` X.O.X.X.O.X.X.X.O.O.X.O.X.X.O.X.X.O.X.O.X.O.O.O.X.X.O.X.X.O.X.O.X.O.O.X.O.X.O.X.O.X.X.Y 9 9 < UXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXw y a X.X.X.O.X.O.O.X.X.X.X.X.X.O.X.X.O.X.X.O.X.X.O.X.X.O.X.O.X.X.O.X.O.X.X.O.X.X.X.X.f t y.KXLXLXLXLXLXLXLXLXLXLXLXLXV.t r V X.O.O.X.O.X.X.O.X.X.X.O.X.O.X.X.O.X.X.O.X.O.X.X.X.O.X.X.O.X.X.O.X.O.X.X.X.X.X.X.O.X.X.O.g 9 9 + UXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUX2 y y G X.X.X.X.X.X.X.X.O.X.X.X.X.X.X.X.X.X.X.X.O.X.X.X.X.X.X.O.X.X.X.X.X.X.X.X.X.X.X.r r {.LXLXLXLXLXLXLXLXLXLXLXLXbXr r g X.X.X.X.X.X.X.X.X.X.O.X.X.X.X.X.O.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.O.X.X.O.) q 8 1 UXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXr y a .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.` y t wXLXLXLXLXLXLXLXLXLXLXLXHXN t i ) X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.k 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUX3 u y G X.X. .X. .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X. .X. .X.G t x LXLXLXLXLXLXLXLXLXLXLXLXa.y y T X.X.X.X.X.X.X.X.X.X.X. .X. .X.X. .X.X.X. .X. .X.X. .X.X.X.X.X.X.X.X.X.X.X.X.X.X. .X. .X.) 9 8 5 UXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXt y i .X.X.X. .X. .X. .X.X. . .X.X. .X. .X. .X.X. .X. .X. .X. .X. .X.X.X.X.X.X.j y a.LXLXLXLXLXLXLXLXLXLXLX+Xy y V .X. . .X.X. .X. .X. .X.X.X.X.X.X. .X.X.X.X.X.X.X.X.X.X. . .X.X.X. .X. .X.X. .X.X.X.X. .X.k 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUX2 t y G .X. .X. . .X. .X. .X. .X. .X.X.X. .X.X. .X. .X. .X. .X. .X.X.X. . .X._ u y +XLXLXLXLXLXLXLXLXLXLXcXx y j _ X. .X. .X.X.X.X.X.X. .X. .X. .X. .X. .X. .X. . .X. .X.X.X. .X. .X. .X. .X. .X.X. .X. .X.T 0 8 5 UXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXr y t ) .X. .X.X. .X.X.X. .X. . . . .X. . . .X. .X. .X. .X. .X. . . .X.X. .W y y bXLXLXLXLXLXLXLXLXLXPXm i s ` X. .X. .X. . . . . .X. . .X. .X. .X. .X. .X. .X. .X. . . .X. .X.X. .X. .X. .X. . .X. .X. .a 8 8 # UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUX, r y j . .X. . . .X. .X. .X. .X. .X. . . .X. . . . .X. . . . . . . . . . .G i m LXLXLXLXLXLXLXLXLXLXs.s i H X. . . .X. . . .X. . .X. . .X.X. . .X. . . .X. . .X. . . .X. . . . .X. . . .X. . . . . . .Z 8 8 2 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUX0 y t T . . . . . . . . . . . . . . . . . . .X. . . . . . .X. .X. . . . .d s s.LXLXLXLXLXLXLXLXLXeXi s B . .X. . . .X. . . . .X. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .q 8 8 o UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXy y i ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .( s s 7XKXLXLXLXLXLXLXLXIXv s j _ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .g 9 8 ; UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUX4 r y z . . . ._ . . ._ . . . . . . . ._ . . . . . . . . . . . . .R s s BXLXLXLXLXLXLXLXLX] s s ` ._ . . ._ . . ._ . . . ._ . . ._ . . . . . . . . . . . . . . . . . . . . . . . . . .G 8 8 3 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUX0 t t G . . ._ . ._ . ._ . . . ._ . . . ._ . . . ._ . . ._ .D s ] LXLXLXLXLXLXLXLXM.s f H _ . . ._ . . ._ . . . . . ._ _ . . . . . . ._ . . . . . ._ . . ._ ._ _ . . ._ . .W q 8 8 + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXw y y W _ . ._ ._ _ ._ ._ ._ . ._ _ . ._ ._ ._ ._ _ ._ f f M.LXLXLXLXLXLXKXjXf s b ._ ._ . . ._ _ . ._ _ _ _ . . . ._ _ _ ._ _ . ._ _ _ _ . ._ ._ ._ . . ._ ._ .) u 8 8 - UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUX: y r i _ _ _ . . . ._ . ._ _ ._ _ . ._ _ ._ _ . ._ . ._ Q d f eXLXLXLXLXLXKXPXc f z ( ._ ._ _ _ ._ ._ ._ ._ ._ ._ ._ ._ ._ ._ ._ ._ . ._ _ ._ . . ._ _ ._ _ .j 8 9 1 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUX3 y r p ) _ _ _ _ _ _ _ _ _ ._ ._ _ _ ._ _ _ ._ _ _ _ ._ K f f HXLXLXLXLXLXPXs.f f W _ _ ._ _ ._ _ ._ _ _ ._ _ _ _ ._ _ ._ _ _ ._ _ _ ._ _ _ _ ._ _ ._ _ _ _ ._ _ .B 8 9 5 . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUX0 y t l _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ S f [ KXLXLXLXLXLX+Xf f F _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ G 8 8 6 + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX0 t r l _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ f f +XLXLXLXLXLXbXf f n _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ G 8 8 8 % UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXw t r l _ _ ( _ ( _ _ _ _ _ _ _ _ _ ( _ _ _ _ _ _ _ _ R f f wXLXLXLXLXHXm f f Q _ _ _ _ _ ( _ _ _ _ _ _ _ _ ( _ ( _ _ _ _ ( _ _ _ ( _ _ ( _ _ _ _ _ _ _ _ ( _ _ _ _ _ _ G 8 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# w t t l _ _ _ _ _ _ _ ( _ ( _ ( _ _ _ _ ( _ ( _ _ ( H f f LXLXLXLXLXs.f f L _ _ ( _ ( _ _ _ _ _ ( _ _ ( _ _ _ _ _ _ _ _ _ ( _ _ _ _ _ _ _ ( _ ( _ _ _ _ _ ( _ _ ( _ D 8 8 8 = UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# w t r l _ _ ( _ _ ( _ _ _ ( ( _ ( _ ( _ _ _ ( _ ( n f s.LXLXLXLX+Xf f S _ _ _ ( _ ( _ ( _ ( ( _ _ ( _ ( _ _ _ ( _ ( _ _ _ ( ( _ _ ( _ ( _ _ _ ( _ _ _ ( _ ( _ ( G 8 8 8 , UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# 0 r r j ( ( ( _ ( ( ( ( _ _ ( _ ( _ ( ( _ _ ( ( f f +XLXLXLXbXc f b ` ( ( ( _ ( _ ( _ ( _ _ ( ( _ ( _ ( ( ( _ ( _ ( ( _ ( _ ( ( _ ( _ ( ( _ ( ( ( ( _ _ ( ( V 8 8 8 - UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX% q r r s R ( ( _ ( ( ( _ ( _ ( ( ( ( _ ( ( ( L f f bXLXLXHX[ f f Q ( ( ( _ ( ( _ _ ( ( ( ( ( _ ( _ ( _ ( _ ( ( ( _ ( ( _ ( ( _ ( _ ( _ ( ( _ ( ( ( _ ( ( j 8 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX% 6 t r u K ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( F f c LXLXLXM.f f P ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( W u 8 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX7 t r t l ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( z f M.LXLXeXf f S ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( G q 8 8 6 + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX< t r r p R ( ( ( ( ( ( ( ( Q ( ( ( Q f f ;XLXBXc f f Q ( Q ( ( ( ( ( ( ( ( ( ( ( ( ( ( Q ( ( ( Q ( ( Q ( ( ( ( Q ( ( Q ( ( ( Q ( ( ( ( Q k 8 8 8 3 . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX& w r r r B ( Q ( Q Q ( ( Q Q ( ( P f f BXLX[ f f E Q ( ( ( Q ( Q ( ( Q Q ( ( Q Q ( ( ( ( ( ( ( ( ( ( ( Q ( ( ( Q ( ( ( ( ( ( ( ( Q K r 9 8 8 > UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXO 7 r r w i K ( Q ( ( Q ( ( ( Q S f m LXM.f f F ( ( Q Q ( ( Q ( Q Q ( ( Q Q ( ( Q Q ( Q ( Q Q Q ( Q Q ( Q Q ( ( Q Q ( Q Q ( Q R p 8 8 8 6 @ UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX: q w w r j E Q Q ( Q Q Q ( b f c M.f f b ( Q ( Q ( Q Q Q ( Q ( Q ( Q Q Q Q ( Q ( Q Q ( Q ( Q Q ( Q ( Q ( Q ( Q ( Q ( E l w 8 8 8 , . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXO 3 q r r q z R Q Q Q Q Q L f f f f f Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q ( Q Q Q Q Q Q Q Q Q E l q 8 8 8 5 @ UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX% 6 w w w q k K Q Q Q Q P n f b L Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q R l w 8 8 8 6 - UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX= 8 q w r w i D E Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q F j 9 8 8 8 6 - . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX= 8 r w q q w d D E Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q E Q Q Q Q Q Q Q E Q Q Q Q Q Q Q Q L z w 8 8 9 8 6 - . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# 2 q w q w q w i S P Q Q Q Q E E Q E Q Q Q Q E Q Q E Q Q Q Q Q E Q Q Q Q L S j q 8 8 0 8 8 3 & . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXO - 5 w q q q q w q i z S F E Q Q Q Q Q E Q Q E Q Q E Q Q Q P F l s r 9 9 9 9 9 9 6 , + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX+ - 5 q q q q w r 9 r q q y a i s b z l l d a i i 9 9 9 9 9 9 9 9 9 9 5 > % UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXX * > 3 q q q q q q q q q 9 q 9 q q 8 q 9 8 9 q 9 8 q 9 5 , = o UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX + & : 1 3 6 q q q 9 q q 9 q q 9 8 2 2 : = # . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", +"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX" +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/images/split_card.sh Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,148 @@ +#!/bin/sh +#This script split an image with cards, used to split cards from the Tarot deck found on Wikimedia Commons +#This script work with any resolution on the initial image +#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/>. + +dest_dir=cards + +get_face_name() +{ + if [ $1 -le 10 ] + then + echo $1 + else + case $1 in + 11) echo valet;; + 12) echo cavalier;; + 13) echo dame;; + 14) echo roi;; + esac + fi +} + +get_card_name() +{ + if [ $1 -le 21 ] + then + echo "atout_$1" + elif [ $1 -eq 22 ]; then + echo "atout_excuse" + elif [ $1 -le 36 ]; then + echo "pique_$(get_face_name $(($1-22)))" + elif [ $1 -le 50 ]; then + echo "coeur_$(get_face_name $(($1-36)))" + elif [ $1 -le 64 ]; then + echo "carreau_$(get_face_name $(($1-50)))" + else + echo "trefle_$(get_face_name $(($1-64)))" + fi +} + +#We check the version of convert +CONVERT_VER=`convert --version | grep Version | grep -o "[0-9]\.[0-9]\.[0-9]"` +CONVERT_MAJOR=`echo $CONVERT_VER | cut -d . -f 1` +CONVERT_MINOR=`echo $CONVERT_VER | cut -d . -f 2` +CONVERT_REV=`echo $CONVERT_VER | cut -d . -f 3` + +if [ $CONVERT_MAJOR -lt 6 -o $CONVERT_MAJOR -eq 6 -a $CONVERT_MINOR -lt 6 ] +then + echo "ImageMagick convert must be at least version 6.6.0 (current: $CONVERT_VER)" + exit 1 +fi + +SYNTAXE="Split card image\nsyntaxe: $0 image_to_split.ext" + +if [ $# -ne 1 ] +then + echo $SYNTAXE + exit 1 +fi + +echo `file -b --mime-type $1` | grep image 2>&1 > /dev/null + +if [ $? -ne 0 ] +then + echo "target file is not an image" + exit 1 +fi + +current=`pwd` +#TODO: check directory presence +#echo "making directory" +if test -e $dest_dir +then + if test -n "`ls -A $dest_dir`" + then + echo "$dest_dir directory exists and is not empty !" + exit 1 + fi +else + mkdir $dest_dir +fi +echo "splitting cards" +convert Tarotcards.jpg -bordercolor black -crop 14x6@ -fuzz 50% -trim $dest_dir/card_%02d.png 2>/dev/null +cd $dest_dir + +#POST PROCESSING + +nb_files=`ls -A1 card*png | wc -l` +num=0 +idx=0 +max_w=0 +max_h=0 +deleted="" +for file in card*png +do + num=$((num+1)) + size=`stat -c%s $file` + width=`identify -format "%w" $file` + height=`identify -format "%h" $file` + + if [ $width -gt $max_w ] + then + max_w=$width + fi + + if [ $height -gt $max_h ] + then + max_h=$height + fi + + echo -n "post processing file [$file] ($num/$nb_files) | " + echo -n `echo "scale=2;$num/$nb_files*100" | bc`% + echo -n "\r" + + if test $size -lt 1000 + then #we delete empty files (areas without card on the initial picture) + deleted="$deleted$file\n" + rm -f $file + else + idx=$((idx+1)) + #We use transparency for the round corners + mogrify -fuzz 80% -fill none -draw "matte 0,0 floodfill" \ + -draw "matte $((width-1)),0 floodfill"\ + -draw "matte 0,$((height-1)) floodfill"\ + -draw "matte $((width-1)),$((height-1)) floodfill"\ + $file + #Time to rename the cards + mv "$file" "$(get_card_name $idx).png" + + fi +done +echo "\nEmpty files deleted:\n$deleted" +echo "\nBiggest size: ${max_w}X${max_h}" +cd "$current" +echo "DONE :)"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/main_window.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,498 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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 quick_frontend.quick_chat_list import QuickChatList +from quick_frontend.quick_app import QuickApp +from quick_frontend.quick_contact_management import QuickContactManagement +import wx +from contact_list import ContactList +from chat import Chat +from param import Param +from xmlui import XMLUI +from gateways import GatewaysManager +from profile import Profile +from profile_manager import ProfileManager +import gobject +import os.path +import pdb +from tools.jid import JID +from logging import debug, info, warning, error +import constants + +idCONNECT,\ +idDISCONNECT,\ +idEXIT,\ +idABOUT,\ +idPARAM,\ +idADD_CONTACT,\ +idREMOVE_CONTACT,\ +idSHOW_PROFILE,\ +idJOIN_ROOM,\ +idFIND_GATEWAYS = range(10) + +class ChatList(QuickChatList): + """This class manage the list of chat windows""" + + def __init__(self, host): + QuickChatList.__init__(self, host) + + def createChat(self, target): + return Chat(target, self.host) + +class MainWindow(wx.Frame, QuickApp): + """main app window""" + + def __init__(self): + wx.Frame.__init__(self,None, title="SàT Wix", size=(300,500)) + self.CM = QuickContactManagement() #FIXME: not the best place + + #sizer + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.sizer) + + #Frame elements + self.contactList = ContactList(self, self) + self.contactList.registerActivatedCB(self.onContactActivated) + self.contactList.Hide() + self.sizer.Add(self.contactList, 1, flag=wx.EXPAND) + + self.chat_wins=ChatList(self) + self.CreateStatusBar() + + #ToolBar + self.tools=self.CreateToolBar() + self.statusBox = wx.ComboBox(self.tools, -1, "Online", choices=[status[1] for status in const_STATUS], + style=wx.CB_DROPDOWN | wx.CB_READONLY) + self.tools.AddControl(self.statusBox) + self.tools.AddSeparator() + self.statusTxt=wx.TextCtrl(self.tools, -1, style = wx.TE_PROCESS_ENTER) + self.tools.AddControl(self.statusTxt) + self.Bind(wx.EVT_COMBOBOX, self.onStatusChange, self.statusBox) + self.Bind(wx.EVT_TEXT_ENTER, self.onStatusChange, self.statusTxt) + self.tools.Disable() + + #tray icon + ticon = wx.Icon(IMAGE_DIR+'/crystal/tray_icon.xpm', wx.BITMAP_TYPE_XPM) + self.tray_icon = wx.TaskBarIcon() + self.tray_icon.SetIcon(ticon, _("Wix jabber client")) + wx.EVT_TASKBAR_LEFT_UP(self.tray_icon, self.onTrayClick) + + + #events + self.Bind(wx.EVT_CLOSE, self.onClose, self) + + + QuickApp.__init__(self) + + #menus + self.createMenus() + for i in range(self.menuBar.GetMenuCount()): + self.menuBar.EnableTop(i, False) + + #profile panel + self.profile_pan = ProfileManager(self) + self.sizer.Add(self.profile_pan, 1, flag=wx.EXPAND) + + self.postInit() + + self.Show() + + def plug_profile(self, profile_key='@DEFAULT@'): + """Hide profile panel then plug profile""" + debug (_('plugin profile %s' % profile_key)) + self.profile_pan.Hide() + self.contactList.Show() + self.sizer.Layout() + for i in range(self.menuBar.GetMenuCount()): + self.menuBar.EnableTop(i, True) + super(MainWindow, self).plug_profile(profile_key) + + def createMenus(self): + info(_("Creating menus")) + connectMenu = wx.Menu() + connectMenu.Append(idCONNECT, _("&Connect CTRL-c"),_(" Connect to the server")) + connectMenu.Append(idDISCONNECT, _("&Disconnect CTRL-d"),_(" Disconnect from the server")) + connectMenu.Append(idPARAM,_("&Parameters"),_(" Configure the program")) + connectMenu.AppendSeparator() + connectMenu.Append(idABOUT,_("A&bout"),_(" About %s") % APP_NAME) + connectMenu.Append(idEXIT,_("E&xit"),_(" Terminate the program")) + contactMenu = wx.Menu() + contactMenu.Append(idADD_CONTACT, _("&Add contact"),_(" Add a contact to your list")) + contactMenu.Append(idREMOVE_CONTACT, _("&Remove contact"),_(" Remove the selected contact from your list")) + contactMenu.AppendSeparator() + contactMenu.Append(idSHOW_PROFILE, _("&Show profile"), _(" Show contact's profile")) + communicationMenu = wx.Menu() + communicationMenu.Append(idJOIN_ROOM, _("&Join Room"),_(" Join a Multi-User Chat room")) + communicationMenu.Append(idFIND_GATEWAYS, _("&Find Gateways"),_(" Find gateways to legacy IM")) + self.menuBar = wx.MenuBar() + self.menuBar.Append(connectMenu,_("&General")) + self.menuBar.Append(contactMenu,_("&Contacts")) + self.menuBar.Append(communicationMenu,_("&Communication")) + self.SetMenuBar(self.menuBar) + + #additionals menus + #FIXME: do this in a more generic way (in quickapp) + add_menus = self.bridge.getMenus() + for menu in add_menus: + category,item,type = menu + assert(type=="NORMAL") #TODO: manage other types + menu_idx = self.menuBar.FindMenu(category) + current_menu = None + if menu_idx == wx.NOT_FOUND: + #the menu is new, we create it + current_menu = wx.Menu() + self.menuBar.Append(current_menu, category) + else: + current_menu = self.menuBar.GetMenu(menu_idx) + assert(current_menu != None) + item_id = wx.NewId() + help_string = self.bridge.getMenuHelp(category, item, type) + current_menu.Append(item_id, item, help = help_string) + #now we register the event + def event_answer(e): + id = self.bridge.callMenu(category, item, type, self.profile) + self.current_action_ids.add(id) + wx.EVT_MENU(self, item_id, event_answer) + + + #events + wx.EVT_MENU(self, idCONNECT, self.onConnectRequest) + wx.EVT_MENU(self, idDISCONNECT, self.onDisconnectRequest) + wx.EVT_MENU(self, idPARAM, self.onParam) + wx.EVT_MENU(self, idABOUT, self.onAbout) + wx.EVT_MENU(self, idEXIT, self.onExit) + wx.EVT_MENU(self, idADD_CONTACT, self.onAddContact) + wx.EVT_MENU(self, idREMOVE_CONTACT, self.onRemoveContact) + wx.EVT_MENU(self, idSHOW_PROFILE, self.onShowProfile) + wx.EVT_MENU(self, idJOIN_ROOM, self.onJoinRoom) + wx.EVT_MENU(self, idFIND_GATEWAYS, self.onFindGateways) + + + def newMessage(self, from_jid, msg, type, to_jid, profile): + QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile) + + def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile): + super(MainWindow, self).roomJoined(room_id, room_service, room_nicks, user_nick, profile) + + def showAlert(self, message): + # TODO: place this in a separate class + popup=wx.PopupWindow(self) + ### following code come from wxpython demo + popup.SetBackgroundColour("CADET BLUE") + st = wx.StaticText(popup, -1, message, pos=(10,10)) + sz = st.GetBestSize() + popup.SetSize( (sz.width+20, sz.height+20) ) + x=(wx.DisplaySize()[0]-popup.GetSize()[0])/2 + popup.SetPosition((x,0)) + popup.Show() + wx.CallLater(5000,popup.Destroy) + + def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None): + if type == 'info': + flags = wx.OK | wx.ICON_INFORMATION + elif type == 'error': + flags = wx.OK | wx.ICON_ERROR + elif type == 'yes/no': + flags = wx.YES_NO | wx.ICON_QUESTION + else: + flags = wx.OK | wx.ICON_INFORMATION + error(_('unmanaged dialog type: %s'), type) + dlg = wx.MessageDialog(self, message, title, flags) + answer = dlg.ShowModal() + dlg.Destroy() + if answer_cb: + data = [answer_data] if answer_data else [] + answer_cb(True if (answer == wx.ID_YES or answer == wx.ID_OK) else False, *data) + + def setStatusOnline(self, online=True): + """enable/disable controls, must be called when local user online status change""" + if online: + self.SetStatusText(msgONLINE) + self.tools.Enable() + else: + self.SetStatusText(msgOFFLINE) + self.tools.Disable() + return + + def askConfirmation(self, type, id, data): + #TODO: refactor this in QuickApp + debug (_("Confirmation asked")) + answer_data={} + if type == "FILE_TRANSFERT": + debug (_("File transfert confirmation asked")) + dlg = wx.MessageDialog(self, _("The contact %(jid)s wants to send you the file %(filename)s\nDo you accept ?") % {'jid':data["from"], 'filename':data["filename"]}, + _('File Request'), + wx.YES_NO | wx.ICON_QUESTION + ) + answer=dlg.ShowModal() + if answer==wx.ID_YES: + filename = wx.FileSelector(_("Where do you want to save the file ?"), flags = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) + if filename: + answer_data["dest_path"] = filename + self.bridge.confirmationAnswer(id, True, answer_data) + self.waitProgress(id, _("File Transfer"), _("Copying %s") % os.path.basename(filename)) + else: + answer = wx.ID_NO + if answer==wx.ID_NO: + self.bridge.confirmationAnswer(id, False, answer_data) + + dlg.Destroy() + + elif type == "YES/NO": + debug (_("Yes/No confirmation asked")) + dlg = wx.MessageDialog(self, data["message"], + _('Confirmation'), + wx.YES_NO | wx.ICON_QUESTION + ) + answer=dlg.ShowModal() + if answer==wx.ID_YES: + self.bridge.confirmationAnswer(id, True, {}) + if answer==wx.ID_NO: + self.bridge.confirmationAnswer(id, False, {}) + + dlg.Destroy() + + def actionResult(self, type, id, data): + debug (_("actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]") % {'type':type, 'id':id, 'data':data}) + if not id in self.current_action_ids: + debug (_('unknown id, ignoring')) + return + if type == "SUPPRESS": + self.current_action_ids.remove(id) + elif type == "SUCCESS": + self.current_action_ids.remove(id) + dlg = wx.MessageDialog(self, data["message"], + _('Success'), + wx.OK | wx.ICON_INFORMATION + ) + dlg.ShowModal() + dlg.Destroy() + elif type == "ERROR": + self.current_action_ids.remove(id) + dlg = wx.MessageDialog(self, data["message"], + _('Error'), + wx.OK | wx.ICON_ERROR + ) + dlg.ShowModal() + dlg.Destroy() + elif type == "XMLUI": + self.current_action_ids.remove(id) + debug (_("XML user interface received")) + misc = {} + #FIXME FIXME FIXME: must clean all this crap ! + title = _('Form') + if data['type'] == _('registration'): + title = _('Registration') + misc['target'] = data['target'] + misc['action_back'] = self.bridge.gatewayRegister + XMLUI(self, title=title, xml_data = data['xml'], misc = misc) + elif type == "RESULT": + self.current_action_ids.remove(id) + if self.current_action_ids_cb.has_key(id): + callback = self.current_action_ids_cb[id] + del self.current_action_ids_cb[id] + callback(data) + elif type == "DICT_DICT": + self.current_action_ids.remove(id) + if self.current_action_ids_cb.has_key(id): + callback = self.current_action_ids_cb[id] + del self.current_action_ids_cb[id] + callback(data) + else: + error (_("FIXME FIXME FIXME: type [%s] not implemented") % type) + raise NotImplementedError + + + + def progressCB(self, id, title, message): + data = self.bridge.getProgress(id) + if data: + if not self.pbar: + #first answer, we must construct the bar + self.pbar = wx.ProgressDialog(title, message, float(data['size']), None, + wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME | wx.PD_REMAINING_TIME) + self.pbar.finish_value = float(data['size']) + + self.pbar.Update(int(data['position'])) + elif self.pbar: + self.pbar.Update(self.pbar.finish_value) + return + + wx.CallLater(10, self.progressCB, id, title, message) + + def waitProgress (self, id, title, message): + self.pbar = None + wx.CallLater(10, self.progressCB, id, title, message) + + + + ### events ### + + def onContactActivated(self, jid): + debug (_("onContactActivated: %s"), jid) + if self.chat_wins[jid.short].IsShown(): + self.chat_wins[jid.short].Hide() + else: + self.chat_wins[jid.short].Show() + + def onConnectRequest(self, e): + self.bridge.connect(self.profile) + + def onDisconnectRequest(self, e): + self.bridge.disconnect(self.profile) + + def __updateStatus(self): + show = filter(lambda x:x[1] == self.statusBox.GetValue(), const_STATUS)[0][0] + status = self.statusTxt.GetValue() + self.bridge.setPresence(show=show, statuses={'default':status}, profile_key=self.profile) #FIXME: manage multilingual statuses + + def onStatusChange(self, e): + debug(_("Status change request")) + self.__updateStatus() + + def onParam(self, e): + debug(_("Param request")) + #xmlui = self.bridge.getParamsUI(self.profile) + #XMLUI(self, xml_data = xmlui) + param=Param(self) + + def onAbout(self, e): + about = wx.AboutDialogInfo() + about.SetName(APP_NAME) + about.SetVersion (unicode(self.bridge.getVersion())) + about.SetCopyright(u"(C) 2009,2010 Jérôme Poisson aka Goffi") + about.SetDescription( _(u"%(name)s is a SàT (Salut à Toi) frontend\n"+ + u"%(name)s is based on WxPython, and is the standard graphic interface of SàT") % {'name':APP_NAME}) + about.SetWebSite(("http://www.goffi.org", "Goffi's non-hebdo (french)")) + about.SetDevelopers([ "Goffi (Jérôme Poisson)"]) + try: + with open(LICENCE_PATH,"r") as licence: + about.SetLicence(''.join(licence.readlines())) + except: + pass + + wx.AboutBox(about) + + def onExit(self, e): + self.Close() + + def onAddContact(self, e): + debug(_("Add contact request")) + dlg = wx.TextEntryDialog( + self, _('Please enter new contact JID'), + _('Adding a contact'), _('name@server.tld')) + + if dlg.ShowModal() == wx.ID_OK: + jid=JID(dlg.GetValue()) + if jid.is_valid(): + self.bridge.addContact(jid.short, profile_key=self.profile) + else: + error (_("'%s' is an invalid JID !"), jid) + #TODO: notice the user + + dlg.Destroy() + + def onRemoveContact(self, e): + debug(_("Remove contact request")) + target = self.contactList.getSelection() + if not target: + dlg = wx.MessageDialog(self, _("You haven't selected any contact !"), + _('Error'), + wx.OK | wx.ICON_ERROR + ) + dlg.ShowModal() + dlg.Destroy() + return + + dlg = wx.MessageDialog(self, _("Are you sure you want to delete %s from your roster list ?") % target.short, + _('Contact suppression'), + wx.YES_NO | wx.ICON_QUESTION + ) + + if dlg.ShowModal() == wx.ID_YES: + info(_("Unsubscribing %s presence"), target.short) + self.bridge.delContact(target.short, profile_key=self.profile) + + dlg.Destroy() + + def onShowProfile(self, e): + debug(_("Show contact's profile request")) + target = self.contactList.getSelection() + if not target: + dlg = wx.MessageDialog(self, _("You haven't selected any contact !"), + _('Error'), + wx.OK | wx.ICON_ERROR + ) + dlg.ShowModal() + dlg.Destroy() + return + id = self.bridge.getCard(target.short, profile_key=self.profile) + self.current_action_ids.add(id) + self.current_action_ids_cb[id] = self.onProfileReceived + + def onProfileReceived(self, data): + """Called when a profile is received""" + debug (_('Profile received: [%s]') % data) + profile=Profile(self, data) + + def onJoinRoom(self, e): + warning('FIXME: temporary menu, must be improved') + #TODO: a proper MUC room joining dialog with nickname etc + dlg = wx.TextEntryDialog( + self, _("Please enter MUC's JID"), + #_('Entering a MUC room'), 'test@conference.necton2.int') + _('Entering a MUC room'), 'room@muc_service.server.tld') + if dlg.ShowModal() == wx.ID_OK: + room_jid=JID(dlg.GetValue()) + if room_jid.is_valid(): + self.bridge.joinMUC(room_jid.domain, room_jid.node, self.profiles[self.profile]['whoami'].node, self.profile) + else: + error (_("'%s' is an invalid JID !"), room_jid) + + def onFindGateways(self, e): + debug(_("Find Gateways request")) + id = self.bridge.findGateways(self.profiles[self.profile]['whoami'].domain, self.profile) + self.current_action_ids.add(id) + self.current_action_ids_cb[id] = self.onGatewaysFound + + def onGatewaysFound(self, data): + """Called when SàT has found the server gateways""" + target = data['__private__']['target'] + del data['__private__'] + gatewayManager = GatewaysManager(self, data, server=target) + + def onClose(self, e): + QuickApp.onExit(self) + info(_("Exiting...")) + for win in self.chat_wins: + self.chat_wins[win].Destroy() + e.Skip() + + def onTrayClick(self, e): + debug(_("Tray Click")) + if self.IsShown(): + self.Hide() + else: + self.Show() + self.Raise() + e.Skip() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/param.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + + + +import wx +import pdb +from xml.dom import minidom +from logging import debug, info, error +from tools.jid import JID + + +class Param(wx.Frame): + def __init__(self, host, title=_("Configuration")): + super(Param, self).__init__(None, title=title) + + self.host = host + + self.modified = {} # dict of modified data (i.e. what we have to save) + self.ctl_list = {} # usefull to access ctrl, key = (name, category) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.notebook=wx.Notebook(self, -1, style=wx.NB_LEFT) + self.sizer.Add(self.notebook, 1, flag=wx.EXPAND) + self.SetSizer(self.sizer) + self.SetAutoLayout(True) + + #events + self.Bind(wx.EVT_CLOSE, self.onClose, self) + + self.MakeModal() + + for category in self.host.bridge.getParamsCategories(): + self.addCategory(category) + + self.Show() + + def addCategory(self, category): + panel=wx.Panel(self.notebook) + panel.sizer = wx.BoxSizer(wx.VERTICAL) + + cat_dom = minidom.parseString(self.host.bridge.getParamsForCategory(category, profile_key = self.host.profile).encode('utf-8')) + + for param in cat_dom.documentElement.getElementsByTagName("param"): + name = param.getAttribute("name") + label = param.getAttribute("label") + type = param.getAttribute("type") + value = param.getAttribute("value") + sizer = wx.BoxSizer(wx.HORIZONTAL) + if type=="string": + label=wx.StaticText(panel, -1, (label or name)+" ") + ctrl = wx.TextCtrl(panel, -1, value) + sizer.Add(label) + elif type=="password": + label=wx.StaticText(panel, -1, (label or name)+" ") + ctrl = wx.TextCtrl(panel, -1, value, style=wx.TE_PASSWORD) + sizer.Add(label) + elif type=="bool": + ctrl = wx.CheckBox(panel, -1, label or name, style = wx.CHK_2STATE) + ctrl.SetValue(value=="true") + elif type=="button": + ctrl = wx.Button(panel, -1, value) + ctrl.callback_id = param.getAttribute("callback_id") + else: + error(_("FIXME FIXME FIXME")) #FIXME ! + raise NotImplementedError + if name: + ctrl.param_id=(name, category) + self.ctl_list[(name, category)] = ctrl + sizer.Add(ctrl, 1, flag=wx.EXPAND) + panel.sizer.Add(sizer, flag=wx.EXPAND) + + if type=="string" or type=="password": + panel.Bind(wx.EVT_TEXT, self.onTextChanged, ctrl) + elif type=="bool": + panel.Bind(wx.EVT_CHECKBOX, self.onCheckBoxClicked, ctrl) + elif type=="button": + panel.Bind(wx.EVT_BUTTON, self.onButtonClicked, ctrl) + + panel.SetSizer(panel.sizer) + panel.SetAutoLayout(True) + self.notebook.AddPage(panel, category) + cat_dom.unlink() + + def onTextChanged(self, event): + """Called when a string paramater is modified""" + self.modified[event.GetEventObject().param_id]=event.GetString() + + ### FIXME # Some hacks for better presentation, should be generic # FIXME ### + if event.GetEventObject().param_id == ('JabberID', 'Connection'): + domain = JID(event.GetString()).domain + self.ctl_list[('Server', 'Connection')].SetValue(domain) + self.modified[('Server', 'Connection')] = domain + + event.Skip() + + def onCheckBoxClicked(self, event): + """Called when a bool paramater is modified""" + self.modified[event.GetEventObject().param_id]="true" if event.GetEventObject().GetValue() else "false" + event.Skip() + + def onButtonClicked(self, event): + """Called when a button paramater is modified""" + self.__save_parameters() + name, category = event.GetEventObject().param_id + callback_id = event.GetEventObject().callback_id + data = {"name":name, "category":category, "callback_id":callback_id} + id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile) + self.host.current_action_ids.add(id) + event.Skip() + + def __save_parameters(self): + for param in self.modified: + self.host.bridge.setParam(param[0], self.modified[param], param[1], profile_key = self.host.profile) + self.modified.clear() + + def onClose(self, event): + """Close event: we have to save the params.""" + debug(_("close")) + #now we save the modifier params + self.__save_parameters() + + self.MakeModal(False) + event.Skip() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/profile.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + +import wx +import pdb +from logging import debug, info, error +from tools.jid import JID + + +class Profile(wx.Frame): + """This class is used to show/modify profile given by SàT""" + + def __init__(self, host, data, title="Profile"): + super(Profile, self).__init__(None, title=title) + self.host = host + + self.name_dict = { 'fullname': _('Full Name'), + 'nick' : _('Nickname'), + 'birthday' : _('Birthday'), + 'phone' : _('Phone #'), + 'website' : _('Website'), + 'email' : _('E-mail'), + 'avatar' : _('Avatar') + } + self.ctl_list = {} # usefull to access ctrl, key = (name) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.notebook=wx.Notebook(self, -1) + self.sizer.Add(self.notebook, 1, flag=wx.EXPAND) + self.SetSizer(self.sizer) + self.SetAutoLayout(True) + + #events + self.Bind(wx.EVT_CLOSE, self.onClose, self) + + self.MakeModal() + self.showData(data) + self.Show() + + def showData(self, data): + flags = wx.TE_READONLY + + #General tab + generaltab = wx.Panel(self.notebook) + sizer = wx.FlexGridSizer(cols=2) + sizer.AddGrowableCol(1) + generaltab.SetSizer(sizer) + generaltab.SetAutoLayout(True) + for field in ['fullname','nick', 'birthday', 'phone', 'website', 'email']: + value = data[field] if data.has_key(field) else '' + label=wx.StaticText(generaltab, -1, self.name_dict[field]+": ") + sizer.Add(label) + self.ctl_list[field] = wx.TextCtrl(generaltab, -1, value, style = flags) + sizer.Add(self.ctl_list[field], 1, flag = wx.EXPAND) + #Avatar + if data.has_key('avatar'): + filename = self.host.bridge.getAvatarFile(data['avatar']) + label=wx.StaticText(generaltab, -1, self.name_dict['avatar']+": ") + sizer.Add(label) + img = wx.Image(filename).ConvertToBitmap() + self.ctl_list['avatar'] = wx.StaticBitmap(generaltab, -1, img) + sizer.Add(self.ctl_list['avatar'], 0) + + + + self.notebook.AddPage(generaltab, _("General")) + + + def onClose(self, event): + """Close event""" + debug(_("close")) + self.MakeModal(False) + event.Skip() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/profile_manager.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + + + +import wx +import pdb +from logging import debug, info, error +from tools.jid import JID +import pdb + + +class ProfileManager(wx.Panel): + def __init__(self, host): + super(ProfileManager, self).__init__(host) + self.host = host + + #self.sizer = wx.FlexGridSizer(cols=2) + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.sizer) + + profiles = self.host.bridge.getProfilesList() + self.profile_name = wx.ComboBox(self, -1, style=wx.CB_READONLY|wx.CB_SORT) + self.__refillProfiles() + self.Bind(wx.EVT_COMBOBOX, self.onProfileChange) + self.panel_id = wx + + self.sizer.Add(wx.Window(self, -1), 1) + self.sizer.Add(wx.StaticText(self, -1, _("Profile:")), 0, flag=wx.ALIGN_CENTER) + self.sizer.Add(self.profile_name, 0, flag=wx.ALIGN_CENTER) + button_panel = wx.Panel(self) + button_panel.sizer = wx.BoxSizer(wx.HORIZONTAL) + button_panel.SetSizer(button_panel.sizer) + button_new = wx.Button(button_panel, -1, _("New")) + button_del = wx.Button(button_panel, -1, _("Delete")) + button_panel.sizer.Add(button_new) + button_panel.sizer.Add(button_del) + self.sizer.Add(button_panel, flag=wx.CENTER) + self.Bind(wx.EVT_BUTTON, self.onNewProfile, button_new) + self.Bind(wx.EVT_BUTTON, self.onDeleteProfile, button_del) + + login_box = wx.StaticBox(self, -1, _("Login")) + self.login_sizer = wx.StaticBoxSizer(login_box, wx.VERTICAL) + self.sizer.Add(self.login_sizer, 1, wx.EXPAND | wx.ALL) + self.login_jid = wx.TextCtrl(self, -1) + self.login_sizer.Add(wx.StaticText(self, -1, "JID:"), 0, flag=wx.ALIGN_CENTER) + self.login_sizer.Add(self.login_jid, flag=wx.EXPAND) + self.login_pass = wx.TextCtrl(self, -1, style = wx.TE_PASSWORD) + self.login_sizer.Add(wx.StaticText(self, -1, _("Password:")), 0, flag=wx.ALIGN_CENTER) + self.login_sizer.Add(self.login_pass, flag=wx.EXPAND) + + loggin_button = wx.Button(self, -1, _("Connect")) + self.Bind(wx.EVT_BUTTON, self.onConnectButton, loggin_button) + self.login_sizer.Add(loggin_button, flag=wx.ALIGN_CENTER) + + self.sizer.Add(wx.Window(self, -1), 1) + + #Now we can set the default value + self.__setDefault() + + + def __setDefault(self): + profile_default = self.host.bridge.getProfileName("@DEFAULT@") + if profile_default: + self.profile_name.SetValue(profile_default) + self.onProfileChange(None) + + def __refillProfiles(self): + """Update profiles with current names. Must be called after a profile change""" + self.profile_name.Clear() + profiles = self.host.bridge.getProfilesList() + profiles.sort() + for profile in profiles: + self.profile_name.Append(profile) + + + def onNewProfile(self, event): + dlg = wx.TextEntryDialog(self, _("Please enter the new profile name"), _("New profile"), style = wx.OK | wx.CANCEL) + if dlg.ShowModal() == wx.ID_OK: + name = dlg.GetValue() + if name: + if name[0]=='@': + wx.MessageDialog(self, _("A profile name can't start with a @"), _("Bad profile name"), wx.ICON_ERROR).ShowModal() + else: + profile = self.host.bridge.createProfile(name) + self.__refillProfiles() + self.profile_name.SetValue(name) + dlg.Destroy() + + def onDeleteProfile(self, event): + name = self.profile_name.GetValue() + if not name: + return + dlg = wx.MessageDialog(self, _("Are you sure to delete the profile [%s]") % name, _("Confirmation"), wx.ICON_QUESTION | wx.YES_NO) + if dlg.ShowModal() == wx.ID_YES: + self.host.bridge.deleteProfile(name) + self.__refillProfiles() + self.__setDefault() + dlg.Destroy() + + def onProfileChange(self, event): + """Called when a profile is choosen in the combo box""" + jabberID = self.host.bridge.getParamA("JabberID", "Connection", profile_key=self.profile_name.GetValue()) + password = self.host.bridge.getParamA("Password", "Connection", profile_key=self.profile_name.GetValue()) + self.login_jid.SetValue(jabberID) + self.login_pass.SetValue(password) + + def onConnectButton(self, event): + """Called when the Connect button is pressed""" + name = self.profile_name.GetValue() + if not name: + wx.MessageDialog(self, _("You must select a profile or create a new one before connecting"), _("No profile selected"), wx.ICON_ERROR).ShowModal() + return + if name[0]=='@': + wx.MessageDialog(self, _("A profile name can't start with a @"), _("Bad profile name"), wx.ICON_ERROR).ShowModal() + return + profile = self.host.bridge.getProfileName(name) + assert(profile) + old_jid = self.host.bridge.getParamA("JabberID", "Connection", profile_key=profile) + old_pass = self.host.bridge.getParamA("Password", "Connection", profile_key=profile) + new_jid = self.login_jid.GetValue() + new_pass = self.login_pass.GetValue() + if old_jid != new_jid: + debug(_('Saving new JID and server')) + self.host.bridge.setParam("JabberID", new_jid, "Connection", profile) + self.host.bridge.setParam("Server", JID(new_jid).domain, "Connection", profile) + if old_pass != new_pass: + debug(_('Saving new password')) + self.host.bridge.setParam("Password", new_pass, "Connection", profile) + self.host.plug_profile(profile) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/wix Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,48 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + + +import wx +from sat_bridge_frontend.DBus import DBusBridgeFrontend +import pdb +import logging +from logging import debug, info, error +from main_window import MainWindow + +### logging configuration FIXME: put this elsewhere ### +logging.basicConfig(level=logging.DEBUG, + format='%(message)s') +### + + +class SATApp(wx.App): + def __init__(self, redirect=False, filename=None, useBestVisual=False, clearSigInt=True): + super(SATApp,self).__init__(redirect, filename, useBestVisual, clearSigInt) + + def OnInit(self): + self.main = MainWindow() + self.main.Show(True) + self.SetTopWindow(self.main) + return True + + +sat = SATApp() +sat.MainLoop()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/wix/xmlui.py Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +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/>. +""" + + + +import wx +import pdb +from xml.dom import minidom +from logging import debug, info, warning, error +from tools.jid import JID + + +class XMLUI(wx.Frame): + """Create an user interface from a SàT xml""" + + def __init__(self, host, xml_data='', title="Form", options=[], misc={}): + style = wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX if 'NO_CANCEL' in options else wx.DEFAULT_FRAME_STYLE #FIXME: gof: Q&D tmp hack + super(XMLUI, self).__init__(None, title=title, style=style) + + self.host = host + self.options = options + self.misc = misc + self.ctrl_list = {} # usefull to access ctrl + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.sizer) + self.SetAutoLayout(True) + + #events + if not 'NO_CANCEL' in self.options: + self.Bind(wx.EVT_CLOSE, self.onClose, self) + + self.MakeModal() + + self.constructUI(xml_data) + + self.Show() + + def __parseElems(self, node, parent): + """Parse elements inside a <layout> tags, and add them to the parent sizer""" + for elem in node.childNodes: + if elem.nodeName != "elem": + message=_("Unmanaged tag") + error(message) + raise Exception(message) + _proportion = 0 + id = elem.getAttribute("id") + name = elem.getAttribute("name") + type = elem.getAttribute("type") + value = elem.getAttribute("value") if elem.hasAttribute('value') else u'' + if type=="empty": + ctrl = wx.Window(parent, -1) + elif type=="text": + try: + value = elem.childNodes[0].wholeText + except IndexError: + warning (_("text node has no child !")) + ctrl = wx.StaticText(parent, -1, value) + elif type=="label": + ctrl = wx.StaticText(parent, -1, value+": ") + elif type=="string": + ctrl = wx.TextCtrl(parent, -1, value) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + _proportion = 1 + elif type=="password": + ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_PASSWORD) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + _proportion = 1 + elif type=="textbox": + ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_MULTILINE) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + _proportion = 1 + elif type=="bool": + ctrl = wx.CheckBox(panel, -1, "", style = wx.CHK_2STATE) + ctrl.SetValue(value=="true") + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + _proportion = 1 + elif type=="list": + style=wx.LB_MULTIPLE if elem.getAttribute("multi")=='yes' else wx.LB_SINGLE + ctrl = wx.ListBox(parent, -1, choices=[option.getAttribute("value") for option in elem.getElementsByTagName("option")], style=style) + self.ctrl_list[name] = ({'type':type, 'control':ctrl}) + _proportion = 1 + elif type=="button": + callback_id = elem.getAttribute("callback_id") + ctrl = wx.Button(parent, -1, value) + ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")]) + parent.Bind(wx.EVT_BUTTON, self.onButtonClicked, ctrl) + else: + error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type) #FIXME ! + raise NotImplementedError + parent.sizer.Add(ctrl, _proportion, flag=wx.EXPAND) + + def __parseChilds(self, parent, current_param, elem, wanted = ['layout']): + """Recursively parse childNodes of an elemen + @param parent: parent wx.Window + @param current_param: current wx.Window (often wx.Panel) or None if we must create one + @param elem: element from which childs will be parsed + @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant""" + for node in elem.childNodes: + if wanted and not node.nodeName in wanted: + raise Exception("Invalid XMLUI") #TODO: make a custom exception + if node.nodeName == "layout": + _proportion = 0 + type = node.getAttribute('type') + if type == "tabs": + current = wx.Notebook(parent, -1, style=wx.NB_LEFT if self.type=='param' else 0) + self.__parseChilds(current, None, node, ['category']) + _proportion = 1 + else: + if current_param == None: + current = wx.Panel(parent, -1) + else: + current = current_param + if type == "vertical": + current.sizer = wx.BoxSizer(wx.VERTICAL) + elif type == "pairs": + current.sizer = wx.FlexGridSizer(cols=2) + current.sizer.AddGrowableCol(1) #The growable column need most of time to be the right one in pairs + else: + warning(_("Unknown layout, using default one")) + current.sizer = wx.BoxSizer(wx.VERTICAL) + current.SetSizer(current.sizer) + self.__parseElems(node, current) + if parent: + parent.sizer.Add(current, _proportion, flag=wx.EXPAND) + elif node.nodeName == "category": + name = node.getAttribute('name') + label = node.getAttribute('label') + if not node.nodeName in wanted or not name or not isinstance(parent,wx.Notebook): + raise Exception("Invalid XMLUI") #TODO: make a custom exception + notebook = parent + tab_panel = wx.Panel(notebook, -1) + tab_panel.sizer = wx.BoxSizer(wx.VERTICAL) + tab_panel.SetSizer(tab_panel.sizer) + notebook.AddPage(tab_panel, label or name) + self.__parseChilds(tab_panel, None, node, ['layout']) + + else: + message=_("Unknown tag") + error(message) + raise Exception(message) #TODO: raise a custom exception here + + + def constructUI(self, xml_data): + panel=wx.Panel(self) + panel.sizer = wx.BoxSizer(wx.VERTICAL) + + cat_dom = minidom.parseString(xml_data.encode('utf-8')) + top= cat_dom.documentElement + self.type = top.getAttribute("type") + self.title = top .getAttribute("title") + if self.title: + self.SetTitle(self.title) + if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']: + raise Exception("Invalid XMLUI") #TODO: make a custom exception + + self.__parseChilds(panel, None, cat_dom.documentElement) + + if self.type == 'form': + dialogButtons = wx.StdDialogButtonSizer() + submitButton = wx.Button(panel,wx.ID_OK, label=_("Submit")) + dialogButtons.AddButton(submitButton) + panel.Bind(wx.EVT_BUTTON, self.onFormSubmitted, submitButton) + if not 'NO_CANCEL' in self.options: + cancelButton = wx.Button(panel,wx.ID_CANCEL) + dialogButtons.AddButton(cancelButton) + panel.Bind(wx.EVT_BUTTON, self.onFormCancelled, cancelButton) + dialogButtons.Realize() + panel.sizer.Add(dialogButtons, flag=wx.ALIGN_CENTER_HORIZONTAL) + + panel.SetSizer(panel.sizer) + panel.SetAutoLayout(True) + panel.sizer.Fit(self) + self.sizer.Add(panel, 1, flag=wx.EXPAND) + cat_dom.unlink() + + ###events + + def onButtonClicked(self, event): + """Called when a button is pushed""" + callback_id, fields = event.GetEventObject().param_id + data = {"callback_id":callback_id} + for field in fields: + ctrl = self.ctrl_list[field] + if isinstance(ctrl['control'], wx.ListBox): + data[field] = '\t'.join([ctrl['control'].GetString(idx) for idx in ctrl['control'].GetSelections()]) + else: + data[field] = ctrl['control'].GetValue() + + id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile) + self.host.current_action_ids.add(id) + event.Skip() + + def onFormSubmitted(self, event): + """Called when submit button is clicked""" + debug(_("Submitting form")) + data = [] + for ctrl_name in self.ctrl_list: + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl['control'], wx.ListBox): + data.append((ctrl_name, ctrl['control'].GetStringSelection())) + elif isinstance(ctrl['control'], wx.CheckBox): + data.append((ctrl_name, "true" if ctrl['control'].GetValue() else "false")) + else: + data.append((ctrl_name, ctrl['control'].GetValue())) + if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned + id = self.misc['action_back']("SUBMIT",self.misc['target'], data) + self.host.current_action_ids.add(id) + elif self.misc.has_key('callback'): + self.misc['callback'](data) + else: + warning (_("The form data is not sent back, the type is not managed properly")) + self.MakeModal(False) + self.Destroy() + + def onFormCancelled(self, event): + """Called when cancel button is clicked""" + debug(_("Cancelling form")) + self.MakeModal(False) + self.Close() + + def onClose(self, event): + """Close event: we have to send the form.""" + debug(_("close")) + self.MakeModal(False) + event.Skip() +
--- a/frontends/wix/card_game.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,254 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - - - -import wx -import os.path, glob -import pdb -from logging import debug, info, error -from tools.jid import JID -from tools.games import TarotCard -from quick_frontend.quick_card_game import QuickCardGame -from xmlui import XMLUI - -CARD_WIDTH = 74 -CARD_HEIGHT = 136 -MIN_WIDTH = 950 #Minimum size of the panel -MIN_HEIGHT = 500 - -class WxCard(TarotCard): - """This class is used to represent a card, graphically and logically""" - - def __init__(self, file): - """@param file: path of the PNG file""" - self.bitmap = wx.Image(file).ConvertToBitmap() - root_name = os.path.splitext(os.path.basename(file))[0] - suit,value = root_name.split('_') - TarotCard.__init__(self, (suit, value)) - print "Carte:",suit, value #, self.bout - - def draw(self, dc, x, y): - """Draw the card on the device context - @param dc: device context - @param x: abscissa - @param y: ordinate""" - dc.DrawBitmap(self.bitmap, x, y, True) - - -class CardPanel(QuickCardGame,wx.Panel): - """This class is used to display the cards""" - - def __init__(self, parent, referee, players, player_nick): - QuickCardGame.__init__(self, parent, referee, players, player_nick) - wx.Panel.__init__(self, parent) - self.SetMinSize(wx.Size(MIN_WIDTH, MIN_HEIGHT)) - self.loadCards("images/cards/") - self.mouse_over_card = None #contain the card to highlight - self.visible_size = CARD_WIDTH/2 #number of pixels visible for cards - self.hand = [] - self.to_show = [] - self.state = None - self.SetBackgroundColour(wx.GREEN) - self.Bind(wx.EVT_SIZE, self.onResize) - self.Bind(wx.EVT_PAINT, self.onPaint) - self.Bind(wx.EVT_MOTION, self.onMouseMove) - self.Bind(wx.EVT_LEFT_UP, self.onMouseClick) - self.parent.host.bridge.tarotGameReady(player_nick, referee, profile_key = self.parent.host.profile) - - def loadCards(self, dir): - """Load all the cards in memory - @param dir: directory where the PNG files are""" - QuickCardGame.loadCards(self) - for file in glob.glob(dir+'/*_*.png'): - card = WxCard(file) - self.cards[card.suit, card.value]=card - self.deck.append(card) - - def newGame(self, hand): - """Start a new game, with given hand""" - QuickCardGame.newGame(self, hand) - self._recalc_ori() - self.Refresh() - - def contratSelected(self, data): - """Called when the contrat has been choosed - @param data: form result""" - debug (_("Contrat choosed")) - contrat = data[0][1] - QuickCardGame.contratSelected(self, contrat) - - def chooseContrat(self, xml_data): - """Called when the player as to select his contrat - @param xml_data: SàT xml representation of the form""" - misc = {'callback': self.contratSelected} - form = XMLUI(self.parent.host, xml_data, title = _('Please choose your contrat'), options = ['NO_CANCEL'], misc = misc) - - def showScores(self, xml_data, winners, loosers): - """Called when the player as to select hist contrat - @param xml_data: SàT xml representation of the form""" - form = XMLUI(self.parent.host, xml_data, title = _('You win \o/') if self.player_nick in winners else _('You loose :('), options = ['NO_CANCEL']) - - def cardsPlayed(self, player, cards): - """A card has been played by player""" - QuickCardGame.cardsPlayed(self, player, cards) - self.Refresh() - - def invalidCards(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - QuickCardGame.invalidCards(self, phase, played_cards, invalid_cards) - - self._recalc_ori() - self.Refresh() - if self._autoplay==None: #No dialog if there is autoplay - wx.MessageDialog(self, _("Cards played are invalid !"), _("Error"), wx.OK | wx.ICON_ERROR).ShowModal() - - def _is_on_hand(self, pos_x, pos_y): - """Return True if the coordinate are on the hand cards""" - if pos_x > self.orig_x and pos_y > self.orig_y \ - and pos_x < self.orig_x + (len(self.hand)+1) * self.visible_size \ - and pos_y < self.end_y: - return True - return False - - def onResize(self, event): - self._recalc_ori() - - def _recalc_ori(self): - """Recalculate origins of hand, must be call when hand size change""" - self.orig_x = (self.GetSizeTuple()[0]-(len(self.hand)+1)*self.visible_size)/2 #where we start to draw cards - self.orig_y = self.GetSizeTuple()[1] - CARD_HEIGHT - 20 - self.end_y = self.orig_y + CARD_HEIGHT - - def onPaint(self, event): - dc = wx.PaintDC(self) - - #We print the names to know who play where TODO: print avatars when available - max_x, max_y = self.GetSize() - border = 10 #border between nick and end of panel - right_y = left_y = 200 - right_width, right_height = dc.GetTextExtent(self.right_nick) - right_x = max_x - right_width - border - left_x = border - top_width, top_height = dc.GetTextExtent(self.top_nick) - top_x = (max_x - top_width) / 2 - top_y = border - dc.DrawText(self.right_nick, right_x, right_y) - dc.DrawText(self.top_nick, top_x, top_y) - dc.DrawText(self.left_nick, left_x, left_y) - - #We draw the played cards: - center_y = 200 #ordinate used as center point - left_x = (max_x - CARD_WIDTH)/2 - CARD_WIDTH - 5 - right_x = (max_x/2) + (CARD_WIDTH/2) + 5 - left_y = right_y = center_y - CARD_HEIGHT/2 - top_x = bottom_x = (max_x - CARD_WIDTH)/2 - top_y = center_y - CARD_HEIGHT - 5 - bottom_y = center_y + 5 - for side in ['left', 'top', 'right', 'bottom']: - card = self.played[getattr(self, side+'_nick')] - if card != None: - card.draw(dc,locals()[side+'_x'], locals()[side+'_y']) - - x=self.orig_x - for card in self.hand: - if (self.state == "play" or self.state == "ecart") and card == self.mouse_over_card \ - or self.state == "ecart" and card in self.selected: - y = self.orig_y - 30 - else: - y = self.orig_y - - card.draw(dc,x,y) - x+=self.visible_size - - if self.to_show: - """There are cards to display in the middle""" - size = len(self.to_show)*(CARD_WIDTH+10)-10 - x = (max_x - size)/2 - for card in self.to_show: - card.draw(dc, x, 150) - x+=CARD_WIDTH+10 - - def onMouseMove(self, event): - pos_x,pos_y = event.GetPosition() - if self._is_on_hand(pos_x, pos_y): - try: - self.mouse_over_card = self.hand[(pos_x-self.orig_x)/self.visible_size] - except IndexError: - self.mouse_over_card = self.hand[-1] - self.Refresh() - else: - self.mouse_over_card = None - self.Refresh() - - def onMouseClick(self, event): - print "mouse click:",event.GetPosition() - pos_x,pos_y = event.GetPosition() - - if self.state == "chien": - self.to_show = [] - self.state = "wait" - return - elif self.state == "wait_for_ecart": - self.state = "ecart" - self.hand.extend(self.to_show) - self.hand.sort() - self.to_show = [] - self._recalc_ori() - self.Refresh() - return - - if self._is_on_hand(pos_x, pos_y): - idx = (pos_x-self.orig_x)/self.visible_size - if idx == len(self.hand): - idx-=1 - if self.hand[idx] == self.mouse_over_card: - if self.state == "ecart": - if self.hand[idx] in self.selected: - self.selected.remove(self.hand[idx]) - else: - self.selected.append(self.hand[idx]) - if len(self.selected) == 6: #TODO: use variable here, as chien len can change with variants - dlg = wx.MessageDialog(self, _("Do you put these cards in chien ?"), _(u"Écart"), wx.YES_NO | wx.ICON_QUESTION) - answer = dlg.ShowModal() - if answer == wx.ID_YES: - ecart = [] - for card in self.selected: - ecart.append((card.suit, card.value)) - self.hand.remove(card) - del self.selected[:] - self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, ecart, profile_key = self.parent.host.profile) - self.state = "wait" - - self._recalc_ori() - self.Refresh() - if self.state == "play": - card = self.hand[idx] - self.parent.host.bridge.tarotGamePlayCards(self.player_nick, self.referee, [(card.suit, card.value)], profile_key = self.parent.host.profile) - del self.hand[idx] - self.state = "wait" - self._recalc_ori() - self.Refresh() - -
--- a/frontends/wix/chat.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,263 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - - - -import wx -import os.path -import time -import pdb -from logging import debug, info, error, warning -from tools.jid import JID -from quick_frontend.quick_chat import QuickChat -from contact_list import ContactList -from card_game import CardPanel - - -idSEND = 1 -idTAROT = 2 - -class Chat(wx.Frame, QuickChat): - """The chat Window for one to one conversations""" - - def __init__(self, target, host, type='one2one'): - wx.Frame.__init__(self, None, title=target, pos=(0,0), size=(400,200)) - QuickChat.__init__(self, target, host, type) - - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.sizer) - - self.splitter = wx.SplitterWindow(self, -1) - self.sizer.Add(self.splitter, 1, flag = wx.EXPAND) - - self.conv_panel = wx.Panel(self.splitter) - self.conv_panel.sizer = wx.BoxSizer(wx.VERTICAL) - self.subjectBox = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_READONLY) - self.chatWindow = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_MULTILINE | wx.TE_RICH | wx.TE_READONLY) - self.textBox = wx.TextCtrl(self.conv_panel, -1, style = wx.TE_PROCESS_ENTER) - self.conv_panel.sizer.Add(self.subjectBox, flag=wx.EXPAND) - self.conv_panel.sizer.Add(self.chatWindow, 1, flag=wx.EXPAND) - self.conv_panel.sizer.Add(self.textBox, 0, flag=wx.EXPAND) - self.conv_panel.SetSizer(self.conv_panel.sizer) - self.splitter.Initialize(self.conv_panel) - self.SetMenuBar(wx.MenuBar()) - - #events - self.Bind(wx.EVT_CLOSE, self.onClose, self) - self.Bind(wx.EVT_TEXT_ENTER, self.onEnterPressed, self.textBox) - - #fonts - self.font={} - self.font["points"] = self.chatWindow.GetFont().GetPointSize() - self.font["family"] = self.chatWindow.GetFont().GetFamily() - - - #misc - self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time - self.setType(self.type) - self.textBox.SetFocus() - self.Hide() #We hide because of the show toggle - - def __createPresents(self): - """Create a list of present people in a group chat""" - self.present_panel = wx.Panel(self.splitter) - self.present_panel.sizer = wx.BoxSizer(wx.VERTICAL) - self.present_panel.SetBackgroundColour(wx.BLUE) - self.present_panel.presents = ContactList(self.present_panel, self.host, type='nicks') - self.present_panel.presents.SetMinSize(wx.Size(80,20)) - self.present_panel.sizer.Add(self.present_panel.presents, 1, wx.EXPAND) - self.present_panel.SetSizer(self.present_panel.sizer) - self.splitter.SplitVertically(self.present_panel, self.conv_panel, 80) - - def setType(self, type): - QuickChat.setType(self, type) - if type is 'group' and not self.splitter.IsSplit(): - self.__createPresents() - self.subjectBox.Show() - self.__eraseMenus() - self.__createMenus_group() - self.sizer.Layout() - elif type is 'one2one' and self.splitter.IsSplit(): - self.splitter.Unsplit(self.present_panel) - del self.present_panel - self.GetMenuBar().Show() - self.subjectBox.Hide() - self.__eraseMenus() - self.__createMenus_O2O() - self.nick = None - else: - self.subjectBox.Hide() - self.__eraseMenus() - self.__createMenus_O2O() - self.historyPrint(profile=self.host.profile) - - def startGame(self, game_type, referee, players): - """Configure the chat window to start a game""" - if game_type=="Tarot": - debug (_("configure chat window for Tarot game")) - self.tarot_panel = CardPanel(self, referee, players, self.nick) - self.sizer.Prepend(self.tarot_panel, 0, flag=wx.EXPAND) - self.sizer.Layout() - self.Fit() - - def getGame(self, game_type): - """Return class managing the game type""" - #TODO: check that the game is launched, and manage errors - if game_type=="Tarot": - return self.tarot_panel - - - def setPresents(self, nicks): - """Set the users presents in the contact list for a group chat - @param nicks: list of nicknames - """ - QuickChat.setPresents(self, nicks) - for nick in nicks: - self.present_panel.presents.replace(nick) - - def replaceUser(self, nick): - """Add user if it is not in the group list""" - debug (_("Replacing user %s") % nick) - if self.type != "group": - error (_("[INTERNAL] trying to replace user for a non group chat window")) - return - QuickChat.replaceUser(self, nick) - self.present_panel.presents.replace(nick) - - def removeUser(self, nick): - """Remove a user from the group list""" - QuickChat.removeUser(self, nick) - self.present_panel.presents.remove(nick) - - def setSubject(self, subject): - """Set title for a group chat""" - QuickChat.setSubject(self, subject) - self.subjectBox.SetValue(subject) - - def __eraseMenus(self): - """erase all menus""" - menuBar = self.GetMenuBar() - for i in range(menuBar.GetMenuCount()): - menuBar.Remove(i) - - def __createMenus_O2O(self): - """create menu bar for one 2 one chat""" - info("Creating menus") - self.__eraseMenus() - menuBar = self.GetMenuBar() - actionMenu = wx.Menu() - actionMenu.Append(idSEND, _("&SendFile CTRL-s"),_(" Send a file to contact")) - menuBar.Append(actionMenu,_("&Action")) - - #events - wx.EVT_MENU(self, idSEND, self.onSendFile) - - def __createMenus_group(self): - """create menu bar for group chat""" - info("Creating menus") - self.__eraseMenus() - menuBar = self.GetMenuBar() - actionMenu = wx.Menu() - actionMenu.Append(idTAROT, _("Start &Tarot game CTRL-t"),_(" Start a Tarot card game")) #tmp - menuBar.Append(actionMenu,_("&Games")) - - #events - wx.EVT_MENU(self, idTAROT, self.onStartTarot) - - def __del__(self): - wx.Frame.__del__(self) - - def onClose(self, event): - """Close event: we only hide the frame.""" - event.Veto() - self.Hide() - - def onEnterPressed(self, event): - """Behaviour when enter pressed in send line.""" - self.host.bridge.sendMessage(self.target.short if self.type=='group' else self.target, - event.GetString(), - type = "groupchat" if self.type=='group' else "chat", - profile_key=self.host.profile) - self.textBox.Clear() - - def __blink(self): - """Do wizzz and buzzz to show window to user or - at least inform him of something new""" - #TODO: use notification system - if not self.IsActive(): - self.RequestUserAttention() - if not self.IsShown(): - self.Show() - - def printMessage(self, from_jid, msg, profile, timestamp=""): - """Print the message with differents colors depending on where it comes from.""" - try: - jid,nick,mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) - except TypeError: - return - print "printMessage, jid=",jid,"type=",self.type - _font_bold = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.BOLD) - _font_normal = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.NORMAL) - _font_italic = wx.Font(self.font["points"], self.font["family"], wx.ITALIC if mymess else wx.NORMAL, wx.NORMAL) - self.chatWindow.SetDefaultStyle(wx.TextAttr("GREY", font=_font_normal)) - msg_time = time.localtime(timestamp or None) - time_format = "%c" if msg_time < self.day_change else "%H:%M" #if the message was sent before today, we print the full date - self.chatWindow.AppendText("[%s]" % time.strftime(time_format, msg_time )) - self.chatWindow.SetDefaultStyle(wx.TextAttr( "BLACK" if mymess else "BLUE", font=_font_bold)) - self.chatWindow.AppendText("[%s] " % nick) - self.chatWindow.SetDefaultStyle(wx.TextAttr("BLACK", font=_font_italic)) - self.chatWindow.AppendText("%s\n" % msg) - if not mymess: - self.__blink() - - def printInfo(self, msg, type='normal'): - """Print general info - @param msg: message to print - @type: one of: - normal: general info like "toto has joined the room" - me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" - """ - _font_bold = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.BOLD) - _font_normal = wx.Font(self.font["points"], self.font["family"], wx.NORMAL, wx.NORMAL) - self.chatWindow.SetDefaultStyle(wx.TextAttr("BLACK", font=_font_bold if type == 'normal' else _font_normal)) - self.chatWindow.AppendText("%s\n" % msg) - if type=="me": - self.__blink() - - ### events ### - - def onSendFile(self, e): - debug(_("Send File")) - filename = wx.FileSelector(_("Choose a file to send"), flags = wx.FD_FILE_MUST_EXIST) - if filename: - debug(_("filename: %s"),filename) - full_jid = self.host.CM.get_full(self.target) - id = self.host.bridge.sendFile(full_jid, filename) - self.host.waitProgress(id, _("File Transfer"), _("Copying %s") % os.path.basename(filename)) - - def onStartTarot(self, e): - debug (_("Starting Tarot game")) - warning (_("FIXME: temporary menu, must be changed")) - if len(self.occupants) != 4: - err_dlg = wx.MessageDialog(self, _("You need to be exactly 4 peoples in the room to start a Tarot game"), _("Can't start game"), style = wx.OK | wx.ICON_ERROR) #FIXME: gof: temporary only, need to choose the people with who the game has to be started - err_dlg.ShowModal() - else: - self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile)
--- a/frontends/wix/constants.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,15 +0,0 @@ -import sys -import __builtin__ - -__builtin__.__dict__['APP_NAME'] = "Wix" -__builtin__.__dict__['LICENCE_PATH'] = "../../COPYING" -__builtin__.__dict__['IMAGE_DIR'] = sys.path[0]+'/images' - -__builtin__.__dict__['msgOFFLINE'] = _("offline") -__builtin__.__dict__['msgONLINE'] = _("online") -__builtin__.__dict__['const_DEFAULT_GROUP'] = "Unclassed" -__builtin__.__dict__['const_STATUS'] = [("", _("Online"), None), - ("chat", _("Free for chat"), "green"), - ("away", _("AFK"), "brown"), - ("dnd", _("DND"), "red"), - ("xa", _("Away"), "red")]
--- a/frontends/wix/contact_list.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,182 +0,0 @@ -import wx -from quick_frontend.quick_contact_list import QuickContactList -from logging import debug, info, error -from cgi import escape -from tools.jid import JID - - -class Group(unicode): - """Class used to recognize groups""" - -class Contact(unicode): - """Class used to recognize groups""" - -class ContactList(wx.SimpleHtmlListBox, QuickContactList): - """Customized control to manage contacts.""" - - def __init__(self, parent, host, type="JID"): - """init the contact list - @param parent: WxWidgets parent of the widget - @param host: wix main app class - @param type: type of contact list: "JID" for the usual big jid contact list - "CUSTOM" for a customized contact list (self.__presentItem must then be overrided) - """ - wx.SimpleHtmlListBox.__init__(self, parent, -1) - QuickContactList.__init__(self, host.CM) - self.host = host - self.type = type - self.__typeSwitch() - self.groups = {} #list contacts in each groups, key = group - self.Bind(wx.EVT_LISTBOX, self.onSelected) - self.Bind(wx.EVT_LISTBOX_DCLICK, self.onActivated) - - def __contains__(self, jid): - return bool(self.__find_idx(jid)) - - def __typeSwitch(self): - if self.type == "JID": - self.__presentItem = self.__presentItemJID - elif type != "CUSTOM": - self.__presentItem = self.__presentItemDefault - - def __find_idx(self, entity): - """Find indexes of given contact (or groups) in contact list, manage jid - @return: list of indexes""" - result=[] - for i in range(self.GetCount()): - if (type(entity) == JID and type(self.GetClientData(i)) == JID and self.GetClientData(i).short == entity.short) or\ - self.GetClientData(i) == entity: - result.append(i) - return result - - def replace(self, contact, groups=None): - debug(_("update %s") % contact) - if not self.__find_idx(contact): - self.add(contact, groups) - else: - for i in self.__find_idx(contact): - self.SetString(i, self.__presentItem(contact)) - - def disconnect(self, contact): - self.remove(contact) #for now, we only show online contacts - - def __eraseGroup(self, group): - """Erase all contacts in group - @param group: group to erase - @return: True if something as been erased""" - erased = False - indexes = self.__find_idx(group) - for idx in indexes: - while idx<self.GetCount()-1 and type(self.GetClientData(idx+1)) != Group: - erased = True - self.Delete(idx+1) - return erased - - - def __presentGroup(self, group): - """Make a nice presentation for the contact groups""" - html = u"""-- [%s] --""" % group - - return html - - def __presentItemDefault(self, contact): - """Make a basic presentation of string contacts in the list.""" - return contact - - def __presentItemJID(self, jid): - """Make a nice presentation of the contact in the list for JID contacts.""" - name = self.CM.getAttr(jid,'name') - nick = self.CM.getAttr(jid,'nick') - show = filter(lambda x:x[0]==self.CM.getAttr(jid,'show'), const_STATUS)[0] - #show[0]==shortcut - #show[1]==human readable - #show[2]==color (or None) - show_html = "<font color='%s'>[%s]</font>" % (show[2], show[1]) if show[2] else "" - status = self.CM.getAttr(jid,'status') or '' - avatar = self.CM.getAttr(jid,'avatar') or IMAGE_DIR+'/empty_avatar.png' - - html = """ - <table border='0'> - <td> - <img height='64' width='64' src='%s' /> - </td> - <td> - <b>%s</b> %s<br /> - <i>%s</i> - </td> - </table> - """ % (avatar, - escape(nick or name or jid.node or jid.short), - show_html, - escape(status)) - - return html - - def clear_contacts(self): - """Clear all the contact list""" - self.Clear() - - def add(self, contact, groups = None): - """add a contact to the list""" - debug (_("adding %s"),contact) - if not groups: - idx = self.Insert(self.__presentItem(contact), 0, contact) - else: - for group in groups: - indexes = self.__find_idx(group) - gp_idx = 0 - if not indexes: #this is a new group, we have to create it - gp_idx = self.Append(self.__presentGroup(group), Group(group)) - else: - gp_idx = indexes[0] - - self.Insert(self.__presentItem(contact), gp_idx+1, contact) - - - - def remove(self, contact): - """remove a contact from the list""" - debug (_("removing %s"), contact) - list_idx = self.__find_idx(contact) - list_idx.reverse() #as we make some deletions, we have to reverse the order - for i in list_idx: - self.Delete(i) - - def onSelected(self, event): - """Called when a contact is selected.""" - data = self.getSelection() - if data == None: #we have a group - first_visible = self.GetVisibleBegin() - group = self.GetClientData(self.GetSelection()) - erased = self.__eraseGroup(group) - if not erased: #the group was already erased, we can add again the contacts - contacts = self.CM.getContFromGroup(group) - contacts.sort() - id_insert = self.GetSelection()+1 - for contact in contacts: - self.Insert(self.__presentItem(contact), id_insert, contact) - self.SetSelection(wx.NOT_FOUND) - self.ScrollToLine(first_visible) - event.Skip(False) - else: - event.Skip() - - def onActivated(self, event): - """Called when a contact is clicked or activated with keyboard.""" - data = self.getSelection() - self.onActivatedCB(data) - event.Skip() - - def getSelection(self): - """Return the selected contact, or an empty string if there is not""" - if self.GetSelection() == wx.NOT_FOUND: - return None - data = self.GetClientData(self.GetSelection()) - if type(data) == Group: - return None - return data - - def registerActivatedCB(self, cb): - """Register a callback with manage contact activation.""" - self.onActivatedCB=cb -
--- a/frontends/wix/gateways.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,169 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - - - -import wx -import pdb -from xml.dom import minidom -from logging import debug, info, error -from tools.jid import JID -from quick_frontend.quick_gateways import QuickGatewaysManager - -class GatewaysManager(wx.Frame,QuickGatewaysManager): - - def __init__(self, host, gateways, title=_("Gateways manager"), server=None): - wx.Frame.__init__(self, None, title=title) - QuickGatewaysManager.__init__(self, host, gateways, server) - - if server: - self.SetTitle(title+" (%s)" % server) - - #Fonts - self.normal_font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL) - self.bold_font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD) - self.italic_font = wx.Font(8, wx.DEFAULT, wx.FONTSTYLE_ITALIC, wx.NORMAL) - self.button_font = wx.Font(6, wx.DEFAULT, wx.NORMAL, wx.BOLD) - - - self.modified = {} # dict of modified data (i.e. what we have to save) - self.ctl_list = {} # usefull to access ctrl, key = (name, category) - - self.sizer = wx.BoxSizer(wx.VERTICAL) - warning = wx.TextCtrl(self, -1, value=self.WARNING_MSG, style = wx.TE_MULTILINE | - wx.TE_READONLY | - wx.TE_LEFT) - warning.SetFont(self.bold_font) - self.sizer.Add(warning, 0, wx.EXPAND) - warning.ShowPosition(0) - self.panel = wx.Panel(self) - self.panel.sizer = wx.FlexGridSizer(cols=5) - self.panel.SetSizer(self.panel.sizer) - self.panel.SetAutoLayout(True) - self.sizer.Add(self.panel, 1, flag=wx.EXPAND) - self.SetSizer(self.sizer) - self.SetAutoLayout(True) - - #events - self.Bind(wx.EVT_CLOSE, self.onClose, self) - - self.MakeModal() - self.panel.sizer.Add(wx.Window(self.panel, -1)) - title_name = wx.StaticText(self.panel, -1, "Name") - title_name.SetFont(self.bold_font) - title_type = wx.StaticText(self.panel, -1, "Type") - title_type.SetFont(self.bold_font) - self.panel.sizer.Add(title_name) - self.panel.sizer.Add(title_type) - self.panel.sizer.Add(wx.Window(self.panel, -1)) - self.panel.sizer.Add(wx.Window(self.panel, -1)) - - for gateway in gateways: - self.addGateway(gateway, gateways[gateway]) - - self.ext_server_panel = wx.Panel(self) - self.ext_server_panel.sizer = wx.BoxSizer(wx.HORIZONTAL) - self.ext_server_panel.SetSizer(self.ext_server_panel.sizer) - self.ext_server_panel.SetAutoLayout(True) - self.sizer.Add(self.ext_server_panel, 0, flag=wx.EXPAND) - - ext_server_label = wx.StaticText(self.ext_server_panel, -1, _("Use external XMPP server: ")) - ext_server_label.SetFont(wx.ITALIC_FONT) - self.ext_server_text = wx.TextCtrl(self.ext_server_panel, -1) - ext_server_button = wx.Button(self.ext_server_panel, -1, _("GO !")) - self.ext_server_panel.Bind(wx.EVT_BUTTON, self.browseExternalServer, ext_server_button) - - self.ext_server_panel.sizer.Add(ext_server_label) - self.ext_server_panel.sizer.Add(self.ext_server_text, 1, flag=wx.EXPAND) - self.ext_server_panel.sizer.Add(ext_server_button) - - #self.panel.sizer.Fit(self) - self.sizer.Fit(self) - - self.Show() - - def browseExternalServer(self, event): - """Open the gateway manager on given server""" - server = self.ext_server_text.GetValue() - debug(_("Opening gateways manager on [%s]") % server) - id = self.host.bridge.findGateways(server, self.host.profile) - self.host.current_action_ids.add(id) - self.host.current_action_ids_cb[id] = self.host.onGatewaysFound - self.MakeModal(False) - self.Destroy() - - - def addGateway(self, gateway, param): - - #First The icon - isz = (16,16) - im_icon = wx.StaticBitmap(self.panel, -1, wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, wx.ART_TOOLBAR, isz)) - - #Then the name - - label=wx.StaticText(self.panel, -1, param['name']) - label.SetFont(self.normal_font) - - #Then the transport type message - type_label_txt = self.getGatewayDesc(param['type']) - - type_label = wx.StaticText(self.panel, -1, type_label_txt) - type_label.SetFont(self.italic_font) - - #The buttons - def register_cb(event): - """Called when register button is clicked""" - gateway_jid = event.GetEventObject().gateway_jid - id = self.host.bridge.in_band_register(gateway_jid, self.host.profile) - self.host.current_action_ids.add(id) - self.MakeModal(False) - self.Destroy() - - def unregister_cb(event): - """Called when unregister button is clicked""" - gateway_jid = event.GetEventObject().gateway_jid - id = self.host.bridge.gatewayRegister("CANCEL",gateway_jid, None, self.host.profile) - self.host.current_action_ids.add(id) - self.MakeModal(False) - self.Destroy() - - reg_button = wx.Button(self.panel, -1, _("Register"), size=wx.Size(-1, 8)) - reg_button.SetFont(self.button_font) - reg_button.gateway_jid = JID(gateway) - self.panel.Bind(wx.EVT_BUTTON, register_cb, reg_button) - unreg_button = wx.Button(self.panel, -1, _("Unregister"), size=wx.Size(-1, 8)) - unreg_button.SetFont(self.button_font) - unreg_button.gateway_jid = JID(gateway) - self.panel.Bind(wx.EVT_BUTTON, unregister_cb, unreg_button) - - self.panel.sizer.Add(im_icon) - self.panel.sizer.Add(label) - self.panel.sizer.Add(type_label) - self.panel.sizer.Add(reg_button, 1, wx.EXPAND) - self.panel.sizer.Add(unreg_button, 1, wx.EXPAND) - - - def onClose(self, event): - """Close event""" - debug(_("close")) - self.MakeModal(False) - event.Skip() -
--- a/frontends/wix/images/cards/.size Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -Biggest size: 74X136
--- a/frontends/wix/images/cards/COPYING Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -The work in this directory is licensed under the Creative Commons BY-SA License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/; or, (b) send a letter to Creative Commons, 171 2nd Street, Suite 300, San Francisco, California, 94105, USA.
--- a/frontends/wix/images/cards/README Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -The Tarot cards pictures come from the deck found on wikicommons (the script used to cut them is available at frontend/wix/images/split_card.sh), and available at http://upload.wikimedia.org/wikipedia/commons/8/8d/Tarotcards.jpg under Creative Commons BY-SA. The authors shown on the site are Piast and Svick. The splitted cards are, according to the license, available under the same conditions. -Notice that the Creative Common BY-SA apply only to this directory -
--- a/frontends/wix/images/crystal/COPYING Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - 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/>. - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - <program> Copyright (C) <year> <name of author> - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -<http://www.gnu.org/licenses/>. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- a/frontends/wix/images/crystal/COPYING.LESSER Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library.
--- a/frontends/wix/images/crystal/README Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -These pictures come from Crystal clear, were made by Everaldo Coelho (www.everaldo.com) and YellowIcon (www.yellowicon.com). They are under LGPL V3 licence (see COPYING.LESSER). - -Found on http://commons.wikimedia.org/wiki/File:Crystal_Clear_app_browser.png and http://commons.wikimedia.org/wiki/File:Crystal_Clear_app_cache.png .
--- a/frontends/wix/images/crystal/tray_icon.xpm Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,309 +0,0 @@ -/* XPM */ -static char *tray_icon[] = { -/* columns rows colors chars-per-pixel */ -"128 128 175 2", -" c #03060F", -". c #030A1D", -"X c #030D27", -"o c #050F29", -"O c #0B1121", -"+ c #08122A", -"@ c #151A26", -"# c #171A28", -"$ c #1B1F2B", -"% c #1A223E", -"& c #20263A", -"* c #24293B", -"= c #061741", -"- c #061843", -"; c #091E56", -": c #091F59", -"> c #142244", -", c #1B2540", -"< c #09215D", -"1 c #202F5A", -"2 c #263257", -"3 c #2A3450", -"4 c #24335C", -"5 c #2A3658", -"6 c #2F3A5A", -"7 c #363B50", -"8 c #383D52", -"9 c #0B276D", -"0 c #0B286F", -"q c #192E63", -"w c #1E3265", -"e c #0C2970", -"r c #0E2E7D", -"t c #102F7E", -"y c #1B3474", -"u c #11317F", -"i c #19347B", -"p c #1E387A", -"a c #213876", -"s c #293F7C", -"d c #2F447E", -"f c #30437B", -"g c #36497D", -"h c #404867", -"j c #434C68", -"k c #4A526C", -"l c #404C74", -"z c #445075", -"x c #153481", -"c c #193783", -"v c #1C3A85", -"b c #203E87", -"n c #223E88", -"m c #25428A", -"M c #2A468D", -"N c #2D498E", -"B c #314B8E", -"V c #2F4990", -"C c #324D91", -"Z c #365093", -"A c #3B5595", -"S c #3E5897", -"D c #3E5698", -"F c #3F5898", -"G c #465787", -"H c #4C5980", -"J c #4C5C8A", -"K c #505C82", -"L c #445993", -"P c #4B5F97", -"I c #40579B", -"U c #445B9C", -"Y c #485F9D", -"T c #556086", -"R c #5A6284", -"E c #47609C", -"W c #4A629E", -"Q c #51639B", -"! c #5A6B9F", -"~ c #606E9E", -"^ c #6A779C", -"/ c #6E789D", -"( c #495EA0", -") c #4D63A1", -"_ c #5066A2", -"` c #556AA4", -"' c #596EA6", -"] c #5769A9", -"[ c #5B6EAA", -"{ c #5C72A7", -"} c #5E72AB", -"| c #606FA2", -" . c #6573A1", -".. c #6A77A5", -"X. c #6F7BA7", -"o. c #6275AB", -"O. c #6579AC", -"+. c #6A7DAE", -"@. c #727CA5", -"#. c #6574B1", -"$. c #6778B0", -"%. c #6C7DB2", -"&. c #6F7DB8", -"*. c #707FB6", -"=. c #717EB9", -"-. c #6F81B1", -";. c #7384B4", -":. c #7986B6", -">. c #7789B6", -",. c #7989B7", -"<. c #7583B9", -"1. c #7984BD", -"2. c #7C8CB9", -"3. c #7F90BA", -"4. c #7C87C0", -"5. c #7E89C0", -"6. c #818EBC", -"7. c #8594BD", -"8. c #8896BF", -"9. c #8898BF", -"0. c #828DC2", -"q. c #8691C4", -"w. c #8A95C5", -"e. c #8C9AC2", -"r. c #8D97C8", -"t. c #8E98C8", -"y. c #919EC4", -"u. c #939CCA", -"i. c #94A1C6", -"p. c #98A6C7", -"a. c #97A2CA", -"s. c #9AA5CB", -"d. c #9DAACA", -"f. c #9EA6D0", -"g. c #9FA8D1", -"h. c #A1ADCD", -"j. c #A5B1CF", -"k. c #A4ACD3", -"l. c #A7B1D1", -"z. c #ABB3D4", -"x. c #AEB8D3", -"c. c #AEB6D8", -"v. c #B3BCD6", -"b. c #B0B7D9", -"n. c #B5BBDA", -"m. c #B8BFDD", -"M. c #B7C0D8", -"N. c #BBC2DD", -"B. c #BFC8DD", -"V. c #BFC4E0", -"C. c #C0C7DF", -"Z. c #C2CADE", -"A. c #C1C6E1", -"S. c #C5CBE2", -"D. c #C9CEE5", -"F. c #CDD2E5", -"G. c #CFD3E8", -"H. c #D0D6E6", -"J. c #D2D8E7", -"K. c #D2D6E9", -"L. c #D5DAEA", -"P. c #DADDED", -"I. c #DDE1EE", -"U. c #DFE2F0", -"Y. c #E1E5EF", -"T. c #E3E5F1", -"R. c #E6E9F3", -"E. c #EAECF5", -"W. c #EEF0F6", -"Q. c #EFF1F8", -"!. c #F0F2F7", -"~. c #F3F5F9", -"^. c #F7F8FB", -"/. c #FDFDFE", -"(. c None", -/* pixels */ -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r r r r r r r r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r r r u r r u r r r r r r t r r r r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r r r r r r r A W W W W W W r u r r r t r r r r r r t r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r r r v W 9.p.Z.Z./././././././././././././.Y.S.j.8.{ A r t r r r t t r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r r r S ,.v.Y././././././././././././././././././././././././././.Z.i.{ v r t r u r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t r t n $.j.W././././././././././././././././././././././././././././././././././.H.>.N e r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r t r r t ` c./.^./././.^.^.^./.^./.^./././././././.^./././.^./.^././.^././././././.^./././.^.^.H.>.M r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t t M 7.P.^.^.^.^.^././.~.^.^.^.^.^.^./././.^././.^.^.^.^.~.^.^.^.~.~.^.^.^././.^.^.^.^.^.~.^.^.^.^.^.x.E r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r t t r M d.~.~.~.^.~.~.~.^.^.^.~.^.!.!.!.~.~.~.^.^./././.^.!.!.~.!.!.!.!.~.~.~.~.~.!.~.~.!.~.^.~.~.!.!.~.~.~.~.~.S.O.r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r M p.!.E.W.~.^.~.~.~.Q.W.~.W.W.W.W.W.W.!.W.!.^./.^.^.^./.W.W.W.!.!.!.!.E.!.!.E.W.W.W.W.!.!.~.!.W.W.W.W.~.W.W.W.W.W.H.E r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t r ,.I.E.E.E.E.~.~.~.~.~.Q.E.W.~.E.E.E.R.W.E.E.E.!.^.~.~.!.W.^.E.E.E.E.R.E.E.E.E.E.E.E.E.E.E.E.!.!.E.E.!.E.~.E.E.W.~.W.E.E.v.M r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r L F.R.R.E.R.R.E.!.~.~.~.~.Q.~.~.~.E.R.R.R.R.R.R.R.~.R.E.W.R.R.~.R.R.E.R.E.E.R.R.R.R.R.R.R.R.R.R.E.E.R.R.E.R.W.R.R.W.~.~.~.R.R.R.,.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r >.R.E.Y.R.Y.Y.Y.T.W.!.Q.Q.Q.Q.W.~.W.E.Y.Y.Y.R.Y.Y.W.R.Y.R.Y.Y.Y.R.^.Y.Y.Y.Y.Y.Y.Y.Y.R.Y.Y.R.Y.R.R.W.E.Y.Y.W.R.Y.Y.Y.R.W.E.E.~.E.Y.Y.B.Z r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r c m.R.Q.Q.R.U.U.U.U.T.W.W.Q.Q.E.Q.W.!.!.R.Y.Y.Y.Y.Y.I.W.Y.Y.Y.Y.Y.Y.Y.!.Y.Y.Y.Y.Y.I.I.Y.I.Y.I.Y.Y.R.E.E.Y.Y.!.!.Y.I.I.I.I.I.Y.I.W.W.W.T.I.J.{ r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r C J.E.E.E.E.E.T.P.P.I.E.E.W.Q.Q.Q.E.W.W.W.!.Y.Y.J.Y.Y.J.W.I.J.I.I.I.I.I.E.E.I.J.J.I.I.I.I.I.J.I.Y.Y.R.R.R.Y.Y.!.!.Y.I.I.I.I.Y.W.I.E.E.W.I.T.I.I.3.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r { J.R.E.R.E.E.E.E.U.P.U.E.E.E.E.E.Q.E.E.W.W.E.E.J.Y.J.J.Y.I.I.J.I.J.J.J.J.I.!.J.I.I.J.I.J.I.J.I.J.I.J.I.I.I.I.Y.R.E.E.L.P.L.L.W.E.I.R.T.I.R.E.I.I.I.9.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r ' I.R.R.R.R.R.R.E.R.R.K.T.E.E.E.E.E.E.E.E.E.W.E.E.!.J.J.J.!.J.J.J.J.J.H.J.J.J.~.J.J.J.J.J.J.J.J.J.J.J.J.J.Y.E.E.Y.Y.E.E.I.K.L.J.E.E.Y.R.E.R.R.R.R.Y.L.L.9.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r ` P.R.T.R.T.T.R.R.R.R.R.T.R.R.R.E.E.R.E.E.E.E.E.E.W.E.J.H.H.R.H.H.H.H.H.H.H.H.H.R.Y.H.H.H.H.H.H.H.H.H.H.H.J.Y.W.W.R.Y.W.W.I.K.K.T.E.E.R.R.R.R.E.R.R.R.R.L.G.7.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r t C K.T.T.T.T.T.T.T.R.T.R.R.R.R.R.R.R.R.E.E.R.E.E.R.R.!.Y.H.Z.J.Y.Z.H.F.H.F.F.Z.H.H.J.R.H.F.F.H.H.Z.H.F.F.H.H.H.J.R.!.Y.W.R.R.P.K.T.R.R.R.R.R.R.^./.T.T.R.T.T.L.F.7.r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r c K.U.U.U.U.T.U.T.T.T.T.T.T.T.R.R.T.R.R.R.R.R.R.R.R.I.H.J.H.Z.Y.H.Z.H.F.Z.F.Z.Z.H.Z.H.!.H.Z.Z.H.Z.Z.H.Z.Z.Z.Z.F.H.I.J.Y.R.E.R.R.R.R.R.R.R.R.T.~.R.~.T.T.T.U.T.I.J.S.! r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r x b.I.I.I.I.U.I.U.U.T.T.T.T.T.T.T.T.R.T.R.T.R.G.F.H.Z.H.Y.R.R.H.I.Z.Z.Z.Z.Z.Z.Z.Z.B.Z.Z.W.Z.Z.Z.Z.Z.Z.Z.Z.Z.Z.Z.B.H.J.I.J.I.Y.R.R.R.R.T.R.T.T.Q.R.Q.E.T.I.T.U.I.I.I.H.Z.C r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.0 r x x w.P.P.P.P.P.U.I.U.U.U.U.U.U.U.T.T.T.T.T.T.T.Y.L.B.B.B.I.R.R.R.I.I.Z.B.Z.B.Z.Z.B.B.B.B.B.Y.H.Z.Z.Z.Z.B.Z.B.Z.B.Z.B.Z.J.J.L.L.R.R.R.R.T.T.T.T.Q.E.T.^.T.U.U.U.I.U.U.P.U.D.f.x r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.x x r x [ K.P.P.I.P.I.P.P.P.U.P.U.U.U.U.T.R.Q.E.Q.E.E.~.~.Y.I.H.Z.I.I.R.I.R.Z.M.B.B.M.B.M.B.B.B.B.H.H.M.B.M.B.B.M.B.B.M.B.B.B.Z.H.G.J.Y.Y.Y.T.R.T.T.R.!.T.R.Q.T.I.I.I.U.P.P.P.P.P.A.o.r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.x x x ( b.P.L.L.L.P.P.P.P.U.P.P.P.I.U.R.T.U.U.T.T.T.T.Y.Y.R.Y.I.I.H.J.W.!.^.W.F.M.M.M.M.M.M.M.M.M.B.J.M.M.M.M.M.M.M.M.M.M.M.M.M.Z.G.J.J.T.T.T.T.T.T.~.T.I.^.U.U.I.U.P.P.P.P.P.P.P.L.x.n r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.0 x x v 0.L.L.L.L.L.K.P.P.P.P.P.P.P.I.I.U.U.U.U.P.T.T.Y.L.P.I.R.Y.R.F.B.W./././.!.J.B.v.M.M.M.x.M.M.M.E.M.M.M.x.M.M.M.x.M.M.M.M.M.M.S.F.J.T.T.T.T.T.~.T.U.R.Q.I.I.P.P.U.P.P.P.P.K.P.L.G.,.r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r x r c =.b.K.L.L.K.L.L.P.U.P.P.P.P.P.P.U.P.I.U.U.I.U.I.M.v.Z.Y.I.Z.M.x.B.^./././.E.Z.J.I.J.M.x.M.M.M.B.Y.Z.Z.x.M.M.x.x.M.x.M.M.M.I.I.I.I.T.T.T.U.I.~.Y.I.I.^.U.P.P.P.P.P.P.L.P.L.L.L.L.K.S.c r r r (.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.x r r #.4.K.K.G.K.K.K.R.T.P.K.P.K.P.L.P.P.P.I.P.U.I.I.B.x.x.x.F.x.x.x.x.M.E./.^.^.I.x.M.M.M.H.J.Z.M.Z.R.^.!.Y.Z.j.x.x.x.j.x.M.x.j.I.R.Y.I.T.T.T.U.I.F.Z.I.E.W.P.D.D.P.P.P.L.P.L.L.K.L.K.J.J.} r r r (.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r x r D 1.f.G.G.G.G.K.E.P.K.K.K.L.P.P.P.L.P.P.P.P.P.G.I.x.j.x.j.j.x.x.j.j.x.B.!.R.Y.M.M.h.j.M.h.x.M.J.^.^././.^.I.M.j.j.j.x.j.j.j.j.R.Y.Y.Y.Y.Y.U.F.E.B.S.I.!.D.P.P.A.L.L.P.L.K.L.K.K.K.K.K.J.m.c r r r (.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r x M =.1.b.G.D.G.P.R.G.K.K.K.K.K.P.K.P.L.P.P.P.P.F.l.k.j.j.j.j.j.j.j.j.j.j.j.J.M.j.d.j.j.j.j.j.j.j.Z.^././././.R.M.j.j.j.j.j.j.j.j.Y.Y.Y.Y.Y.Y.b.I.F.Z.Z.W.S.z.K.P.D.A.P.P.K.L.L.K.K.K.K.K.G.G.$.r r r r (.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r x r ] 1.1.D.D.D.K.E.G.G.G.G.G.K.K.K.P.K.L.L.L.P.P.D.h.h.h.h.h.d.h.h.h.d.h.h.h.H.d.h.h.h.d.h.h.h.h.h.F.~././././.W.M.h.h.h.h.h.h.d.h.T.T.T.T.T.U.F.Y.v.h.N.E.j.z.D.P.D.j.L.K.N.D.H.H.G.G.G.F.F.D.f.Z x x r (.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r x ( =.1.r.S.D.D.T.D.G.D.D.G.G.G.K.K.K.P.L.K.P.L.L.m.d.d.d.d.d.d.d.p.d.d.d.d.p.M.p.d.d.d.d.d.d.d.p.p.M.R./././.^.Y.I.M.d.d.d.d.d.d.x.T.T.T.U.U.R.F.N.v.l.L.B.C.K.L.K.l.d.S.L.N.h.G.G.G.G.F.F.D.D.c.=.x x r r (.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r v =.4.=.u.S.S.U.D.D.D.D.G.G.G.G.G.K.K.K.L.P.L.L.D.s.p.p.p.p.p.p.p.p.p.p.p.p.M.j.p.p.p.p.p.p.p.p.p.p.d.Z.R.~.~.I.M.p.M.J.M.p.p.p.s.p.h.x.x.N.A.^.I.I.I.Y.~.P.D.z.V.x.s.p.d.F.Z.v.l.S.G.G.F.D.D.D.b.1.I x r r (.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.r r r ) 1.1.1.f.A.T.G.A.D.D.D.D.G.D.G.G.G.K.K.K.L.K.P.S.i.p.i.p.i.i.p.i.p.i.p.p.p.j.p.p.p.p.p.i.p.i.p.p.p.p.d.x.Z.Y.M.p.p.i.p.M.H.d.i.p.i.u.K.T.T.~.T.I.I.P.~.Y.P.D.l.u.i.i.p.i.l.J.G.l.p.V.D.D.D.D.D.A.4.=.x r r r (.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.r r c =.4.=.1.f.G.U.A.A.D.A.D.G.D.D.G.G.G.K.K.K.K.l.h.d.i.i.i.i.i.i.e.p.i.9.p.i.p.B.i.9.i.p.p.9.p.i.i.i.i.i.y.p.i.Y.i.i.i.i.e.i.p.F.B.i.i.M.Y.T.E.R.U.I.P.T.W.P.P.D.n.x.i.e.i.y.y.z.x.i.i.i.i.D.D.D.S.S.4.4.] x r r (.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.r r r U 4.=.4.1.c.Q.D.V.D.A.D.D.D.D.D.G.G.G.G.G.K.n.y.e.9.y.9.e.9.y.i.9.9.p.i.9.9.9.M.p.i.9.9.p.9.9.e.i.e.e.e.9.e.e.Y.e.9.e.e.e.e.e.9.x.P.M.T.T.R.^.R.E.E.T.^.P.P.P.P.K.K.z.e.N.Z.N.n.d.i.t.e.d.D.S.S.A.A.4.4.4.v r r r (.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.r r r U =.4.4.=.n.E.V.V.A.A.D.D.A.D.D.D.G.K.G.G.K.y.8.8.9.9.9.9.9.9.9.9.9.9.9.9.9.9.J.3.3.9.9.9.9.9.9.9.9.9.9.9.9.9.B.j.9.9.e.9.9.9.9.9.y.R.!.T.!.!.~.~.^.^.~.U.P.P.P.K.K.K.D.J.G.G.G.F.D.D.D.D.D.A.A.S.A.5.4.4.( x x r (.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.r r r x U 4.4.=.4.m.L.V.V.A.A.A.A.D.D.D.n.u.u.u.K.G.8.8.8.9.9.7.8.8.9.7.9.3.9.9.9.9.3.J.3.9.9.9.3.9.9.9.3.9.9.9.9.9.9.Z.j.9.9.9.9.7.9.9.7.e.T.E.^.^.^././././.!.T.P.L.L.P.L.K.G.G.G.F.F.F.D.D.A.D.D.A.S.A.A.5.4.4.4.c x r r (.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.r r r r D 4.4.=.4.U.V.V.V.V.V.V.M.e.9.3.5.3.q.3.F.H.7.3.7.7.7.7.7.7.7.7.3.3.3.3.3.9.3.B.3.3.3.3.3.3.3.3.3.7.7.7.7.7.7.M.M.7.7.7.7.7.7.7.7.B.T.R.^./././././././.E.T.L.L.K.K.K.K.K.G.D.G.D.D.D.S.S.A.A.A.A.A.4.4.4.1.( x r r (.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.r r r r r x } 4.4.4.Q.m.m.V.V.V.M.3.3.3.3.3.3.3.3.N.G.3.3.3.3.3.3.3.3.3.3.3.3.3.3.3.3.3.M.3.3.3.3.3.3.3.3.3.3.3.3.3.,.3.p.B.3.3.3.3.3.3.7.j.R.R.^.~./././././././.~.T.L.L.L.K.K.H.G.G.G.D.D.D.S.S.A.M.V.A.V.c.0.4.4.1.#.x r r r (.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.r r r r r r b -.4.3.U.m.m.V.V.V.x.>.3.>.2.>.3.>.3.i.v.>.3.>.,.,.3.,.,.2.2.,.3.>.>.>.3.>.p.3.3.3.3.3.3.3.>.3.>.3.>.3.,.,.p.Z.2.,.,.2.>.,.e.R.T.^.E.~./././././././.^.T.P.P.K.G.G.G.G.D.D.D.D.D.S.S.A.k.l.V.V.k.0.4.4.4.1.Z x r r (.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.r r r r r r x Z ;.3.V.n.m.m.V.M.d.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.-.3.i.p.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.p.H.>.>.,.>.>.7.Y.R.!.E.R.~./././././././.~.T.L.K.K.K.G.F.F.D.D.D.D.S.S.A.A.c.2.n.N.f.0.0.4.4.=.[ x r r (.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.r r r r r r r r r x i.g.g.b.V.M.M.u.<.;.;.;.;.>.>.l.N.F.D.p.>.>.-.;.-.>.>.-.>.-.-.-.-.>.p.>.-.>.>.-.>.-.-.-.-.>.-.-.-.>.-.>.I.>.;.;.;.1.L.E.R.~.R.R.Q.^././././././.Q.U.K.K.K.G.G.G.D.D.D.D.A.A.A.V.V.V.a.u.N.r.0.0.4.1.1.1.c r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.r r r r r r r r r r 5.[ r.n.n.M.V.i.-.-.-.e.c.x.3.i.z.V.F.G.V.e.>.-.-.-.-.-.-.-.-.-.-.-.j.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.I.-.-.-.-.z.E.R.Q.R.T.T.E.~././././.^.^.~.P.L.K.K.G.G.G.D.D.S.D.A.D.A.V.V.V.V.0.z.0.0.0.4.4.1.1.A r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.r r r r r r r r r r e.) b g.M.n.M.M.r.-.>.M.V.N.-.-.-.-.l.F.G.H.J.B.x.-.-.-.-.-.-.-.-.-.9.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.J.+.+.-.+.z.R.~.E.R.R.T.T.!./.~.^.~.~.R.R.Q.K.K.K.G.F.F.D.D.D.S.S.A.A.V.V.m.m.5.u.q.0.0.4.4.1.1.] r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.r r r r r r r r r r 5.E x ) f.n.b.V.M.z.M.V.V.d.-.O.O.O.-.e.p.V.H.H.F.-.-.M.>.-.-.-.-.-.9.-.` -.-.-.-.-.! -.-.-.-.-.{ ^ -.-.^.+.+.+.O.x.W.^.R.R.R.R.R.^.R.R.R.R.T.P.P.R.~.K.G.G.F.D.D.D.S.S.V.V.V.V.V.m.m.k.} q.q.0.5.1.1.1.1.r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r r r r r q.>.r c O.3.M.M.M.V.M.V.V.n.O.e.>.O.O.O.-.F.G.G.N.O.n.L.i.O.O.O.O.O.e.-.{ -.{ { { -.{ -.{ { -.{ -.-.-.` I.-.-.-.Q d.!.E.R.R.R.R.~.Q.U.U.U.P.P.L.L.K.R.~.K.D.G.D.D.D.D.A.V.V.V.V.m.m.m.k.Z V =.0.5.4.4.4.1.V x x x (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r r r r r >.9.r t F { -.e.b.b.k.V.V.V.n.S.V.>.} } -.i.x.2.} { 7.p.-.{ } { { { { O.{ ' -.{ -.{ { ! -.{ { { ' ` -.-.Z.-._ -._ j.E.E.E.R.R.R.^.T.U.P.U.P.P.P.K.K.G.P.Q.G.D.D.S.A.A.A.V.V.V.m.m.m.n.t.4.) ] 0.5.4.4.1.1.Z x 0 x (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r x r r r r r r r E j.r r c ' { { { { { -.e.B.V.V.z.{ { { { { { { { { { ' ' { { { { { { { { { { { { { { { { { { { { -._ -.P.E -.E -.M.W.E.R.R.R.^.E.U.I.P.P.P.P.K.K.K.K.G.P.~.G.D.S.A.A.V.V.V.m.m.m.n.g.r.r.4.m 4.4.4.4.=.&.I x x x (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r r r r r M Z.r r r N ' { ' ' ' ` ' V.V.V.-.{ { ' { +.' ' ' { { { ' { { { ' ! { { { { ` { { ' ' ' ' ' ' ! ' ' ` _ J.-.E -.E 3.W.E.E.R.Q.~.T.T.I.I.P.P.L.P.L.G.G.G.G.U.E.S.S.D.A.V.V.m.m.m.m.b.t.r.r.q.M c #.4.4.( x x x 0 x (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r r r r r r /.t t r r S ` ` ` ` ` ` N.V.V.z.' ` _ -.V.V.s.;.O.] _ ] ` ` ` ! ` ` ` ` ` ` ` ` ` ` ` ` _ ! ' ` ` ` ` J.>.` ` ! -.W.E.R.E./.T.T.U.P.I.P.P.K.L.G.K.G.G.D.D.U.E.A.V.V.V.V.V.m.m.n.f.t.r.r.q.&.x x m c x x r r r r (.(.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r r Z.W r r r x W ` _ _ _ _ d.N.V.V.x.` O.B.D.F.F.G.G.K.F.<._ _ _ ` W ` ` W _ _ W ` W ` W ! _ _ W ` ` W _ B.>._ _ _ _ 9.E.E.^.Q.T.T.U.U.P.P.P.K.L.K.G.G.D.D.S.S.U.U.V.V.V.m.n.n.n.g.u.t.r.r.q.q.A x [ } I x r r r r x (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r t 7.9.r r r r p E W W ) W 5.N.V.V.V.c.x.S.D.F.F.G.G.K.L.L.i.W W W W W W W W _ _ W W W W W E _ W _ ` ' ` M.>.W _ W W _ 3.!.^.T.T.U.U.U.I.L.P.L.L.G.G.G.D.D.S.S.A.E.L.V.m.m.m.n.k.f.u.t.r.r.r.q.4.&.4.4.} x r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r r A J.r r r r r m W W E E E ' p.V.V.V.V.S.S.S.F.G.G.G.K.L.B.E E W W >.E W W E W W W W E E E E E E -.-.-.M.H.E E W E E ` Y.E.R.U.U.U.P.P.P.L.K.G.G.G.D.D.D.S.S.A.V.Q.D.m.m.n.k.f.u.t.r.r.r.q.q.5.5.4.4.] x r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r x r r r r r r r r I.N r r r r r m E E E U E U O.k.V.V.S.S.D.D.F.G.G.K.L.B.e.i.;.' e.U E E E E F E E E E E E E W -.-.-.M.p.S E E E { B.N.g.U.U.U.U.U.P.L.L.J.G.G.G.D.S.S.A.A.V.V.V.E.V.n.k.f.f.f.t.t.r.r.q.q.0.0.4.4.C r r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r r r p.9.r r r r r r b A S F U F F 2.V.V.A.A.D.D.F.G.G.G.K.L.I.L.I.L.g.F F F F F F S S F F S I F F S F ] U.3.S S ` -.5.R.-.-.g.U.U.I.I.L.B.l.z.G.G.D.D.D.S.A.A.V.V.V.S.R.c.f.f.u.u.u.t.r.r.r.q.0.5.4.=.x r r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r t r r r A I.r r r r r r u i A S A A S } V.V.V.A.D.A.F.F.G.K.K.L.L.L.I.I.U.{ S S A F F A S S A S A S ) [ ) i.B.3._ { { -.U.5.;.-.-.d.p.;.' E ` D } 1.A.D.D.S.S.A.A.V.m.m.m.P.A.f.f.f.u.u.u.r.r.q.q.0.0.4.) r r r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r t r r r r r u U.N r r r r r r r r C A A _ l.m.V.V.A.A.A.D.D.D.G.G.J.J.J.I.I.E.L.F A A A A A Z A A A F } } ] { e.M.9.` -.-.M.N.-.-.$.' A A Z A A A A Z A A #.S.S.A.A.A.V.V.n.c.k.R.k.f.u.f.u.t.r.r.r.0.0.5.&.v r r r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r -.h.r r r r r r r r r b Z q.M.N.V.V.V.A.S.D.D.D.G.G.K.J.L.L.I.U.U.} Z Z Z Z Z Z Z Z Z E _ Z Z p.Z d.-.S o.>.U.-.#.-.$.{ Z Z Z Z Z Z ) _ W Z =.S.S.A.V.V.m.n.c.k.k.b.R.f.a.u.u.r.r.q.q.q.0.[ x r r r x r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r b U.b r r r r r r r r t c s.n.n.n.V.V.A.V.D.D.D.D.G.K.J.L.L.U.U.I.l.' C C Z C V C C C C C C e.W C p.-.[ } L.;.` } } _ Z C C ) ] U Z _ $.A C &.A.A.V.V.m.n.c.k.k.k.f.G.A.u.u.u.r.r.r.q.0.( x r r r r r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r r 7.9.r r r r r r r r r r w.c.n.m.V.V.V.V.A.A.D.D.D.G.K.K.L.L.U.U.U.I.M.>.-.E N N N N N N -.>.C A d.;.-.j.V.N V I V N N N N Z _ ' D C Z A O.0.V.V.V.n.c.c.k.k.k.f.f.f.E.f.u.u.r.r.q.5.D x x r r r r r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r t r r r r r r r r r b I.b r r r r r r r r b 3.l.z.b.m.V.V.V.V.A.D.D.D.D.G.G.K.U.U.L.U.I.I.U.T.U.i.Z M M M E L.C -.3.J.V.p.Q.E M N _ S M M M V Z Z C M M M C } #.m.m.n.b.c.c.k.k.k.k.f.f.n.L.u.u.r.r.q.&.x r r r r r r r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r u r r r r 9.j.r r r r r r r r M k.k.c.c.b.m.V.V.V.A.S.S.D.D.G.G.G.U.U.L.P.I.I.I.T.T.T.P.-.m M B.;.;.v.I.~.W.~.H.>.Z U ' ` n n V ] ] ] [ A m m m M M #.M.b.c.c.c.k.k.k.g.g.f.u.U.b.t.r.q.r.n x x x 0 x x x x x x x 0 (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r r r r r r r r r r v U.N r r r r r r r t u.k.k.k.c.c.b.m.m.A.A.A.D.D.D.G.G.P.R.L.P.P.P.P.U.P.T.T.T.d.3.B.{ v.E././././.W.B.+.] _ ] V n D ] ] ] ] ] Z I D m x x f.c.c.c.c.k.k.g.g.f.f.u.b.R.t.r.r.) x x 0 x x x x 0 0 x x 0 x (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r b r r r r r r r r r -.B.r r r r r r r t V k.c.k.c.c.b.n.m.m.V.A.A.A.D.D.G.P.R.K.K.L.L.I.P.U.U.U.U.M.R.A 5.I././././././.R.k.o._ _ V v ( ] ] ] ] ] ( ) ) ( Z x ) b.c.k.c.k.g.g.f.f.f.u.u.L.c.r.r.&.x x x x x x x x x 0 x x x (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r m r r r r r r r r r r B.-.r r r r t r t t u.k.c.k.c.b.b.m.m.m.V.V.A.D.D.D.P.R.G.K.K.L.L.L.P.P.U.U.Q.3.N h.E././././././.~.N.&.( ( I v ( ( ) ) ( ( ) ( ( ) ( A x g.k.c.k.k.k.g.f.a.u.f.u.k.U.r.q.q.x x 0 x x x x 0 x x x x x (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r r M r r r r r r r r r r M U.v r r r r r r r ] k.z.c.c.b.b.n.m.m.m.V.V.A.A.D.P.T.G.G.K.K.K.L.L.L.P.R.B.x M d.W.^./././././.~.N.%.) ( ( v I I I ( ( ( ( ( ( ( ( ( n 5.c.k.k.k.g.g.g.f.a.a.u.t.U.g.q.r.x x x x x x x x r r r r r (.(.(.", -"(.(.(.(.(.(.(.u r r r r r r r C r r r r r r r r r r r >.M.r r r r r r r n k.k.z.k.b.b.b.n.m.m.V.V.V.A.A.L.T.D.G.G.K.K.K.L.L.P.^.S v N p.U././././././.W.n.[ U U I ( ( ( ( ( I ( ( ( ( ( ( ( ( 4.c.c.k.k.g.g.g.u.a.u.u.u.b.L.q.q.c x x x v x x x r r r r r (.(.(.", -"(.(.(.(.(.(.(.r r r r r r r u C c r r r r r r r r r r r B.>.r r r r r r r 0.k.c.k.c.b.b.n.n.m.m.V.V.A.A.K.T.D.D.D.G.K.K.L.P.R.p.C C A 3.H././././././.I.u.) I U I I ( I I ( I ( I ( ( ( ) ( ] r.k.k.g.g.g.f.f.u.a.u.u.t.r.E.t.5.n x x x n n n x r r r r r (.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r C b r r r r r r r r r r r b !.A r r r r r x V u.k.k.c.c.b.b.n.m.m.V.V.A.A.K.T.D.G.K.K.L.L.L.I.R.S C C C W M.^.Q././.^.T.n.1.) ( ( ( I I I ( I ( ( ( ( I ( ( [ k.c.c.k.g.g.f.f.f.u.u.u.t.r.r.V.n.q.n x x x n =.#.x r r r r (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r C M r r r r r r r r r r r r W R.b r r r r r r c 1.k.c.c.c.b.b.n.m.m.m.V.V.K.Q.G.G.G.J.K.K.K.^.O.C B C B L ~.h.M.R.I.Z.z.5.$.} } } _ ( I I I ( ( I I ( ( ( ( #.z.z.g.k.g.g.f.f.u.u.u.u.r.r.w.a.I.5.n r r ( =.1.[ r r r r r (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r C C r r r r r r r r r r r r r 3.B.r r r r r r r r [ k.k.c.b.b.b.n.n.V.V.D.P.Q.G.D.G.G.G.K.R.~.B Z C C C B.p.S -.B.i.7.-.#.} } } } } } ] ( I I I ( ( I ( I ( #.k.z.k.k.g.g.f.f.u.u.u.u.r.r.w.q.U.} x r &.r.r.1.[ r r r r r (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r N S u u r r r r r r r r r r r r M.k.t t r r r r r r U k.c.c.c.c.n.V.V.A.S.G.Q.D.D.G.D.G.L.^.A.C B C C -.I.C C B F.2.} } } } } } } } #.} #.] ( I I I ( ( ( I } k.k.g.g.g.g.u.a.u.u.r.r.r.q.w.q.M.' r V 4.u.w.1.I r r r r r (.(.(.(.", -"(.(.(.(.(.(.(.(.r r r r r r r m S b r r r r r r r r r r r r r b J.O.r r r r r r x Z c.k.c.c.b.V.V.V.A.A.S.Q.S.D.D.D.D.^.R.&.C C C S !.{ C C B F.%.} } o.} #.} } } } } } [ [ ) I I I I ( I ( f.k.g.g.f.f.f.f.u.u.r.r.r.r.5.#.) i.x c 0.u.0.1.n r r r r r (.(.(.(.", -"(.(.(.(.(.(.(.(.(.u u r r r r b S C r r r r u r r r r u r r r r v W.+.r r r r r r V k.k.k.n.m.V.m.V.A.A.A.Q.S.S.S.D.P.^.M.B C C C B.p.B C C C F.} } } } } } } #.} } #.} } #.] ] ( I I ( ( I &.g.g.g.f.u.u.u.u.t.t.r.r.q.5.c x F.r n q.u.0.=.x r r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.r r r r r r c S S u r r r r r r r r r r r r r r A W.' r r r r x A k.k.n.n.m.m.V.V.V.V.A./.S.S.S.D.^.L.-.C C C -.I.C C C C C F.} } } } } } } } [ #.[ } } } } #.] ( I I I ( I u.g.g.f.a.f.u.t.t.r.r.q.r.5.x x V.x #.u.u.4.] r r r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.u r r r r r u S S m r r r r r r r r r r r r r r r A Y.A r r r r ] k.b.n.m.n.m.m.m.V.V.V./.A.A.Z.R.I.e.A B C S !.{ B C V C V P.) } } } } } } } } } #.} } } } } [ ] ( I I I I q.f.f.u.f.u.u.t.t.r.r.q.q.5.x x t.] 6.u.u.4.C r r r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.r r r r r r r A S A r r r r r r r r r r r r r r r r { !.A r r x ] c.b.b.b.n.m.m.m.m.m.V.~.A.A.F.R.S C C B B B.p.B C C Z V Z /.( } #.} } } #.} } } } } } } } [ #.#.] ( ( I U <.f.a.u.u.u.u.t.t.q.q.0.5.4.x x 4.] #.u.r.$.r r r r r r (.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.r r r r r r N S S m r r r r r u r r r r r r r r t r -.W.E r r O.c.c.c.n.b.n.m.m.m.m.V.E.V.V.Q.;.C C B B ;.I.C C C C C C C R.A } } } } } } } } } } } } } } } } } } ' U U I #.f.a.a.u.u.r.r.r.w.q.5.4.A r r { s.A q.w.U r r r r r (.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.r r r r r r b S S Z r r r r r r r r r r r r r r r r r ' W.-.m -.k.c.c.b.c.n.b.n.n.m.m.Q.G.L.D.C C C Z W R.W C C Z Z C C B F.C Z } } } } #.} #.} } } #.} } [ } ` A ) _ U ( ] u.a.u.u.r.r.r.q.q.q.q.E r r r W h.L Y _ M r r r r r (.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.u t r r r r u S S S b r r r r r r r r u r r r r r r r r A W.3.7.z.c.c.c.c.n.n.V.V.A.S.Q.K.^.) C C Y ` G.e.C Z Z Z Z Z C W H.Z Z D } } } } } } } } } ] } } ) Q S C B Z C D I 5.a.u.r.r.r.r.q.q.5.5.v r r r W d.D I u r r r r r r (.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.t r r r r r Z S S Z u r r r r r r r r r r r r t r r r u ' W.F.l.l.c.k.c.m.V.S.F.G.G.Q.~.x.W A A [ l.G.A A Z Z A Z Z C -.H.Z Z Z [ #.} } } } } #.} [ ( ) A C C C C V V x c m [ t.r.r.r.q.w.w.5.A r r r r -.h.L C t r r r r r (.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.r r r r r r m S S S m r r r r r r r r r r r t t r r r m C W U.U.k.k.c.m.V.G.L.P.U.I.W.~.e.%.} _ 3.E._ A A A A A A S Z -.j.A Z A ) [ #.o.} #.} } } } ) C C B Z B B C C b r x M r.r.r.q.q.5.5.Z x r r r m -.d.F m t r r r t r (.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.r r r r u u S S S S i r r r r r r r r r r r r r t u C N Z r.R.Q.c.n.V.F.P.R.E.Q.W./.I.x.h.8.>.Y.i.F S F S S S S S S 9.h.A A A F #.o.#.O.} #.} #._ Z C C C C B Z C C N r x M r.q.q.q.q.4.m x r r r u S 9.h.S u r t r r r (.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.r r r t r r C S S S Z u r r r r r r r r r r t r r m N C C q.k.L./.A.F.P.E.~.^.^././.W.G.N.z.Z.G.$.) F I F F A S S S h.h.F F S S ` #.#.O.#.#.] ) Z C C C B C C B C C C b x V q.q.5.q.4.c r r r r r Z S d.-.Z r r r r r r (.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r b S S S S M r u r r r r r r r r r r r N C Z E a.g.k.G.^.R.E.~.^././././.^.E.L.S.^.7.[ E U U E E E E E S h.3.U L I I D Y ) _ F A A A A A Z Z Z C C C C C C N x v ) Z M m x r r x r r N S S H.` m r r r r r (.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.r r t r r r A S S S S b r r r r r r r r r r r c V C Z %.f.f.k.c.K.^.^.^././././././.~.Y.^.Z.;.' ] Y Y E U E E U U Z.;.U U U U U F U F F F S S A A Z Z Z Z Z C V Z C N x x 0 x x x x x r r r v F F S E.A r r r r r r (.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r m S S S S A x r r r r r r r r r r b Z Z Z 1.s.f.k.n.D.E.^./././././././.~.^.R.l.,.*.%.O.Y E E ) E W W J.>.E E E U U U ' $.$.F F S S A D A Z Z Z Z Z V C C x x x r x 0 0 x r r i A S S } M.m r r r r r (.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r u A S S S S A c r r r r r r r r r N Z Z A q.f.f.k.n.D.R.^././././././././.^.J.d.w.;.%.%.o._ ) ) E W ) I.W W W E E Y U $.$.} U F F L F S S D Z Z A Z Z C Z c x x r x x x x r u A S S S x.>.u r r r r r (.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r m S S S F F A x r r r r r r x x V Z A A 2.s.u.k.n.D.R.~././././././././.E.G.s.;.;.&.%.%.-.} ) ) ) ) /.) W W W W Y ) O.%.' U F ) U I S S D D A A Z Z Z Z n x x r 0 x 0 x u Z S S S W R.m r r r r r (.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.0 r r r r r Z S S A S S A c r r r x r x x Z A F F [ t.a.f.z.D.U.T.~././././././.^.Y.Z.h.-.] o.*.*.&.-.%.' ) { I.Q _ Q ) W ) W ) o.) ) E E U U U S S D A A A A Z Z c 0 x x x x 0 x Z S S S S d.d.u r r r r 0 (.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r b S S F F S S A x r t x x x x A D F F D r.a.s.k.V.L.J.~././././././.^.J.v.e.-.` ' %.*.<.&.&.o.` 2.L._ ` _ _ _ _ _ ) W ) E W W E E U U U F F S A A Z Z V x x x x x x v S S S S ` R.N r r r r r (.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.< u r r r r C S S S S S F D v x x c x c F F F ( ] u.u.u.k.c.m.S.^.^.~.~././.^./.~.N.3.O.` ' ' O.%.%.$.} ` 6.v.` ' _ ` _ _ _ Q _ ) ) W W E W E Y U U F F F F A A V x x x x x V I S S S S J.-.r r r r r : (.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.e r r r r u A S S S S D F F M x c c v F U U ' r.u.u.u.u.k.a.A.~.J.Y.E./.E.I.R.B.W.I.8.' ] ' &.<.} ' ` ' x.x.' ' ` ' ` ` ` ' _ ) _ ) ) W W W E Y U U U F F S A V x x x c I Z I S S S h.M.u r r r r 0 (.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.r r r r r m A S S S U F E U A v v v A U Y ) r.u.r.u.u.u.:.!.v.v.Z.!.E.Z.Z.E.y.;.h.E.L.8.' ;.;.' { ' o.v.y.' ' ] ' ' ] ' ` ` ` _ _ _ _ ) ) W W U E U U F F S V c c V I I I I S S 7.I.N r r r r r (.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.+ r r r r r B S S F U U U E W W N m A E Y W 5.r.r.r.t.u.v.Y.6.p.D.!.h.l.p./.;.O.[ } p.E.Y.s.%.o.o.} | L.8.' | o.' ' ' ' ' ` ` ` ` _ _ _ ) ) W W W U E U U F V n I U P I S L S 9.I.S r r r r r - (.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.9 r r r r u Z L I E U E E W W _ U Z Y W W ;.w.r.r.t.u.~.2.O.e.^.y.3.>.,.R.:.o.o.O.o.o.y.P.E.b.;.O.o.E.o.o.o.} ' [ | ' ' ' { ` ' _ _ ` _ Q W W W W E U Y U ( ( ( ( U U I E M.G.I x r r r r < (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.9 r r r r c F U U E W W W _ _ Q ' { W ) o.r.r.r.r.Z.Z.' O.^.y.O.$.O. .F.y.o.o.O.O.O.O.O.:.N.~.F.h.E.O.O.o.o.o.o.o.o.o.' { ' ' ' ` ` _ _ _ _ W W W E _ O.] ( ( ( E U ;.R.d.A c r r r r 9 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.e r r t t b U E W W W _ _ _ ` ` -.-.} ` r.r.r.u.^.*.' L.z.o.o.O.} O.x.b.O.+.O.O.O.O.O.+.O.O.h.P.^.Z.h.+.O.O.o.o.o.o.[ { ' ' ' ' ` ' ` ` _ Q ) { +.%.O._ W W W >.J.H...F n r r r r e (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.. r t x x c N E W W Q _ _ ` ' ` O.;.;.;.1.r.r.N.S.o.Z.F.o.o.O.O.O.O.r.P.O.O.O.+.%.%.+.O.+.+.%.l.B.h.Z.^.P.v.8.O.O.o.o.o.o.o.' ' ' ' ` ` ` { -.-.-.-.{ ! O.d.R.J.>.E U n r r r r r . (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.X t x x c v N W _ ` _ ` ` ' { { ;.>.,.,.u.s.Q.u.z.Y.+.O.$.O.+.O.O.;.^.+.+.X.*.+.*.X.*.+.*.+.Z.d.+.O.+.8.z.H.^.T.Z.v.8.6.o.' { { { { %.;.>.;.;.p.j.n.R.I.d.O.W W E m x x r r e o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.o t c c v m Z W ` ` ` ' ' { { %.,.,.,.2.N.T.A.^.q.O.O.O.O.%.O.+.%.W.2.-.*.*.*.*.*.*.-.*.*.R.6.*.;.%.%.%.O.O.2.p.n.F.P.^./.P.L.L.I.I.I.I.^.W.I.x.e.} ` ` _ W W N c x x x r o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.o i v b n m Z ` ' ` ' ' ' o.o.,.,.2.2.L.P.^.s.7.6.:.*.O.X.*.*.*.Z.h.;.;.=.*.:.X.-.;.;.2.~.;.X.;.X.*.*.X.-.+.O.O.O.;.;.2.6.s.d.d.h.9.,.,.;.;.[ ' { ` ` _ _ C v v c x t o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.+ c m m m m A ` ] { o.o.o.o.%.3.6.6.Y.^.s.7.7.8.8.8.9.7.6.:.:.l.H.;.;.=.@.:.:.;.:.:.h.J.;.;.;.:.*.*.*.;.,.2.7.8.7.7.7.7.6.7.6.2.2.2.,.,.O.{ { ` ` ` _ C n n v v x o (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.X a n M M V A ` [ [ o.O.O.O.>.6.6.y.9.8.8.8.8.e.y.e.y.8.9.y.h.!.8.8.8.8.8.6.6.6.2.Z.N.8.6.7.8.8.y.y.e.e.w.9.8.8.8.7.7.7.7.2.2.2.2.2.-. .{ } { { ` C M n n b p . (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(. a M N V C Z ` o. .O.O.$.+.2.7.7.7.8.8.8.e.e.e.e.y.y.y.y.y.~.h.u.y.i.i.i.i.i.i.E.z.u.y.y.y.y.y.9.y.y.e.e.9.e.8.8.7.7.7.7.7.6.2.:.o.o.{ } { W C M M m n y (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.w C C C A A ( o.$.O.%.+.-.7.7.8.8.w.e.e.e.y.y.y.y.y.s.y.L.Z.y.y.h.i.p.i.s.h.~.p.p.u.i.y.y.p.y.y.y.y.y.e.e.w.e.8.8.7.7.7.6.,.O.O.o.o.o.E V V N M M w (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.> N C A A A P o.+.+.-.-.;.7.8.e.e.e.y.y.y.y.y.y.y.y.i.m.E.h.s.y.s.s.s.s.S.J.p.u.p.s.s.y.y.s.y.y.y.y.y.e.e.e.8.q.8.7.7.6.+.+.$.O.] I Z Z C V m > (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.O f A A I I U ] +.*.;.;.;.7.e.8.y.y.y.y.y.u.y.s.p.d.p.^.l.h.s.s.s.s.h.E.c.y.h.h.y.p.i.s.u.a.i.y.y.y.y.e.e.9.w.8.8.2.%.%.%.O.` D A Z Z C s . (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.2 A I U U P W o.;.;.:.:.7.y.y.y.u.y.p.s.s.s.p.s.s.J.P.v.x.h.s.s.x.~.s.h.h.y.h.s.s.d.p.u.p.i.y.y.y.y.e.e.e.e.,.;.;.+.o.P I L A D B 4 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.@ g Y Y Y _ _ ] $.:.:.2.6.y.y.i.i.s.p.d.s.s.j.Z.~././.^.^.~.Y.~.T.v.z.d.d.d.d.s.s.s.s.s.p.p.y.y.m.x.9.9.6.;.*.O._ Y Y U I A f O (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.% L W _ _ ` ` +.P.N.9.7.y.i.s.s.s.s.l.D.E.E.E.~.^.E.W.W.!.^.E.E.E.E.L.F.Z.x.h.s.s.s.y.p.l.L.E.E.E.J.v.:.' Q ) Y Y Y L % (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.5 P ` ` ] ' %.s.Z.m.d.8.y.s.h.Z.I.I.Y.I.P.I./.E.I.Y.^.R.Y.I.I.I.I.I.I.P.P.Z.x.s.h.S.P.P.L.H.H.C.y.$.` _ _ ) L 2 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.6 Q [ | | o.O.6.s.M.x.M.S.F.F.H.H.H.H.H.P.^.^.^.Y.H.H.H.H.H.H.H.H.H.H.F.H.D.H.D.Z.C.v.p.6.' | ' ` ` P 5 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.3 J o.O.$.O.@.X.4.8.h.c.v.N.C.Z.Z.Z.Z.Z.H.S.S.S.S.S.S.S.S.Z.Z.Z.A.A.N.C.v.s.8.:.O.o.o. .} { G 3 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.# l | %.X.=.@.=.=.:.6.8.9.h.h.c.c.m.m.N.N.N.m.m.N.M.n.v.z.k.s.t.6.@.@.@.*.+.$.$.O.! g @ (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.* K / 1.=.=.:.:.:.6.6.6.6.6.w.6.8.8.8.8.8.8.6.6.6.6.:.:.:.1.1.:.@.*.+.~ H & (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.$ j R / 6.6.6.6.6.6.6.6.8.8.8.7.6.8.6.8.6.6.6.6.4.:.:.1.X.R h $ (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.8 k R @.@.6.8.e.w.8.r.8.y.8.8.8.6./ / R k 8 (.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", -"(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(." -};
--- a/frontends/wix/images/crystal/tray_icon_warning.xpm Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,390 +0,0 @@ -/* XPM */ -static char *tray_icon_warning[] = { -/* columns rows colors chars-per-pixel */ -"128 128 256 2", -" c black", -". c #110700", -"X c #130800", -"o c #220D00", -"O c #2A1200", -"+ c #331500", -"@ c #3F1A00", -"# c #441D00", -"$ c #491F00", -"% c #4B2000", -"& c #5A2600", -"* c #572600", -"= c #632B00", -"- c #6E2F00", -"; c #702F00", -": c #743300", -"> c #7A3500", -", c #843900", -"< c #873B00", -"1 c #913F00", -"2 c #954200", -"3 c #9C4400", -"4 c #8F4100", -"5 c #A54700", -"6 c #AD4C00", -"7 c #A44900", -"8 c #B64E00", -"9 c #B84F00", -"0 c #B45300", -"q c #BB5300", -"w c #BF5900", -"e c #B55608", -"r c #C15700", -"t c #C45A00", -"y c #CA5E00", -"u c #C46200", -"i c #CD6200", -"p c #CD6B00", -"a c #C66900", -"s c #D16400", -"d c #D46C00", -"f c #D96A00", -"g c #CD7400", -"h c #CD7A00", -"j c #D47300", -"k c #D37A00", -"l c #DB7D00", -"z c #DB7500", -"x c #C96510", -"c c #DB7310", -"v c #D46E10", -"b c #E27B00", -"n c #E07600", -"m c #DB7A20", -"M c #C77333", -"N c #CC7127", -"B c #DB8200", -"V c #DC8A00", -"C c #D38700", -"Z c #DC9400", -"A c #DB9800", -"S c #E48400", -"D c #E68A00", -"F c #EA8D00", -"G c #E39300", -"H c #EC9400", -"J c #E39D00", -"K c #EC9B00", -"L c #F39B00", -"P c #F29700", -"I c #DFA600", -"U c #E4A400", -"Y c #E5AB00", -"T c #ECA900", -"R c #F5A300", -"E c #FAA400", -"W c #F4AB00", -"Q c #FFAC00", -"! c #E6B200", -"~ c #EAB200", -"^ c #ECBB00", -"/ c #E6B900", -"( c #FFB300", -") c #F4BB00", -"_ c #FFBB00", -"` c #F6B200", -"' c #E5AB1C", -"] c #DD8230", -"[ c #E08630", -"{ c #EAC200", -"} c #EECB00", -"| c #F4C400", -" . c #FFC300", -".. c #F2CC00", -"X. c #FFCB00", -"o. c #EFD500", -"O. c #FFD300", -"+. c #F4DA00", -"@. c #FFDB00", -"#. c #F9D806", -"$. c #FFDD1C", -"%. c #FFDC14", -"&. c #F6E100", -"*. c #FFE300", -"=. c #FEEB00", -"-. c #FCE708", -";. c #FEF400", -":. c #FFFF00", -">. c #FFFE0C", -",. c #FFEA19", -"<. c #FFFF1B", -"1. c #FFFA14", -"2. c #FEDE34", -"3. c #FFDE29", -"4. c #FFE42D", -"5. c #FFE826", -"6. c #FFFF24", -"7. c #FFFE2B", -"8. c #FFF227", -"9. c #FFE43B", -"0. c #FFE636", -"q. c #FFFE34", -"w. c #FFFE3A", -"e. c #FFF535", -"r. c #CA834E", -"t. c #CE884E", -"y. c #D38645", -"u. c #CA8453", -"i. c #C8875A", -"p. c #CA8A5C", -"a. c #D69753", -"s. c #E39952", -"d. c #DCA755", -"f. c #DDA249", -"g. c #E6B54D", -"h. c #E3A95C", -"j. c #C78B66", -"k. c #C78D6C", -"l. c #C98A60", -"z. c #CC9369", -"x. c #D59869", -"c. c #C79174", -"v. c #C7957C", -"b. c #CD9B78", -"n. c #D3A979", -"m. c #DBB373", -"M. c #E9AA6F", -"N. c #E2BD6A", -"B. c #E3BB63", -"V. c #E7B07F", -"C. c #F7D64E", -"Z. c #F6D95A", -"A. c #EEC854", -"S. c #FFE54F", -"D. c #FFE847", -"F. c #FFFE45", -"G. c #FFFE4B", -"H. c #FFF545", -"J. c #FFE753", -"K. c #FFE956", -"L. c #FFEB5B", -"P. c #FCE55E", -"I. c #FFFE5C", -"U. c #FFF65A", -"Y. c #F2D868", -"T. c #E7C976", -"R. c #ECD173", -"E. c #FFED63", -"W. c #FCE865", -"Q. c #FFF067", -"!. c #FFFF63", -"~. c #FFF16C", -"^. c #FFFD6C", -"/. c #FFF473", -"(. c #FFF67C", -"). c #FFFF79", -"_. c #F7E679", -"`. c #C89A84", -"'. c #C89D8B", -"]. c #C79781", -"[. c #CEA288", -"{. c #D6A88A", -"}. c #CBA393", -"|. c #CCA79B", -" X c #CCA89D", -".X c #CDA996", -"XX c #D5AC95", -"oX c #D8B599", -"OX c #DBBD89", -"+X c #EBBB8F", -"@X c #E5BD9F", -"#X c #CEACA3", -"$X c #D0B2AB", -"%X c #D6B9A9", -"&X c #D4B9B3", -"*X c #D6BDB9", -"=X c #D3B8B1", -"-X c #ECD886", -";X c #E7CA94", -":X c #F1E183", -">X c #FFF983", -",X c #FFFB8B", -"<X c #FCF385", -"1X c #FFFD94", -"2X c #FFFF9B", -"3X c #F8F191", -"4X c #F4EC9B", -"5X c #DCC3A8", -"6X c #D9C3BC", -"7X c #E8C7AE", -"8X c #E8DAA7", -"9X c #E3CEBB", -"0X c #E2CAB5", -"qX c #EAD6BB", -"wX c #F4D8BF", -"eX c #F2CFAF", -"rX c #FFFFA3", -"tX c #FFFFAC", -"yX c #F6F0A9", -"uX c #FFFFB3", -"iX c #FFFFBB", -"pX c #F8F4B8", -"aX c #F0E7B1", -"sX c #DFC99D", -"dX c #DAC6C3", -"fX c #DDCCCA", -"gX c #DCC9C4", -"hX c #E0CEC3", -"jX c #E7D8C8", -"kX c #E2D3D3", -"lX c #E6D9D9", -"zX c #E8DCDC", -"xX c #EADBD2", -"cX c #EDE2CC", -"vX c #EFE7C6", -"bX c #F4E5CD", -"nX c #FFFFC4", -"mX c #FFFFCB", -"MX c #F5F0C1", -"NX c #EEE5D7", -"BX c #F4EADC", -"VX c #F3E8D4", -"CX c #F9F5D6", -"ZX c #FFFFD3", -"AX c #F9F4DA", -"SX c #FFFFDD", -"DX c #F6F0DA", -"FX c #F9F5E0", -"GX c #FFFFE3", -"HX c #FBF4ED", -"JX c #FFFFEB", -"KX c #FFFFF4", -"LX c #FFFFFE", -"PX c #FCF7F4", -"IX c #F8EFE8", -"UX c None", -/* pixels */ -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXy y y i s s i s i i i i i i y y y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXy y s s s i s s i i s i i i i i i i i i y i y s i y y y UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s i s i s s s i l V V Z Y ~ ! Y J V V B p i i y i y y y i y y t UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s s i s s V U { +.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.+...! Z j y i y y i i y w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s s s s V ^ &.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.=.{ A p y y y y y y UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s s s Z } ;.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.&.I j u y y y t q UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s s B { ;.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.} A y y y y y 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s i J +.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.:.;.I a y y y y 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s i U =.:.:.:.:.:.:.:.:.:.:.:.:.:.:.w.!.).tXtXSXJXJXGXJXJXGXJXJXJXiXtX1X).w.<.:.:.:.:.:.:.:.:.:.:.:.:.:.;.Y a y y y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s Z =.:.:.:.:.:.:.:.:.:.:.:.>.F.>XiXGXGXGXJXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXmX2X!.<.:.:.:.:.:.:.:.:.:.:.:.;.Y a y t y q UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s z +.:.:.:.:.:.:.:.:.:.:.>.!.tXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXnX).7.:.:.:.:.:.:.:.:.:.:.=.V y t y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s ~ :.:.:.:.:.:.:.:.:.:.F.2XGXSXGXGXSXGXGXGXGXSXGXGXSXGXGXGXSXGXGXSXGXGXGXGXGXGXSXGXSXGXGXGXGXGXSXGXnX!.>.:.:.:.:.:.:.:.:.:.o.g t t t 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXi s s s B &.:.:.:.:.:.:.:.:.>.^.nXSXGXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXGXSX,X<.:.:.:.:.:.:.:.:.;.U y t t w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s s Y :.:.:.:.:.:.:.:.>.^.mXSXSXSXSXSXSXSXSXSXSXBXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXzXlXzXzXFXGX,X<.:.:.:.:.:.:.:.:.} a r t t UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s d o.:.:.:.:.:.:.:.:.!.mXSXSXZXSXSXZXSXZXSXZXZXAXzXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXlXzXZXSXZX,X>.:.:.:.:.:.:.:.=.k t t t UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s B &.:.:.:.:.:.:.:.q.iXSXZXZXSXZXSXSXZXZXSXZXSXZXNXlXzXFXFXFXFXAXFXFXFXFXFXFXFXAXFXAXFXFXFXFXFXFXFXFXFXFXAXFXFXFXFXlXlXCXZXSXZXmXI.:.:.:.:.:.:.:.:.A t r y UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s V ;.:.:.:.:.:.:.>.).ZXZXZXZXZXZXZXZXZXZXZXZXSXZXZXxXkXNXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXNXkXkXCXZXZXZXZXZXtX7.:.:.:.:.:.:.:.^ y r y 6 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s U :.:.:.:.:.:.:.q.iXZXZXZXmXZXZXZXZXZXZXZXZXZXZXZXCXkXkXDXGXGXGXGXGXGXGXGXGXSXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXAXkXkXxXZXZXZXZXZXZXZXZX^.:.:.:.:.:.:.:.{ r r t 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s J :.:.:.:.:.:.:.^.mXZXmXZXZXZXZXmXZXZXZXZXmXZXZXZXmXcXkXkXSXGXGXSXGXSXGXGXGXGXGXGXSXSXGXGXSXGXGXGXGXGXSXSXGXSXGXSXGXlXkXkXCXmXZXZXZXZXmXZXZXmX2X>.:.:.:.:.:.:.{ t t r 3 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s d J :.:.:.:.:.:.>.1XmXZXmXmXZXmXmXZXmXmXmXmXmXmXZXmXmXmXkXfXxXSXSXGXSXGXGXSXSXGXSXSXGXGXGXSXSXSXGXSXSXGXSXGXSXGXGXSXGXNXfXfXvXmXmXZXmXZXmXmXZXmXmXmXiX6.:.:.:.:.:.:.{ r t r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s U :.:.:.:.:.:.<.rXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXmXfXfXBXGXSXSXGXSXSXSXGXSXSXGXSXSXSXGXSXGXSXSXGXSXSXSXGXSXSXSXAXfXfXxXmXmXmXmXmXmXmXmXmXmXmXmXmXiXG.:.:.:.:.:.:.{ t y r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXs s s Z :.:.:.:.:.:.6.iXnXmXmXmXmXnXmXnXmXmXmXnXnXmXmXmXmXnXmXvXfXfXAXSXGXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXGXSXSXjXfXfXMXmXmXmXmXmXmXnXmXnXmXnXmXmXmXnXG.:.:.:.:.:.:.{ r t w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXi s s B ;.:.:.:.:.:.6.iXnXmXnXnXnXmXnXmXnXnXnXnXmXmXnXnXnXnXnXnXjXdXhXSXSXSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXSXSXSXSXNXdXdXvXnXnXnXnXnXnXnXnXmXnXmXnXnXnXnXmXnXG.:.:.:.:.:.:.Y r r q UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXs s j ;.:.:.:.:.:.6.uXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXdXdXxXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXCXdXdXjXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXnXG.:.:.:.:.:.:.A r r 8 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXs s s &.:.:.:.:.:.6.iXnXnXnXnXiXnXiXnXnXiXnXnXnXnXnXiXnXnXnXnXnXMXdX6XDXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXgXdXgXMXnXnXiXnXnXnXnXnXiXnXnXnXnXnXnXiXnXnXnXnXF.:.:.:.:.:.;.h r r 3 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXi s s ^ :.:.:.:.:.<.uXiXnXiXnXiXiXiXiXnXiXiXiXiXnXnXiXiXiXiXnXiXnXjX6XdXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXcXdX6XqXnXiXiXnXiXiXiXnXiXiXiXiXiXiXiXiXnXiXiXiXiXiXF.:.:.:.:.:.&.u r r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXi s s V :.:.:.:.:.>.1XiXiXiXiXiXiXiXiXnXiXiXiXiXiXiXiXiXiXiXiXiXiXiX6X6XjXSXSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXAX*X6X9XiXiXiXiXiXiXiXiXnXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiX6.:.:.:.:.:.{ w r 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXs s d ;.:.:.:.:.:.>XiXuXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXpX*X*XcXSXSXSXSXSXSXSXSXSXSXSXSXSXZXSXSXSXSXSXSXSXSXZXSXSXhX*X*XpXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXiXtX<.:.:.:.:.:.V r r 2 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXs s s o.:.:.:.:.:.I.iXiXiXuXiXiXiXuXiXuXiXiXiXiXuXiXuXiXiXiXiXiXiXiXaX*X*XZXSXSXSXSXSXSXZXSXSXZXSXSXSXSXSXSXSXSXSXSXZXSXSXSXcX*X*XqXiXiXiXuXiXiXiXiXuXiXiXuXiXiXiXiXiXiXiXuXiXiXiXiXiXiX1X:.:.:.:.:.;.u r r UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXs s V :.:.:.:.:.7.iXuXuXiXiXuXuXiXuXuXuXuXiXiXuXuXuXiXiXuXuXuXiXuXuX6X&X6XSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXSXSXZXZXSXSXSXSXSXAX*X=X6XuXuXuXuXuXuXiXuXuXiXiXuXuXuXuXiXuXiXuXiXuXiXuXiXiXuXuXiX!.:.:.:.:.:./ q r 6 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXs s i =.:.:.:.:.>.2XuXuXuXtXuXuXuXuXuXuXuXiXtXuXuXuXuXtXuXuXuXuXuXuXuX=X&XcXSXSXSXSXSXSXSXSXZXSXSXSXZXSXSXSXSXSXSXSXSXSXSXSXgX=X=XpXuXuXuXuXuXuXtXuXuXuXtXuXuXiXuXuXtXuXuXuXtXuXuXtXuXuXuXuXuX7.:.:.:.:.:.C w w UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXi s s ~ :.:.:.:.:.!.uXuXuXuXuXuXuXtXuXtXuXuXtXuXuXuXuXuXuXuXuXuXuXtXuXaX=X$XCXSXSXSXSXSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXVX=X=X8XuXuXuXuXuXuXuXuXtXiXtXuXuXuXtXtXuXuXuXtXuXuXuXuXuXuXuXuXuXuX2X>.:.:.:.:.o.q w 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXi s d ;.:.:.:.:.7.uXtXtXtXtXtXtXtXtXuXtXtXuXtXtXtXtXtXtXtXtXtXtXtXtXtX0X%X&XSXSXZXSXSXSXSXSXSXSXSXSXSXSXSXZXSXSXSXSXSXSXSXCX%X$X5XpXuXtXtXuXtXtXtXuXtXtXtXtXtXuXtXtXtXtXuXtXtXtXtXtXtXtXtXtXtXtXtX!.:.:.:.:.:.A q r , UXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXy s s ! :.:.:.:.:.,XtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtX%X$X9XSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXjX$X$XaXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtXtX6.:.:.:.:.=.q r 0 UXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXi s d ;.:.:.:.:.G.tXtXtXrXtXtXtXtXrXtXtXtXtXrXtXrXtXtXtXrXtXtXtXtXtXrXyX$X$XVXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXbX$X$X5XrXrXtXtXrXrXtXtXtXtXrXtXtXtXtXtXrXtXtXrXtXrXtXtXtXtXtXrXtXtXtXrXtXtX).:.:.:.:.:.Z q w > UXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXy s s ! :.:.:.:.>.2XrXtXrXtXrXrXrXrXtXrXtXtXrXtXrXtXtXrXtXrXtXrXtXrXrXtX8X$X#XSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSXSX&X#X$XrXtXrXtXrXtXrXtXrXrXrXtXrXtXtXrXtXrXtXtXtXrXrXtXrXrXrXrXrXtXrXtXrXtXrXtX7.:.:.:.:.=.r q 6 UXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXs s s ;.:.:.:.:.I.rXrXrXrXrXtXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXtXrX%X#X6XSXSXSXSXSXSXSXSXSXSXGXSXSXSXSXSXSXSXSXSXSXSXjX#X#X8XrXrXrXrXrXrXrXtXrXrXrXrXrXrXrXrXrXrXrXrXrXrXtXrXrXrXtXrXtXrXrXrXrXrXrXrX1X:.:.:.:.:.C r w UXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXs s A :.:.:.:.>.2XrXrXrXrXrXrXrXrXrXrXrXrXrXrX2XrXrXrXrXrXrXrXrXrXrXrXrX X#XjXSXSXSXGXSXGXSXSXSXGXSXSXSXSXSXGXGXSXSXSXSXCX#X#X5XrXrXrXrXrXrXrX2XrXrXrXrXrXrXrXrXrXrX2XrXrXrXrXrXrXrXrXrXrXrXrXrXrX2XrXrXrXrXw.:.:.:.:.o.r q 3 UXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXi s s &.:.:.:.:.F.2XrX2X2XrX2XrXrX2XrXrX2X2XrXrXrXrX2XrX2XrX2XrXrX2XrX2X8X#X#XCXSXSXSXSXSXGXSXGXSXSXSXGXGXGXSXSXSXSXGXGXSX9X X#X4XrX2X2XrX2X2XrX2XrXrXrXrX2XrX2X2XrX2XrX2XrX2XrXrX2XrXrX2XrXrX2XrXrXrX2XrX2XrX).:.:.:.:.:.a w q UXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXi s l :.:.:.:.:.,X2X2X2XrX2X2X2X2X2X2X2X2XrX2X2X2X2X2X2X2X2X2X2X2X2X2X2XsX|.$XSXSXSXSXGXSXSXSXGXGXSXGXSXSXSXSXSXSXGXSXSXCX X X X X X X X X X|.|. X X X XoX4X2XrX2X2X2XrX2XrX2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X6.:.:.:.:.I r q > UXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXs s ~ :.:.:.:.q.2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2XXX|.9XSXGXGXSXGXGXSXGXSXGXSXGXSXGXGXGXGXSXGXGXSXAX|.|.|. X|. X X|.|.|.|.|.|.|.|. XsX2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X2X!.:.:.:.:.=.q w 3 UXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXi s s =.:.;.:.:.^.2X2X2X1X2X2X2X2X1X2X1X2X2X2X2X2X2X2X1X2X2X2X2X2X2X2X1X4X|.|.bXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXSXGXGXGXGXGXGXAXbXVXbXcXVXVXVXVXVXVXVXVXbX9X}.|.2X2X2X2X1X2X2X2X2X2X2X2X2X2X2X2X2X2X1X2X2X2X2X2X2X2X2X>.:.:.:.:.a w r UXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXi s B :.:.:.:.>.1X1X2X2X1X1X1X1X1X1X2X1X1X1X1X1X1X2X1X1X2X2X1X1X1X1X1X1XsX}.|.GXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXSXGXSXGXGX%X}..X1X1X1X2X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X2X1X1X1X1Xq.;.:.:.:.! q q : UXUXUXUXUXUXUXUXUXUX", -"UXUXUXs s Y :.:.;.:.w.1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1XXX}.5XGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXGXqX}.}.sX1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X1X^.;.:.;.:.+.q w 1 UXUXUXUXUXUXUXUXUXUX", -"UXUXt s s +.;.;.:.;.^.1X1X1X,X1X1X,X1X1X1X1X1X1X1X,X1X1X,X1X1X1X1X,X1X1X1X1X3X}.}.jXJXGXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXGXGXJXGXGXGXCX.X}.XX1X1X,X1X1X1X1X1X3X1X,X1X1X1X1X1X1X1X1X1X1X1X1X1X,X1X1X1X1X>.;.:.;.:.a q q UXUXUXUXUXUXUXUXUXUX", -"UXUXi i d :.;.:.;.>.,X1X,X1X1X,X,X1X1X,X1X,X1X,X1X,X,X,X1X,X1X,X,X1X1X,X,X,X;X}.}.AXGXJXGXGXGXJXGXJXJXGXGXJXGXJXJXGXGXJXGXJXJXGXGXGXGXJXGXGXJXGXJXGXGXJXGX%X}.'.3X,X3X1X,X1X,X1X,X1X1X,X1X,X1X,X,X1X1X,X1X,X1X,X,X1X1X,X,X1Xe.:.;.;.;.Z q q UXUXUXUXUXUXUXUXUXUX", -"UXUXs i V ;.;.;.;.e.,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X1X,X,X,X,X,X,X,X,X,X1XOX'.#XGXJXGXGXJXGXJXJXGXGXGXJXGXJXGXGXGXJXGXJXGXGXGXJXJXJXGXJXGXGXJXGXJXJXJXcX'.[.;X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,XU.;.;.:.;./ r q ; UXUXUXUXUXUXUXUXUX", -"UXUXs s Y ;.;.;.;.G.,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X>X,X,X,X,X,X,X,X'.'.9XGXJXJXJXJXJXGXJXGXJXJXJXJXJXGXJXJXGXJXJXGXJXJXJXJXJXGXGXJXJXJXJXJXGXFX}.'.[.,X,X,X>X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X,X>X;.;.:.;.+.q q < UXUXUXUXUXUXUXUXUX", -"UXr i s ..:.;.;.;.^.>X,X,X>X>X,X,X>X,X>X>X,X>X,X>X,X>X,X,X,X,X>X,X>X>X,X,X:X'.`.jXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJX5X`.'.:X,X,X>X,X,X>X,X>X,X>X,X>X,X>X,X>X,X,X>X,X>X,X>X,X,X>X,X>X,X>X,X>X1.;.;.;.;.q q 7 UXUXUXUXUXUXUXUXUX", -"UXt s i =.;.;.;.;.>X>X>X>X,X>X>X>X,X>X>X>X>X,X>X>X>X,X>X>X,X>X>X,X>X>X>X>X>X[.`.`.`.`.`.`.`.`.`.`.'.`.'.`.`.XXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXJXVX`.`.OX>X>X>X>X>X,X,X>X>X>X>X>X>X>X,X>X>X>X>X>X>X,X>X>X>X>X>X>X,X>X>X>X>Xe.;.;.;.;.h q q UXUXUXUXUXUXUXUXUX", -"UXt s i ;.;.;.;.8.>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X-X`.].`.`.`.`.`.].].].].].`.`.].'.JXJXJXJXKXJXKXJXJXJXJXKXJXJXJXJXJXJXGX}.].`.<X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>X>XG.;.;.;.;.A q r UXUXUXUXUXUXUXUXUX", -"UXi s B ;.=.=.;.e.>X>X>X>X>X>X(.>X>X>X(.>X>X>X>X>X(.>X>X>X>X>X(.>X>X>X>X(.>X(.<X:X:X-X:X:X:X:X:X-X_.:XT.].v.9XJXJXKXKXJXKXJXKXJXJXJXKXJXKXJXKXKXKX5X].].R.>X>X(.>X>X(.>X>X>X>X>X>X>X>X(.>X>X(.>X>X(.>X>X>X>X>X>X>X>X>X>X>X>X(.Q.=.;.;.;.I q q % UXUXUXUXUXUXUXUX", -"UXs s V ;.;.;.=.H.(.(.>X(.(.>X(.(.(.>X(.(.(.<X(.>X(.(.>X(.(.>X(.(.(.(.(.>X>X(.(.(.(.(.(.>X(.(.>X(.>X).{.v.[.HXKXKXJXKXKXJXKXKXKXKXKXJXKXKXJXKXJXBX].v.OX>X>X(.>X(.(.>X>X(.(.(.>X>X(.(.<X(.>X>X(.(.<X(.(.(.>X(.(.(.(.>X(.(.>X(.~.;.=.=.;.{ q q > UXUXUXUXUXUXUXUX", -"UXs i T =.;.=.=.U.>X(.(.(.(.(.(.(.(.(.>X(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(._.].].hXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXXXv.`._.(.(.>X(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.;.=.=.;.{ q q , UXUXUXUXUXUXUXUX", -"UXi i U ;.=.=.;.U.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.b.v.[.KXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKXKX0Xv.v.T.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.-.;.=.=.@.9 q > UXUXUXUXUXUXUXUX", -"UXy s ^ =.=.=.=.,.~.(./.(.(.(.).(././.(.(././.(.(.(.(.(.(.(.(.(.(.(.(.(./.(.(././.(.(././.(.(./.(.R.v.v.jXKXKXLXKXKXKXKXKXKXKXKXKXKXKXLXKXKXIX`.v.n./././.(./.(.(.(.(.).(.(././.(./.(./.(.(.(./.(./.(.(.(.(.(./.(.(.(.)./.(./.e.;.=.=.;.=.q q 3 UXUXUXUXUXUXUXUX", -"UXs i ..=.=.=.=.;.,./././././././.(././.(./././././././././././././././.(./././.(./.(./.(././././.b.c.`.LXKXLXKXLXLXKXLXLXKXLXLXLXLXKXLXKXLXXXc.c._./.(./.(./././././././././.(./.(.(././././././.(./././././.(././././.(./.8.=.=.=.=.=.=.q 9 3 UXUXUXUXUXUXUXUX", -"q i s | =.=.=.=.=.=.,.~././././././././././././././././././././././././././././././././././././.R.c.c.jXKXLXKXLXLXLXKXLXLXKXKXLXLXKXLXLXKXhXc.k.T././././././././././././././././././././././././././././././././././././.5.=.=.=.=.=.=.=.9 9 3 UXUXUXUXUXUXUXUX", -"0 i i | =.=.=.=.=.=.=.-.U././.~.~./././.~./././.~././././.~.~./.~././././.~./././.~./././.~././.b.c.`.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXb.c.n.~./.~././././.~././.~././.~./././.~././././.~./././.~././.~.~./././.~.~.8.=.*.=.=.=.=.=.=.9 q 5 UXUXUXUXUXUXUXUX", -"0 i i ..=.=.=.*.=.=.=.=.-.K.~./.~./.~.~.~./.~.~./.~.~.~.~.~./.~./.~.~.~.~.~./.~.~.~./.~.~.~.~.R.k.c.xXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXoXc.k.R.~.~.~./.~.~.~./.~.~./.~.~.~.~.~./.~.~.~.~.~./.~.~.~./.~.~.~./.~.~.~.E.,.*.-.*.=.=.*.=.*.=.9 q 3 UXUXUXUXUXUXUXUX", -"0 s i | =.*.*.=.=.*.*.*.=.*.9.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.z.k.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXxXk.k.N.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.L.-.*.=.=.=.*.=.=.*.=.=.9 q 3 UXUXUXUXUXUXUXUX", -"0 s i | =.*.*.*.=.*.*.=.*.=.*.5.Q.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.E.~.~.~.Q.~.E.~.E.~.~.~.~.~.N.j.k.NXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXb.k.k.m.N.N.N.N.N.N.T.T.L.~.E.~.~.Q.~.~.~.E.~.E.~.~.E.~.~.E.~.~.~.Q.~.E.~.9.=.=.=.*.*.*.=.*.*.*.=.*.9 q 3 UXUXUXUXUXUXUXUX", -"6 y i | *.*.=.=.*.=.*.*.=.*.*.=.-.S.E.~.E.Q.E.Q.Q.E.Q.~.E.Q.E.E.Q.Q.Q.~.Q.Q.~.~.~.~.E.E.W.W.z.j.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXNXj.j.j.j.j.j.j.j.k.j.j.j.B.~.E.E.~.Q.E.Q.~.~.Q.Q.Q.~.~.E.~.~.E.Q.Q.Q.Q.L.5.*.=.*.*.*.=.*.=.*.=.*.*.=.9 9 3 UXUXUXUXUXUXUXUX", -"UXi s ~ *.*.*.*.@.*.*.*.@.*.*.*.*.*.0.U.E.E.~.E.E.E.E.E.E.Q.E.~.E.E.E.E.E.E.E.E.E.E.E.~.E.N.j.j.zXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXoX{.{.{.{.{.{.{.{.{.v.j.j.Y.E.E.E.E.E.Q.E.~.E.E.E.E.E.E.E.E.E.E.Q.Q.D.*.=.*.*.*.*.*.*.*.*.*.*.*.*.=.9 9 3 UXUXUXUXUXUXUXUX", -"UXi y U *.@.*.*.*.*.*.*.*.*.*.*.*.*.*.-.D.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.l.l.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXjXi.j.Y.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.E.L.5.@.*.*.*.*.*.*.@.*.*.*.@.*.*.*...9 9 > UXUXUXUXUXUXUXUX", -"UXi y U *.@.*.*.*.*.@.*.@.*.*.*.*.*.@.*.*.,.L.E.E.E.L.E.E.L.E.E.E.E.L.E.E.E.L.E.E.E.E.L.B.l.l.NXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXk.l.l.P.E.L.E.E.E.E.L.L.E.E.E.L.E.E.E.E.E.0.*.*.*.*.@.*.*.@.*.*.*.*.@.*.*.*.@.^ q 9 > UXUXUXUXUXUXUXUX", -"UXi y V *.*.*.@.@.*.@.*.*.*.@.*.@.*.@.*.@.*.@.4.K.L.L.L.L.L.L.L.L.E.L.L.L.L.L.E.L.L.L.P.l.i.oXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX{.i.l.Z.L.L.L.E.L.L.L.L.L.L.L.L.L.L.L.E.9.-.*.@.@.*.*.*.*.@.*.@.@.@.*.*.*.@.*.*.^ q 9 - UXUXUXUXUXUXUXUX", -"UXy i l *.@.@.*.@.*.*.@.*.@.@.*.@.*.*.@.@.*.*.@.-.4.L.L.L.L.L.L.L.L.L.L.L.L.L.L.L.L.L.d.i.i.IXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX7Xi.i.h.L.L.L.L.L.L.L.L.L.L.L.L.L.L.K.9.-.@.@.@.*.*.*.@.@.*.*.@.*.*.@.@.*.@.*.@.@.A q 9 # UXUXUXUXUXUXUXUX", -"UXw y i @.*.@.@.*.@.@.*.@.*.@.@.*.@.*.@.*.@.@.@.@.*.@.4.K.K.K.L.K.L.L.L.K.L.K.L.L.L.Z.p.i.oXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXBXi.i.a.L.L.L.L.K.K.L.L.L.L.L.L.L.K.9.%.*.@.@.*.*.@.@.@.@.*.@.@.*.@.@.*.*.@.*.@.@.*.V 0 q O UXUXUXUXUXUXUXUX", -"UX0 y i O.@.@.*.@.@.@.@.@.@.@.@.@.@.@.@.@.@.*.@.@.@.@.@.@.5.D.K.K.K.K.K.K.L.J.L.J.K.d.i.u.HXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXz.u.p.Z.K.K.J.K.K.K.K.K.K.K.K.J.4.#.*.@.@.@.@.@.*.@.@.@.*.@.@.@.*.@.@.@.*.@.@.@.*.@.g 9 q UXUXUXUXUXUXUXUXUX", -"UX6 y s ) @.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.%.0.S.J.J.J.J.J.J.J.J.a.i.{.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX{.u.t.A.J.J.J.J.J.J.J.J.J.J.9.5.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.9 q 1 UXUXUXUXUXUXUXUXUX", -"UXUXi y J @.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.@.$.0.J.J.J.J.J.J.d.u.t.u.u.u.u.u.u.u.x.IXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX0Xu.u.d.J.J.J.J.J.J.J.J.2.5.#.@.O.@.@.@.@.@.@.@.@.@.@.@.@.O.O.@.@.@.@.@.@.O.O.@.@.@.@.) 9 9 < UXUXUXUXUXUXUXUXUX", -"UXUXi y V @.O.@.@.@.O.@.@.@.@.@.@.@.O.@.@.O.@.@.@.@.O.@.O.@.@.O.O.@.@.O.O.%.3.9.S.J.C.u.r.r.r.r.u.r.r.r.r.9XLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXBXp.r.a.C.S.S.S.S.D.0.$.@.#.O.O.@.O.@.@.@.@.O.O.@.@.O.@.@.@.@.@.@.@.O.@.@.@.@.@.@.@.@.@.U 8 q = UXUXUXUXUXUXUXUXUX", -"UXUXw y i O.@.O.@.@.O.@.@.@.@.@.O.@.@.O.O.@.@.O.@.@.O.@.@.@.@.@.@.O.@.@.@.@.O.@.@.$.3.2.g.g.g.g.g.g.f.r.r.IXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXPXx.r.r.C.S.9.2.$.%.@.@.O.@.@.@.@.@.@.O.@.@.@.O.@.@.O.@.@.O.@.O.@.@.O.@.@.O.@.O.@.@.O.@.O.C 8 8 + UXUXUXUXUXUXUXUXUX", -"UXUX6 i y | O.@.O.@.@.O.O.O.O.O.@.@.O.@.@.O.@.O.O.@.@.O.O.O.O.O.O.@.O.O.O.@.@.O.O.@.O.@.@.%.%.3.2.2.f.r.p.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX{.M N ' %.@.@.O.@.O.O.@.@.O.O.O.O.O.O.@.@.O.O.@.O.@.@.O.@.O.O.@.O.@.@.O.@.O.O.@.O.@.@.O.O.u 8 8 UXUXUXUXUXUXUXUXUXUX", -"UXUXUXy y J O.@.@.O.O.@.O.@.O.@.O.O.@.O.@.O.O.@.O.O.O.@.O.@.O.@.O.@.O.@.O.O.O.O.@.O.@.O.O.O.O.O.O.O.t 8 x.LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLX7X8 8 a O.@.O.@.O.O.O.O.O.O.@.O.@.O.@.O.O.O.O.@.O.@.O.O.@.O.@.@.O.@.O.O.@.O.@.O.O.@.O.O.@.^ q 9 < UXUXUXUXUXUXUXUXUXUX", -"UXUXUXy y z O.O.O.O.O.O.O.O.O.@.O.O.O.O.O.O.O.O.@.O.O.O.O.O.O.O.O.O.O.O.@.@.O.O.O.O.O.O.O.@.@.O.O.| 9 9 @XLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXIXe 0 u X.O.O.O.O.O.@.O.@.O.O.O.O.O.O.@.O.@.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.@.O.O.O.O.O.A 8 8 = UXUXUXUXUXUXUXUXUXUX", -"UXUXUX0 y y | O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.U 9 q HXLXLXLXLXLXLXLXLXLXLXLXLXLXLXLXM q 9 ) O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.a 8 8 . UXUXUXUXUXUXUXUXUXUX", -"UXUXUX2 y y U O.X.O.O.O.O.O.O.X.O.O.O.X.O.O.O.O.X.O.O.O.O.O.X.O.O.O.O.O.O.O.X.O.O.O.X.O.O.O.X.O.#.k q M LXLXLXLXLXLXLXLXLXLXLXLXLXLXLXx.q q Z O.X.O.O.O.X.O.O.O.O.O.O.O.O.O.O.X.O.O.X.O.O.O.X.O.O.O.X.O.O.O.O.O.X.O.O.O.O.X.O.O.O.| 8 9 1 UXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXi y z O.O.O.O.X.X.O.O.X.O.X.O.X.O.X.O.O.O.X.X.O.O.O.O.O.O.X.X.O.O.O.O.O.X.O.O.O.O.O.O.O.X.t r x.LXLXLXLXLXLXLXLXLXLXLXLXLXLXjXr q g X.O.O.O.O.O.O.O.X.X.X.O.O.X.O.O.O.X.X.O.O.O.X.O.O.O.X.O.O.O.X.O.O.X.O.O.X.O.O.X.O.O.O.Z 9 8 = UXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUX6 y y ` X.O.X.O.O.O.X.O.O.X.O.O.X.O.O.X.O.O.O.X.X.O.X.X.O.O.O.X.X.O.X.X.O.X.X.O.X.X.O.X.) r r 7XLXLXLXLXLXLXLXLXLXLXLXLXLXHXy q r X.O.X.X.O.X.O.X.O.O.O.O.X.X.O.X.X.O.O.O.O.X.X.O.X.X.O.X.O.O.X.O.X.O.X.O.X.O.O.X.O.X.O.X.w q 6 X UXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXi y V X.O.O.X.X.X.O.X.O.X.O.O.O.X.X.O.X.X.O.X.X.O.X.O.X.X.O.X.O.X.X.O.X.O.O.X.O.O.X.O.J r r LXLXLXLXLXLXLXLXLXLXLXLXLXLXy.r r ` X.O.X.X.O.X.X.X.O.O.X.O.X.X.O.X.X.O.X.O.X.O.O.O.X.X.O.X.X.O.X.O.X.O.O.X.O.X.O.X.O.X.X.Y 9 9 < UXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXw y a X.X.X.O.X.O.O.X.X.X.X.X.X.O.X.X.O.X.X.O.X.X.O.X.X.O.X.O.X.X.O.X.O.X.X.O.X.X.X.X.f t y.KXLXLXLXLXLXLXLXLXLXLXLXLXV.t r V X.O.O.X.O.X.X.O.X.X.X.O.X.O.X.X.O.X.X.O.X.O.X.X.X.O.X.X.O.X.X.O.X.O.X.X.X.X.X.X.O.X.X.O.g 9 9 + UXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUX2 y y G X.X.X.X.X.X.X.X.O.X.X.X.X.X.X.X.X.X.X.X.O.X.X.X.X.X.X.O.X.X.X.X.X.X.X.X.X.X.X.r r {.LXLXLXLXLXLXLXLXLXLXLXLXbXr r g X.X.X.X.X.X.X.X.X.X.O.X.X.X.X.X.O.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.O.X.X.O.) q 8 1 UXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXr y a .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.` y t wXLXLXLXLXLXLXLXLXLXLXLXHXN t i ) X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.k 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUX3 u y G X.X. .X. .X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X. .X. .X.G t x LXLXLXLXLXLXLXLXLXLXLXLXa.y y T X.X.X.X.X.X.X.X.X.X.X. .X. .X.X. .X.X.X. .X. .X.X. .X.X.X.X.X.X.X.X.X.X.X.X.X.X. .X. .X.) 9 8 5 UXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXt y i .X.X.X. .X. .X. .X.X. . .X.X. .X. .X. .X.X. .X. .X. .X. .X. .X.X.X.X.X.X.j y a.LXLXLXLXLXLXLXLXLXLXLX+Xy y V .X. . .X.X. .X. .X. .X.X.X.X.X.X. .X.X.X.X.X.X.X.X.X.X. . .X.X.X. .X. .X.X. .X.X.X.X. .X.k 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUX2 t y G .X. .X. . .X. .X. .X. .X. .X.X.X. .X.X. .X. .X. .X. .X. .X.X.X. . .X._ u y +XLXLXLXLXLXLXLXLXLXLXcXx y j _ X. .X. .X.X.X.X.X.X. .X. .X. .X. .X. .X. .X. . .X. .X.X.X. .X. .X. .X. .X. .X.X. .X. .X.T 0 8 5 UXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXr y t ) .X. .X.X. .X.X.X. .X. . . . .X. . . .X. .X. .X. .X. .X. . . .X.X. .W y y bXLXLXLXLXLXLXLXLXLXPXm i s ` X. .X. .X. . . . . .X. . .X. .X. .X. .X. .X. .X. .X. . . .X. .X.X. .X. .X. .X. . .X. .X. .a 8 8 # UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUX, r y j . .X. . . .X. .X. .X. .X. .X. . . .X. . . . .X. . . . . . . . . . .G i m LXLXLXLXLXLXLXLXLXLXs.s i H X. . . .X. . . .X. . .X. . .X.X. . .X. . . .X. . .X. . . .X. . . . .X. . . .X. . . . . . .Z 8 8 2 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUX0 y t T . . . . . . . . . . . . . . . . . . .X. . . . . . .X. .X. . . . .d s s.LXLXLXLXLXLXLXLXLXeXi s B . .X. . . .X. . . . .X. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .q 8 8 o UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXy y i ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .( s s 7XKXLXLXLXLXLXLXLXIXv s j _ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .g 9 8 ; UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUX4 r y z . . . ._ . . ._ . . . . . . . ._ . . . . . . . . . . . . .R s s BXLXLXLXLXLXLXLXLX] s s ` ._ . . ._ . . ._ . . . ._ . . ._ . . . . . . . . . . . . . . . . . . . . . . . . . .G 8 8 3 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUX0 t t G . . ._ . ._ . ._ . . . ._ . . . ._ . . . ._ . . ._ .D s ] LXLXLXLXLXLXLXLXM.s f H _ . . ._ . . ._ . . . . . ._ _ . . . . . . ._ . . . . . ._ . . ._ ._ _ . . ._ . .W q 8 8 + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXw y y W _ . ._ ._ _ ._ ._ ._ . ._ _ . ._ ._ ._ ._ _ ._ f f M.LXLXLXLXLXLXKXjXf s b ._ ._ . . ._ _ . ._ _ _ _ . . . ._ _ _ ._ _ . ._ _ _ _ . ._ ._ ._ . . ._ ._ .) u 8 8 - UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUX: y r i _ _ _ . . . ._ . ._ _ ._ _ . ._ _ ._ _ . ._ . ._ Q d f eXLXLXLXLXLXKXPXc f z ( ._ ._ _ _ ._ ._ ._ ._ ._ ._ ._ ._ ._ ._ ._ ._ . ._ _ ._ . . ._ _ ._ _ .j 8 9 1 UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUX3 y r p ) _ _ _ _ _ _ _ _ _ ._ ._ _ _ ._ _ _ ._ _ _ _ ._ K f f HXLXLXLXLXLXPXs.f f W _ _ ._ _ ._ _ ._ _ _ ._ _ _ _ ._ _ ._ _ _ ._ _ _ ._ _ _ _ ._ _ ._ _ _ _ ._ _ .B 8 9 5 . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUX0 y t l _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ S f [ KXLXLXLXLXLX+Xf f F _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ G 8 8 6 + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUX0 t r l _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ f f +XLXLXLXLXLXbXf f n _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ G 8 8 8 % UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXw t r l _ _ ( _ ( _ _ _ _ _ _ _ _ _ ( _ _ _ _ _ _ _ _ R f f wXLXLXLXLXHXm f f Q _ _ _ _ _ ( _ _ _ _ _ _ _ _ ( _ ( _ _ _ _ ( _ _ _ ( _ _ ( _ _ _ _ _ _ _ _ ( _ _ _ _ _ _ G 8 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# w t t l _ _ _ _ _ _ _ ( _ ( _ ( _ _ _ _ ( _ ( _ _ ( H f f LXLXLXLXLXs.f f L _ _ ( _ ( _ _ _ _ _ ( _ _ ( _ _ _ _ _ _ _ _ _ ( _ _ _ _ _ _ _ ( _ ( _ _ _ _ _ ( _ _ ( _ D 8 8 8 = UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# w t r l _ _ ( _ _ ( _ _ _ ( ( _ ( _ ( _ _ _ ( _ ( n f s.LXLXLXLX+Xf f S _ _ _ ( _ ( _ ( _ ( ( _ _ ( _ ( _ _ _ ( _ ( _ _ _ ( ( _ _ ( _ ( _ _ _ ( _ _ _ ( _ ( _ ( G 8 8 8 , UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# 0 r r j ( ( ( _ ( ( ( ( _ _ ( _ ( _ ( ( _ _ ( ( f f +XLXLXLXbXc f b ` ( ( ( _ ( _ ( _ ( _ _ ( ( _ ( _ ( ( ( _ ( _ ( ( _ ( _ ( ( _ ( _ ( ( _ ( ( ( ( _ _ ( ( V 8 8 8 - UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX% q r r s R ( ( _ ( ( ( _ ( _ ( ( ( ( _ ( ( ( L f f bXLXLXHX[ f f Q ( ( ( _ ( ( _ _ ( ( ( ( ( _ ( _ ( _ ( _ ( ( ( _ ( ( _ ( ( _ ( _ ( _ ( ( _ ( ( ( _ ( ( j 8 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX% 6 t r u K ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( F f c LXLXLXM.f f P ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( W u 8 8 8 & UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX7 t r t l ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( z f M.LXLXeXf f S ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( G q 8 8 6 + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX< t r r p R ( ( ( ( ( ( ( ( Q ( ( ( Q f f ;XLXBXc f f Q ( Q ( ( ( ( ( ( ( ( ( ( ( ( ( ( Q ( ( ( Q ( ( Q ( ( ( ( Q ( ( Q ( ( ( Q ( ( ( ( Q k 8 8 8 3 . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX& w r r r B ( Q ( Q Q ( ( Q Q ( ( P f f BXLX[ f f E Q ( ( ( Q ( Q ( ( Q Q ( ( Q Q ( ( ( ( ( ( ( ( ( ( ( Q ( ( ( Q ( ( ( ( ( ( ( ( Q K r 9 8 8 > UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXO 7 r r w i K ( Q ( ( Q ( ( ( Q S f m LXM.f f F ( ( Q Q ( ( Q ( Q Q ( ( Q Q ( ( Q Q ( Q ( Q Q Q ( Q Q ( Q Q ( ( Q Q ( Q Q ( Q R p 8 8 8 6 @ UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX: q w w r j E Q Q ( Q Q Q ( b f c M.f f b ( Q ( Q ( Q Q Q ( Q ( Q ( Q Q Q Q ( Q ( Q Q ( Q ( Q Q ( Q ( Q ( Q ( Q ( Q ( E l w 8 8 8 , . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXO 3 q r r q z R Q Q Q Q Q L f f f f f Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q ( Q Q Q Q Q Q Q Q Q E l q 8 8 8 5 @ UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX% 6 w w w q k K Q Q Q Q P n f b L Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q R l w 8 8 8 6 - UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX= 8 q w r w i D E Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q F j 9 8 8 8 6 - . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX= 8 r w q q w d D E Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q E Q Q Q Q Q Q Q E Q Q Q Q Q Q Q Q L z w 8 8 9 8 6 - . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX# 2 q w q w q w i S P Q Q Q Q E E Q E Q Q Q Q E Q Q E Q Q Q Q Q E Q Q Q Q L S j q 8 8 0 8 8 3 & . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXO - 5 w q q q q w q i z S F E Q Q Q Q Q E Q Q E Q Q E Q Q Q P F l s r 9 9 9 9 9 9 6 , + UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX+ - 5 q q q q w r 9 r q q y a i s b z l l d a i i 9 9 9 9 9 9 9 9 9 9 5 > % UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXX * > 3 q q q q q q q q q 9 q 9 q q 8 q 9 8 9 q 9 8 q 9 5 , = o UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX + & : 1 3 6 q q q 9 q q 9 q q 9 8 2 2 : = # . UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX", -"UXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUXUX" -};
--- a/frontends/wix/images/split_card.sh Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +0,0 @@ -#!/bin/sh -#This script split an image with cards, used to split cards from the Tarot deck found on Wikimedia Commons -#This script work with any resolution on the initial image -#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/>. - -dest_dir=cards - -get_face_name() -{ - if [ $1 -le 10 ] - then - echo $1 - else - case $1 in - 11) echo valet;; - 12) echo cavalier;; - 13) echo dame;; - 14) echo roi;; - esac - fi -} - -get_card_name() -{ - if [ $1 -le 21 ] - then - echo "atout_$1" - elif [ $1 -eq 22 ]; then - echo "atout_excuse" - elif [ $1 -le 36 ]; then - echo "pique_$(get_face_name $(($1-22)))" - elif [ $1 -le 50 ]; then - echo "coeur_$(get_face_name $(($1-36)))" - elif [ $1 -le 64 ]; then - echo "carreau_$(get_face_name $(($1-50)))" - else - echo "trefle_$(get_face_name $(($1-64)))" - fi -} - -#We check the version of convert -CONVERT_VER=`convert --version | grep Version | grep -o "[0-9]\.[0-9]\.[0-9]"` -CONVERT_MAJOR=`echo $CONVERT_VER | cut -d . -f 1` -CONVERT_MINOR=`echo $CONVERT_VER | cut -d . -f 2` -CONVERT_REV=`echo $CONVERT_VER | cut -d . -f 3` - -if [ $CONVERT_MAJOR -lt 6 -o $CONVERT_MAJOR -eq 6 -a $CONVERT_MINOR -lt 6 ] -then - echo "ImageMagick convert must be at least version 6.6.0 (current: $CONVERT_VER)" - exit 1 -fi - -SYNTAXE="Split card image\nsyntaxe: $0 image_to_split.ext" - -if [ $# -ne 1 ] -then - echo $SYNTAXE - exit 1 -fi - -echo `file -b --mime-type $1` | grep image 2>&1 > /dev/null - -if [ $? -ne 0 ] -then - echo "target file is not an image" - exit 1 -fi - -current=`pwd` -#TODO: check directory presence -#echo "making directory" -if test -e $dest_dir -then - if test -n "`ls -A $dest_dir`" - then - echo "$dest_dir directory exists and is not empty !" - exit 1 - fi -else - mkdir $dest_dir -fi -echo "splitting cards" -convert Tarotcards.jpg -bordercolor black -crop 14x6@ -fuzz 50% -trim $dest_dir/card_%02d.png 2>/dev/null -cd $dest_dir - -#POST PROCESSING - -nb_files=`ls -A1 card*png | wc -l` -num=0 -idx=0 -max_w=0 -max_h=0 -deleted="" -for file in card*png -do - num=$((num+1)) - size=`stat -c%s $file` - width=`identify -format "%w" $file` - height=`identify -format "%h" $file` - - if [ $width -gt $max_w ] - then - max_w=$width - fi - - if [ $height -gt $max_h ] - then - max_h=$height - fi - - echo -n "post processing file [$file] ($num/$nb_files) | " - echo -n `echo "scale=2;$num/$nb_files*100" | bc`% - echo -n "\r" - - if test $size -lt 1000 - then #we delete empty files (areas without card on the initial picture) - deleted="$deleted$file\n" - rm -f $file - else - idx=$((idx+1)) - #We use transparency for the round corners - mogrify -fuzz 80% -fill none -draw "matte 0,0 floodfill" \ - -draw "matte $((width-1)),0 floodfill"\ - -draw "matte 0,$((height-1)) floodfill"\ - -draw "matte $((width-1)),$((height-1)) floodfill"\ - $file - #Time to rename the cards - mv "$file" "$(get_card_name $idx).png" - - fi -done -echo "\nEmpty files deleted:\n$deleted" -echo "\nBiggest size: ${max_w}X${max_h}" -cd "$current" -echo "DONE :)"
--- a/frontends/wix/main_window.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,498 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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 quick_frontend.quick_chat_list import QuickChatList -from quick_frontend.quick_app import QuickApp -from quick_frontend.quick_contact_management import QuickContactManagement -import wx -from contact_list import ContactList -from chat import Chat -from param import Param -from xmlui import XMLUI -from gateways import GatewaysManager -from profile import Profile -from profile_manager import ProfileManager -import gobject -import os.path -import pdb -from tools.jid import JID -from logging import debug, info, warning, error -import constants - -idCONNECT,\ -idDISCONNECT,\ -idEXIT,\ -idABOUT,\ -idPARAM,\ -idADD_CONTACT,\ -idREMOVE_CONTACT,\ -idSHOW_PROFILE,\ -idJOIN_ROOM,\ -idFIND_GATEWAYS = range(10) - -class ChatList(QuickChatList): - """This class manage the list of chat windows""" - - def __init__(self, host): - QuickChatList.__init__(self, host) - - def createChat(self, target): - return Chat(target, self.host) - -class MainWindow(wx.Frame, QuickApp): - """main app window""" - - def __init__(self): - wx.Frame.__init__(self,None, title="SàT Wix", size=(300,500)) - self.CM = QuickContactManagement() #FIXME: not the best place - - #sizer - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.sizer) - - #Frame elements - self.contactList = ContactList(self, self) - self.contactList.registerActivatedCB(self.onContactActivated) - self.contactList.Hide() - self.sizer.Add(self.contactList, 1, flag=wx.EXPAND) - - self.chat_wins=ChatList(self) - self.CreateStatusBar() - - #ToolBar - self.tools=self.CreateToolBar() - self.statusBox = wx.ComboBox(self.tools, -1, "Online", choices=[status[1] for status in const_STATUS], - style=wx.CB_DROPDOWN | wx.CB_READONLY) - self.tools.AddControl(self.statusBox) - self.tools.AddSeparator() - self.statusTxt=wx.TextCtrl(self.tools, -1, style = wx.TE_PROCESS_ENTER) - self.tools.AddControl(self.statusTxt) - self.Bind(wx.EVT_COMBOBOX, self.onStatusChange, self.statusBox) - self.Bind(wx.EVT_TEXT_ENTER, self.onStatusChange, self.statusTxt) - self.tools.Disable() - - #tray icon - ticon = wx.Icon(IMAGE_DIR+'/crystal/tray_icon.xpm', wx.BITMAP_TYPE_XPM) - self.tray_icon = wx.TaskBarIcon() - self.tray_icon.SetIcon(ticon, _("Wix jabber client")) - wx.EVT_TASKBAR_LEFT_UP(self.tray_icon, self.onTrayClick) - - - #events - self.Bind(wx.EVT_CLOSE, self.onClose, self) - - - QuickApp.__init__(self) - - #menus - self.createMenus() - for i in range(self.menuBar.GetMenuCount()): - self.menuBar.EnableTop(i, False) - - #profile panel - self.profile_pan = ProfileManager(self) - self.sizer.Add(self.profile_pan, 1, flag=wx.EXPAND) - - self.postInit() - - self.Show() - - def plug_profile(self, profile_key='@DEFAULT@'): - """Hide profile panel then plug profile""" - debug (_('plugin profile %s' % profile_key)) - self.profile_pan.Hide() - self.contactList.Show() - self.sizer.Layout() - for i in range(self.menuBar.GetMenuCount()): - self.menuBar.EnableTop(i, True) - super(MainWindow, self).plug_profile(profile_key) - - def createMenus(self): - info(_("Creating menus")) - connectMenu = wx.Menu() - connectMenu.Append(idCONNECT, _("&Connect CTRL-c"),_(" Connect to the server")) - connectMenu.Append(idDISCONNECT, _("&Disconnect CTRL-d"),_(" Disconnect from the server")) - connectMenu.Append(idPARAM,_("&Parameters"),_(" Configure the program")) - connectMenu.AppendSeparator() - connectMenu.Append(idABOUT,_("A&bout"),_(" About %s") % APP_NAME) - connectMenu.Append(idEXIT,_("E&xit"),_(" Terminate the program")) - contactMenu = wx.Menu() - contactMenu.Append(idADD_CONTACT, _("&Add contact"),_(" Add a contact to your list")) - contactMenu.Append(idREMOVE_CONTACT, _("&Remove contact"),_(" Remove the selected contact from your list")) - contactMenu.AppendSeparator() - contactMenu.Append(idSHOW_PROFILE, _("&Show profile"), _(" Show contact's profile")) - communicationMenu = wx.Menu() - communicationMenu.Append(idJOIN_ROOM, _("&Join Room"),_(" Join a Multi-User Chat room")) - communicationMenu.Append(idFIND_GATEWAYS, _("&Find Gateways"),_(" Find gateways to legacy IM")) - self.menuBar = wx.MenuBar() - self.menuBar.Append(connectMenu,_("&General")) - self.menuBar.Append(contactMenu,_("&Contacts")) - self.menuBar.Append(communicationMenu,_("&Communication")) - self.SetMenuBar(self.menuBar) - - #additionals menus - #FIXME: do this in a more generic way (in quickapp) - add_menus = self.bridge.getMenus() - for menu in add_menus: - category,item,type = menu - assert(type=="NORMAL") #TODO: manage other types - menu_idx = self.menuBar.FindMenu(category) - current_menu = None - if menu_idx == wx.NOT_FOUND: - #the menu is new, we create it - current_menu = wx.Menu() - self.menuBar.Append(current_menu, category) - else: - current_menu = self.menuBar.GetMenu(menu_idx) - assert(current_menu != None) - item_id = wx.NewId() - help_string = self.bridge.getMenuHelp(category, item, type) - current_menu.Append(item_id, item, help = help_string) - #now we register the event - def event_answer(e): - id = self.bridge.callMenu(category, item, type, self.profile) - self.current_action_ids.add(id) - wx.EVT_MENU(self, item_id, event_answer) - - - #events - wx.EVT_MENU(self, idCONNECT, self.onConnectRequest) - wx.EVT_MENU(self, idDISCONNECT, self.onDisconnectRequest) - wx.EVT_MENU(self, idPARAM, self.onParam) - wx.EVT_MENU(self, idABOUT, self.onAbout) - wx.EVT_MENU(self, idEXIT, self.onExit) - wx.EVT_MENU(self, idADD_CONTACT, self.onAddContact) - wx.EVT_MENU(self, idREMOVE_CONTACT, self.onRemoveContact) - wx.EVT_MENU(self, idSHOW_PROFILE, self.onShowProfile) - wx.EVT_MENU(self, idJOIN_ROOM, self.onJoinRoom) - wx.EVT_MENU(self, idFIND_GATEWAYS, self.onFindGateways) - - - def newMessage(self, from_jid, msg, type, to_jid, profile): - QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile) - - def roomJoined(self, room_id, room_service, room_nicks, user_nick, profile): - super(MainWindow, self).roomJoined(room_id, room_service, room_nicks, user_nick, profile) - - def showAlert(self, message): - # TODO: place this in a separate class - popup=wx.PopupWindow(self) - ### following code come from wxpython demo - popup.SetBackgroundColour("CADET BLUE") - st = wx.StaticText(popup, -1, message, pos=(10,10)) - sz = st.GetBestSize() - popup.SetSize( (sz.width+20, sz.height+20) ) - x=(wx.DisplaySize()[0]-popup.GetSize()[0])/2 - popup.SetPosition((x,0)) - popup.Show() - wx.CallLater(5000,popup.Destroy) - - def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None): - if type == 'info': - flags = wx.OK | wx.ICON_INFORMATION - elif type == 'error': - flags = wx.OK | wx.ICON_ERROR - elif type == 'yes/no': - flags = wx.YES_NO | wx.ICON_QUESTION - else: - flags = wx.OK | wx.ICON_INFORMATION - error(_('unmanaged dialog type: %s'), type) - dlg = wx.MessageDialog(self, message, title, flags) - answer = dlg.ShowModal() - dlg.Destroy() - if answer_cb: - data = [answer_data] if answer_data else [] - answer_cb(True if (answer == wx.ID_YES or answer == wx.ID_OK) else False, *data) - - def setStatusOnline(self, online=True): - """enable/disable controls, must be called when local user online status change""" - if online: - self.SetStatusText(msgONLINE) - self.tools.Enable() - else: - self.SetStatusText(msgOFFLINE) - self.tools.Disable() - return - - def askConfirmation(self, type, id, data): - #TODO: refactor this in QuickApp - debug (_("Confirmation asked")) - answer_data={} - if type == "FILE_TRANSFERT": - debug (_("File transfert confirmation asked")) - dlg = wx.MessageDialog(self, _("The contact %(jid)s wants to send you the file %(filename)s\nDo you accept ?") % {'jid':data["from"], 'filename':data["filename"]}, - _('File Request'), - wx.YES_NO | wx.ICON_QUESTION - ) - answer=dlg.ShowModal() - if answer==wx.ID_YES: - filename = wx.FileSelector(_("Where do you want to save the file ?"), flags = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) - if filename: - answer_data["dest_path"] = filename - self.bridge.confirmationAnswer(id, True, answer_data) - self.waitProgress(id, _("File Transfer"), _("Copying %s") % os.path.basename(filename)) - else: - answer = wx.ID_NO - if answer==wx.ID_NO: - self.bridge.confirmationAnswer(id, False, answer_data) - - dlg.Destroy() - - elif type == "YES/NO": - debug (_("Yes/No confirmation asked")) - dlg = wx.MessageDialog(self, data["message"], - _('Confirmation'), - wx.YES_NO | wx.ICON_QUESTION - ) - answer=dlg.ShowModal() - if answer==wx.ID_YES: - self.bridge.confirmationAnswer(id, True, {}) - if answer==wx.ID_NO: - self.bridge.confirmationAnswer(id, False, {}) - - dlg.Destroy() - - def actionResult(self, type, id, data): - debug (_("actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]") % {'type':type, 'id':id, 'data':data}) - if not id in self.current_action_ids: - debug (_('unknown id, ignoring')) - return - if type == "SUPPRESS": - self.current_action_ids.remove(id) - elif type == "SUCCESS": - self.current_action_ids.remove(id) - dlg = wx.MessageDialog(self, data["message"], - _('Success'), - wx.OK | wx.ICON_INFORMATION - ) - dlg.ShowModal() - dlg.Destroy() - elif type == "ERROR": - self.current_action_ids.remove(id) - dlg = wx.MessageDialog(self, data["message"], - _('Error'), - wx.OK | wx.ICON_ERROR - ) - dlg.ShowModal() - dlg.Destroy() - elif type == "XMLUI": - self.current_action_ids.remove(id) - debug (_("XML user interface received")) - misc = {} - #FIXME FIXME FIXME: must clean all this crap ! - title = _('Form') - if data['type'] == _('registration'): - title = _('Registration') - misc['target'] = data['target'] - misc['action_back'] = self.bridge.gatewayRegister - XMLUI(self, title=title, xml_data = data['xml'], misc = misc) - elif type == "RESULT": - self.current_action_ids.remove(id) - if self.current_action_ids_cb.has_key(id): - callback = self.current_action_ids_cb[id] - del self.current_action_ids_cb[id] - callback(data) - elif type == "DICT_DICT": - self.current_action_ids.remove(id) - if self.current_action_ids_cb.has_key(id): - callback = self.current_action_ids_cb[id] - del self.current_action_ids_cb[id] - callback(data) - else: - error (_("FIXME FIXME FIXME: type [%s] not implemented") % type) - raise NotImplementedError - - - - def progressCB(self, id, title, message): - data = self.bridge.getProgress(id) - if data: - if not self.pbar: - #first answer, we must construct the bar - self.pbar = wx.ProgressDialog(title, message, float(data['size']), None, - wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME | wx.PD_REMAINING_TIME) - self.pbar.finish_value = float(data['size']) - - self.pbar.Update(int(data['position'])) - elif self.pbar: - self.pbar.Update(self.pbar.finish_value) - return - - wx.CallLater(10, self.progressCB, id, title, message) - - def waitProgress (self, id, title, message): - self.pbar = None - wx.CallLater(10, self.progressCB, id, title, message) - - - - ### events ### - - def onContactActivated(self, jid): - debug (_("onContactActivated: %s"), jid) - if self.chat_wins[jid.short].IsShown(): - self.chat_wins[jid.short].Hide() - else: - self.chat_wins[jid.short].Show() - - def onConnectRequest(self, e): - self.bridge.connect(self.profile) - - def onDisconnectRequest(self, e): - self.bridge.disconnect(self.profile) - - def __updateStatus(self): - show = filter(lambda x:x[1] == self.statusBox.GetValue(), const_STATUS)[0][0] - status = self.statusTxt.GetValue() - self.bridge.setPresence(show=show, statuses={'default':status}, profile_key=self.profile) #FIXME: manage multilingual statuses - - def onStatusChange(self, e): - debug(_("Status change request")) - self.__updateStatus() - - def onParam(self, e): - debug(_("Param request")) - #xmlui = self.bridge.getParamsUI(self.profile) - #XMLUI(self, xml_data = xmlui) - param=Param(self) - - def onAbout(self, e): - about = wx.AboutDialogInfo() - about.SetName(APP_NAME) - about.SetVersion (unicode(self.bridge.getVersion())) - about.SetCopyright(u"(C) 2009,2010 Jérôme Poisson aka Goffi") - about.SetDescription( _(u"%(name)s is a SàT (Salut à Toi) frontend\n"+ - u"%(name)s is based on WxPython, and is the standard graphic interface of SàT") % {'name':APP_NAME}) - about.SetWebSite(("http://www.goffi.org", "Goffi's non-hebdo (french)")) - about.SetDevelopers([ "Goffi (Jérôme Poisson)"]) - try: - with open(LICENCE_PATH,"r") as licence: - about.SetLicence(''.join(licence.readlines())) - except: - pass - - wx.AboutBox(about) - - def onExit(self, e): - self.Close() - - def onAddContact(self, e): - debug(_("Add contact request")) - dlg = wx.TextEntryDialog( - self, _('Please enter new contact JID'), - _('Adding a contact'), _('name@server.tld')) - - if dlg.ShowModal() == wx.ID_OK: - jid=JID(dlg.GetValue()) - if jid.is_valid(): - self.bridge.addContact(jid.short, profile_key=self.profile) - else: - error (_("'%s' is an invalid JID !"), jid) - #TODO: notice the user - - dlg.Destroy() - - def onRemoveContact(self, e): - debug(_("Remove contact request")) - target = self.contactList.getSelection() - if not target: - dlg = wx.MessageDialog(self, _("You haven't selected any contact !"), - _('Error'), - wx.OK | wx.ICON_ERROR - ) - dlg.ShowModal() - dlg.Destroy() - return - - dlg = wx.MessageDialog(self, _("Are you sure you want to delete %s from your roster list ?") % target.short, - _('Contact suppression'), - wx.YES_NO | wx.ICON_QUESTION - ) - - if dlg.ShowModal() == wx.ID_YES: - info(_("Unsubscribing %s presence"), target.short) - self.bridge.delContact(target.short, profile_key=self.profile) - - dlg.Destroy() - - def onShowProfile(self, e): - debug(_("Show contact's profile request")) - target = self.contactList.getSelection() - if not target: - dlg = wx.MessageDialog(self, _("You haven't selected any contact !"), - _('Error'), - wx.OK | wx.ICON_ERROR - ) - dlg.ShowModal() - dlg.Destroy() - return - id = self.bridge.getCard(target.short, profile_key=self.profile) - self.current_action_ids.add(id) - self.current_action_ids_cb[id] = self.onProfileReceived - - def onProfileReceived(self, data): - """Called when a profile is received""" - debug (_('Profile received: [%s]') % data) - profile=Profile(self, data) - - def onJoinRoom(self, e): - warning('FIXME: temporary menu, must be improved') - #TODO: a proper MUC room joining dialog with nickname etc - dlg = wx.TextEntryDialog( - self, _("Please enter MUC's JID"), - #_('Entering a MUC room'), 'test@conference.necton2.int') - _('Entering a MUC room'), 'room@muc_service.server.tld') - if dlg.ShowModal() == wx.ID_OK: - room_jid=JID(dlg.GetValue()) - if room_jid.is_valid(): - self.bridge.joinMUC(room_jid.domain, room_jid.node, self.profiles[self.profile]['whoami'].node, self.profile) - else: - error (_("'%s' is an invalid JID !"), room_jid) - - def onFindGateways(self, e): - debug(_("Find Gateways request")) - id = self.bridge.findGateways(self.profiles[self.profile]['whoami'].domain, self.profile) - self.current_action_ids.add(id) - self.current_action_ids_cb[id] = self.onGatewaysFound - - def onGatewaysFound(self, data): - """Called when SàT has found the server gateways""" - target = data['__private__']['target'] - del data['__private__'] - gatewayManager = GatewaysManager(self, data, server=target) - - def onClose(self, e): - QuickApp.onExit(self) - info(_("Exiting...")) - for win in self.chat_wins: - self.chat_wins[win].Destroy() - e.Skip() - - def onTrayClick(self, e): - debug(_("Tray Click")) - if self.IsShown(): - self.Hide() - else: - self.Show() - self.Raise() - e.Skip() -
--- a/frontends/wix/param.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - - - -import wx -import pdb -from xml.dom import minidom -from logging import debug, info, error -from tools.jid import JID - - -class Param(wx.Frame): - def __init__(self, host, title=_("Configuration")): - super(Param, self).__init__(None, title=title) - - self.host = host - - self.modified = {} # dict of modified data (i.e. what we have to save) - self.ctl_list = {} # usefull to access ctrl, key = (name, category) - - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.notebook=wx.Notebook(self, -1, style=wx.NB_LEFT) - self.sizer.Add(self.notebook, 1, flag=wx.EXPAND) - self.SetSizer(self.sizer) - self.SetAutoLayout(True) - - #events - self.Bind(wx.EVT_CLOSE, self.onClose, self) - - self.MakeModal() - - for category in self.host.bridge.getParamsCategories(): - self.addCategory(category) - - self.Show() - - def addCategory(self, category): - panel=wx.Panel(self.notebook) - panel.sizer = wx.BoxSizer(wx.VERTICAL) - - cat_dom = minidom.parseString(self.host.bridge.getParamsForCategory(category, profile_key = self.host.profile).encode('utf-8')) - - for param in cat_dom.documentElement.getElementsByTagName("param"): - name = param.getAttribute("name") - label = param.getAttribute("label") - type = param.getAttribute("type") - value = param.getAttribute("value") - sizer = wx.BoxSizer(wx.HORIZONTAL) - if type=="string": - label=wx.StaticText(panel, -1, (label or name)+" ") - ctrl = wx.TextCtrl(panel, -1, value) - sizer.Add(label) - elif type=="password": - label=wx.StaticText(panel, -1, (label or name)+" ") - ctrl = wx.TextCtrl(panel, -1, value, style=wx.TE_PASSWORD) - sizer.Add(label) - elif type=="bool": - ctrl = wx.CheckBox(panel, -1, label or name, style = wx.CHK_2STATE) - ctrl.SetValue(value=="true") - elif type=="button": - ctrl = wx.Button(panel, -1, value) - ctrl.callback_id = param.getAttribute("callback_id") - else: - error(_("FIXME FIXME FIXME")) #FIXME ! - raise NotImplementedError - if name: - ctrl.param_id=(name, category) - self.ctl_list[(name, category)] = ctrl - sizer.Add(ctrl, 1, flag=wx.EXPAND) - panel.sizer.Add(sizer, flag=wx.EXPAND) - - if type=="string" or type=="password": - panel.Bind(wx.EVT_TEXT, self.onTextChanged, ctrl) - elif type=="bool": - panel.Bind(wx.EVT_CHECKBOX, self.onCheckBoxClicked, ctrl) - elif type=="button": - panel.Bind(wx.EVT_BUTTON, self.onButtonClicked, ctrl) - - panel.SetSizer(panel.sizer) - panel.SetAutoLayout(True) - self.notebook.AddPage(panel, category) - cat_dom.unlink() - - def onTextChanged(self, event): - """Called when a string paramater is modified""" - self.modified[event.GetEventObject().param_id]=event.GetString() - - ### FIXME # Some hacks for better presentation, should be generic # FIXME ### - if event.GetEventObject().param_id == ('JabberID', 'Connection'): - domain = JID(event.GetString()).domain - self.ctl_list[('Server', 'Connection')].SetValue(domain) - self.modified[('Server', 'Connection')] = domain - - event.Skip() - - def onCheckBoxClicked(self, event): - """Called when a bool paramater is modified""" - self.modified[event.GetEventObject().param_id]="true" if event.GetEventObject().GetValue() else "false" - event.Skip() - - def onButtonClicked(self, event): - """Called when a button paramater is modified""" - self.__save_parameters() - name, category = event.GetEventObject().param_id - callback_id = event.GetEventObject().callback_id - data = {"name":name, "category":category, "callback_id":callback_id} - id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile) - self.host.current_action_ids.add(id) - event.Skip() - - def __save_parameters(self): - for param in self.modified: - self.host.bridge.setParam(param[0], self.modified[param], param[1], profile_key = self.host.profile) - self.modified.clear() - - def onClose(self, event): - """Close event: we have to save the params.""" - debug(_("close")) - #now we save the modifier params - self.__save_parameters() - - self.MakeModal(False) - event.Skip() -
--- a/frontends/wix/profile.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - -import wx -import pdb -from logging import debug, info, error -from tools.jid import JID - - -class Profile(wx.Frame): - """This class is used to show/modify profile given by SàT""" - - def __init__(self, host, data, title="Profile"): - super(Profile, self).__init__(None, title=title) - self.host = host - - self.name_dict = { 'fullname': _('Full Name'), - 'nick' : _('Nickname'), - 'birthday' : _('Birthday'), - 'phone' : _('Phone #'), - 'website' : _('Website'), - 'email' : _('E-mail'), - 'avatar' : _('Avatar') - } - self.ctl_list = {} # usefull to access ctrl, key = (name) - - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.notebook=wx.Notebook(self, -1) - self.sizer.Add(self.notebook, 1, flag=wx.EXPAND) - self.SetSizer(self.sizer) - self.SetAutoLayout(True) - - #events - self.Bind(wx.EVT_CLOSE, self.onClose, self) - - self.MakeModal() - self.showData(data) - self.Show() - - def showData(self, data): - flags = wx.TE_READONLY - - #General tab - generaltab = wx.Panel(self.notebook) - sizer = wx.FlexGridSizer(cols=2) - sizer.AddGrowableCol(1) - generaltab.SetSizer(sizer) - generaltab.SetAutoLayout(True) - for field in ['fullname','nick', 'birthday', 'phone', 'website', 'email']: - value = data[field] if data.has_key(field) else '' - label=wx.StaticText(generaltab, -1, self.name_dict[field]+": ") - sizer.Add(label) - self.ctl_list[field] = wx.TextCtrl(generaltab, -1, value, style = flags) - sizer.Add(self.ctl_list[field], 1, flag = wx.EXPAND) - #Avatar - if data.has_key('avatar'): - filename = self.host.bridge.getAvatarFile(data['avatar']) - label=wx.StaticText(generaltab, -1, self.name_dict['avatar']+": ") - sizer.Add(label) - img = wx.Image(filename).ConvertToBitmap() - self.ctl_list['avatar'] = wx.StaticBitmap(generaltab, -1, img) - sizer.Add(self.ctl_list['avatar'], 0) - - - - self.notebook.AddPage(generaltab, _("General")) - - - def onClose(self, event): - """Close event""" - debug(_("close")) - self.MakeModal(False) - event.Skip() -
--- a/frontends/wix/profile_manager.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,149 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - - - -import wx -import pdb -from logging import debug, info, error -from tools.jid import JID -import pdb - - -class ProfileManager(wx.Panel): - def __init__(self, host): - super(ProfileManager, self).__init__(host) - self.host = host - - #self.sizer = wx.FlexGridSizer(cols=2) - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.sizer) - - profiles = self.host.bridge.getProfilesList() - self.profile_name = wx.ComboBox(self, -1, style=wx.CB_READONLY|wx.CB_SORT) - self.__refillProfiles() - self.Bind(wx.EVT_COMBOBOX, self.onProfileChange) - self.panel_id = wx - - self.sizer.Add(wx.Window(self, -1), 1) - self.sizer.Add(wx.StaticText(self, -1, _("Profile:")), 0, flag=wx.ALIGN_CENTER) - self.sizer.Add(self.profile_name, 0, flag=wx.ALIGN_CENTER) - button_panel = wx.Panel(self) - button_panel.sizer = wx.BoxSizer(wx.HORIZONTAL) - button_panel.SetSizer(button_panel.sizer) - button_new = wx.Button(button_panel, -1, _("New")) - button_del = wx.Button(button_panel, -1, _("Delete")) - button_panel.sizer.Add(button_new) - button_panel.sizer.Add(button_del) - self.sizer.Add(button_panel, flag=wx.CENTER) - self.Bind(wx.EVT_BUTTON, self.onNewProfile, button_new) - self.Bind(wx.EVT_BUTTON, self.onDeleteProfile, button_del) - - login_box = wx.StaticBox(self, -1, _("Login")) - self.login_sizer = wx.StaticBoxSizer(login_box, wx.VERTICAL) - self.sizer.Add(self.login_sizer, 1, wx.EXPAND | wx.ALL) - self.login_jid = wx.TextCtrl(self, -1) - self.login_sizer.Add(wx.StaticText(self, -1, "JID:"), 0, flag=wx.ALIGN_CENTER) - self.login_sizer.Add(self.login_jid, flag=wx.EXPAND) - self.login_pass = wx.TextCtrl(self, -1, style = wx.TE_PASSWORD) - self.login_sizer.Add(wx.StaticText(self, -1, _("Password:")), 0, flag=wx.ALIGN_CENTER) - self.login_sizer.Add(self.login_pass, flag=wx.EXPAND) - - loggin_button = wx.Button(self, -1, _("Connect")) - self.Bind(wx.EVT_BUTTON, self.onConnectButton, loggin_button) - self.login_sizer.Add(loggin_button, flag=wx.ALIGN_CENTER) - - self.sizer.Add(wx.Window(self, -1), 1) - - #Now we can set the default value - self.__setDefault() - - - def __setDefault(self): - profile_default = self.host.bridge.getProfileName("@DEFAULT@") - if profile_default: - self.profile_name.SetValue(profile_default) - self.onProfileChange(None) - - def __refillProfiles(self): - """Update profiles with current names. Must be called after a profile change""" - self.profile_name.Clear() - profiles = self.host.bridge.getProfilesList() - profiles.sort() - for profile in profiles: - self.profile_name.Append(profile) - - - def onNewProfile(self, event): - dlg = wx.TextEntryDialog(self, _("Please enter the new profile name"), _("New profile"), style = wx.OK | wx.CANCEL) - if dlg.ShowModal() == wx.ID_OK: - name = dlg.GetValue() - if name: - if name[0]=='@': - wx.MessageDialog(self, _("A profile name can't start with a @"), _("Bad profile name"), wx.ICON_ERROR).ShowModal() - else: - profile = self.host.bridge.createProfile(name) - self.__refillProfiles() - self.profile_name.SetValue(name) - dlg.Destroy() - - def onDeleteProfile(self, event): - name = self.profile_name.GetValue() - if not name: - return - dlg = wx.MessageDialog(self, _("Are you sure to delete the profile [%s]") % name, _("Confirmation"), wx.ICON_QUESTION | wx.YES_NO) - if dlg.ShowModal() == wx.ID_YES: - self.host.bridge.deleteProfile(name) - self.__refillProfiles() - self.__setDefault() - dlg.Destroy() - - def onProfileChange(self, event): - """Called when a profile is choosen in the combo box""" - jabberID = self.host.bridge.getParamA("JabberID", "Connection", profile_key=self.profile_name.GetValue()) - password = self.host.bridge.getParamA("Password", "Connection", profile_key=self.profile_name.GetValue()) - self.login_jid.SetValue(jabberID) - self.login_pass.SetValue(password) - - def onConnectButton(self, event): - """Called when the Connect button is pressed""" - name = self.profile_name.GetValue() - if not name: - wx.MessageDialog(self, _("You must select a profile or create a new one before connecting"), _("No profile selected"), wx.ICON_ERROR).ShowModal() - return - if name[0]=='@': - wx.MessageDialog(self, _("A profile name can't start with a @"), _("Bad profile name"), wx.ICON_ERROR).ShowModal() - return - profile = self.host.bridge.getProfileName(name) - assert(profile) - old_jid = self.host.bridge.getParamA("JabberID", "Connection", profile_key=profile) - old_pass = self.host.bridge.getParamA("Password", "Connection", profile_key=profile) - new_jid = self.login_jid.GetValue() - new_pass = self.login_pass.GetValue() - if old_jid != new_jid: - debug(_('Saving new JID and server')) - self.host.bridge.setParam("JabberID", new_jid, "Connection", profile) - self.host.bridge.setParam("Server", JID(new_jid).domain, "Connection", profile) - if old_pass != new_pass: - debug(_('Saving new password')) - self.host.bridge.setParam("Password", new_pass, "Connection", profile) - self.host.plug_profile(profile) -
--- a/frontends/wix/wix Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - - -import wx -from sat_bridge_frontend.DBus import DBusBridgeFrontend -import pdb -import logging -from logging import debug, info, error -from main_window import MainWindow - -### logging configuration FIXME: put this elsewhere ### -logging.basicConfig(level=logging.DEBUG, - format='%(message)s') -### - - -class SATApp(wx.App): - def __init__(self, redirect=False, filename=None, useBestVisual=False, clearSigInt=True): - super(SATApp,self).__init__(redirect, filename, useBestVisual, clearSigInt) - - def OnInit(self): - self.main = MainWindow() - self.main.Show(True) - self.SetTopWindow(self.main) - return True - - -sat = SATApp() -sat.MainLoop()
--- a/frontends/wix/xmlui.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,245 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -wix: a SAT frontend -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/>. -""" - - - -import wx -import pdb -from xml.dom import minidom -from logging import debug, info, warning, error -from tools.jid import JID - - -class XMLUI(wx.Frame): - """Create an user interface from a SàT xml""" - - def __init__(self, host, xml_data='', title="Form", options=[], misc={}): - style = wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX if 'NO_CANCEL' in options else wx.DEFAULT_FRAME_STYLE #FIXME: gof: Q&D tmp hack - super(XMLUI, self).__init__(None, title=title, style=style) - - self.host = host - self.options = options - self.misc = misc - self.ctrl_list = {} # usefull to access ctrl - - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.sizer) - self.SetAutoLayout(True) - - #events - if not 'NO_CANCEL' in self.options: - self.Bind(wx.EVT_CLOSE, self.onClose, self) - - self.MakeModal() - - self.constructUI(xml_data) - - self.Show() - - def __parseElems(self, node, parent): - """Parse elements inside a <layout> tags, and add them to the parent sizer""" - for elem in node.childNodes: - if elem.nodeName != "elem": - message=_("Unmanaged tag") - error(message) - raise Exception(message) - _proportion = 0 - id = elem.getAttribute("id") - name = elem.getAttribute("name") - type = elem.getAttribute("type") - value = elem.getAttribute("value") if elem.hasAttribute('value') else u'' - if type=="empty": - ctrl = wx.Window(parent, -1) - elif type=="text": - try: - value = elem.childNodes[0].wholeText - except IndexError: - warning (_("text node has no child !")) - ctrl = wx.StaticText(parent, -1, value) - elif type=="label": - ctrl = wx.StaticText(parent, -1, value+": ") - elif type=="string": - ctrl = wx.TextCtrl(parent, -1, value) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - _proportion = 1 - elif type=="password": - ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_PASSWORD) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - _proportion = 1 - elif type=="textbox": - ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_MULTILINE) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - _proportion = 1 - elif type=="bool": - ctrl = wx.CheckBox(panel, -1, "", style = wx.CHK_2STATE) - ctrl.SetValue(value=="true") - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - _proportion = 1 - elif type=="list": - style=wx.LB_MULTIPLE if elem.getAttribute("multi")=='yes' else wx.LB_SINGLE - ctrl = wx.ListBox(parent, -1, choices=[option.getAttribute("value") for option in elem.getElementsByTagName("option")], style=style) - self.ctrl_list[name] = ({'type':type, 'control':ctrl}) - _proportion = 1 - elif type=="button": - callback_id = elem.getAttribute("callback_id") - ctrl = wx.Button(parent, -1, value) - ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")]) - parent.Bind(wx.EVT_BUTTON, self.onButtonClicked, ctrl) - else: - error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type) #FIXME ! - raise NotImplementedError - parent.sizer.Add(ctrl, _proportion, flag=wx.EXPAND) - - def __parseChilds(self, parent, current_param, elem, wanted = ['layout']): - """Recursively parse childNodes of an elemen - @param parent: parent wx.Window - @param current_param: current wx.Window (often wx.Panel) or None if we must create one - @param elem: element from which childs will be parsed - @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant""" - for node in elem.childNodes: - if wanted and not node.nodeName in wanted: - raise Exception("Invalid XMLUI") #TODO: make a custom exception - if node.nodeName == "layout": - _proportion = 0 - type = node.getAttribute('type') - if type == "tabs": - current = wx.Notebook(parent, -1, style=wx.NB_LEFT if self.type=='param' else 0) - self.__parseChilds(current, None, node, ['category']) - _proportion = 1 - else: - if current_param == None: - current = wx.Panel(parent, -1) - else: - current = current_param - if type == "vertical": - current.sizer = wx.BoxSizer(wx.VERTICAL) - elif type == "pairs": - current.sizer = wx.FlexGridSizer(cols=2) - current.sizer.AddGrowableCol(1) #The growable column need most of time to be the right one in pairs - else: - warning(_("Unknown layout, using default one")) - current.sizer = wx.BoxSizer(wx.VERTICAL) - current.SetSizer(current.sizer) - self.__parseElems(node, current) - if parent: - parent.sizer.Add(current, _proportion, flag=wx.EXPAND) - elif node.nodeName == "category": - name = node.getAttribute('name') - label = node.getAttribute('label') - if not node.nodeName in wanted or not name or not isinstance(parent,wx.Notebook): - raise Exception("Invalid XMLUI") #TODO: make a custom exception - notebook = parent - tab_panel = wx.Panel(notebook, -1) - tab_panel.sizer = wx.BoxSizer(wx.VERTICAL) - tab_panel.SetSizer(tab_panel.sizer) - notebook.AddPage(tab_panel, label or name) - self.__parseChilds(tab_panel, None, node, ['layout']) - - else: - message=_("Unknown tag") - error(message) - raise Exception(message) #TODO: raise a custom exception here - - - def constructUI(self, xml_data): - panel=wx.Panel(self) - panel.sizer = wx.BoxSizer(wx.VERTICAL) - - cat_dom = minidom.parseString(xml_data.encode('utf-8')) - top= cat_dom.documentElement - self.type = top.getAttribute("type") - self.title = top .getAttribute("title") - if self.title: - self.SetTitle(self.title) - if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']: - raise Exception("Invalid XMLUI") #TODO: make a custom exception - - self.__parseChilds(panel, None, cat_dom.documentElement) - - if self.type == 'form': - dialogButtons = wx.StdDialogButtonSizer() - submitButton = wx.Button(panel,wx.ID_OK, label=_("Submit")) - dialogButtons.AddButton(submitButton) - panel.Bind(wx.EVT_BUTTON, self.onFormSubmitted, submitButton) - if not 'NO_CANCEL' in self.options: - cancelButton = wx.Button(panel,wx.ID_CANCEL) - dialogButtons.AddButton(cancelButton) - panel.Bind(wx.EVT_BUTTON, self.onFormCancelled, cancelButton) - dialogButtons.Realize() - panel.sizer.Add(dialogButtons, flag=wx.ALIGN_CENTER_HORIZONTAL) - - panel.SetSizer(panel.sizer) - panel.SetAutoLayout(True) - panel.sizer.Fit(self) - self.sizer.Add(panel, 1, flag=wx.EXPAND) - cat_dom.unlink() - - ###events - - def onButtonClicked(self, event): - """Called when a button is pushed""" - callback_id, fields = event.GetEventObject().param_id - data = {"callback_id":callback_id} - for field in fields: - ctrl = self.ctrl_list[field] - if isinstance(ctrl['control'], wx.ListBox): - data[field] = '\t'.join([ctrl['control'].GetString(idx) for idx in ctrl['control'].GetSelections()]) - else: - data[field] = ctrl['control'].GetValue() - - id = self.host.bridge.launchAction("button", data, profile_key = self.host.profile) - self.host.current_action_ids.add(id) - event.Skip() - - def onFormSubmitted(self, event): - """Called when submit button is clicked""" - debug(_("Submitting form")) - data = [] - for ctrl_name in self.ctrl_list: - ctrl = self.ctrl_list[ctrl_name] - if isinstance(ctrl['control'], wx.ListBox): - data.append((ctrl_name, ctrl['control'].GetStringSelection())) - elif isinstance(ctrl['control'], wx.CheckBox): - data.append((ctrl_name, "true" if ctrl['control'].GetValue() else "false")) - else: - data.append((ctrl_name, ctrl['control'].GetValue())) - if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned - id = self.misc['action_back']("SUBMIT",self.misc['target'], data) - self.host.current_action_ids.add(id) - elif self.misc.has_key('callback'): - self.misc['callback'](data) - else: - warning (_("The form data is not sent back, the type is not managed properly")) - self.MakeModal(False) - self.Destroy() - - def onFormCancelled(self, event): - """Called when cancel button is clicked""" - debug(_("Cancelling form")) - self.MakeModal(False) - self.Close() - - def onClose(self, event): - """Close event: we have to send the form.""" - debug(_("close")) - self.MakeModal(False) - event.Skip() -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/i18n/fr.po Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,1070 @@ +# SàT french translation. +# Copyright (C) 2009, 2010 Jérôme Poisson +# This file is distributed under the same license as the SàT package. +# Jérôme Poisson <goffi@goffi.org>, 2009, 2010. +# Goffi <goffi@goffi.org>, 2010. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.0.2D\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-08-19 21:54+0800\n" +"PO-Revision-Date: 2010-08-19 22:14+0800\n" +"Last-Translator: Goffi <goffi@goffi.org>\n" +"Language-Team: French <goffi@goffi.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: sat.tac:87 +#, python-format +msgid "********** [%s] CONNECTED **********" +msgstr "********** [%s] CONNECTÉ **********" + +#: sat.tac:93 +msgid "XML stream is initialized" +msgstr "Le flux XML est initialisé" + +#: sat.tac:113 +#, python-format +msgid "********** [%s] DISCONNECTED **********" +msgstr "********** [%s] DÉCONNECTÉ **********" + +#: sat.tac:117 +msgid "No keep_alife" +msgstr "Pas de \"keep_alife\"" + +#: sat.tac:128 +#, python-format +msgid "got message from: %s" +msgstr "message reçu de: %s" + +#: sat.tac:171 +#, python-format +msgid "new contact in roster list: %s" +msgstr "nouveau contact: %s" + +#: sat.tac:178 +#, python-format +msgid "removing %s from roster list" +msgstr "supppression du contact %s" + +#: sat.tac:188 +#, python-format +msgid "" +"presence update for [%(entity)s] (available, show=%(show)s statuses=%" +"(statuses)s priority=%(priority)d)" +msgstr "" +"Mise à jour des information de présence pour [%(entity)s] (available, show=%" +"(show)s statuses=%(statuses)s priority=%(priority)d)" + +#: sat.tac:202 +#, python-format +msgid "presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)" +msgstr "" +"Mise à jour de l'information de présence pour [%(entity)s] (unavailable, " +"statuses=%(statuses)s)" + +#: sat.tac:219 +#, python-format +msgid "subscription approved for [%s]" +msgstr "inscription approuvée pour [%s]" + +#: sat.tac:224 +#, python-format +msgid "unsubscription confirmed for [%s]" +msgstr "désinscription confirmée pour [%s]" + +#: sat.tac:229 +#, python-format +msgid "subscription request for [%s]" +msgstr "demande d'inscription pour [%s]" + +#: sat.tac:234 +#, python-format +msgid "unsubscription asked for [%s]" +msgstr "demande de désinscription pour [%s]" + +#: sat.tac:259 +msgid "Registration asked for" +msgstr "inscription demandée pour" + +#: sat.tac:277 plugins/plugin_xep_0077.py:83 plugins/plugin_xep_0077.py:97 +#, python-format +msgid "registration answer: %s" +msgstr "réponse à la demande d'inscription: %s" + +#: sat.tac:279 plugins/plugin_xep_0077.py:99 +msgid "Registration successfull" +msgstr "Inscription réussie" + +#: sat.tac:284 plugins/plugin_xep_0077.py:75 plugins/plugin_xep_0077.py:107 +#, python-format +msgid "Registration failure: %s" +msgstr "Échec de l'inscription: %s" + +#: sat.tac:289 plugins/plugin_xep_0077.py:113 +msgid "Username already exists, please choose an other one" +msgstr "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre" + +#: sat.tac:292 +#, python-format +msgid "Registration failed (%s)" +msgstr "Éched de l'insciption (%s)" + +#: sat.tac:305 +msgid "Trying to access an undefined constant" +msgstr "Vous essayer d'utiliser une constante indéfinie" + +#: sat.tac:312 +msgid "Trying to redefine a constant" +msgstr "Vous essayez de ré-attribuer une constante" + +#: sat.tac:379 +#, python-format +msgid "importing plugin: %s" +msgstr "Importation du plugin: %s" + +#: sat.tac:392 +msgid "Trying to connect a non-exsitant profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat.tac:396 +msgid "already connected !" +msgstr "Vous êtes déjà connecté !" + +#: sat.tac:419 +msgid "setting plugins parents" +msgstr "Configuration des parents des extensions" + +#: sat.tac:430 +msgid "not connected !" +msgstr "Vous n'êtes pas connecté !" + +#: sat.tac:433 +msgid "Disconnecting..." +msgstr "Déconnexion..." + +#: sat.tac:445 +msgid "running app" +msgstr "Lancement de l'application" + +#: sat.tac:449 +msgid "stopping app" +msgstr "Arrêt de l'application" + +#: sat.tac:486 +msgid "No user or server given" +msgstr "L'utilisateur ou le serveur n'ont pas été spécifié" + +#: sat.tac:488 +msgid "No user, password or server given, can't register new account." +msgstr "" +"L'utilisateur, le mot de passe ou le serveur n'ont pas été spécifiés, " +"impossible d'inscrire un nouveau compte." + +#: sat.tac:495 +#, python-format +msgid "Are you sure to register new account [%(user)s] to server %(server)s ?" +msgstr "" +"Êtes vous sûr de vouloir inscrire le nouveau compte [%(user)s] au serveur %" +"(server)s ?" + +#: sat.tac:502 +#, python-format +msgid "register Confirmation CB ! (%s)" +msgstr "Callback de confirmation d'inscription !" + +#: sat.tac:534 +#, python-format +msgid "FIXME FIXME FIXME: Unmanaged action (%s) in submitForm" +msgstr "" +"CORRIGEZ-MOI CORRIGEZ-MOI CORRIGEZ-MOI: Action non gérée (%s) dans " +"\"submitForm\"" + +#: sat.tac:544 +#, python-format +msgid "setting param: %(name)s=%(value)s in category %(category)s" +msgstr "" +"Le paramètre %(name)s vaut désormais %(value)s dans la catégorie %(category)s" + +#: sat.tac:554 +msgid "asking connection status for a non-existant profile" +msgstr "demande de l'état de connexion pour un profile qui n'existe pas" + +#: sat.tac:569 +#, fuzzy +msgid "trying to launch action with a non-existant profile" +msgstr "Tentative d'ajout d'un contact à un profile inexistant" + +#: sat.tac:575 +msgid "Incomplete data" +msgstr "Données incomplétes" + +#: sat.tac:581 +msgid "Unknown action type" +msgstr "Type d'action inconnu" + +#: sat.tac:592 +#, python-format +msgid "Sending jabber message to %s..." +msgstr "Envoi du message jabber à %s" + +#: sat.tac:619 +#, fuzzy, python-format +msgid "subsciption request [%(subs_type)s] for %(jid)s" +msgstr "demande d'inscription [%(type)s] pour %(jid)s" + +#: sat.tac:626 +msgid "sending automatic \"to\" subscription request" +msgstr "envoi automatique de la demande d'inscription \"to\"" + +#: sat.tac:657 +#, python-format +msgid "Feature found: %s" +msgstr "Fonctionnalité trouvée: %s" + +#: sat.tac:660 +#, python-format +msgid "Identity found: [%(category)s/%(type)s] %(identity)s" +msgstr "Identité trouvée: [%(category)s/%(type)s] %(identity)s" + +#: sat.tac:680 +msgid "type for actionResultExt must be DICT_DICT, fixing it" +msgstr "Le type pour actionResultExt doit être DICT_DICT, correction" + +#: sat.tac:694 +msgid "Attempt to register two callbacks for the same confirmation" +msgstr "Tentative de déclaration de 2 callbacks pour la même configuration" + +#: sat.tac:702 +#, python-format +msgid "Received confirmation answer for id [%(id)s]: %(success)s" +msgstr "Réponse pour confirmation reçu (id [%(id)s]): %(success)s" + +#: sat.tac:702 +msgid "accepted" +msgstr "accepté" + +#: sat.tac:702 +msgid "refused" +msgstr "refusé" + +#: sat.tac:704 +msgid "Received an unknown confirmation" +msgstr "Confirmation inconnue reçue" + +#: sat.tac:717 +msgid "Trying to remove an unknow progress callback" +msgstr "Tentative d'effacement d'une callback de progression inconnue." + +#: sat.tac:741 +msgid "Trying to remove an unknow general callback" +msgstr "Tentative d'effacement d'une callback générale inconnue." + +#: sat.tac:750 +#, fuzzy, python-format +msgid "Trying to call unknown function (%s)" +msgstr "Tentative d'appel d'une fonction inconnue" + +#: sat.tac:774 +#, fuzzy +msgid "Trying to access an unknown menu" +msgstr "Tentative d'accès à un profile inconnu" + +#: sat.tac:781 +#, fuzzy +msgid "Non-exsitant profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat.tac:788 +#, fuzzy, python-format +msgid "Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)" +msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" + +#: tools/memory.py:62 +#, fuzzy +msgid "Connection" +msgstr "Déconnexion..." + +#: tools/memory.py:63 +msgid "Register new account" +msgstr "Enregistrement d'un nouveau compte" + +#: tools/memory.py:64 +msgid "Connect on frontend startup" +msgstr "Connexion au démarrage des frontends" + +#: tools/memory.py:65 +msgid "Disconnect on frontend closure" +msgstr "Déconnexion à la fermeture des frontends" + +#: tools/memory.py:66 +msgid "Misc" +msgstr "Divers" + +#: tools/memory.py:85 +msgid "general params data loaded" +msgstr "Paramètres généraux chargés" + +#: tools/memory.py:87 +msgid "Can't load general params data !" +msgstr "Impossible de charger les paramètres généraux !" + +#: tools/memory.py:93 +msgid "individual params data loaded" +msgstr "Paramètres individuels chargés" + +#: tools/memory.py:95 +msgid "Can't load individual params data !" +msgstr "Impossible de charger les paramètres individuels !" + +#: tools/memory.py:132 +msgid "The profile name already exists" +msgstr "Ce nom de profile existe déjà" + +#: tools/memory.py:141 +msgid "Trying to delete an unknown profile" +msgstr "Tentative d'appel d'un profile inconnue" + +#: tools/memory.py:157 +msgid "No default profile, returning first one" +msgstr "Pas de profile par défaut, envoi du premier" + +#: tools/memory.py:162 +msgid "Trying to access an unknown profile" +msgstr "Tentative d'accès à un profile inconnu" + +#: tools/memory.py:202 +#, python-format +msgid "Can't determine default value for [%(category)s/%(name)s]: %(reason)s" +msgstr "" +"Impossible de déterminer la valeur par défaut pour [%(category)s/%(name)s]: %" +"(reason)s" + +#: tools/memory.py:215 tools/memory.py:233 +#, python-format +msgid "Requested param [%(name)s] in category [%(category)s] doesn't exist !" +msgstr "" +"Le paramètre demandé [%(name)s] dans la catégorie [%(category)s] n'existe " +"pas !" + +#: tools/memory.py:244 +msgid "Requesting a param for an non-existant profile" +msgstr "Demande d'un paramètre pour un profile inconnu" + +#: tools/memory.py:296 tools/memory.py:306 tools/memory.py:319 +msgid "Asking params for inexistant profile" +msgstr "Demande de paramètres pour un profile inconnu" + +#: tools/memory.py:365 +#, python-format +msgid "Requesting an unknown parameter (%(category)s/%(name)s)" +msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" + +#: tools/memory.py:377 +msgid "Trying to set parameter for an unknown profile" +msgstr "Tentative d'assigner un paramètre à un profile inconnu" + +#: tools/memory.py:391 +msgid "Memory manager init" +msgstr "Initialisation du gestionnaire de mémoire" + +#: tools/memory.py:418 +msgid "params template loaded" +msgstr "Modèle des paramètres chargé" + +#: tools/memory.py:420 +msgid "Can't load params template !" +msgstr "Impossible de charger le modèle des paramètres !" + +#: tools/memory.py:423 +msgid "No params template, using default template" +msgstr "Pas de modèle de paramètres, utilisation du modèle par défaut" + +#: tools/memory.py:428 +msgid "params loaded" +msgstr "paramètres chargés" + +#: tools/memory.py:430 +msgid "Can't load params !" +msgstr "Impossible de charger les paramètres !" + +#: tools/memory.py:437 +msgid "history loaded" +msgstr "Historique chargée" + +#: tools/memory.py:439 +msgid "Can't load history !" +msgstr "Impossible de charger l'historique !" + +#: tools/memory.py:446 +msgid "private values loaded" +msgstr "Données privées chargées" + +#: tools/memory.py:448 +msgid "Can't load private values !" +msgstr "Impossible de charger les données privées !" + +#: tools/memory.py:464 +msgid "params saved" +msgstr "Paramètres sauvés" + +#: tools/memory.py:467 +msgid "history saved" +msgstr "Historique sauvée" + +#: tools/memory.py:470 +msgid "private values saved" +msgstr "Données privées sauvées" + +#: tools/memory.py:513 +msgid "source JID not found !" +msgstr "JID source introuvable !" + +#: tools/memory.py:517 +msgid "dest JID not found !" +msgstr "JID destination introuvable !" + +#: tools/memory.py:544 +msgid "Trying to add a contact to a non-existant profile" +msgstr "Tentative d'ajout d'un contact à un profile inexistant" + +#: tools/memory.py:556 +msgid "Trying to delete a contact for a non-existant profile" +msgstr "Tentative de suppression d'un contact pour un profile inexistant" + +#: tools/memory.py:564 +msgid "Asking a contact for a non-existant profile" +msgstr "Demande d'un contact pour un profile inexistant" + +#: tools/memory.py:578 tools/memory.py:627 +msgid "Asking contacts for a non-existant profile" +msgstr "Demande de contacts pour un profile inexistant" + +#: tools/memory.py:589 +msgid "Trying to add presence status to a non-existant profile" +msgstr "Tentative d'ajout d'informations de présence à un profile inexistant" + +#: tools/memory.py:617 +msgid "Asking waiting subscriptions for a non-existant profile" +msgstr "Demande des inscriptions en attente pour un profile inexistant" + +#: tools/xml_tools.py:79 +msgid "INTERNAL ERROR: parameters xml not valid" +msgstr "ERREUR INTERNE: paramètres xml non valides" + +#: tools/xml_tools.py:86 +msgid "INTERNAL ERROR: params categories must have a name" +msgstr "ERREUR INTERNE: les catégories des paramètres doivent avoir un nom" + +#: tools/xml_tools.py:93 +msgid "INTERNAL ERROR: params must have a name" +msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" + +#: tools/xml_tools.py:127 +#, fuzzy, python-format +msgid "Unknown panel type [%s]" +msgstr "Type d'action inconnu" + +#: tools/xml_tools.py:150 +#, fuzzy, python-format +msgid "Unknown layout type [%s]" +msgstr "Type d'action inconnu" + +#: tools/xml_tools.py:286 +msgid "Trying to add a category without parent tabs layout" +msgstr "" +"Tentative d'ajout d'une catégorie sans disposition dans l'onglet parent" + +#: tools/xml_tools.py:289 +msgid "parent layout of a category is not tabs" +msgstr "la disposition parente d'une catégorie n'est pas \"tabs\" (onglets)" + +#: plugins/plugin_misc_cs.py:52 +msgid "" +"This plugin allow to manage your CouchSurfing account throught your SàT " +"frontend" +msgstr "" +"Cette extension vous permet de gérer votre compte CouchSurfing à travers " +"votre frontend SàT" + +#: plugins/plugin_misc_cs.py:71 +#, fuzzy +msgid "Plugin CS initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: plugins/plugin_misc_cs.py:76 +msgid "Plugin" +msgstr "Extension" + +#: plugins/plugin_misc_cs.py:76 +msgid "Launch CoushSurfing mangement interface" +msgstr "Lancement de l'interface de gestion de CouchSurfing" + +#: plugins/plugin_misc_cs.py:84 +msgid "" +"Impossible to contact CS website, please check your login/password, " +"connection or try again later" +msgstr "" +"Impossible de contacter le site CouchSurfing, veuillez vérifier vos " +"identifiant/mot de passe, votre connexion, ou essayez un peu plus tard" + +#: plugins/plugin_misc_cs.py:92 +msgid "" +"You have to fill your CouchSurfing login & password in parameters before " +"using this interface" +msgstr "" +"Vous devez remplir vos identifiant & mot de passe CouchSurfing dans les " +"paramètres avant d'utiliser cette interface" + +#: plugins/plugin_misc_cs.py:167 +msgid "Messages" +msgstr "Messages" + +#: plugins/plugin_misc_cs.py:168 +#, python-format +msgid "" +"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\n" +"If you want to send a message, select the recipient(s) in the list below" +msgstr "" +"Bonjour %(name)s, vous avez %(nb_message)i message%(plural_mess)s non lus et " +"%(unread_CR_mess)s requête%(plural_CR)s d'hébergement en attente\n" +"Si vous voulez envoyer un message, sélectionnez le(s) destinataire(s) dans " +"la liste ci-dessous." + +#: plugins/plugin_misc_cs.py:170 +#, python-format +msgid "Show unread message%(plural)s in external web browser" +msgstr "Afficher le%(plural)s message%(plural)s non lu dans un navigateur web" + +#: plugins/plugin_misc_cs.py:173 +msgid "Subject" +msgstr "Sujet" + +#: plugins/plugin_misc_cs.py:176 +msgid "Message" +msgstr "Message" + +#: plugins/plugin_misc_cs.py:179 +msgid "send" +msgstr "envoyer" + +#: plugins/plugin_misc_cs.py:200 +#, python-format +msgid "" +"CS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" +msgstr "" +"Amis CS trouvé: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" + +#: plugins/plugin_misc_cs.py:225 +msgid "" +"INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has " +"been modified ?" +msgstr "" +"ERREUR INTERNE: aucune confirmation du message envoyée par CS, peut être que " +"le site a été modifié ?" + +#: plugins/plugin_misc_cs.py:236 +#, fuzzy, python-format +msgid "Sending message to %s" +msgstr "Envoi du message jabber à %s" + +#: plugins/plugin_misc_cs.py:237 +#, python-format +msgid "" +"\n" +"subject: %(subject)s\n" +"message: \n" +"---\n" +"%(message)s\n" +"---\n" +"\n" +msgstr "" +"\n" +"sujet: %(subject)s\n" +"message: \n" +"---\n" +"%(message)s\n" +"---\n" +"\n" + +#: plugins/plugin_misc_cs.py:243 +msgid "Message sent" +msgstr "Message envoyé" + +#: plugins/plugin_misc_cs.py:244 +msgid "The message has been sent to every recipients" +msgstr "Le message a été envoyé à tous les destinataires" + +#: plugins/plugin_misc_cs.py:257 +msgid "There is not recipient selected for this message !" +msgstr "Il n'y a aucun destinataire pour ce message !" + +#: plugins/plugin_misc_cs.py:264 +#, python-format +msgid "sending message to %(friends)s with subject [%(subject)s]" +msgstr "Envoi du message à %(friends)s avec le sujet [%(subject)s]" + +#: plugins/plugin_misc_tarot.py:56 +#, fuzzy +msgid "Implementation of Tarot card game" +msgstr "Implementation de vcard-temp" + +#: plugins/plugin_misc_tarot.py:63 +#, fuzzy +msgid "Plugin Tarot initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Passe" +msgstr "Passe" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Petite" +msgstr "Petite" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Garde" +msgstr "Garde" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Garde Sans" +msgstr "Garde Sans" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Garde Contre" +msgstr "Garde Contre" + +#: plugins/plugin_misc_tarot.py:126 +msgid "contrat selection" +msgstr "Sélection du contrat" + +#: plugins/plugin_misc_tarot.py:139 +msgid "scores" +msgstr "points" + +#: plugins/plugin_misc_tarot.py:221 plugins/plugin_misc_tarot.py:252 +#, python-format +msgid "" +"Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for " +"Excuse compensation" +msgstr "" +"Le joueur %(excuse_owner)s donne %(card_waited)s à %(player_waiting)s en " +"compensation pour l'Excuse" + +#: plugins/plugin_misc_tarot.py:257 +#, python-format +msgid "" +"%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is " +"waiting for one" +msgstr "" +"%(excuse_owner)s garde l'Excuse mais n'a aucune carte à donner, %(winner)s " +"en attend une" + +#: plugins/plugin_misc_tarot.py:305 +msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" +msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" + +#: plugins/plugin_misc_tarot.py:324 +#, python-format +msgid "" +"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" +msgstr "" +"L'attaquant (%(attaquant)s) fait %(points)i et joue pour %(point_limit)i (%" +"(nb_bouts)s bout%(plural)s%(separator)s%(bouts)s): il %(victory)s" + +#: plugins/plugin_misc_tarot.py:327 +#, python-format +msgid "" +"\n" +"--\n" +"%(player)s:\n" +"score for this game ==> %(score_game)i\n" +"total score ==> %(total_score)i" +msgstr "" +"\n" +"--\n" +"%(player)s:\n" +"points pour cette partie ==> %(score_game)i\n" +"point au total ==> %(total_score)i" + +#: plugins/plugin_misc_tarot.py:385 +msgid "Internal error: unmanaged game stage" +msgstr "ERREUR INTERNE: état de jeu inconnu" + +#: plugins/plugin_misc_tarot.py:402 +msgid "Creating Tarot game" +msgstr "Construction du jeu de Tarot" + +#: plugins/plugin_misc_tarot.py:406 plugins/plugin_misc_tarot.py:431 +#: plugins/plugin_misc_tarot.py:448 plugins/plugin_misc_tarot.py:465 +#, python-format +msgid "profile %s is unknown" +msgstr "le profil %s est inconnu" + +#: plugins/plugin_misc_tarot.py:409 +#, python-format +msgid "Tarot game already started in room %s" +msgstr "Un jeu de Tarot est déjà lancé dans le salon %s" + +#: plugins/plugin_misc_tarot.py:450 +#, python-format +msgid "contrat [%(contrat)s] choosed by %(profile)s" +msgstr "contrat [%(contrat)s] choisi par %(profile)s" + +#: plugins/plugin_misc_tarot.py:467 +#, python-format +msgid "Cards played by %(profile)s: [%(cards)s]" +msgstr "Cartes jouées par %(profile)s: [%(cards)s]" + +#: plugins/plugin_misc_tarot.py:475 +msgid "new Tarot game" +msgstr "nouveau jeu de Tarot" + +#: plugins/plugin_misc_tarot.py:532 +#, python-format +msgid "Player %(player)s is ready to start [status: %(status)s]" +msgstr "Le joueur %(player)s est prêt à commencer [statut: %(status)s]" + +#: plugins/plugin_misc_tarot.py:567 +#, python-format +msgid "%(player)s win the bid with %(contrat)s" +msgstr "%(player)s remporte l'enchère avec %(contrat)s" + +#: plugins/plugin_misc_tarot.py:591 +msgid "tarot: chien received" +msgstr "tarot: chien reçu" + +#: plugins/plugin_misc_tarot.py:646 +#, python-format +msgid "The winner of this trick is %s" +msgstr "le vainqueur de cette main est %s" + +#: plugins/plugin_misc_tarot.py:691 +#, python-format +msgid "Unmanaged error type: %s" +msgstr "type d'erreur inconnu: %s" + +#: plugins/plugin_misc_tarot.py:693 +#, python-format +msgid "Unmanaged card game element: %s" +msgstr "élément de jeu de carte inconnu: %s" + +#: plugins/plugin_xep_0045.py:62 +#, fuzzy +msgid "Implementation of Multi-User Chat" +msgstr "" +"Implémentation de l'initialisation de flux pour le transfert de fichier " + +#: plugins/plugin_xep_0045.py:68 +#, fuzzy +msgid "Plugin XEP_0045 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: plugins/plugin_xep_0045.py:85 +#, python-format +msgid "Unknown or disconnected profile (%s)" +msgstr "Profil inconnu ou déconnecté (%s)" + +#: plugins/plugin_xep_0045.py:99 +msgid "Error when joining the room" +msgstr "Erreur en tentant de rejoindre le salon" + +#: plugins/plugin_xep_0045.py:101 +msgid "Group chat error" +msgstr "Erreur de salon de discussion" + +#: plugins/plugin_xep_0045.py:137 +#, python-format +msgid "%(profile)s is already in room %(room_jid)s" +msgstr "%(profile)s est déjà dans le salon %(room_jid)s" + +#: plugins/plugin_xep_0045.py:139 +#, python-format +msgid "[%(profile)s] is joining room %(room)s with nick %(nick)s" +msgstr "[%(profile)s] rejoint %(room)s avec %(nick)s" + +#: plugins/plugin_xep_0045.py:164 +#, python-format +msgid "user %(nick)s has joined room (%(room_id)s)" +msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" + +#: plugins/plugin_xep_0045.py:169 +#, python-format +msgid "user %(nick)s left room (%(room_id)s)" +msgstr "L'utilisateur %(nick)s a quitté le salon (%(room_id)s)" + +#: plugins/plugin_xep_0045.py:178 +#, python-format +msgid "New subject for room (%(room_id)s): %(subject)s" +msgstr "Nouveau sujet pour le salon (%(room_id)s): %(subject)s" + +#: plugins/plugin_xep_0054.py:62 +msgid "Implementation of vcard-temp" +msgstr "Implementation de vcard-temp" + +#: plugins/plugin_xep_0054.py:68 +msgid "Plugin XEP_0054 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: plugins/plugin_xep_0054.py:111 +#, python-format +msgid "Photo of type [%s] found" +msgstr "Photo du type [%s] trouvée" + +#: plugins/plugin_xep_0054.py:113 +msgid "Decoding binary" +msgstr "Décodage des données" + +#: plugins/plugin_xep_0054.py:120 +#, python-format +msgid "file saved to %s" +msgstr "fichier enregistré dans %s" + +#: plugins/plugin_xep_0054.py:122 +#, python-format +msgid "file [%s] already in cache" +msgstr "fichier [%s] déjà en cache" + +#: plugins/plugin_xep_0054.py:128 +msgid "parsing vcard" +msgstr "Analyse de la vcard" + +#: plugins/plugin_xep_0054.py:154 +#, python-format +msgid "FIXME: [%s] VCard tag is not managed yet" +msgstr "CORRIGEZ-MOI: la balise VCard [%s] VCard n'est pas encore gérée" + +#: plugins/plugin_xep_0054.py:160 +msgid "VCard found" +msgstr "VCard trouvée" + +#: plugins/plugin_xep_0054.py:166 +msgid "FIXME: vCard not found as first child element" +msgstr "CORRIGEZ-MOI: la vCard n'est pas le premier élément enfant" + +#: plugins/plugin_xep_0054.py:171 +#, python-format +msgid "Can't find VCard of %s" +msgstr "Impossible de trouver la VCard de %s" + +#: plugins/plugin_xep_0054.py:180 +msgid "Asking vcard for an non-existant or not connected profile" +msgstr "Demande de vcard pour un profile inexistant ou non connecté" + +#: plugins/plugin_xep_0054.py:183 +#, python-format +msgid "Asking for %s's VCard" +msgstr "Demande de la VCard de %s" + +#: plugins/plugin_xep_0054.py:198 +#, python-format +msgid "Asking for an uncached avatar [%s]" +msgstr "Demande d'un avatar qui n'est pas en cache [%s]" + +#: plugins/plugin_xep_0054.py:245 +msgid "New avatar found, requesting vcard" +msgstr "Nouvel avatar trouvé, demande de vcard" + +#: plugins/plugin_xep_0065.py:89 +msgid "Implementation of SOCKS5 Bytestreams" +msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" + +#: plugins/plugin_xep_0065.py:135 +msgid "Protocol init" +msgstr "Initialisation du protocole" + +#: plugins/plugin_xep_0065.py:217 +#, python-format +msgid "Adding connection: %(address)s, %(connection)s" +msgstr "Ajout d'une connexion: %(address)s, %(connection)s" + +#: plugins/plugin_xep_0065.py:313 +#, python-format +msgid "Saving file in %s." +msgstr "Sauvegarde du fichier dans %s." + +#: plugins/plugin_xep_0065.py:364 +msgid "File transfer completed, closing connection" +msgstr "Transfert de fichier terminé, fermeture de la connexion" + +#: plugins/plugin_xep_0065.py:442 +msgid "Socks 5 server connection started" +msgstr "Connexion du serveur SOCKS 5 démarrée" + +#: plugins/plugin_xep_0065.py:445 +#, python-format +msgid "Socks 5 server connection lost (reason: %s)" +msgstr "Connexion du serveur SOCKS5 perdue (raison: %s)" + +#: plugins/plugin_xep_0065.py:452 +msgid "Socks 5 client connection started" +msgstr "Connexion du client SOCKS 5 démarrée" + +#: plugins/plugin_xep_0065.py:455 +#, python-format +msgid "Socks 5 client connection lost (reason: %s)" +msgstr "Connexion du client SOCKS5 perdue (raison: %s)" + +#: plugins/plugin_xep_0065.py:472 +msgid "Plugin XEP_0065 initialization" +msgstr "Initialisation du plugin XEP_0065" + +#: plugins/plugin_xep_0065.py:474 +msgid "registering" +msgstr "enregistrement" + +#: plugins/plugin_xep_0065.py:484 +#, python-format +msgid "Launching Socks5 Stream server on port %d" +msgstr "Lancement du serveur de flux Socks5 sur le port %d" + +#: plugins/plugin_xep_0065.py:500 +msgid "Launching socks5 initiator" +msgstr "Lancement de socks5 en mode initiateur" + +#: plugins/plugin_xep_0065.py:515 +#, fuzzy, python-format +msgid "Stream proposed: host=[%(host)s] port=[%(port)s]" +msgstr "Flux proposé: serveur=[%(host)s] port=[%(post)s]" + +#: plugins/plugin_xep_0065.py:531 +msgid "activating stream" +msgstr "Lancement du flux" + +#: plugins/plugin_xep_0077.py:41 +msgid "Implementation of in-band registration" +msgstr "Implémentation de l'enregistrement en ligne" + +#: plugins/plugin_xep_0077.py:47 +msgid "Plugin XEP_0077 initialization" +msgstr "Initialisation du plugin XEP_0077" + +#: plugins/plugin_xep_0077.py:62 +msgid "No data form found" +msgstr "Aucune donnée trouvée" + +#: plugins/plugin_xep_0077.py:64 +msgid "This gateway can't be managed by SàT, sorry :(" +msgstr "Ce transport ne peut être gérée par SàT, désolé :(" + +#: plugins/plugin_xep_0077.py:85 +msgid "Your are now unregistred" +msgstr "Vous êtes maintenant désinscrit" + +#: plugins/plugin_xep_0077.py:89 +#, python-format +msgid "Unregistration failure: %s" +msgstr "Échec de la désinscription: %s" + +#: plugins/plugin_xep_0077.py:93 +#, python-format +msgid "Unregistration failed: %s" +msgstr "Échec de la désinscription: %s" + +#: plugins/plugin_xep_0077.py:116 +msgid "Registration failed" +msgstr "Échec de l'inscription" + +#: plugins/plugin_xep_0077.py:134 plugins/plugin_xep_0096.py:154 +msgid "Asking for an non-existant or not connected profile" +msgstr "Demande d'un profile inexistant ou non connecté" + +#: plugins/plugin_xep_0077.py:137 +#, python-format +msgid "Asking registration for [%s]" +msgstr "Demande d'enregistrement pour [%s]" + +#: plugins/plugin_xep_0096.py:52 +msgid "Implementation of SI File Transfert" +msgstr "" +"Implémentation de l'initialisation de flux pour le transfert de fichier " + +#: plugins/plugin_xep_0096.py:58 +msgid "Plugin XEP_0096 initialization" +msgstr "Initialisation du plugin XEP_0096" + +#: plugins/plugin_xep_0096.py:67 +msgid "XEP-0096 management" +msgstr "Gestion de XEP-0096" + +#: plugins/plugin_xep_0096.py:75 +#, python-format +msgid "File proposed: name=[%(name)s] size=%(size)s" +msgstr "Fichier proposé: nom=[%(name)s] taille=%(size)s" + +#: plugins/plugin_xep_0096.py:91 +#, python-format +msgid "Transfert [%s] refused" +msgstr "Transfert [%s] refusé" + +#: plugins/plugin_xep_0096.py:96 +#, python-format +msgid "Transfert [%s] accepted" +msgstr "Transfert [%s] accepté" + +#: plugins/plugin_xep_0096.py:99 +msgid "Approved unknow id !" +msgstr "id inconnue approuvée !" + +#: plugins/plugin_xep_0096.py:111 +msgid "Feature negociation" +msgstr "Négociation de fonctionnalités" + +#: plugins/plugin_xep_0100.py:38 +msgid "Implementation of Gateways protocol" +msgstr "Implémentation du protocole de transports" + +#: plugins/plugin_xep_0100.py:44 +msgid "Gateways plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: plugins/plugin_xep_0100.py:54 +#, python-format +msgid "All items checked for id [%s]" +msgstr "Tous les points ont été vérifiés pour l'id [%s]" + +#: plugins/plugin_xep_0100.py:65 +#, python-format +msgid "Found gateway (%(jid)s): %(identity)s" +msgstr "Transport trouvé (%(jid)s): %(identity)s" + +#: plugins/plugin_xep_0100.py:76 +#, fuzzy, python-format +msgid "Error when discovering [%(jid)s]: %(error)s" +msgstr "Erreur en analysant [%(jid)s]: %(condition)s" + +#: plugins/plugin_xep_0100.py:85 +msgid "No gateway found" +msgstr "Aucun transport trouvé" + +#: plugins/plugin_xep_0100.py:92 +#, python-format +msgid "item found: %s" +msgstr "object trouvé: %s" + +#: plugins/plugin_xep_0100.py:97 +#, fuzzy, python-format +msgid "Error when discovering [%(target)s]: %(condition)s" +msgstr "Erreur en analysant [%(jid)s]: %(condition)s" + +#: plugins/plugin_xep_0100.py:98 +#, python-format +msgid "Error while trying to discover %(target)s gateways: %(error_mess)s" +msgstr "Erreur en essayant d'analyser %(target)s portails: %(error_mess)s" + +#: plugins/plugin_xep_0100.py:104 +msgid "Registration successful, doing the rest" +msgstr "Inscription réussie, lancement du reste de la procédure" + +#: plugins/plugin_xep_0100.py:124 +#, fuzzy, python-format +msgid "find gateways (target = %(target)s, profile = %(profile)s)" +msgstr "transports trouvée (cible = %s)" + +#~ msgid "presence update for [%s]" +#~ msgstr "mise à jour de l'information de présence pour [%s]"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/i18n/sat.po Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,1009 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-08-19 21:54+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: sat.tac:87 +#, python-format +msgid "********** [%s] CONNECTED **********" +msgstr "" + +#: sat.tac:93 +msgid "XML stream is initialized" +msgstr "" + +#: sat.tac:113 +#, python-format +msgid "********** [%s] DISCONNECTED **********" +msgstr "" + +#: sat.tac:117 +msgid "No keep_alife" +msgstr "" + +#: sat.tac:128 +#, python-format +msgid "got message from: %s" +msgstr "" + +#: sat.tac:171 +#, python-format +msgid "new contact in roster list: %s" +msgstr "" + +#: sat.tac:178 +#, python-format +msgid "removing %s from roster list" +msgstr "" + +#: sat.tac:188 +#, python-format +msgid "" +"presence update for [%(entity)s] (available, show=%(show)s statuses=%" +"(statuses)s priority=%(priority)d)" +msgstr "" + +#: sat.tac:202 +#, python-format +msgid "presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)" +msgstr "" + +#: sat.tac:219 +#, python-format +msgid "subscription approved for [%s]" +msgstr "" + +#: sat.tac:224 +#, python-format +msgid "unsubscription confirmed for [%s]" +msgstr "" + +#: sat.tac:229 +#, python-format +msgid "subscription request for [%s]" +msgstr "" + +#: sat.tac:234 +#, python-format +msgid "unsubscription asked for [%s]" +msgstr "" + +#: sat.tac:259 +msgid "Registration asked for" +msgstr "" + +#: sat.tac:277 plugins/plugin_xep_0077.py:83 plugins/plugin_xep_0077.py:97 +#, python-format +msgid "registration answer: %s" +msgstr "" + +#: sat.tac:279 plugins/plugin_xep_0077.py:99 +msgid "Registration successfull" +msgstr "" + +#: sat.tac:284 plugins/plugin_xep_0077.py:75 plugins/plugin_xep_0077.py:107 +#, python-format +msgid "Registration failure: %s" +msgstr "" + +#: sat.tac:289 plugins/plugin_xep_0077.py:113 +msgid "Username already exists, please choose an other one" +msgstr "" + +#: sat.tac:292 +#, python-format +msgid "Registration failed (%s)" +msgstr "" + +#: sat.tac:305 +msgid "Trying to access an undefined constant" +msgstr "" + +#: sat.tac:312 +msgid "Trying to redefine a constant" +msgstr "" + +#: sat.tac:379 +#, python-format +msgid "importing plugin: %s" +msgstr "" + +#: sat.tac:392 +msgid "Trying to connect a non-exsitant profile" +msgstr "" + +#: sat.tac:396 +msgid "already connected !" +msgstr "" + +#: sat.tac:419 +msgid "setting plugins parents" +msgstr "" + +#: sat.tac:430 +msgid "not connected !" +msgstr "" + +#: sat.tac:433 +msgid "Disconnecting..." +msgstr "" + +#: sat.tac:445 +msgid "running app" +msgstr "" + +#: sat.tac:449 +msgid "stopping app" +msgstr "" + +#: sat.tac:486 +msgid "No user or server given" +msgstr "" + +#: sat.tac:488 +msgid "No user, password or server given, can't register new account." +msgstr "" + +#: sat.tac:495 +#, python-format +msgid "Are you sure to register new account [%(user)s] to server %(server)s ?" +msgstr "" + +#: sat.tac:502 +#, python-format +msgid "register Confirmation CB ! (%s)" +msgstr "" + +#: sat.tac:534 +#, python-format +msgid "FIXME FIXME FIXME: Unmanaged action (%s) in submitForm" +msgstr "" + +#: sat.tac:544 +#, python-format +msgid "setting param: %(name)s=%(value)s in category %(category)s" +msgstr "" + +#: sat.tac:554 +msgid "asking connection status for a non-existant profile" +msgstr "" + +#: sat.tac:569 +msgid "trying to launch action with a non-existant profile" +msgstr "" + +#: sat.tac:575 +msgid "Incomplete data" +msgstr "" + +#: sat.tac:581 +msgid "Unknown action type" +msgstr "" + +#: sat.tac:592 +#, python-format +msgid "Sending jabber message to %s..." +msgstr "" + +#: sat.tac:619 +#, python-format +msgid "subsciption request [%(subs_type)s] for %(jid)s" +msgstr "" + +#: sat.tac:626 +msgid "sending automatic \"to\" subscription request" +msgstr "" + +#: sat.tac:657 +#, python-format +msgid "Feature found: %s" +msgstr "" + +#: sat.tac:660 +#, python-format +msgid "Identity found: [%(category)s/%(type)s] %(identity)s" +msgstr "" + +#: sat.tac:680 +msgid "type for actionResultExt must be DICT_DICT, fixing it" +msgstr "" + +#: sat.tac:694 +msgid "Attempt to register two callbacks for the same confirmation" +msgstr "" + +#: sat.tac:702 +#, python-format +msgid "Received confirmation answer for id [%(id)s]: %(success)s" +msgstr "" + +#: sat.tac:702 +msgid "accepted" +msgstr "" + +#: sat.tac:702 +msgid "refused" +msgstr "" + +#: sat.tac:704 +msgid "Received an unknown confirmation" +msgstr "" + +#: sat.tac:717 +msgid "Trying to remove an unknow progress callback" +msgstr "" + +#: sat.tac:741 +msgid "Trying to remove an unknow general callback" +msgstr "" + +#: sat.tac:750 +#, python-format +msgid "Trying to call unknown function (%s)" +msgstr "" + +#: sat.tac:774 +msgid "Trying to access an unknown menu" +msgstr "" + +#: sat.tac:781 +msgid "Non-exsitant profile" +msgstr "" + +#: sat.tac:788 +#, python-format +msgid "Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)" +msgstr "" + +#: tools/memory.py:62 +msgid "Connection" +msgstr "" + +#: tools/memory.py:63 +msgid "Register new account" +msgstr "" + +#: tools/memory.py:64 +msgid "Connect on frontend startup" +msgstr "" + +#: tools/memory.py:65 +msgid "Disconnect on frontend closure" +msgstr "" + +#: tools/memory.py:66 +msgid "Misc" +msgstr "" + +#: tools/memory.py:85 +msgid "general params data loaded" +msgstr "" + +#: tools/memory.py:87 +msgid "Can't load general params data !" +msgstr "" + +#: tools/memory.py:93 +msgid "individual params data loaded" +msgstr "" + +#: tools/memory.py:95 +msgid "Can't load individual params data !" +msgstr "" + +#: tools/memory.py:132 +msgid "The profile name already exists" +msgstr "" + +#: tools/memory.py:141 +msgid "Trying to delete an unknown profile" +msgstr "" + +#: tools/memory.py:157 +msgid "No default profile, returning first one" +msgstr "" + +#: tools/memory.py:162 +msgid "Trying to access an unknown profile" +msgstr "" + +#: tools/memory.py:202 +#, python-format +msgid "Can't determine default value for [%(category)s/%(name)s]: %(reason)s" +msgstr "" + +#: tools/memory.py:215 tools/memory.py:233 +#, python-format +msgid "Requested param [%(name)s] in category [%(category)s] doesn't exist !" +msgstr "" + +#: tools/memory.py:244 +msgid "Requesting a param for an non-existant profile" +msgstr "" + +#: tools/memory.py:296 tools/memory.py:306 tools/memory.py:319 +msgid "Asking params for inexistant profile" +msgstr "" + +#: tools/memory.py:365 +#, python-format +msgid "Requesting an unknown parameter (%(category)s/%(name)s)" +msgstr "" + +#: tools/memory.py:377 +msgid "Trying to set parameter for an unknown profile" +msgstr "" + +#: tools/memory.py:391 +msgid "Memory manager init" +msgstr "" + +#: tools/memory.py:418 +msgid "params template loaded" +msgstr "" + +#: tools/memory.py:420 +msgid "Can't load params template !" +msgstr "" + +#: tools/memory.py:423 +msgid "No params template, using default template" +msgstr "" + +#: tools/memory.py:428 +msgid "params loaded" +msgstr "" + +#: tools/memory.py:430 +msgid "Can't load params !" +msgstr "" + +#: tools/memory.py:437 +msgid "history loaded" +msgstr "" + +#: tools/memory.py:439 +msgid "Can't load history !" +msgstr "" + +#: tools/memory.py:446 +msgid "private values loaded" +msgstr "" + +#: tools/memory.py:448 +msgid "Can't load private values !" +msgstr "" + +#: tools/memory.py:464 +msgid "params saved" +msgstr "" + +#: tools/memory.py:467 +msgid "history saved" +msgstr "" + +#: tools/memory.py:470 +msgid "private values saved" +msgstr "" + +#: tools/memory.py:513 +msgid "source JID not found !" +msgstr "" + +#: tools/memory.py:517 +msgid "dest JID not found !" +msgstr "" + +#: tools/memory.py:544 +msgid "Trying to add a contact to a non-existant profile" +msgstr "" + +#: tools/memory.py:556 +msgid "Trying to delete a contact for a non-existant profile" +msgstr "" + +#: tools/memory.py:564 +msgid "Asking a contact for a non-existant profile" +msgstr "" + +#: tools/memory.py:578 tools/memory.py:627 +msgid "Asking contacts for a non-existant profile" +msgstr "" + +#: tools/memory.py:589 +msgid "Trying to add presence status to a non-existant profile" +msgstr "" + +#: tools/memory.py:617 +msgid "Asking waiting subscriptions for a non-existant profile" +msgstr "" + +#: tools/xml_tools.py:79 +msgid "INTERNAL ERROR: parameters xml not valid" +msgstr "" + +#: tools/xml_tools.py:86 +msgid "INTERNAL ERROR: params categories must have a name" +msgstr "" + +#: tools/xml_tools.py:93 +msgid "INTERNAL ERROR: params must have a name" +msgstr "" + +#: tools/xml_tools.py:127 +#, python-format +msgid "Unknown panel type [%s]" +msgstr "" + +#: tools/xml_tools.py:150 +#, python-format +msgid "Unknown layout type [%s]" +msgstr "" + +#: tools/xml_tools.py:286 +msgid "Trying to add a category without parent tabs layout" +msgstr "" + +#: tools/xml_tools.py:289 +msgid "parent layout of a category is not tabs" +msgstr "" + +#: plugins/plugin_misc_cs.py:52 +msgid "" +"This plugin allow to manage your CouchSurfing account throught your SàT " +"frontend" +msgstr "" + +#: plugins/plugin_misc_cs.py:71 +msgid "Plugin CS initialization" +msgstr "" + +#: plugins/plugin_misc_cs.py:76 +msgid "Plugin" +msgstr "" + +#: plugins/plugin_misc_cs.py:76 +msgid "Launch CoushSurfing mangement interface" +msgstr "" + +#: plugins/plugin_misc_cs.py:84 +msgid "" +"Impossible to contact CS website, please check your login/password, " +"connection or try again later" +msgstr "" + +#: plugins/plugin_misc_cs.py:92 +msgid "" +"You have to fill your CouchSurfing login & password in parameters before " +"using this interface" +msgstr "" + +#: plugins/plugin_misc_cs.py:167 +msgid "Messages" +msgstr "" + +#: plugins/plugin_misc_cs.py:168 +#, python-format +msgid "" +"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\n" +"If you want to send a message, select the recipient(s) in the list below" +msgstr "" + +#: plugins/plugin_misc_cs.py:170 +#, python-format +msgid "Show unread message%(plural)s in external web browser" +msgstr "" + +#: plugins/plugin_misc_cs.py:173 +msgid "Subject" +msgstr "" + +#: plugins/plugin_misc_cs.py:176 +msgid "Message" +msgstr "" + +#: plugins/plugin_misc_cs.py:179 +msgid "send" +msgstr "" + +#: plugins/plugin_misc_cs.py:200 +#, python-format +msgid "" +"CS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" +msgstr "" + +#: plugins/plugin_misc_cs.py:225 +msgid "" +"INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has " +"been modified ?" +msgstr "" + +#: plugins/plugin_misc_cs.py:236 +#, python-format +msgid "Sending message to %s" +msgstr "" + +#: plugins/plugin_misc_cs.py:237 +#, python-format +msgid "" +"\n" +"subject: %(subject)s\n" +"message: \n" +"---\n" +"%(message)s\n" +"---\n" +"\n" +msgstr "" + +#: plugins/plugin_misc_cs.py:243 +msgid "Message sent" +msgstr "" + +#: plugins/plugin_misc_cs.py:244 +msgid "The message has been sent to every recipients" +msgstr "" + +#: plugins/plugin_misc_cs.py:257 +msgid "There is not recipient selected for this message !" +msgstr "" + +#: plugins/plugin_misc_cs.py:264 +#, python-format +msgid "sending message to %(friends)s with subject [%(subject)s]" +msgstr "" + +#: plugins/plugin_misc_tarot.py:56 +msgid "Implementation of Tarot card game" +msgstr "" + +#: plugins/plugin_misc_tarot.py:63 +msgid "Plugin Tarot initialization" +msgstr "" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Passe" +msgstr "" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Petite" +msgstr "" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Garde" +msgstr "" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Garde Sans" +msgstr "" + +#: plugins/plugin_misc_tarot.py:66 +msgid "Garde Contre" +msgstr "" + +#: plugins/plugin_misc_tarot.py:126 +msgid "contrat selection" +msgstr "" + +#: plugins/plugin_misc_tarot.py:139 +msgid "scores" +msgstr "" + +#: plugins/plugin_misc_tarot.py:221 plugins/plugin_misc_tarot.py:252 +#, python-format +msgid "" +"Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for " +"Excuse compensation" +msgstr "" + +#: plugins/plugin_misc_tarot.py:257 +#, python-format +msgid "" +"%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is " +"waiting for one" +msgstr "" + +#: plugins/plugin_misc_tarot.py:305 +msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" +msgstr "" + +#: plugins/plugin_misc_tarot.py:324 +#, python-format +msgid "" +"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" +msgstr "" + +#: plugins/plugin_misc_tarot.py:327 +#, python-format +msgid "" +"\n" +"--\n" +"%(player)s:\n" +"score for this game ==> %(score_game)i\n" +"total score ==> %(total_score)i" +msgstr "" + +#: plugins/plugin_misc_tarot.py:385 +msgid "Internal error: unmanaged game stage" +msgstr "" + +#: plugins/plugin_misc_tarot.py:402 +msgid "Creating Tarot game" +msgstr "" + +#: plugins/plugin_misc_tarot.py:406 plugins/plugin_misc_tarot.py:431 +#: plugins/plugin_misc_tarot.py:448 plugins/plugin_misc_tarot.py:465 +#, python-format +msgid "profile %s is unknown" +msgstr "" + +#: plugins/plugin_misc_tarot.py:409 +#, python-format +msgid "Tarot game already started in room %s" +msgstr "" + +#: plugins/plugin_misc_tarot.py:450 +#, python-format +msgid "contrat [%(contrat)s] choosed by %(profile)s" +msgstr "" + +#: plugins/plugin_misc_tarot.py:467 +#, python-format +msgid "Cards played by %(profile)s: [%(cards)s]" +msgstr "" + +#: plugins/plugin_misc_tarot.py:475 +msgid "new Tarot game" +msgstr "" + +#: plugins/plugin_misc_tarot.py:532 +#, python-format +msgid "Player %(player)s is ready to start [status: %(status)s]" +msgstr "" + +#: plugins/plugin_misc_tarot.py:567 +#, python-format +msgid "%(player)s win the bid with %(contrat)s" +msgstr "" + +#: plugins/plugin_misc_tarot.py:591 +msgid "tarot: chien received" +msgstr "" + +#: plugins/plugin_misc_tarot.py:646 +#, python-format +msgid "The winner of this trick is %s" +msgstr "" + +#: plugins/plugin_misc_tarot.py:691 +#, python-format +msgid "Unmanaged error type: %s" +msgstr "" + +#: plugins/plugin_misc_tarot.py:693 +#, python-format +msgid "Unmanaged card game element: %s" +msgstr "" + +#: plugins/plugin_xep_0045.py:62 +msgid "Implementation of Multi-User Chat" +msgstr "" + +#: plugins/plugin_xep_0045.py:68 +msgid "Plugin XEP_0045 initialization" +msgstr "" + +#: plugins/plugin_xep_0045.py:85 +#, python-format +msgid "Unknown or disconnected profile (%s)" +msgstr "" + +#: plugins/plugin_xep_0045.py:99 +msgid "Error when joining the room" +msgstr "" + +#: plugins/plugin_xep_0045.py:101 +msgid "Group chat error" +msgstr "" + +#: plugins/plugin_xep_0045.py:137 +#, python-format +msgid "%(profile)s is already in room %(room_jid)s" +msgstr "" + +#: plugins/plugin_xep_0045.py:139 +#, python-format +msgid "[%(profile)s] is joining room %(room)s with nick %(nick)s" +msgstr "" + +#: plugins/plugin_xep_0045.py:164 +#, python-format +msgid "user %(nick)s has joined room (%(room_id)s)" +msgstr "" + +#: plugins/plugin_xep_0045.py:169 +#, python-format +msgid "user %(nick)s left room (%(room_id)s)" +msgstr "" + +#: plugins/plugin_xep_0045.py:178 +#, python-format +msgid "New subject for room (%(room_id)s): %(subject)s" +msgstr "" + +#: plugins/plugin_xep_0054.py:62 +msgid "Implementation of vcard-temp" +msgstr "" + +#: plugins/plugin_xep_0054.py:68 +msgid "Plugin XEP_0054 initialization" +msgstr "" + +#: plugins/plugin_xep_0054.py:111 +#, python-format +msgid "Photo of type [%s] found" +msgstr "" + +#: plugins/plugin_xep_0054.py:113 +msgid "Decoding binary" +msgstr "" + +#: plugins/plugin_xep_0054.py:120 +#, python-format +msgid "file saved to %s" +msgstr "" + +#: plugins/plugin_xep_0054.py:122 +#, python-format +msgid "file [%s] already in cache" +msgstr "" + +#: plugins/plugin_xep_0054.py:128 +msgid "parsing vcard" +msgstr "" + +#: plugins/plugin_xep_0054.py:154 +#, python-format +msgid "FIXME: [%s] VCard tag is not managed yet" +msgstr "" + +#: plugins/plugin_xep_0054.py:160 +msgid "VCard found" +msgstr "" + +#: plugins/plugin_xep_0054.py:166 +msgid "FIXME: vCard not found as first child element" +msgstr "" + +#: plugins/plugin_xep_0054.py:171 +#, python-format +msgid "Can't find VCard of %s" +msgstr "" + +#: plugins/plugin_xep_0054.py:180 +msgid "Asking vcard for an non-existant or not connected profile" +msgstr "" + +#: plugins/plugin_xep_0054.py:183 +#, python-format +msgid "Asking for %s's VCard" +msgstr "" + +#: plugins/plugin_xep_0054.py:198 +#, python-format +msgid "Asking for an uncached avatar [%s]" +msgstr "" + +#: plugins/plugin_xep_0054.py:245 +msgid "New avatar found, requesting vcard" +msgstr "" + +#: plugins/plugin_xep_0065.py:89 +msgid "Implementation of SOCKS5 Bytestreams" +msgstr "" + +#: plugins/plugin_xep_0065.py:135 +msgid "Protocol init" +msgstr "" + +#: plugins/plugin_xep_0065.py:217 +#, python-format +msgid "Adding connection: %(address)s, %(connection)s" +msgstr "" + +#: plugins/plugin_xep_0065.py:313 +#, python-format +msgid "Saving file in %s." +msgstr "" + +#: plugins/plugin_xep_0065.py:364 +msgid "File transfer completed, closing connection" +msgstr "" + +#: plugins/plugin_xep_0065.py:442 +msgid "Socks 5 server connection started" +msgstr "" + +#: plugins/plugin_xep_0065.py:445 +#, python-format +msgid "Socks 5 server connection lost (reason: %s)" +msgstr "" + +#: plugins/plugin_xep_0065.py:452 +msgid "Socks 5 client connection started" +msgstr "" + +#: plugins/plugin_xep_0065.py:455 +#, python-format +msgid "Socks 5 client connection lost (reason: %s)" +msgstr "" + +#: plugins/plugin_xep_0065.py:472 +msgid "Plugin XEP_0065 initialization" +msgstr "" + +#: plugins/plugin_xep_0065.py:474 +msgid "registering" +msgstr "" + +#: plugins/plugin_xep_0065.py:484 +#, python-format +msgid "Launching Socks5 Stream server on port %d" +msgstr "" + +#: plugins/plugin_xep_0065.py:500 +msgid "Launching socks5 initiator" +msgstr "" + +#: plugins/plugin_xep_0065.py:515 +#, python-format +msgid "Stream proposed: host=[%(host)s] port=[%(port)s]" +msgstr "" + +#: plugins/plugin_xep_0065.py:531 +msgid "activating stream" +msgstr "" + +#: plugins/plugin_xep_0077.py:41 +msgid "Implementation of in-band registration" +msgstr "" + +#: plugins/plugin_xep_0077.py:47 +msgid "Plugin XEP_0077 initialization" +msgstr "" + +#: plugins/plugin_xep_0077.py:62 +msgid "No data form found" +msgstr "" + +#: plugins/plugin_xep_0077.py:64 +msgid "This gateway can't be managed by SàT, sorry :(" +msgstr "" + +#: plugins/plugin_xep_0077.py:85 +msgid "Your are now unregistred" +msgstr "" + +#: plugins/plugin_xep_0077.py:89 +#, python-format +msgid "Unregistration failure: %s" +msgstr "" + +#: plugins/plugin_xep_0077.py:93 +#, python-format +msgid "Unregistration failed: %s" +msgstr "" + +#: plugins/plugin_xep_0077.py:116 +msgid "Registration failed" +msgstr "" + +#: plugins/plugin_xep_0077.py:134 plugins/plugin_xep_0096.py:154 +msgid "Asking for an non-existant or not connected profile" +msgstr "" + +#: plugins/plugin_xep_0077.py:137 +#, python-format +msgid "Asking registration for [%s]" +msgstr "" + +#: plugins/plugin_xep_0096.py:52 +msgid "Implementation of SI File Transfert" +msgstr "" + +#: plugins/plugin_xep_0096.py:58 +msgid "Plugin XEP_0096 initialization" +msgstr "" + +#: plugins/plugin_xep_0096.py:67 +msgid "XEP-0096 management" +msgstr "" + +#: plugins/plugin_xep_0096.py:75 +#, python-format +msgid "File proposed: name=[%(name)s] size=%(size)s" +msgstr "" + +#: plugins/plugin_xep_0096.py:91 +#, python-format +msgid "Transfert [%s] refused" +msgstr "" + +#: plugins/plugin_xep_0096.py:96 +#, python-format +msgid "Transfert [%s] accepted" +msgstr "" + +#: plugins/plugin_xep_0096.py:99 +msgid "Approved unknow id !" +msgstr "" + +#: plugins/plugin_xep_0096.py:111 +msgid "Feature negociation" +msgstr "" + +#: plugins/plugin_xep_0100.py:38 +msgid "Implementation of Gateways protocol" +msgstr "" + +#: plugins/plugin_xep_0100.py:44 +msgid "Gateways plugin initialization" +msgstr "" + +#: plugins/plugin_xep_0100.py:54 +#, python-format +msgid "All items checked for id [%s]" +msgstr "" + +#: plugins/plugin_xep_0100.py:65 +#, python-format +msgid "Found gateway (%(jid)s): %(identity)s" +msgstr "" + +#: plugins/plugin_xep_0100.py:76 +#, python-format +msgid "Error when discovering [%(jid)s]: %(error)s" +msgstr "" + +#: plugins/plugin_xep_0100.py:85 +msgid "No gateway found" +msgstr "" + +#: plugins/plugin_xep_0100.py:92 +#, python-format +msgid "item found: %s" +msgstr "" + +#: plugins/plugin_xep_0100.py:97 +#, python-format +msgid "Error when discovering [%(target)s]: %(condition)s" +msgstr "" + +#: plugins/plugin_xep_0100.py:98 +#, python-format +msgid "Error while trying to discover %(target)s gateways: %(error_mess)s" +msgstr "" + +#: plugins/plugin_xep_0100.py:104 +msgid "Registration successful, doing the rest" +msgstr "" + +#: plugins/plugin_xep_0100.py:124 +#, python-format +msgid "find gateways (target = %(target)s, profile = %(profile)s)" +msgstr ""
--- a/plugins/plugin_misc_cs.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,266 +0,0 @@ -#!/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 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) -
--- a/plugins/plugin_misc_tarot.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,713 +0,0 @@ -#!/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 tools.xml_tools import dataForm2xml -from 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 [] -
--- a/plugins/plugin_xep_0045.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,197 +0,0 @@ -#!/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 [] -
--- a/plugins/plugin_xep_0054.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,246 +0,0 @@ -#!/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)
--- a/plugins/plugin_xep_0065.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,558 +0,0 @@ -#!/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 []
--- a/plugins/plugin_xep_0077.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ -#!/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 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"]
--- a/plugins/plugin_xep_0096.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,208 +0,0 @@ -#!/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 [] -
--- a/plugins/plugin_xep_0100.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,127 +0,0 @@ -#!/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
--- a/sat Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -#!/bin/sh -twistd -noy sat.tac -
--- a/sat.po Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1009 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-08-19 21:54+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: sat.tac:87 -#, python-format -msgid "********** [%s] CONNECTED **********" -msgstr "" - -#: sat.tac:93 -msgid "XML stream is initialized" -msgstr "" - -#: sat.tac:113 -#, python-format -msgid "********** [%s] DISCONNECTED **********" -msgstr "" - -#: sat.tac:117 -msgid "No keep_alife" -msgstr "" - -#: sat.tac:128 -#, python-format -msgid "got message from: %s" -msgstr "" - -#: sat.tac:171 -#, python-format -msgid "new contact in roster list: %s" -msgstr "" - -#: sat.tac:178 -#, python-format -msgid "removing %s from roster list" -msgstr "" - -#: sat.tac:188 -#, python-format -msgid "" -"presence update for [%(entity)s] (available, show=%(show)s statuses=%" -"(statuses)s priority=%(priority)d)" -msgstr "" - -#: sat.tac:202 -#, python-format -msgid "presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)" -msgstr "" - -#: sat.tac:219 -#, python-format -msgid "subscription approved for [%s]" -msgstr "" - -#: sat.tac:224 -#, python-format -msgid "unsubscription confirmed for [%s]" -msgstr "" - -#: sat.tac:229 -#, python-format -msgid "subscription request for [%s]" -msgstr "" - -#: sat.tac:234 -#, python-format -msgid "unsubscription asked for [%s]" -msgstr "" - -#: sat.tac:259 -msgid "Registration asked for" -msgstr "" - -#: sat.tac:277 plugins/plugin_xep_0077.py:83 plugins/plugin_xep_0077.py:97 -#, python-format -msgid "registration answer: %s" -msgstr "" - -#: sat.tac:279 plugins/plugin_xep_0077.py:99 -msgid "Registration successfull" -msgstr "" - -#: sat.tac:284 plugins/plugin_xep_0077.py:75 plugins/plugin_xep_0077.py:107 -#, python-format -msgid "Registration failure: %s" -msgstr "" - -#: sat.tac:289 plugins/plugin_xep_0077.py:113 -msgid "Username already exists, please choose an other one" -msgstr "" - -#: sat.tac:292 -#, python-format -msgid "Registration failed (%s)" -msgstr "" - -#: sat.tac:305 -msgid "Trying to access an undefined constant" -msgstr "" - -#: sat.tac:312 -msgid "Trying to redefine a constant" -msgstr "" - -#: sat.tac:379 -#, python-format -msgid "importing plugin: %s" -msgstr "" - -#: sat.tac:392 -msgid "Trying to connect a non-exsitant profile" -msgstr "" - -#: sat.tac:396 -msgid "already connected !" -msgstr "" - -#: sat.tac:419 -msgid "setting plugins parents" -msgstr "" - -#: sat.tac:430 -msgid "not connected !" -msgstr "" - -#: sat.tac:433 -msgid "Disconnecting..." -msgstr "" - -#: sat.tac:445 -msgid "running app" -msgstr "" - -#: sat.tac:449 -msgid "stopping app" -msgstr "" - -#: sat.tac:486 -msgid "No user or server given" -msgstr "" - -#: sat.tac:488 -msgid "No user, password or server given, can't register new account." -msgstr "" - -#: sat.tac:495 -#, python-format -msgid "Are you sure to register new account [%(user)s] to server %(server)s ?" -msgstr "" - -#: sat.tac:502 -#, python-format -msgid "register Confirmation CB ! (%s)" -msgstr "" - -#: sat.tac:534 -#, python-format -msgid "FIXME FIXME FIXME: Unmanaged action (%s) in submitForm" -msgstr "" - -#: sat.tac:544 -#, python-format -msgid "setting param: %(name)s=%(value)s in category %(category)s" -msgstr "" - -#: sat.tac:554 -msgid "asking connection status for a non-existant profile" -msgstr "" - -#: sat.tac:569 -msgid "trying to launch action with a non-existant profile" -msgstr "" - -#: sat.tac:575 -msgid "Incomplete data" -msgstr "" - -#: sat.tac:581 -msgid "Unknown action type" -msgstr "" - -#: sat.tac:592 -#, python-format -msgid "Sending jabber message to %s..." -msgstr "" - -#: sat.tac:619 -#, python-format -msgid "subsciption request [%(subs_type)s] for %(jid)s" -msgstr "" - -#: sat.tac:626 -msgid "sending automatic \"to\" subscription request" -msgstr "" - -#: sat.tac:657 -#, python-format -msgid "Feature found: %s" -msgstr "" - -#: sat.tac:660 -#, python-format -msgid "Identity found: [%(category)s/%(type)s] %(identity)s" -msgstr "" - -#: sat.tac:680 -msgid "type for actionResultExt must be DICT_DICT, fixing it" -msgstr "" - -#: sat.tac:694 -msgid "Attempt to register two callbacks for the same confirmation" -msgstr "" - -#: sat.tac:702 -#, python-format -msgid "Received confirmation answer for id [%(id)s]: %(success)s" -msgstr "" - -#: sat.tac:702 -msgid "accepted" -msgstr "" - -#: sat.tac:702 -msgid "refused" -msgstr "" - -#: sat.tac:704 -msgid "Received an unknown confirmation" -msgstr "" - -#: sat.tac:717 -msgid "Trying to remove an unknow progress callback" -msgstr "" - -#: sat.tac:741 -msgid "Trying to remove an unknow general callback" -msgstr "" - -#: sat.tac:750 -#, python-format -msgid "Trying to call unknown function (%s)" -msgstr "" - -#: sat.tac:774 -msgid "Trying to access an unknown menu" -msgstr "" - -#: sat.tac:781 -msgid "Non-exsitant profile" -msgstr "" - -#: sat.tac:788 -#, python-format -msgid "Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)" -msgstr "" - -#: tools/memory.py:62 -msgid "Connection" -msgstr "" - -#: tools/memory.py:63 -msgid "Register new account" -msgstr "" - -#: tools/memory.py:64 -msgid "Connect on frontend startup" -msgstr "" - -#: tools/memory.py:65 -msgid "Disconnect on frontend closure" -msgstr "" - -#: tools/memory.py:66 -msgid "Misc" -msgstr "" - -#: tools/memory.py:85 -msgid "general params data loaded" -msgstr "" - -#: tools/memory.py:87 -msgid "Can't load general params data !" -msgstr "" - -#: tools/memory.py:93 -msgid "individual params data loaded" -msgstr "" - -#: tools/memory.py:95 -msgid "Can't load individual params data !" -msgstr "" - -#: tools/memory.py:132 -msgid "The profile name already exists" -msgstr "" - -#: tools/memory.py:141 -msgid "Trying to delete an unknown profile" -msgstr "" - -#: tools/memory.py:157 -msgid "No default profile, returning first one" -msgstr "" - -#: tools/memory.py:162 -msgid "Trying to access an unknown profile" -msgstr "" - -#: tools/memory.py:202 -#, python-format -msgid "Can't determine default value for [%(category)s/%(name)s]: %(reason)s" -msgstr "" - -#: tools/memory.py:215 tools/memory.py:233 -#, python-format -msgid "Requested param [%(name)s] in category [%(category)s] doesn't exist !" -msgstr "" - -#: tools/memory.py:244 -msgid "Requesting a param for an non-existant profile" -msgstr "" - -#: tools/memory.py:296 tools/memory.py:306 tools/memory.py:319 -msgid "Asking params for inexistant profile" -msgstr "" - -#: tools/memory.py:365 -#, python-format -msgid "Requesting an unknown parameter (%(category)s/%(name)s)" -msgstr "" - -#: tools/memory.py:377 -msgid "Trying to set parameter for an unknown profile" -msgstr "" - -#: tools/memory.py:391 -msgid "Memory manager init" -msgstr "" - -#: tools/memory.py:418 -msgid "params template loaded" -msgstr "" - -#: tools/memory.py:420 -msgid "Can't load params template !" -msgstr "" - -#: tools/memory.py:423 -msgid "No params template, using default template" -msgstr "" - -#: tools/memory.py:428 -msgid "params loaded" -msgstr "" - -#: tools/memory.py:430 -msgid "Can't load params !" -msgstr "" - -#: tools/memory.py:437 -msgid "history loaded" -msgstr "" - -#: tools/memory.py:439 -msgid "Can't load history !" -msgstr "" - -#: tools/memory.py:446 -msgid "private values loaded" -msgstr "" - -#: tools/memory.py:448 -msgid "Can't load private values !" -msgstr "" - -#: tools/memory.py:464 -msgid "params saved" -msgstr "" - -#: tools/memory.py:467 -msgid "history saved" -msgstr "" - -#: tools/memory.py:470 -msgid "private values saved" -msgstr "" - -#: tools/memory.py:513 -msgid "source JID not found !" -msgstr "" - -#: tools/memory.py:517 -msgid "dest JID not found !" -msgstr "" - -#: tools/memory.py:544 -msgid "Trying to add a contact to a non-existant profile" -msgstr "" - -#: tools/memory.py:556 -msgid "Trying to delete a contact for a non-existant profile" -msgstr "" - -#: tools/memory.py:564 -msgid "Asking a contact for a non-existant profile" -msgstr "" - -#: tools/memory.py:578 tools/memory.py:627 -msgid "Asking contacts for a non-existant profile" -msgstr "" - -#: tools/memory.py:589 -msgid "Trying to add presence status to a non-existant profile" -msgstr "" - -#: tools/memory.py:617 -msgid "Asking waiting subscriptions for a non-existant profile" -msgstr "" - -#: tools/xml_tools.py:79 -msgid "INTERNAL ERROR: parameters xml not valid" -msgstr "" - -#: tools/xml_tools.py:86 -msgid "INTERNAL ERROR: params categories must have a name" -msgstr "" - -#: tools/xml_tools.py:93 -msgid "INTERNAL ERROR: params must have a name" -msgstr "" - -#: tools/xml_tools.py:127 -#, python-format -msgid "Unknown panel type [%s]" -msgstr "" - -#: tools/xml_tools.py:150 -#, python-format -msgid "Unknown layout type [%s]" -msgstr "" - -#: tools/xml_tools.py:286 -msgid "Trying to add a category without parent tabs layout" -msgstr "" - -#: tools/xml_tools.py:289 -msgid "parent layout of a category is not tabs" -msgstr "" - -#: plugins/plugin_misc_cs.py:52 -msgid "" -"This plugin allow to manage your CouchSurfing account throught your SàT " -"frontend" -msgstr "" - -#: plugins/plugin_misc_cs.py:71 -msgid "Plugin CS initialization" -msgstr "" - -#: plugins/plugin_misc_cs.py:76 -msgid "Plugin" -msgstr "" - -#: plugins/plugin_misc_cs.py:76 -msgid "Launch CoushSurfing mangement interface" -msgstr "" - -#: plugins/plugin_misc_cs.py:84 -msgid "" -"Impossible to contact CS website, please check your login/password, " -"connection or try again later" -msgstr "" - -#: plugins/plugin_misc_cs.py:92 -msgid "" -"You have to fill your CouchSurfing login & password in parameters before " -"using this interface" -msgstr "" - -#: plugins/plugin_misc_cs.py:167 -msgid "Messages" -msgstr "" - -#: plugins/plugin_misc_cs.py:168 -#, python-format -msgid "" -"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\n" -"If you want to send a message, select the recipient(s) in the list below" -msgstr "" - -#: plugins/plugin_misc_cs.py:170 -#, python-format -msgid "Show unread message%(plural)s in external web browser" -msgstr "" - -#: plugins/plugin_misc_cs.py:173 -msgid "Subject" -msgstr "" - -#: plugins/plugin_misc_cs.py:176 -msgid "Message" -msgstr "" - -#: plugins/plugin_misc_cs.py:179 -msgid "send" -msgstr "" - -#: plugins/plugin_misc_cs.py:200 -#, python-format -msgid "" -"CS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" -msgstr "" - -#: plugins/plugin_misc_cs.py:225 -msgid "" -"INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has " -"been modified ?" -msgstr "" - -#: plugins/plugin_misc_cs.py:236 -#, python-format -msgid "Sending message to %s" -msgstr "" - -#: plugins/plugin_misc_cs.py:237 -#, python-format -msgid "" -"\n" -"subject: %(subject)s\n" -"message: \n" -"---\n" -"%(message)s\n" -"---\n" -"\n" -msgstr "" - -#: plugins/plugin_misc_cs.py:243 -msgid "Message sent" -msgstr "" - -#: plugins/plugin_misc_cs.py:244 -msgid "The message has been sent to every recipients" -msgstr "" - -#: plugins/plugin_misc_cs.py:257 -msgid "There is not recipient selected for this message !" -msgstr "" - -#: plugins/plugin_misc_cs.py:264 -#, python-format -msgid "sending message to %(friends)s with subject [%(subject)s]" -msgstr "" - -#: plugins/plugin_misc_tarot.py:56 -msgid "Implementation of Tarot card game" -msgstr "" - -#: plugins/plugin_misc_tarot.py:63 -msgid "Plugin Tarot initialization" -msgstr "" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Passe" -msgstr "" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Petite" -msgstr "" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Garde" -msgstr "" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Garde Sans" -msgstr "" - -#: plugins/plugin_misc_tarot.py:66 -msgid "Garde Contre" -msgstr "" - -#: plugins/plugin_misc_tarot.py:126 -msgid "contrat selection" -msgstr "" - -#: plugins/plugin_misc_tarot.py:139 -msgid "scores" -msgstr "" - -#: plugins/plugin_misc_tarot.py:221 plugins/plugin_misc_tarot.py:252 -#, python-format -msgid "" -"Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for " -"Excuse compensation" -msgstr "" - -#: plugins/plugin_misc_tarot.py:257 -#, python-format -msgid "" -"%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is " -"waiting for one" -msgstr "" - -#: plugins/plugin_misc_tarot.py:305 -msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" -msgstr "" - -#: plugins/plugin_misc_tarot.py:324 -#, python-format -msgid "" -"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" -msgstr "" - -#: plugins/plugin_misc_tarot.py:327 -#, python-format -msgid "" -"\n" -"--\n" -"%(player)s:\n" -"score for this game ==> %(score_game)i\n" -"total score ==> %(total_score)i" -msgstr "" - -#: plugins/plugin_misc_tarot.py:385 -msgid "Internal error: unmanaged game stage" -msgstr "" - -#: plugins/plugin_misc_tarot.py:402 -msgid "Creating Tarot game" -msgstr "" - -#: plugins/plugin_misc_tarot.py:406 plugins/plugin_misc_tarot.py:431 -#: plugins/plugin_misc_tarot.py:448 plugins/plugin_misc_tarot.py:465 -#, python-format -msgid "profile %s is unknown" -msgstr "" - -#: plugins/plugin_misc_tarot.py:409 -#, python-format -msgid "Tarot game already started in room %s" -msgstr "" - -#: plugins/plugin_misc_tarot.py:450 -#, python-format -msgid "contrat [%(contrat)s] choosed by %(profile)s" -msgstr "" - -#: plugins/plugin_misc_tarot.py:467 -#, python-format -msgid "Cards played by %(profile)s: [%(cards)s]" -msgstr "" - -#: plugins/plugin_misc_tarot.py:475 -msgid "new Tarot game" -msgstr "" - -#: plugins/plugin_misc_tarot.py:532 -#, python-format -msgid "Player %(player)s is ready to start [status: %(status)s]" -msgstr "" - -#: plugins/plugin_misc_tarot.py:567 -#, python-format -msgid "%(player)s win the bid with %(contrat)s" -msgstr "" - -#: plugins/plugin_misc_tarot.py:591 -msgid "tarot: chien received" -msgstr "" - -#: plugins/plugin_misc_tarot.py:646 -#, python-format -msgid "The winner of this trick is %s" -msgstr "" - -#: plugins/plugin_misc_tarot.py:691 -#, python-format -msgid "Unmanaged error type: %s" -msgstr "" - -#: plugins/plugin_misc_tarot.py:693 -#, python-format -msgid "Unmanaged card game element: %s" -msgstr "" - -#: plugins/plugin_xep_0045.py:62 -msgid "Implementation of Multi-User Chat" -msgstr "" - -#: plugins/plugin_xep_0045.py:68 -msgid "Plugin XEP_0045 initialization" -msgstr "" - -#: plugins/plugin_xep_0045.py:85 -#, python-format -msgid "Unknown or disconnected profile (%s)" -msgstr "" - -#: plugins/plugin_xep_0045.py:99 -msgid "Error when joining the room" -msgstr "" - -#: plugins/plugin_xep_0045.py:101 -msgid "Group chat error" -msgstr "" - -#: plugins/plugin_xep_0045.py:137 -#, python-format -msgid "%(profile)s is already in room %(room_jid)s" -msgstr "" - -#: plugins/plugin_xep_0045.py:139 -#, python-format -msgid "[%(profile)s] is joining room %(room)s with nick %(nick)s" -msgstr "" - -#: plugins/plugin_xep_0045.py:164 -#, python-format -msgid "user %(nick)s has joined room (%(room_id)s)" -msgstr "" - -#: plugins/plugin_xep_0045.py:169 -#, python-format -msgid "user %(nick)s left room (%(room_id)s)" -msgstr "" - -#: plugins/plugin_xep_0045.py:178 -#, python-format -msgid "New subject for room (%(room_id)s): %(subject)s" -msgstr "" - -#: plugins/plugin_xep_0054.py:62 -msgid "Implementation of vcard-temp" -msgstr "" - -#: plugins/plugin_xep_0054.py:68 -msgid "Plugin XEP_0054 initialization" -msgstr "" - -#: plugins/plugin_xep_0054.py:111 -#, python-format -msgid "Photo of type [%s] found" -msgstr "" - -#: plugins/plugin_xep_0054.py:113 -msgid "Decoding binary" -msgstr "" - -#: plugins/plugin_xep_0054.py:120 -#, python-format -msgid "file saved to %s" -msgstr "" - -#: plugins/plugin_xep_0054.py:122 -#, python-format -msgid "file [%s] already in cache" -msgstr "" - -#: plugins/plugin_xep_0054.py:128 -msgid "parsing vcard" -msgstr "" - -#: plugins/plugin_xep_0054.py:154 -#, python-format -msgid "FIXME: [%s] VCard tag is not managed yet" -msgstr "" - -#: plugins/plugin_xep_0054.py:160 -msgid "VCard found" -msgstr "" - -#: plugins/plugin_xep_0054.py:166 -msgid "FIXME: vCard not found as first child element" -msgstr "" - -#: plugins/plugin_xep_0054.py:171 -#, python-format -msgid "Can't find VCard of %s" -msgstr "" - -#: plugins/plugin_xep_0054.py:180 -msgid "Asking vcard for an non-existant or not connected profile" -msgstr "" - -#: plugins/plugin_xep_0054.py:183 -#, python-format -msgid "Asking for %s's VCard" -msgstr "" - -#: plugins/plugin_xep_0054.py:198 -#, python-format -msgid "Asking for an uncached avatar [%s]" -msgstr "" - -#: plugins/plugin_xep_0054.py:245 -msgid "New avatar found, requesting vcard" -msgstr "" - -#: plugins/plugin_xep_0065.py:89 -msgid "Implementation of SOCKS5 Bytestreams" -msgstr "" - -#: plugins/plugin_xep_0065.py:135 -msgid "Protocol init" -msgstr "" - -#: plugins/plugin_xep_0065.py:217 -#, python-format -msgid "Adding connection: %(address)s, %(connection)s" -msgstr "" - -#: plugins/plugin_xep_0065.py:313 -#, python-format -msgid "Saving file in %s." -msgstr "" - -#: plugins/plugin_xep_0065.py:364 -msgid "File transfer completed, closing connection" -msgstr "" - -#: plugins/plugin_xep_0065.py:442 -msgid "Socks 5 server connection started" -msgstr "" - -#: plugins/plugin_xep_0065.py:445 -#, python-format -msgid "Socks 5 server connection lost (reason: %s)" -msgstr "" - -#: plugins/plugin_xep_0065.py:452 -msgid "Socks 5 client connection started" -msgstr "" - -#: plugins/plugin_xep_0065.py:455 -#, python-format -msgid "Socks 5 client connection lost (reason: %s)" -msgstr "" - -#: plugins/plugin_xep_0065.py:472 -msgid "Plugin XEP_0065 initialization" -msgstr "" - -#: plugins/plugin_xep_0065.py:474 -msgid "registering" -msgstr "" - -#: plugins/plugin_xep_0065.py:484 -#, python-format -msgid "Launching Socks5 Stream server on port %d" -msgstr "" - -#: plugins/plugin_xep_0065.py:500 -msgid "Launching socks5 initiator" -msgstr "" - -#: plugins/plugin_xep_0065.py:515 -#, python-format -msgid "Stream proposed: host=[%(host)s] port=[%(port)s]" -msgstr "" - -#: plugins/plugin_xep_0065.py:531 -msgid "activating stream" -msgstr "" - -#: plugins/plugin_xep_0077.py:41 -msgid "Implementation of in-band registration" -msgstr "" - -#: plugins/plugin_xep_0077.py:47 -msgid "Plugin XEP_0077 initialization" -msgstr "" - -#: plugins/plugin_xep_0077.py:62 -msgid "No data form found" -msgstr "" - -#: plugins/plugin_xep_0077.py:64 -msgid "This gateway can't be managed by SàT, sorry :(" -msgstr "" - -#: plugins/plugin_xep_0077.py:85 -msgid "Your are now unregistred" -msgstr "" - -#: plugins/plugin_xep_0077.py:89 -#, python-format -msgid "Unregistration failure: %s" -msgstr "" - -#: plugins/plugin_xep_0077.py:93 -#, python-format -msgid "Unregistration failed: %s" -msgstr "" - -#: plugins/plugin_xep_0077.py:116 -msgid "Registration failed" -msgstr "" - -#: plugins/plugin_xep_0077.py:134 plugins/plugin_xep_0096.py:154 -msgid "Asking for an non-existant or not connected profile" -msgstr "" - -#: plugins/plugin_xep_0077.py:137 -#, python-format -msgid "Asking registration for [%s]" -msgstr "" - -#: plugins/plugin_xep_0096.py:52 -msgid "Implementation of SI File Transfert" -msgstr "" - -#: plugins/plugin_xep_0096.py:58 -msgid "Plugin XEP_0096 initialization" -msgstr "" - -#: plugins/plugin_xep_0096.py:67 -msgid "XEP-0096 management" -msgstr "" - -#: plugins/plugin_xep_0096.py:75 -#, python-format -msgid "File proposed: name=[%(name)s] size=%(size)s" -msgstr "" - -#: plugins/plugin_xep_0096.py:91 -#, python-format -msgid "Transfert [%s] refused" -msgstr "" - -#: plugins/plugin_xep_0096.py:96 -#, python-format -msgid "Transfert [%s] accepted" -msgstr "" - -#: plugins/plugin_xep_0096.py:99 -msgid "Approved unknow id !" -msgstr "" - -#: plugins/plugin_xep_0096.py:111 -msgid "Feature negociation" -msgstr "" - -#: plugins/plugin_xep_0100.py:38 -msgid "Implementation of Gateways protocol" -msgstr "" - -#: plugins/plugin_xep_0100.py:44 -msgid "Gateways plugin initialization" -msgstr "" - -#: plugins/plugin_xep_0100.py:54 -#, python-format -msgid "All items checked for id [%s]" -msgstr "" - -#: plugins/plugin_xep_0100.py:65 -#, python-format -msgid "Found gateway (%(jid)s): %(identity)s" -msgstr "" - -#: plugins/plugin_xep_0100.py:76 -#, python-format -msgid "Error when discovering [%(jid)s]: %(error)s" -msgstr "" - -#: plugins/plugin_xep_0100.py:85 -msgid "No gateway found" -msgstr "" - -#: plugins/plugin_xep_0100.py:92 -#, python-format -msgid "item found: %s" -msgstr "" - -#: plugins/plugin_xep_0100.py:97 -#, python-format -msgid "Error when discovering [%(target)s]: %(condition)s" -msgstr "" - -#: plugins/plugin_xep_0100.py:98 -#, python-format -msgid "Error while trying to discover %(target)s gateways: %(error_mess)s" -msgstr "" - -#: plugins/plugin_xep_0100.py:104 -msgid "Registration successful, doing the rest" -msgstr "" - -#: plugins/plugin_xep_0100.py:124 -#, python-format -msgid "find gateways (target = %(target)s, profile = %(profile)s)" -msgstr ""
--- a/sat.tac Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,796 +0,0 @@ -#!/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 tools.memory import Memory -from 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)
--- a/sat_bridge/DBus.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,401 +0,0 @@ -#!/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)) -
--- a/sat_bridge/bridge.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,68 +0,0 @@ -#!/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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_misc_cs.py Wed Dec 29 01:06:29 2010 +0100 @@ -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) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_misc_tarot.py Wed Dec 29 01:06:29 2010 +0100 @@ -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 [] +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0045.py Wed Dec 29 01:06:29 2010 +0100 @@ -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 [] +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0054.py Wed Dec 29 01:06:29 2010 +0100 @@ -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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0065.py Wed Dec 29 01:06:29 2010 +0100 @@ -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 []
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0077.py Wed Dec 29 01:06:29 2010 +0100 @@ -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"]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0096.py Wed Dec 29 01:06:29 2010 +0100 @@ -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 [] +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0100.py Wed Dec 29 01:06:29 2010 +0100 @@ -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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat.sh Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,3 @@ +#!/bin/sh +twistd -noy sat.tac +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat.tac Wed Dec 29 01:06:29 2010 +0100 @@ -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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat/bridge/DBus.py Wed Dec 29 01:06:29 2010 +0100 @@ -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)) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat/bridge/bridge.py Wed Dec 29 01:06:29 2010 +0100 @@ -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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat/tools/games.py Wed Dec 29 01:06:29 2010 +0100 @@ -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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat/tools/jid.py Wed Dec 29 01:06:29 2010 +0100 @@ -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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat/tools/memory.py Wed Dec 29 01:06:29 2010 +0100 @@ -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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat/tools/xml_tools.py Wed Dec 29 01:06:29 2010 +0100 @@ -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()
--- a/tools/games.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,80 +0,0 @@ -#!/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)
--- a/tools/jid.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -#!/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
--- a/tools/memory.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,655 +0,0 @@ -#!/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 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)
--- a/tools/xml_tools.py Tue Dec 28 23:10:13 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,302 +0,0 @@ -#!/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()